Skip to content

Commit

Permalink
Add semantics-aware sorting for Java imports (#1709 fixes #522)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored May 22, 2023
2 parents 3506cf7 + d72a11c commit 8d4f402
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
### Added
* `Jvm.Support` now accepts `-SNAPSHOT` versions, treated as the non`-SNAPSHOT`. ([#1583](https://github.com/diffplug/spotless/issues/1583))
* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663))
* Add semantics-aware Java import ordering (i.e. sort by package, then class, then member). ([#522](https://github.com/diffplug/spotless/issues/522))
### Fixed
* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698))
* Fixed a regression which changed the import sorting order in `googleJavaFormat` introduced in `2.38.0`. ([#1680](https://github.com/diffplug/spotless/pull/1680))
Expand Down
41 changes: 30 additions & 11 deletions lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2021 DiffPlug
* Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,6 +28,8 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -39,6 +41,9 @@

public final class ImportOrderStep {
private static final boolean WILDCARDS_LAST_DEFAULT = false;
private static final boolean SEMANTIC_SORT_DEFAULT = false;
private static final Set<String> TREAT_AS_PACKAGE_DEFAULT = Set.of();
private static final Set<String> TREAT_AS_CLASS_DEFAULT = Set.of();

private final String lineFormat;

Expand All @@ -55,27 +60,33 @@ private ImportOrderStep(String lineFormat) {
}

public FormatterStep createFrom(String... importOrder) {
return createFrom(WILDCARDS_LAST_DEFAULT, importOrder);
return createFrom(WILDCARDS_LAST_DEFAULT, SEMANTIC_SORT_DEFAULT, TREAT_AS_PACKAGE_DEFAULT,
TREAT_AS_CLASS_DEFAULT, importOrder);
}

public FormatterStep createFrom(boolean wildcardsLast, String... importOrder) {
public FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
Set<String> treatAsClass, String... importOrder) {
// defensive copying and null checking
List<String> importOrderList = requireElementsNonNull(Arrays.asList(importOrder));
return createFrom(wildcardsLast, () -> importOrderList);
return createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, () -> importOrderList);
}

public FormatterStep createFrom(File importsFile) {
return createFrom(WILDCARDS_LAST_DEFAULT, importsFile);
return createFrom(WILDCARDS_LAST_DEFAULT, SEMANTIC_SORT_DEFAULT, TREAT_AS_PACKAGE_DEFAULT,
TREAT_AS_CLASS_DEFAULT, importsFile);
}

public FormatterStep createFrom(boolean wildcardsLast, File importsFile) {
public FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
Set<String> treatAsClass, File importsFile) {
Objects.requireNonNull(importsFile);
return createFrom(wildcardsLast, () -> getImportOrder(importsFile));
return createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, () -> getImportOrder(importsFile));
}

private FormatterStep createFrom(boolean wildcardsLast, Supplier<List<String>> importOrder) {
private FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
Set<String> treatAsClass, Supplier<List<String>> importOrder) {
return FormatterStep.createLazy("importOrder",
() -> new State(importOrder.get(), lineFormat, wildcardsLast),
() -> new State(importOrder.get(), lineFormat, wildcardsLast, semanticSort, treatAsPackage,
treatAsClass),
State::toFormatter);
}

Expand Down Expand Up @@ -106,15 +117,23 @@ private static final class State implements Serializable {
private final List<String> importOrder;
private final String lineFormat;
private final boolean wildcardsLast;
private final boolean semanticSort;
private final TreeSet<String> treatAsPackage;
private final TreeSet<String> treatAsClass;

State(List<String> importOrder, String lineFormat, boolean wildcardsLast) {
State(List<String> importOrder, String lineFormat, boolean wildcardsLast, boolean semanticSort,
Set<String> treatAsPackage, Set<String> treatAsClass) {
this.importOrder = importOrder;
this.lineFormat = lineFormat;
this.wildcardsLast = wildcardsLast;
this.semanticSort = semanticSort;
this.treatAsPackage = treatAsPackage == null ? null : new TreeSet<>(treatAsPackage);
this.treatAsClass = treatAsClass == null ? null : new TreeSet<>(treatAsClass);
}

FormatterFunc toFormatter() {
return raw -> new ImportSorter(importOrder, wildcardsLast).format(raw, lineFormat);
return raw -> new ImportSorter(importOrder, wildcardsLast, semanticSort, treatAsPackage, treatAsClass)
.format(raw, lineFormat);
}
}
}
15 changes: 12 additions & 3 deletions lib/src/main/java/com/diffplug/spotless/java/ImportSorter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2021 DiffPlug
* Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

/**
* @author Vojtech Krasa
Expand All @@ -30,10 +31,17 @@ final class ImportSorter {

private final List<String> importsOrder;
private final boolean wildcardsLast;
private final boolean semanticSort;
private final Set<String> treatAsPackage;
private final Set<String> treatAsClass;

ImportSorter(List<String> importsOrder, boolean wildcardsLast) {
ImportSorter(List<String> importsOrder, boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
Set<String> treatAsClass) {
this.importsOrder = new ArrayList<>(importsOrder);
this.wildcardsLast = wildcardsLast;
this.semanticSort = semanticSort;
this.treatAsPackage = treatAsPackage;
this.treatAsClass = treatAsClass;
}

String format(String raw, String lineFormat) {
Expand Down Expand Up @@ -81,7 +89,8 @@ String format(String raw, String lineFormat) {
}
scanner.close();

List<String> sortedImports = ImportSorterImpl.sort(imports, importsOrder, wildcardsLast, lineFormat);
List<String> sortedImports = ImportSorterImpl.sort(imports, importsOrder, wildcardsLast, semanticSort,
treatAsPackage, treatAsClass, lineFormat);
return applyImportsToDocument(raw, firstImportLine, lastImportLine, sortedImports);
}

Expand Down
203 changes: 186 additions & 17 deletions lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ public List<String> getSubGroups() {
}
}

static List<String> sort(List<String> imports, List<String> importsOrder, boolean wildcardsLast, String lineFormat) {
ImportSorterImpl importsSorter = new ImportSorterImpl(importsOrder, wildcardsLast);
static List<String> sort(List<String> imports, List<String> importsOrder, boolean wildcardsLast,
boolean semanticSort, Set<String> treatAsPackage, Set<String> treatAsClass, String lineFormat) {
ImportSorterImpl importsSorter = new ImportSorterImpl(importsOrder, wildcardsLast, semanticSort, treatAsPackage,
treatAsClass);
return importsSorter.sort(imports, lineFormat);
}

Expand All @@ -76,12 +78,17 @@ private List<String> sort(List<String> imports, String lineFormat) {
return getResult(sortedImported, lineFormat);
}

private ImportSorterImpl(List<String> importOrder, boolean wildcardsLast) {
private ImportSorterImpl(List<String> importOrder, boolean wildcardsLast, boolean semanticSort,
Set<String> treatAsPackage, Set<String> treatAsClass) {
importsGroups = importOrder.stream().filter(Objects::nonNull).map(ImportsGroup::new).collect(Collectors.toList());
putStaticItemIfNotExists(importsGroups);
putCatchAllGroupIfNotExists(importsGroups);

ordering = new OrderingComparator(wildcardsLast);
if (semanticSort) {
ordering = new SemanticOrderingComparator(wildcardsLast, treatAsPackage, treatAsClass);
} else {
ordering = new LexicographicalOrderingComparator(wildcardsLast);
}

List<String> subgroups = importsGroups.stream().map(ImportsGroup::getSubGroups).flatMap(Collection::stream).collect(Collectors.toList());
this.allImportOrderItems.addAll(subgroups);
Expand Down Expand Up @@ -233,30 +240,192 @@ private List<String> getResult(List<String> sortedImported, String lineFormat) {
return null;
}

private static class OrderingComparator implements Comparator<String>, Serializable {
private static int compareWithWildcare(String string1, String string2, boolean wildcardsLast) {
int string1WildcardIndex = string1.indexOf('*');
int string2WildcardIndex = string2.indexOf('*');
boolean string1IsWildcard = string1WildcardIndex >= 0;
boolean string2IsWildcard = string2WildcardIndex >= 0;
if (string1IsWildcard == string2IsWildcard) {
return string1.compareTo(string2);
}
int prefixLength = string1IsWildcard ? string1WildcardIndex : string2WildcardIndex;
boolean samePrefix = string1.regionMatches(0, string2, 0, prefixLength);
if (!samePrefix) {
return string1.compareTo(string2);
}
return (string1IsWildcard == wildcardsLast) ? 1 : -1;
}

private static class LexicographicalOrderingComparator implements Comparator<String>, Serializable {
private static final long serialVersionUID = 1;

private final boolean wildcardsLast;

private OrderingComparator(boolean wildcardsLast) {
private LexicographicalOrderingComparator(boolean wildcardsLast) {
this.wildcardsLast = wildcardsLast;
}

@Override
public int compare(String string1, String string2) {
int string1WildcardIndex = string1.indexOf('*');
int string2WildcardIndex = string2.indexOf('*');
boolean string1IsWildcard = string1WildcardIndex >= 0;
boolean string2IsWildcard = string2WildcardIndex >= 0;
if (string1IsWildcard == string2IsWildcard) {
return string1.compareTo(string2);
return compareWithWildcare(string1, string2, wildcardsLast);
}
}

private static class SemanticOrderingComparator implements Comparator<String>, Serializable {
private static final long serialVersionUID = 1;

private final boolean wildcardsLast;
private final Set<String> treatAsPackage;
private final Set<String> treatAsClass;

private SemanticOrderingComparator(boolean wildcardsLast, Set<String> treatAsPackage,
Set<String> treatAsClass) {
this.wildcardsLast = wildcardsLast;
this.treatAsPackage = treatAsPackage;
this.treatAsClass = treatAsClass;
}

@Override
public int compare(String string1, String string2) {
/*
* Ordering uses semantics of the import string by splitting it into package,
* class name(s) and static member (for static imports) and then comparing by
* each of those three substrings in sequence.
*
* When comparing static imports, the last segment in the dot-separated string
* is considered to be the member (field, method, type) name.
*
* The first segment starting with an upper case letter is considered to be the
* (first) class name. Since this comparator has no actual type information,
* this auto-detection will fail for upper case package names and lower case
* class names. treatAsPackage and treatAsClass can be used respectively to
* provide hints to the auto-detection.
*/
if (string1.startsWith(STATIC_KEYWORD)) {
String[] split = splitFqcnAndMember(string1);
String fqcn1 = split[0];
String member1 = split[1];

split = splitFqcnAndMember(string2);
String fqcn2 = split[0];
String member2 = split[1];

int result = compareFullyQualifiedClassName(fqcn1, fqcn2);
if (result != 0)
return result;

return compareWithWildcare(member1, member2, wildcardsLast);
} else {
return compareFullyQualifiedClassName(string1, string2);
}
}

/**
* Compares two fully qualified class names by splitting them into package and
* (nested) class names.
*/
private int compareFullyQualifiedClassName(String fqcn1, String fqcn2) {
String[] split = splitPackageAndClasses(fqcn1);
String p1 = split[0];
String c1 = split[1];

split = splitPackageAndClasses(fqcn2);
String p2 = split[0];
String c2 = split[1];

int result = p1.compareTo(p2);
if (result != 0)
return result;

return compareWithWildcare(c1, c2, wildcardsLast);
}

/**
* Splits the provided static import string into fully qualified class name and
* the imported static member (field, method or type).
*/
private String[] splitFqcnAndMember(String importString) {
String s = importString.substring(STATIC_KEYWORD.length()).trim();

String fqcn;
String member;

int dot = s.lastIndexOf(".");
if (!Character.isUpperCase(s.charAt(dot + 1))) {
fqcn = s.substring(0, dot);
member = s.substring(dot + 1);
} else {
fqcn = s;
member = null;
}
int prefixLength = string1IsWildcard ? string1WildcardIndex : string2WildcardIndex;
boolean samePrefix = string1.regionMatches(0, string2, 0, prefixLength);
if (!samePrefix) {
return string1.compareTo(string2);

return new String[]{fqcn, member};
}

/**
* Splits the fully qualified class name into package and class name(s).
*/
private String[] splitPackageAndClasses(String fqcn) {
String packageNames = null;
String classNames = null;

/*
* The first segment that starts with an upper case letter starts the class
* name(s), unless it matches treatAsPackage (then it's explicitly declared as
* package via configuration). If no segment starts with an upper case letter
* then the last segment must be a class name (unless the method input is
* garbage).
*/
int dot = fqcn.indexOf('.');
while (dot > -1) {
int nextDot = fqcn.indexOf('.', dot + 1);
if (nextDot > -1) {
if (Character.isUpperCase(fqcn.charAt(dot + 1))) {
// if upper case, check if should be treated as package nonetheless
if (!treatAsPackage(fqcn.substring(0, nextDot))) {
packageNames = fqcn.substring(0, dot);
classNames = fqcn.substring(dot + 1);
break;
}
} else {
// if lower case, check if should be treated as class nonetheless
if (treatAsClass(fqcn.substring(0, nextDot))) {
packageNames = fqcn.substring(0, dot);
classNames = fqcn.substring(dot + 1);
break;
}
}
}

dot = nextDot;
}
return (string1IsWildcard == wildcardsLast) ? 1 : -1;

if (packageNames == null) {
int i = fqcn.lastIndexOf(".");
packageNames = fqcn.substring(0, i);
classNames = fqcn.substring(i + 1);
}

return new String[]{packageNames, classNames};
}

/**
* Returns whether the provided prefix matches any entry of
* {@code treatAsPackage}.
*/
private boolean treatAsPackage(String prefix) {
// This would be the place to introduce wild cards or even regex matching.
return treatAsPackage != null && treatAsPackage.contains(prefix);
}

/**
* Returns whether the provided prefix name matches any entry of
* {@code treatAsClass}.
*/
private boolean treatAsClass(String prefix) {
// This would be the place to introduce wild cards or even regex matching.
return treatAsClass != null && treatAsClass.contains(prefix);
}

}
}
1 change: 1 addition & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663))
* Add semantics-aware Java import ordering (i.e. sort by package, then class, then member). ([#522](https://github.com/diffplug/spotless/issues/522))
### Fixed
* Added `@DisableCachingByDefault` to `RegisterDependenciesTask`. ([#1666](https://github.com/diffplug/spotless/pull/1666))
* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698))
Expand Down
Loading

0 comments on commit 8d4f402

Please sign in to comment.