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