1414package com .google .googlejavaformat .java ;
1515
1616import static com .google .common .collect .Iterables .getLast ;
17+ import static com .google .common .primitives .Booleans .trueFirst ;
1718
1819import com .google .common .base .CharMatcher ;
1920import com .google .common .base .Preconditions ;
21+ import com .google .common .base .Splitter ;
2022import com .google .common .collect .ImmutableList ;
2123import com .google .common .collect .ImmutableSet ;
2224import com .google .common .collect .ImmutableSortedSet ;
2325import com .google .googlejavaformat .Newlines ;
26+ import com .google .googlejavaformat .java .JavaFormatterOptions .Style ;
2427import com .google .googlejavaformat .java .JavaInput .Tok ;
2528import java .util .ArrayList ;
29+ import java .util .Comparator ;
2630import java .util .List ;
2731import java .util .Optional ;
32+ import java .util .function .BiFunction ;
33+ import java .util .stream .Stream ;
2834import org .openjdk .tools .javac .parser .Tokens .TokenKind ;
2935
3036/** Orders imports in Java source code. */
3137public class ImportOrderer {
38+
39+ private static final Splitter DOT_SPLITTER = Splitter .on ('.' );
40+
3241 /**
3342 * Reorder the inputs in {@code text}, a complete Java program. On success, another complete Java
3443 * program is returned, which is the same as the original except the imports are in order.
3544 *
3645 * @throws FormatterException if the input could not be parsed.
3746 */
38- public static String reorderImports (String text ) throws FormatterException {
47+ public static String reorderImports (String text , Style style ) throws FormatterException {
3948 ImmutableList <Tok > toks = JavaInput .buildToks (text , CLASS_START );
40- return new ImportOrderer (text , toks ).reorderImports ();
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
61+ public static String reorderImports (String text ) throws FormatterException {
62+ return reorderImports (text , Style .GOOGLE );
63+ }
64+
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 ();
41105 }
42106
43107 /**
@@ -55,45 +119,127 @@ public static String reorderImports(String text) throws FormatterException {
55119 private static final ImmutableSet <String > IMPORT_OR_CLASS_START =
56120 ImmutableSet .of ("import" , "class" , "interface" , "enum" );
57121
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+ */
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+ */
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+
58165 private final String text ;
59166 private final ImmutableList <Tok > toks ;
60167 private final String lineSeparator ;
168+ private final Comparator <Import > importComparator ;
169+ private final BiFunction <Import , Import , Boolean > shouldInsertBlankLineFn ;
61170
62- private ImportOrderer (String text , ImmutableList <Tok > toks ) throws FormatterException {
171+ private ImportOrderer (String text , ImmutableList <Tok > toks , Style style ) {
63172 this .text = text ;
64173 this .toks = toks ;
65174 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+ }
66184 }
67185
68186 /** An import statement. */
69- private class Import implements Comparable <Import > {
187+ class Import {
188+ private final String imported ;
189+ private final boolean isStatic ;
190+ private final String trailing ;
191+
192+ Import (String imported , String trailing , boolean isStatic ) {
193+ this .imported = imported ;
194+ this .trailing = trailing ;
195+ this .isStatic = isStatic ;
196+ }
197+
70198 /** The name being imported, for example {@code java.util.List}. */
71- final String imported ;
199+ String imported () {
200+ return imported ;
201+ }
202+
203+ /** True if this is {@code import static}. */
204+ boolean isStatic () {
205+ return isStatic ;
206+ }
207+
208+ /** The top-level package of the import. */
209+ String topLevel () {
210+ return DOT_SPLITTER .split (imported ()).iterator ().next ();
211+ }
212+
213+ /** True if this is an Android import per AOSP style. */
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. */
220+ boolean isJava () {
221+ switch (topLevel ()) {
222+ case "java" :
223+ case "javax" :
224+ return true ;
225+ default :
226+ return false ;
227+ }
228+ }
72229
73230 /**
74231 * The {@code //} comment lines after the final {@code ;}, up to and including the line
75232 * terminator of the last one. Note: In case two imports were separated by a space (which is
76233 * disallowed by the style guide), the trailing whitespace of the first import does not include
77234 * a line terminator.
78235 */
79- final String trailing ;
80-
81- /** True if this is {@code import static}. */
82- final boolean isStatic ;
83-
84- Import (String imported , String trailing , boolean isStatic ) {
85- this .imported = imported ;
86- this .trailing = trailing ;
87- this .isStatic = isStatic ;
236+ String trailing () {
237+ return trailing ;
88238 }
89239
90- // This is how the sorting happens, including sorting static imports before non-static ones.
91- @ Override
92- public int compareTo (Import that ) {
93- if (this .isStatic != that .isStatic ) {
94- return this .isStatic ? -1 : +1 ;
95- }
96- return this .imported .compareTo (that .imported );
240+ /** True if this is a third-party import per AOSP style. */
241+ public boolean isThirdParty () {
242+ return !(isAndroid () || isJava ());
97243 }
98244
99245 // One or multiple lines, the import itself and following comments, including the line
@@ -102,61 +248,19 @@ public int compareTo(Import that) {
102248 public String toString () {
103249 StringBuilder sb = new StringBuilder ();
104250 sb .append ("import " );
105- if (isStatic ) {
251+ if (isStatic () ) {
106252 sb .append ("static " );
107253 }
108- sb .append (imported ).append (';' );
109- if (trailing .trim ().isEmpty ()) {
254+ sb .append (imported () ).append (';' );
255+ if (trailing () .trim ().isEmpty ()) {
110256 sb .append (lineSeparator );
111257 } else {
112- sb .append (trailing );
258+ sb .append (trailing () );
113259 }
114260 return sb .toString ();
115261 }
116262 }
117263
118- private String reorderImports () throws FormatterException {
119- int firstImportStart ;
120- Optional <Integer > maybeFirstImport = findIdentifier (0 , IMPORT_OR_CLASS_START );
121- if (!maybeFirstImport .isPresent () || !tokenAt (maybeFirstImport .get ()).equals ("import" )) {
122- // No imports, so nothing to do.
123- return text ;
124- }
125- firstImportStart = maybeFirstImport .get ();
126- int unindentedFirstImportStart = unindent (firstImportStart );
127-
128- ImportsAndIndex imports = scanImports (firstImportStart );
129- int afterLastImport = imports .index ;
130-
131- // Make sure there are no more imports before the next class (etc) definition.
132- Optional <Integer > maybeLaterImport = findIdentifier (afterLastImport , IMPORT_OR_CLASS_START );
133- if (maybeLaterImport .isPresent () && tokenAt (maybeLaterImport .get ()).equals ("import" )) {
134- throw new FormatterException ("Imports not contiguous (perhaps a comment separates them?)" );
135- }
136-
137- StringBuilder result = new StringBuilder ();
138- String prefix = tokString (0 , unindentedFirstImportStart );
139- result .append (prefix );
140- if (!prefix .isEmpty () && Newlines .getLineEnding (prefix ) == null ) {
141- result .append (lineSeparator ).append (lineSeparator );
142- }
143- result .append (reorderedImportsString (imports .imports ));
144-
145- List <String > tail = new ArrayList <>();
146- tail .add (CharMatcher .whitespace ().trimLeadingFrom (tokString (afterLastImport , toks .size ())));
147- if (!toks .isEmpty ()) {
148- Tok lastTok = getLast (toks );
149- int tailStart = lastTok .getPosition () + lastTok .length ();
150- tail .add (text .substring (tailStart ));
151- }
152- if (tail .stream ().anyMatch (s -> !s .isEmpty ())) {
153- result .append (lineSeparator );
154- tail .forEach (result ::append );
155- }
156-
157- return result .toString ();
158- }
159-
160264 private String tokString (int start , int end ) {
161265 StringBuilder sb = new StringBuilder ();
162266 for (int i = start ; i < end ; i ++) {
@@ -191,7 +295,7 @@ private static class ImportsAndIndex {
191295 */
192296 private ImportsAndIndex scanImports (int i ) throws FormatterException {
193297 int afterLastImport = i ;
194- ImmutableSortedSet .Builder <Import > imports = ImmutableSortedSet .naturalOrder ( );
298+ ImmutableSortedSet .Builder <Import > imports = ImmutableSortedSet .orderedBy ( importComparator );
195299 // JavaInput.buildToks appends a zero-width EOF token after all tokens. It won't match any
196300 // of our tests here and protects us from running off the end of the toks list. Since it is
197301 // zero-width it doesn't matter if we include it in our string concatenation at the end.
@@ -258,20 +362,18 @@ private ImportsAndIndex scanImports(int i) throws FormatterException {
258362 private String reorderedImportsString (ImmutableSortedSet <Import > imports ) {
259363 Preconditions .checkArgument (!imports .isEmpty (), "imports" );
260364
261- Import firstImport = imports .iterator ().next ();
262-
263- // Pretend that the first import was preceded by another import of the same kind
264- // (static or non-static), so we don't insert a newline there.
265- boolean lastWasStatic = firstImport .isStatic ;
365+ // Pretend that the first import was preceded by another import of the same kind, so we don't
366+ // insert a newline there.
367+ Import prevImport = imports .iterator ().next ();
266368
267369 StringBuilder sb = new StringBuilder ();
268- for (Import thisImport : imports ) {
269- if (lastWasStatic && ! thisImport . isStatic ) {
370+ for (Import currImport : imports ) {
371+ if (shouldInsertBlankLineFn . apply ( prevImport , currImport ) ) {
270372 // Blank line between static and non-static imports.
271373 sb .append (lineSeparator );
272374 }
273- lastWasStatic = thisImport . isStatic ;
274- sb . append ( thisImport ) ;
375+ sb . append ( currImport ) ;
376+ prevImport = currImport ;
275377 }
276378 return sb .toString ();
277379 }
0 commit comments