Package org.waveprotocol.pst.style

Source Code of org.waveprotocol.pst.style.PstStyler$StyleBuilder$LineGenerator

/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.waveprotocol.pst.style;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.List;

/**
* A code styler using a Builder approach to configure styles using smaller
* reformatting components.
*
* TODO(kalman): take string literals into account.
*
* @author kalman@google.com (Benjamin Kalman)
*/
public final class PstStyler implements Styler {

  private static final String BACKUP_SUFFIX = ".prePstStyler";
  private static final String INDENT = "  ";
  private static final String[] ATOMIC_TOKENS = new String[] {
      "} else {",
      "} else if (",
      "for (",
      "/*-{",
      "}-*/",
  };

  /**
   * Builder for a series of composed style components.
   */
  private static class StyleBuilder {

    /**
     * Styles a single line, outputting generated lines to a generator.
     * The styler may output anywhere between 0 and infinite lines.
     */
    private interface LineStyler {
      void next(String line, LineGenerator generator);
    }

    /**
     * Generates lines to some output sink.
     */
    private interface LineGenerator {
      void yield(CharSequence s);
    }

    /**
     * A pipeline of line stylers as a single line generator.
     */
    private static final class LinePipeline implements LineGenerator {
      private final LineStyler lineStyler;
      private final LineGenerator next;

      private LinePipeline(LineStyler lineStyler, LineGenerator next) {
        this.lineStyler = lineStyler;
        this.next = next;
      }

      /**
       * Constructs a pipeline of line stylers.
       *
       * @param ls the line stylers to place in the pipeline
       * @param sink the line generator at the end of the pipeline
       * @return the head of the pipeline
       */
      public static LinePipeline construct(Iterable<LineStyler> ls, LineGenerator sink) {
        return new LinePipeline(head(ls),
            (Iterables.size(ls) == 1) ? sink : construct(tail(ls), sink));
      }

      private static LineStyler head(Iterable<LineStyler> ls) {
        return ls.iterator().next();
      }

      private static Iterable<LineStyler> tail(final Iterable<LineStyler> ls) {
        return new Iterable<LineStyler>() {
          @Override public Iterator<LineStyler> iterator() {
            Iterator<LineStyler> tail = ls.iterator();
            tail.next();
            return tail;
          }
        };
      }

      @Override
      public void yield(CharSequence s) {
        lineStyler.next(s.toString(), next);
      }
    }

    /**
     * Generates lines into a list.
     */
    private static final class ListGenerator implements LineGenerator {
      private final List<String> list = Lists.newArrayList();

      @Override
      public void yield(CharSequence s) {
        list.add(s.toString());
      }

      public List<String> getList() {
        return list;
      }
    }

    /**
     * Maintains some helpful state across a styling.
     */
    private abstract class StatefulLineStyler implements LineStyler {
      private boolean inShortComment = false;
      private boolean inLongComment = false;
      private boolean inStartOfComment = false;
      private boolean previousWasComment = false;
      private int lineNumber = 0;

      protected boolean inComment() {
        return inShortComment || inLongComment;
      }

      protected boolean inStartOfComment() {
        return inStartOfComment;
      }

      protected boolean inLongComment() {
        return inLongComment;
      }

      protected int getLineNumber() {
        return lineNumber;
      }

      protected boolean previousWasComment() {
        return previousWasComment;
      }

      @Override
      public final void next(String line, LineGenerator generator) {
        lineNumber++;
        // TODO(kalman): JSNI?
        if (line.matches("^[ \t]*/\\*.*")) {
          inLongComment = true;
          inStartOfComment = true;
        }
        if (line.matches("^[ \t]*//.*")) {
          inShortComment = true;
          inStartOfComment = true;
        }
        doNext(line, generator);
        previousWasComment = inShortComment || inLongComment;
        if (line.contains("*/")) {
          inLongComment = false;
        }
        inShortComment = false;
        inStartOfComment = false;
      }

      abstract void doNext(String line, LineGenerator generator);
    }

    private final List<LineStyler> lineStylers = Lists.newArrayList();

    /**
     * Applies the state of the styler to a list of lines.
     *
     * @return the styled lines
     */
    public List<String> apply(List<String> lines) {
      ListGenerator result = new ListGenerator();
      LinePipeline pipeline = LinePipeline.construct(lineStylers, result);
      for (String line : lines) {
        pipeline.yield(line);
      }
      return result.getList();
    }

