• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package com.google.googlejavaformat.java;
15 
16 import static com.google.common.collect.Iterables.getLast;
17 import static com.google.common.primitives.Booleans.trueFirst;
18 
19 import com.google.common.base.CharMatcher;
20 import com.google.common.base.Preconditions;
21 import com.google.common.base.Splitter;
22 import com.google.common.collect.ImmutableList;
23 import com.google.common.collect.ImmutableSet;
24 import com.google.common.collect.ImmutableSortedSet;
25 import com.google.googlejavaformat.Newlines;
26 import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
27 import com.google.googlejavaformat.java.JavaInput.Tok;
28 import com.sun.tools.javac.parser.Tokens.TokenKind;
29 import java.util.ArrayList;
30 import java.util.Comparator;
31 import java.util.List;
32 import java.util.Optional;
33 import java.util.function.BiFunction;
34 import java.util.stream.Stream;
35 
36 /** Orders imports in Java source code. */
37 public class ImportOrderer {
38 
39   private static final Splitter DOT_SPLITTER = Splitter.on('.');
40 
41   /**
42    * Reorder the inputs in {@code text}, a complete Java program. On success, another complete Java
43    * program is returned, which is the same as the original except the imports are in order.
44    *
45    * @throws FormatterException if the input could not be parsed.
46    */
reorderImports(String text, Style style)47   public static String reorderImports(String text, Style style) throws FormatterException {
48     ImmutableList<Tok> toks = JavaInput.buildToks(text, CLASS_START);
49     return new ImportOrderer(text, toks, style).reorderImports();
50   }
51 
52   /**
53    * Reorder the inputs in {@code text}, a complete Java program, in Google style. On success,
54    * another complete Java program is returned, which is the same as the original except the imports
55    * are in order.
56    *
57    * @deprecated Use {@link #reorderImports(String, Style)} instead
58    * @throws FormatterException if the input could not be parsed.
59    */
60   @Deprecated
reorderImports(String text)61   public static String reorderImports(String text) throws FormatterException {
62     return reorderImports(text, Style.GOOGLE);
63   }
64 
reorderImports()65   private String reorderImports() throws FormatterException {
66     int firstImportStart;
67     Optional<Integer> maybeFirstImport = findIdentifier(0, IMPORT_OR_CLASS_START);
68     if (!maybeFirstImport.isPresent() || !tokenAt(maybeFirstImport.get()).equals("import")) {
69       // No imports, so nothing to do.
70       return text;
71     }
72     firstImportStart = maybeFirstImport.get();
73     int unindentedFirstImportStart = unindent(firstImportStart);
74 
75     ImportsAndIndex imports = scanImports(firstImportStart);
76     int afterLastImport = imports.index;
77 
78     // Make sure there are no more imports before the next class (etc) definition.
79     Optional<Integer> maybeLaterImport = findIdentifier(afterLastImport, IMPORT_OR_CLASS_START);
80     if (maybeLaterImport.isPresent() && tokenAt(maybeLaterImport.get()).equals("import")) {
81       throw new FormatterException("Imports not contiguous (perhaps a comment separates them?)");
82     }
83 
84     StringBuilder result = new StringBuilder();
85     String prefix = tokString(0, unindentedFirstImportStart);
86     result.append(prefix);
87     if (!prefix.isEmpty() && Newlines.getLineEnding(prefix) == null) {
88       result.append(lineSeparator).append(lineSeparator);
89     }
90     result.append(reorderedImportsString(imports.imports));
91 
92     List<String> tail = new ArrayList<>();
93     tail.add(CharMatcher.whitespace().trimLeadingFrom(tokString(afterLastImport, toks.size())));
94     if (!toks.isEmpty()) {
95       Tok lastTok = getLast(toks);
96       int tailStart = lastTok.getPosition() + lastTok.length();
97       tail.add(text.substring(tailStart));
98     }
99     if (tail.stream().anyMatch(s -> !s.isEmpty())) {
100       result.append(lineSeparator);
101       tail.forEach(result::append);
102     }
103 
104     return result.toString();
105   }
106 
107   /**
108    * {@link TokenKind}s that indicate the start of a type definition. We use this to avoid scanning
109    * the whole file, since we know that imports must precede any type definition.
110    */
111   private static final ImmutableSet<TokenKind> CLASS_START =
112       ImmutableSet.of(TokenKind.CLASS, TokenKind.INTERFACE, TokenKind.ENUM);
113 
114   /**
115    * We use this set to find the first import, and again to check that there are no imports after
116    * the place we stopped gathering them. An annotation definition ({@code @interface}) is two
117    * tokens, the second which is {@code interface}, so we don't need a separate entry for that.
118    */
119   private static final ImmutableSet<String> IMPORT_OR_CLASS_START =
120       ImmutableSet.of("import", "class", "interface", "enum");
121 
122   /**
123    * A {@link Comparator} that orders {@link Import}s by Google Style, defined at
124    * https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing.
125    */
126   private static final Comparator<Import> GOOGLE_IMPORT_COMPARATOR =
127       Comparator.comparing(Import::isStatic, trueFirst()).thenComparing(Import::imported);
128 
129   /**
130    * A {@link Comparator} that orders {@link Import}s by AOSP Style, defined at
131    * https://source.android.com/setup/contribute/code-style#order-import-statements and implemented
132    * in IntelliJ at
133    * https://android.googlesource.com/platform/development/+/master/ide/intellij/codestyles/AndroidStyle.xml.
134    */
135   private static final Comparator<Import> AOSP_IMPORT_COMPARATOR =
136       Comparator.comparing(Import::isStatic, trueFirst())
137           .thenComparing(Import::isAndroid, trueFirst())
138           .thenComparing(Import::isThirdParty, trueFirst())
139           .thenComparing(Import::isJava, trueFirst())
140           .thenComparing(Import::imported);
141 
142   /**
143    * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
144    * Import}s based on Google style.
145    */
shouldInsertBlankLineGoogle(Import prev, Import curr)146   private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) {
147     return prev.isStatic() && !curr.isStatic();
148   }
149 
150   /**
151    * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
152    * Import}s based on AOSP style.
153    */
shouldInsertBlankLineAosp(Import prev, Import curr)154   private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) {
155     if (prev.isStatic() && !curr.isStatic()) {
156       return true;
157     }
158     // insert blank line between "com.android" from "com.anythingelse"
159     if (prev.isAndroid() && !curr.isAndroid()) {
160       return true;
161     }
162     return !prev.topLevel().equals(curr.topLevel());
163   }
164 
165   private final String text;
166   private final ImmutableList<Tok> toks;
167   private final String lineSeparator;
168   private final Comparator<Import> importComparator;
169   private final BiFunction<Import, Import, Boolean> shouldInsertBlankLineFn;
170 
ImportOrderer(String text, ImmutableList<Tok> toks, Style style)171   private ImportOrderer(String text, ImmutableList<Tok> toks, Style style) {
172     this.text = text;
173     this.toks = toks;
174     this.lineSeparator = Newlines.guessLineSeparator(text);
175     if (style.equals(Style.GOOGLE)) {
176       this.importComparator = GOOGLE_IMPORT_COMPARATOR;
177       this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineGoogle;
178     } else if (style.equals(Style.AOSP)) {
179       this.importComparator = AOSP_IMPORT_COMPARATOR;
180       this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineAosp;
181     } else {
182       throw new IllegalArgumentException("Unsupported code style: " + style);
183     }
184   }
185 
186   /** An import statement. */
187   class Import {
188     private final String imported;
189     private final boolean isStatic;
190     private final String trailing;
191 
Import(String imported, String trailing, boolean isStatic)192     Import(String imported, String trailing, boolean isStatic) {
193       this.imported = imported;
194       this.trailing = trailing;
195       this.isStatic = isStatic;
196     }
197 
198     /** The name being imported, for example {@code java.util.List}. */
imported()199     String imported() {
200       return imported;
201     }
202 
203     /** True if this is {@code import static}. */
isStatic()204     boolean isStatic() {
205       return isStatic;
206     }
207 
208     /** The top-level package of the import. */
topLevel()209     String topLevel() {
210       return DOT_SPLITTER.split(imported()).iterator().next();
211     }
212 
213     /** True if this is an Android import per AOSP style. */
isAndroid()214     boolean isAndroid() {
215       return Stream.of("android.", "androidx.", "dalvik.", "libcore.", "com.android.")
216           .anyMatch(imported::startsWith);
217     }
218 
219     /** True if this is a Java import per AOSP style. */
isJava()220     boolean isJava() {
221       switch (topLevel()) {
222         case "java":
223         case "javax":
224           return true;
225         default:
226           return false;
227       }
228     }
229 
230     /**
231      * The {@code //} comment lines after the final {@code ;}, up to and including the line
232      * terminator of the last one. Note: In case two imports were separated by a space (which is
233      * disallowed by the style guide), the trailing whitespace of the first import does not include
234      * a line terminator.
235      */
trailing()236     String trailing() {
237       return trailing;
238     }
239 
240     /** True if this is a third-party import per AOSP style. */
isThirdParty()241     public boolean isThirdParty() {
242       return !(isAndroid() || isJava());
243     }
244 
245     // One or multiple lines, the import itself and following comments, including the line
246     // terminator.
247     @Override
toString()248     public String toString() {
249       StringBuilder sb = new StringBuilder();
250       sb.append("import ");
251       if (isStatic()) {
252         sb.append("static ");
253       }
254       sb.append(imported()).append(';');
255       if (trailing().trim().isEmpty()) {
256         sb.append(lineSeparator);
257       } else {
258         sb.append(trailing());
259       }
260       return sb.toString();
261     }
262   }
263 
tokString(int start, int end)264   private String tokString(int start, int end) {
265     StringBuilder sb = new StringBuilder();
266     for (int i = start; i < end; i++) {
267       sb.append(toks.get(i).getOriginalText());
268     }
269     return sb.toString();
270   }
271 
272   private static class ImportsAndIndex {
273     final ImmutableSortedSet<Import> imports;
274     final int index;
275 
ImportsAndIndex(ImmutableSortedSet<Import> imports, int index)276     ImportsAndIndex(ImmutableSortedSet<Import> imports, int index) {
277       this.imports = imports;
278       this.index = index;
279     }
280   }
281 
282   /**
283    * Scans a sequence of import lines. The parsing uses this approximate grammar:
284    *
285    * <pre>{@code
286    * <imports> -> (<end-of-line> | <import>)*
287    * <import> -> "import" <whitespace> ("static" <whitespace>)?
288    *    <identifier> ("." <identifier>)* ("." "*")? <whitespace>? ";"
289    *    <whitespace>? <end-of-line>? (<line-comment> <end-of-line>)*
290    * }</pre>
291    *
292    * @param i the index to start parsing at.
293    * @return the result of parsing the imports.
294    * @throws FormatterException if imports could not parsed according to the grammar.
295    */
scanImports(int i)296   private ImportsAndIndex scanImports(int i) throws FormatterException {
297     int afterLastImport = i;
298     ImmutableSortedSet.Builder<Import> imports = ImmutableSortedSet.orderedBy(importComparator);
299     // JavaInput.buildToks appends a zero-width EOF token after all tokens. It won't match any
300     // of our tests here and protects us from running off the end of the toks list. Since it is
301     // zero-width it doesn't matter if we include it in our string concatenation at the end.
302     while (i < toks.size() && tokenAt(i).equals("import")) {
303       i++;
304       if (isSpaceToken(i)) {
305         i++;
306       }
307       boolean isStatic = tokenAt(i).equals("static");
308       if (isStatic) {
309         i++;
310         if (isSpaceToken(i)) {
311           i++;
312         }
313       }
314       if (!isIdentifierToken(i)) {
315         throw new FormatterException("Unexpected token after import: " + tokenAt(i));
316       }
317       StringAndIndex imported = scanImported(i);
318       String importedName = imported.string;
319       i = imported.index;
320       if (isSpaceToken(i)) {
321         i++;
322       }
323       if (!tokenAt(i).equals(";")) {
324         throw new FormatterException("Expected ; after import");
325       }
326       while (tokenAt(i).equals(";")) {
327         // Extra semicolons are not allowed by the JLS but are accepted by javac.
328         i++;
329       }
330       StringBuilder trailing = new StringBuilder();
331       if (isSpaceToken(i)) {
332         trailing.append(tokenAt(i));
333         i++;
334       }
335       if (isNewlineToken(i)) {
336         trailing.append(tokenAt(i));
337         i++;
338       }
339       // Gather (if any) all single line comments and accompanied line terminators following this
340       // import
341       while (isSlashSlashCommentToken(i)) {
342         trailing.append(tokenAt(i));
343         i++;
344         if (isNewlineToken(i)) {
345           trailing.append(tokenAt(i));
346           i++;
347         }
348       }
349       while (tokenAt(i).equals(";")) {
350         // Extra semicolons are not allowed by the JLS but are accepted by javac.
351         i++;
352       }
353       imports.add(new Import(importedName, trailing.toString(), isStatic));
354       // Remember the position just after the import we just saw, before skipping blank lines.
355       // If the next thing after the blank lines is not another import then we don't want to
356       // include those blank lines in the text to be replaced.
357       afterLastImport = i;
358       while (isNewlineToken(i) || isSpaceToken(i)) {
359         i++;
360       }
361     }
362     return new ImportsAndIndex(imports.build(), afterLastImport);
363   }
364 
365   // Produces the sorted output based on the imports we have scanned.
reorderedImportsString(ImmutableSortedSet<Import> imports)366   private String reorderedImportsString(ImmutableSortedSet<Import> imports) {
367     Preconditions.checkArgument(!imports.isEmpty(), "imports");
368 
369     // Pretend that the first import was preceded by another import of the same kind, so we don't
370     // insert a newline there.
371     Import prevImport = imports.iterator().next();
372 
373     StringBuilder sb = new StringBuilder();
374     for (Import currImport : imports) {
375       if (shouldInsertBlankLineFn.apply(prevImport, currImport)) {
376         // Blank line between static and non-static imports.
377         sb.append(lineSeparator);
378       }
379       sb.append(currImport);
380       prevImport = currImport;
381     }
382     return sb.toString();
383   }
384 
385   private static class StringAndIndex {
386     private final String string;
387     private final int index;
388 
StringAndIndex(String string, int index)389     StringAndIndex(String string, int index) {
390       this.string = string;
391       this.index = index;
392     }
393   }
394 
395   /**
396    * Scans the imported thing, the dot-separated name that comes after import [static] and before
397    * the semicolon. We don't allow spaces inside the dot-separated name. Wildcard imports are
398    * supported: if the input is {@code import java.util.*;} then the returned string will be {@code
399    * java.util.*}.
400    *
401    * @param start the index of the start of the identifier. If the import is {@code import
402    *     java.util.List;} then this index points to the token {@code java}.
403    * @return the parsed import ({@code java.util.List} in the example) and the index of the first
404    *     token after the imported thing ({@code ;} in the example).
405    * @throws FormatterException if the imported name could not be parsed.
406    */
scanImported(int start)407   private StringAndIndex scanImported(int start) throws FormatterException {
408     int i = start;
409     StringBuilder imported = new StringBuilder();
410     // At the start of each iteration of this loop, i points to an identifier.
411     // On exit from the loop, i points to a token after an identifier or after *.
412     while (true) {
413       Preconditions.checkState(isIdentifierToken(i));
414       imported.append(tokenAt(i));
415       i++;
416       if (!tokenAt(i).equals(".")) {
417         return new StringAndIndex(imported.toString(), i);
418       }
419       imported.append('.');
420       i++;
421       if (tokenAt(i).equals("*")) {
422         imported.append('*');
423         return new StringAndIndex(imported.toString(), i + 1);
424       } else if (!isIdentifierToken(i)) {
425         throw new FormatterException("Could not parse imported name, at: " + tokenAt(i));
426       }
427     }
428   }
429 
430   /**
431    * Returns the index of the first place where one of the given identifiers occurs, or {@code
432    * Optional.empty()} if there is none.
433    *
434    * @param start the index to start looking at
435    * @param identifiers the identifiers to look for
436    */
findIdentifier(int start, ImmutableSet<String> identifiers)437   private Optional<Integer> findIdentifier(int start, ImmutableSet<String> identifiers) {
438     for (int i = start; i < toks.size(); i++) {
439       if (isIdentifierToken(i)) {
440         String id = tokenAt(i);
441         if (identifiers.contains(id)) {
442           return Optional.of(i);
443         }
444       }
445     }
446     return Optional.empty();
447   }
448 
449   /** Returns the given token, or the preceding token if it is a whitespace token. */
unindent(int i)450   private int unindent(int i) {
451     if (i > 0 && isSpaceToken(i - 1)) {
452       return i - 1;
453     } else {
454       return i;
455     }
456   }
457 
tokenAt(int i)458   private String tokenAt(int i) {
459     return toks.get(i).getOriginalText();
460   }
461 
isIdentifierToken(int i)462   private boolean isIdentifierToken(int i) {
463     String s = tokenAt(i);
464     return !s.isEmpty() && Character.isJavaIdentifierStart(s.codePointAt(0));
465   }
466 
isSpaceToken(int i)467   private boolean isSpaceToken(int i) {
468     String s = tokenAt(i);
469     if (s.isEmpty()) {
470       return false;
471     } else {
472       return " \t\f".indexOf(s.codePointAt(0)) >= 0;
473     }
474   }
475 
isSlashSlashCommentToken(int i)476   private boolean isSlashSlashCommentToken(int i) {
477     return toks.get(i).isSlashSlashComment();
478   }
479 
isNewlineToken(int i)480   private boolean isNewlineToken(int i) {
481     return toks.get(i).isNewline();
482   }
483 }
484