Skip to content

Commit 8d4f402

Browse files
authored
Add semantics-aware sorting for Java imports (#1709 fixes #522)
2 parents 3506cf7 + d72a11c commit 8d4f402

File tree

13 files changed

+365
-38
lines changed

13 files changed

+365
-38
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1313
### Added
1414
* `Jvm.Support` now accepts `-SNAPSHOT` versions, treated as the non`-SNAPSHOT`. ([#1583](https://github.com/diffplug/spotless/issues/1583))
1515
* 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))
16+
* Add semantics-aware Java import ordering (i.e. sort by package, then class, then member). ([#522](https://github.com/diffplug/spotless/issues/522))
1617
### Fixed
1718
* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698))
1819
* Fixed a regression which changed the import sorting order in `googleJavaFormat` introduced in `2.38.0`. ([#1680](https://github.com/diffplug/spotless/pull/1680))

lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 DiffPlug
2+
* Copyright 2016-2023 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.Objects;
31+
import java.util.Set;
32+
import java.util.TreeSet;
3133
import java.util.function.Supplier;
3234
import java.util.stream.Collectors;
3335
import java.util.stream.Stream;
@@ -39,6 +41,9 @@
3941

4042
public final class ImportOrderStep {
4143
private static final boolean WILDCARDS_LAST_DEFAULT = false;
44+
private static final boolean SEMANTIC_SORT_DEFAULT = false;
45+
private static final Set<String> TREAT_AS_PACKAGE_DEFAULT = Set.of();
46+
private static final Set<String> TREAT_AS_CLASS_DEFAULT = Set.of();
4247

4348
private final String lineFormat;
4449

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

5762
public FormatterStep createFrom(String... importOrder) {
58-
return createFrom(WILDCARDS_LAST_DEFAULT, importOrder);
63+
return createFrom(WILDCARDS_LAST_DEFAULT, SEMANTIC_SORT_DEFAULT, TREAT_AS_PACKAGE_DEFAULT,
64+
TREAT_AS_CLASS_DEFAULT, importOrder);
5965
}
6066

61-
public FormatterStep createFrom(boolean wildcardsLast, String... importOrder) {
67+
public FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
68+
Set<String> treatAsClass, String... importOrder) {
6269
// defensive copying and null checking
6370
List<String> importOrderList = requireElementsNonNull(Arrays.asList(importOrder));
64-
return createFrom(wildcardsLast, () -> importOrderList);
71+
return createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, () -> importOrderList);
6572
}
6673

6774
public FormatterStep createFrom(File importsFile) {
68-
return createFrom(WILDCARDS_LAST_DEFAULT, importsFile);
75+
return createFrom(WILDCARDS_LAST_DEFAULT, SEMANTIC_SORT_DEFAULT, TREAT_AS_PACKAGE_DEFAULT,
76+
TREAT_AS_CLASS_DEFAULT, importsFile);
6977
}
7078

71-
public FormatterStep createFrom(boolean wildcardsLast, File importsFile) {
79+
public FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
80+
Set<String> treatAsClass, File importsFile) {
7281
Objects.requireNonNull(importsFile);
73-
return createFrom(wildcardsLast, () -> getImportOrder(importsFile));
82+
return createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, () -> getImportOrder(importsFile));
7483
}
7584

76-
private FormatterStep createFrom(boolean wildcardsLast, Supplier<List<String>> importOrder) {
85+
private FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
86+
Set<String> treatAsClass, Supplier<List<String>> importOrder) {
7787
return FormatterStep.createLazy("importOrder",
78-
() -> new State(importOrder.get(), lineFormat, wildcardsLast),
88+
() -> new State(importOrder.get(), lineFormat, wildcardsLast, semanticSort, treatAsPackage,
89+
treatAsClass),
7990
State::toFormatter);
8091
}
8192

@@ -106,15 +117,23 @@ private static final class State implements Serializable {
106117
private final List<String> importOrder;
107118
private final String lineFormat;
108119
private final boolean wildcardsLast;
120+
private final boolean semanticSort;
121+
private final TreeSet<String> treatAsPackage;
122+
private final TreeSet<String> treatAsClass;
109123

110-
State(List<String> importOrder, String lineFormat, boolean wildcardsLast) {
124+
State(List<String> importOrder, String lineFormat, boolean wildcardsLast, boolean semanticSort,
125+
Set<String> treatAsPackage, Set<String> treatAsClass) {
111126
this.importOrder = importOrder;
112127
this.lineFormat = lineFormat;
113128
this.wildcardsLast = wildcardsLast;
129+
this.semanticSort = semanticSort;
130+
this.treatAsPackage = treatAsPackage == null ? null : new TreeSet<>(treatAsPackage);
131+
this.treatAsClass = treatAsClass == null ? null : new TreeSet<>(treatAsClass);
114132
}
115133

116134
FormatterFunc toFormatter() {
117-
return raw -> new ImportSorter(importOrder, wildcardsLast).format(raw, lineFormat);
135+
return raw -> new ImportSorter(importOrder, wildcardsLast, semanticSort, treatAsPackage, treatAsClass)
136+
.format(raw, lineFormat);
118137
}
119138
}
120139
}

lib/src/main/java/com/diffplug/spotless/java/ImportSorter.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 DiffPlug
2+
* Copyright 2016-2023 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
import java.util.ArrayList;
1919
import java.util.List;
2020
import java.util.Scanner;
21+
import java.util.Set;
2122

2223
/**
2324
* @author Vojtech Krasa
@@ -30,10 +31,17 @@ final class ImportSorter {
3031

3132
private final List<String> importsOrder;
3233
private final boolean wildcardsLast;
34+
private final boolean semanticSort;
35+
private final Set<String> treatAsPackage;
36+
private final Set<String> treatAsClass;
3337

34-
ImportSorter(List<String> importsOrder, boolean wildcardsLast) {
38+
ImportSorter(List<String> importsOrder, boolean wildcardsLast, boolean semanticSort, Set<String> treatAsPackage,
39+
Set<String> treatAsClass) {
3540
this.importsOrder = new ArrayList<>(importsOrder);
3641
this.wildcardsLast = wildcardsLast;
42+
this.semanticSort = semanticSort;
43+
this.treatAsPackage = treatAsPackage;
44+
this.treatAsClass = treatAsClass;
3745
}
3846

3947
String format(String raw, String lineFormat) {
@@ -81,7 +89,8 @@ String format(String raw, String lineFormat) {
8189
}
8290
scanner.close();
8391

84-
List<String> sortedImports = ImportSorterImpl.sort(imports, importsOrder, wildcardsLast, lineFormat);
92+
List<String> sortedImports = ImportSorterImpl.sort(imports, importsOrder, wildcardsLast, semanticSort,
93+
treatAsPackage, treatAsClass, lineFormat);
8594
return applyImportsToDocument(raw, firstImportLine, lastImportLine, sortedImports);
8695
}
8796

lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java

Lines changed: 186 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ public List<String> getSubGroups() {
6262
}
6363
}
6464

65-
static List<String> sort(List<String> imports, List<String> importsOrder, boolean wildcardsLast, String lineFormat) {
66-
ImportSorterImpl importsSorter = new ImportSorterImpl(importsOrder, wildcardsLast);
65+
static List<String> sort(List<String> imports, List<String> importsOrder, boolean wildcardsLast,
66+
boolean semanticSort, Set<String> treatAsPackage, Set<String> treatAsClass, String lineFormat) {
67+
ImportSorterImpl importsSorter = new ImportSorterImpl(importsOrder, wildcardsLast, semanticSort, treatAsPackage,
68+
treatAsClass);
6769
return importsSorter.sort(imports, lineFormat);
6870
}
6971

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

79-
private ImportSorterImpl(List<String> importOrder, boolean wildcardsLast) {
81+
private ImportSorterImpl(List<String> importOrder, boolean wildcardsLast, boolean semanticSort,
82+
Set<String> treatAsPackage, Set<String> treatAsClass) {
8083
importsGroups = importOrder.stream().filter(Objects::nonNull).map(ImportsGroup::new).collect(Collectors.toList());
8184
putStaticItemIfNotExists(importsGroups);
8285
putCatchAllGroupIfNotExists(importsGroups);
8386

84-
ordering = new OrderingComparator(wildcardsLast);
87+
if (semanticSort) {
88+
ordering = new SemanticOrderingComparator(wildcardsLast, treatAsPackage, treatAsClass);
89+
} else {
90+
ordering = new LexicographicalOrderingComparator(wildcardsLast);
91+
}
8592

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

236-
private static class OrderingComparator implements Comparator<String>, Serializable {
243+
private static int compareWithWildcare(String string1, String string2, boolean wildcardsLast) {
244+
int string1WildcardIndex = string1.indexOf('*');
245+
int string2WildcardIndex = string2.indexOf('*');
246+
boolean string1IsWildcard = string1WildcardIndex >= 0;
247+
boolean string2IsWildcard = string2WildcardIndex >= 0;
248+
if (string1IsWildcard == string2IsWildcard) {
249+
return string1.compareTo(string2);
250+
}
251+
int prefixLength = string1IsWildcard ? string1WildcardIndex : string2WildcardIndex;
252+
boolean samePrefix = string1.regionMatches(0, string2, 0, prefixLength);
253+
if (!samePrefix) {
254+
return string1.compareTo(string2);
255+
}
256+
return (string1IsWildcard == wildcardsLast) ? 1 : -1;
257+
}
258+
259+
private static class LexicographicalOrderingComparator implements Comparator<String>, Serializable {
237260
private static final long serialVersionUID = 1;
238261

239262
private final boolean wildcardsLast;
240263

241-
private OrderingComparator(boolean wildcardsLast) {
264+
private LexicographicalOrderingComparator(boolean wildcardsLast) {
242265
this.wildcardsLast = wildcardsLast;
243266
}
244267

245268
@Override
246269
public int compare(String string1, String string2) {
247-
int string1WildcardIndex = string1.indexOf('*');
248-
int string2WildcardIndex = string2.indexOf('*');
249-
boolean string1IsWildcard = string1WildcardIndex >= 0;
250-
boolean string2IsWildcard = string2WildcardIndex >= 0;
251-
if (string1IsWildcard == string2IsWildcard) {
252-
return string1.compareTo(string2);
270+
return compareWithWildcare(string1, string2, wildcardsLast);
271+
}
272+
}
273+
274+
private static class SemanticOrderingComparator implements Comparator<String>, Serializable {
275+
private static final long serialVersionUID = 1;
276+
277+
private final boolean wildcardsLast;
278+
private final Set<String> treatAsPackage;
279+
private final Set<String> treatAsClass;
280+
281+
private SemanticOrderingComparator(boolean wildcardsLast, Set<String> treatAsPackage,
282+
Set<String> treatAsClass) {
283+
this.wildcardsLast = wildcardsLast;
284+
this.treatAsPackage = treatAsPackage;
285+
this.treatAsClass = treatAsClass;
286+
}
287+
288+
@Override
289+
public int compare(String string1, String string2) {
290+
/*
291+
* Ordering uses semantics of the import string by splitting it into package,
292+
* class name(s) and static member (for static imports) and then comparing by
293+
* each of those three substrings in sequence.
294+
*
295+
* When comparing static imports, the last segment in the dot-separated string
296+
* is considered to be the member (field, method, type) name.
297+
*
298+
* The first segment starting with an upper case letter is considered to be the
299+
* (first) class name. Since this comparator has no actual type information,
300+
* this auto-detection will fail for upper case package names and lower case
301+
* class names. treatAsPackage and treatAsClass can be used respectively to
302+
* provide hints to the auto-detection.
303+
*/
304+
if (string1.startsWith(STATIC_KEYWORD)) {
305+
String[] split = splitFqcnAndMember(string1);
306+
String fqcn1 = split[0];
307+
String member1 = split[1];
308+
309+
split = splitFqcnAndMember(string2);
310+
String fqcn2 = split[0];
311+
String member2 = split[1];
312+
313+
int result = compareFullyQualifiedClassName(fqcn1, fqcn2);
314+
if (result != 0)
315+
return result;
316+
317+
return compareWithWildcare(member1, member2, wildcardsLast);
318+
} else {
319+
return compareFullyQualifiedClassName(string1, string2);
320+
}
321+
}
322+
323+
/**
324+
* Compares two fully qualified class names by splitting them into package and
325+
* (nested) class names.
326+
*/
327+
private int compareFullyQualifiedClassName(String fqcn1, String fqcn2) {
328+
String[] split = splitPackageAndClasses(fqcn1);
329+
String p1 = split[0];
330+
String c1 = split[1];
331+
332+
split = splitPackageAndClasses(fqcn2);
333+
String p2 = split[0];
334+
String c2 = split[1];
335+
336+
int result = p1.compareTo(p2);
337+
if (result != 0)
338+
return result;
339+
340+
return compareWithWildcare(c1, c2, wildcardsLast);
341+
}
342+
343+
/**
344+
* Splits the provided static import string into fully qualified class name and
345+
* the imported static member (field, method or type).
346+
*/
347+
private String[] splitFqcnAndMember(String importString) {
348+
String s = importString.substring(STATIC_KEYWORD.length()).trim();
349+
350+
String fqcn;
351+
String member;
352+
353+
int dot = s.lastIndexOf(".");
354+
if (!Character.isUpperCase(s.charAt(dot + 1))) {
355+
fqcn = s.substring(0, dot);
356+
member = s.substring(dot + 1);
357+
} else {
358+
fqcn = s;
359+
member = null;
253360
}
254-
int prefixLength = string1IsWildcard ? string1WildcardIndex : string2WildcardIndex;
255-
boolean samePrefix = string1.regionMatches(0, string2, 0, prefixLength);
256-
if (!samePrefix) {
257-
return string1.compareTo(string2);
361+
362+
return new String[]{fqcn, member};
363+
}
364+
365+
/**
366+
* Splits the fully qualified class name into package and class name(s).
367+
*/
368+
private String[] splitPackageAndClasses(String fqcn) {
369+
String packageNames = null;
370+
String classNames = null;
371+
372+
/*
373+
* The first segment that starts with an upper case letter starts the class
374+
* name(s), unless it matches treatAsPackage (then it's explicitly declared as
375+
* package via configuration). If no segment starts with an upper case letter
376+
* then the last segment must be a class name (unless the method input is
377+
* garbage).
378+
*/
379+
int dot = fqcn.indexOf('.');
380+
while (dot > -1) {
381+
int nextDot = fqcn.indexOf('.', dot + 1);
382+
if (nextDot > -1) {
383+
if (Character.isUpperCase(fqcn.charAt(dot + 1))) {
384+
// if upper case, check if should be treated as package nonetheless
385+
if (!treatAsPackage(fqcn.substring(0, nextDot))) {
386+
packageNames = fqcn.substring(0, dot);
387+
classNames = fqcn.substring(dot + 1);
388+
break;
389+
}
390+
} else {
391+
// if lower case, check if should be treated as class nonetheless
392+
if (treatAsClass(fqcn.substring(0, nextDot))) {
393+
packageNames = fqcn.substring(0, dot);
394+
classNames = fqcn.substring(dot + 1);
395+
break;
396+
}
397+
}
398+
}
399+
400+
dot = nextDot;
258401
}
259-
return (string1IsWildcard == wildcardsLast) ? 1 : -1;
402+
403+
if (packageNames == null) {
404+
int i = fqcn.lastIndexOf(".");
405+
packageNames = fqcn.substring(0, i);
406+
classNames = fqcn.substring(i + 1);
407+
}
408+
409+
return new String[]{packageNames, classNames};
260410
}
411+
412+
/**
413+
* Returns whether the provided prefix matches any entry of
414+
* {@code treatAsPackage}.
415+
*/
416+
private boolean treatAsPackage(String prefix) {
417+
// This would be the place to introduce wild cards or even regex matching.
418+
return treatAsPackage != null && treatAsPackage.contains(prefix);
419+
}
420+
421+
/**
422+
* Returns whether the provided prefix name matches any entry of
423+
* {@code treatAsClass}.
424+
*/
425+
private boolean treatAsClass(String prefix) {
426+
// This would be the place to introduce wild cards or even regex matching.
427+
return treatAsClass != null && treatAsClass.contains(prefix);
428+
}
429+
261430
}
262431
}

plugin-gradle/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
55
## [Unreleased]
66
### Added
77
* 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))
8+
* Add semantics-aware Java import ordering (i.e. sort by package, then class, then member). ([#522](https://github.com/diffplug/spotless/issues/522))
89
### Fixed
910
* Added `@DisableCachingByDefault` to `RegisterDependenciesTask`. ([#1666](https://github.com/diffplug/spotless/pull/1666))
1011
* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698))

0 commit comments

Comments
 (0)