    public StyleBuilder addNewLineBefore(final char newLineBefore) {
      lineStylers.add(new StatefulLineStyler() {
        @Override public void doNext(String line, LineGenerator generator) {
          // TODO(kalman): this is heavy-handed; be fine-grained and just don't
          // split over tokens (need regexp, presumably).
          if (inComment() || containsAtomicToken(line)) {
            generator.yield(line);
            return;
          }

          StringBuilder s = new StringBuilder();
          for (char c : line.toCharArray()) {
            if (c == newLineBefore) {
              generator.yield(s);
              s = new StringBuilder();
            }
            s.append(c);
          }
          generator.yield(s);
        }
      });
      return this;
    }

    public StyleBuilder addNewLineAfter(final char newLineAfter) {
      lineStylers.add(new StatefulLineStyler() {
        @Override public void doNext(String line, LineGenerator generator) {
          // TODO(kalman): same as above.
          if (inComment() || containsAtomicToken(line)) {
            generator.yield(line);
            return;
          }

          StringBuilder s = new StringBuilder();
          for (char c : line.toCharArray()) {
            s.append(c);
            if (c == newLineAfter) {
              generator.yield(s);
              s = new StringBuilder();
            }
          }
          generator.yield(s);
        }
      });
      return this;
    }

    public StyleBuilder trim() {
      lineStylers.add(new LineStyler() {
        @Override public void next(String line, LineGenerator generator) {
          generator.yield(line.trim());
        }
      });
      return this;
    }

    public StyleBuilder removeRepeatedSpacing() {
      lineStylers.add(new LineStyler() {
        @Override public void next(String line, LineGenerator generator) {
          generator.yield(line.replaceAll("[ \t]+", " "));
        }
      });
      return this;
    }

    public StyleBuilder stripBlankLines() {
      lineStylers.add(new LineStyler() {
        @Override public void next(String line, LineGenerator generator) {
          if (!line.isEmpty()) {
            generator.yield(line);
          }
        }
      });
      return this;
    }

    public StyleBuilder stripInitialBlankLine() {
      lineStylers.add(new LineStyler() {
        boolean firstLine = true;
        @Override public void next(String line, LineGenerator generator) {
          if (!firstLine || !line.isEmpty()) {
            generator.yield(line);
          }
          firstLine = false;
        }
      });
      return this;
    }

    public StyleBuilder stripDuplicateBlankLines() {
      lineStylers.add(new LineStyler() {
        boolean previousWasEmpty = false;
        @Override public void next(String line, LineGenerator generator) {
          if (!previousWasEmpty || !line.isEmpty()) {
            generator.yield(line);
          }
          previousWasEmpty = line.isEmpty();
        }
      });
      return this;
    }

    public StyleBuilder indentBraces() {
      lineStylers.add(new StatefulLineStyler() {
        private int indentLevel = 0;

        @Override public void doNext(String line, LineGenerator generator) {
          if (!ignore(line) && line.contains("}")) {
            indentLevel--;
            Preconditions.checkState(indentLevel >= 0,
                "Indentation level reached < 0 on line " + getLineNumber() + " (" + line + ")");
          }
          String result = "";
          if (!line.isEmpty()) {
            result = Strings.repeat(INDENT, indentLevel) + line;
          }
          if (!ignore(line) && line.contains("{")) {
            indentLevel++;
          }
          generator.yield(result.toString());
        }

        private boolean ignore(String line) {
          // Ignore self-closing braces.
          return line.contains("{")
              && line.contains("}")
              && line.indexOf('{') < line.lastIndexOf('}');
        }
      });
      return this;
    }

    public StyleBuilder indentLongComments() {
      lineStylers.add(new StatefulLineStyler() {
        @Override void doNext(String line, LineGenerator generator) {
          if (inLongComment() && !inStartOfComment()) {
            generator.yield(" " + line);
          } else {
            generator.yield(line);
          }
        }
      });
      return this;
    }

    public StyleBuilder doubleIndentUnfinishedLines() {
      lineStylers.add(new StatefulLineStyler() {
        boolean previousUnfinished = false;

        @Override public void doNext(String line, LineGenerator generator) {
          generator.yield((previousUnfinished ? Strings.repeat(INDENT, 2) : "") + line);
          previousUnfinished =
              !inComment() &&
              !line.matches("^.*[;{},\\-/]$") && // Ends with an expected character.
              !line.contains("@Override") &&    // or an annotation.
              !line.isEmpty() &&
              !line.contains("//"); // Single-line comment.
        }
      });
      return this;
    }

    public StyleBuilder addBlankLineBeforeMatching(final String regex) {
      lineStylers.add(new StatefulLineStyler() {
        @Override public void doNext(String line, LineGenerator generator) {
          if ((!inComment() || inStartOfComment()) && line.matches(regex)) {
            generator.yield("");
          }
          generator.yield(line);
        }
      });
      return this;
    }

    public StyleBuilder addBlankLineBeforeClasslikeWithNoPrecedingComment() {
      lineStylers.add(new StatefulLineStyler() {
        @Override public void doNext(String line, LineGenerator generator) {
          if (!previousWasComment()
              && line.matches(".*\\b(class|interface|enum)\\b.*")) {
            generator.yield("");
          }
          generator.yield(line);
        }
      });
      return this;
    }

    public StyleBuilder addBlankLineAfterBraceUnlessInMethod() {
      lineStylers.add(new StatefulLineStyler() {
        // true for every level of braces that is a class-like construct
        ArrayDeque<Boolean> stack = new ArrayDeque<Boolean>();
        boolean sawClasslike = false;

        @Override public void doNext(String line, LineGenerator generator) {
          if (inComment()) {
            generator.yield(line);
          } else if (line.endsWith("}") && !line.contains("{")) {
            generator.yield(line);
            stack.pop();
            if (!stack.isEmpty() && stack.peek()) {
              generator.yield("");
            }
          } else {
            // Perhaps we could match anonymous classes by adding "new" here,
            // but this is not currently needed.
            if (line.matches(".*\\b(class|interface|enum)\\b.*")) {
              sawClasslike = true;
            }
            if (line.endsWith("{")) {
              if (line.contains("}")) {
                stack.pop();
              }
              stack.push(sawClasslike);
              sawClasslike = false;
            } else if (line.endsWith(";")) {
              sawClasslike = false;
            }
            generator.yield(line);
          }
        }
      });
      return this;
    }

    public StyleBuilder addBlankLineAfterMatching(final String regex) {
      lineStylers.add(new StatefulLineStyler() {
        boolean previousLineMatched = false;

        @Override public void doNext(String line, LineGenerator generator) {
          if (previousLineMatched) {
            generator.yield("");
          }
          generator.yield(line);
          previousLineMatched = line.matches(regex);
        }
      });
      return this;
    }

    private boolean containsAtomicToken(String line) {
      for (String token : ATOMIC_TOKENS) {
        if (line.contains(token)) {
          return true;
        }
      }
      return false;
    }
  }

  @Override
  public void style(File f, boolean saveBackup) {
    List<String> lines = null;
    try {
      lines = CharStreams.readLines(new FileReader(f));
    } catch (IOException e) {
      System.err.println("Couldn't find file " + f.getName() + " to style: " + e.getMessage());
      return;
    }

    Joiner newlineJoiner = Joiner.on('\n');

    if (saveBackup) {
      File backup = new File(f.getAbsolutePath() + BACKUP_SUFFIX);
      try {
        Files.write(newlineJoiner.join(lines), backup, Charset.defaultCharset());
      } catch (IOException e) {
        System.err.println("Couldn't write backup " + backup.getName() + ": " + e.getMessage());
        return;
      }
    }

    try {
      Files.write(newlineJoiner.join(styleLines(lines)), f, Charset.defaultCharset());
    } catch (IOException e) {
      System.err.println("Couldn't write styled file " + f.getName() + ": " + e.getMessage());
      return;
    }
  }

  private List<String> styleLines(List<String> lines) {
    return new StyleBuilder()
        .trim()
        .removeRepeatedSpacing()
        .addNewLineBefore('}')
        .addNewLineAfter('{')
        .addNewLineAfter('}')
        .addNewLineAfter(';')
        .trim()
        .removeRepeatedSpacing()
        .stripBlankLines()
        .trim()
        .indentBraces()
        .indentLongComments()
        .addBlankLineBeforeMatching("[ \t]*@Override.*")
        .addBlankLineBeforeMatching(".*/\\*\\*.*")
        .addBlankLineAfterMatching("package.*")
        .addBlankLineBeforeMatching("package.*")
        .addBlankLineBeforeClasslikeWithNoPrecedingComment()
        .addBlankLineAfterBraceUnlessInMethod()
        .stripDuplicateBlankLines()
        .doubleIndentUnfinishedLines()
        .stripInitialBlankLine()
        // TODO: blank line before first method or constructor if that has no javadoc
        .apply(lines);
  }
}
TOP

Related Classes of org.waveprotocol.pst.style.PstStyler$StyleBuilder$LineGenerator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.