Skip to content

Commit ac55c05

Browse files
SONARJAVA-6146 S8445: Relax the rule to allow more styles of sorting imports (#5494)
1 parent a82667f commit ac55c05

File tree

12 files changed

+444
-134
lines changed

12 files changed

+444
-134
lines changed

java-checks-test-sources/default/src/main/java/checks/ImportDeclarationOrderCheckSample.java

Lines changed: 0 additions & 22 deletions
This file was deleted.

java-checks-test-sources/default/src/main/java/checks/ImportDeclarationOrderCheckSampleNoIssuesSample.java

Lines changed: 0 additions & 26 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package checks.ImportDeclarationsOrderCheck;
2+
3+
// fix@qf1 {{Reorganize imports}}
4+
// edit@qf1 [[sl=6;sc=1;el=10;ec=36]] {{import module java.base;\n\nimport java.sql.Date;\nimport java.util.List;\n\nimport static java.util.Collections.*;\nimport static java.lang.System.out;}}
5+
// Module import not first, regular first
6+
import java.sql.Date; // Noncompliant [[quickfixes=qf1]]
7+
import java.util.List;
8+
import module java.base;
9+
import static java.util.Collections.*;
10+
import static java.lang.System.out;
11+
12+
class ImportDeclarationOrderCheckWithModulesAndRegularFirstSample {
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package checks.ImportDeclarationsOrderCheck;
2+
3+
// fix@qf1 {{Reorganize imports}}
4+
// edit@qf1 [[sl=6;sc=1;el=10;ec=23]] {{import module java.base;\n\nimport static java.util.Collections.*;\nimport static java.lang.System.out;\n\nimport java.sql.Date;\nimport java.util.List;}}
5+
// Module import not first, static first
6+
import static java.util.Collections.*; // Noncompliant [[quickfixes=qf1]]
7+
import static java.lang.System.out;
8+
import module java.base;
9+
import java.sql.Date;
10+
import java.util.List;
11+
12+
class ImportDeclarationOrderCheckWithModulesAndStaticFirstSample {
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package checks.ImportDeclarationsOrderCheck;
2+
3+
import module java.base;
4+
5+
import java.sql.Date;
6+
import java.util.List;
7+
8+
import static java.util.Collections.*;
9+
import static java.lang.System.out;
10+
11+
public class ImportDeclarationOrderCheckWithModulesNoIssues {
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package checks.ImportDeclarationsOrderCheck;
2+
3+
// No issues expected - rule only applies when module imports are present
4+
5+
class ImportDeclarationOrderCheckWithoutModulesAndRegularFirstSample {
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package checks.ImportDeclarationsOrderCheck;
2+
3+
// No issues expected - rule only applies when module imports are present
4+
import static java.util.Collections.emptyList;
5+
import static java.util.Collections.*;
6+
import java.io.*;
7+
import java.util.List;
8+
import static java.lang.Math.PI;
9+
import java.util.Map;
10+
11+
class ImportDeclarationOrderCheckWithoutModulesAndStaticFirstSample {
12+
}

java-checks/src/main/java/org/sonar/java/checks/ImportDeclarationOrderCheck.java

Lines changed: 142 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,19 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222
import org.sonar.check.Rule;
23+
import org.sonar.java.annotations.VisibleForTesting;
24+
import org.sonar.java.checks.helpers.ExpressionsHelper;
25+
import org.sonar.java.checks.helpers.QuickFixHelper;
26+
import org.sonar.java.reporting.JavaQuickFix;
27+
import org.sonar.java.reporting.JavaTextEdit;
2328
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
24-
import org.sonar.plugins.java.api.JavaVersion;
25-
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
2629
import org.sonar.plugins.java.api.tree.CompilationUnitTree;
30+
import org.sonar.plugins.java.api.tree.ExpressionTree;
2731
import org.sonar.plugins.java.api.tree.ImportTree;
2832
import org.sonar.plugins.java.api.tree.Tree;
2933

3034
@Rule(key = "S8445")
31-
public class ImportDeclarationOrderCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {
32-
33-
@Override
34-
public boolean isCompatibleWithJavaVersion(JavaVersion version) {
35-
return version.isJava25Compatible();
36-
}
35+
public class ImportDeclarationOrderCheck extends IssuableSubscriptionVisitor {
3736

3837
@Override
3938
public List<Tree.Kind> nodesToVisit() {
@@ -44,75 +43,170 @@ public List<Tree.Kind> nodesToVisit() {
4443
public void visitNode(Tree tree) {
4544
CompilationUnitTree compilationUnit = (CompilationUnitTree) tree;
4645

47-
List<ImportTree> imports = new ArrayList<>();
48-
// Collect all import statements
49-
compilationUnit.imports()
46+
List<ImportTree> imports = compilationUnit.imports()
5047
.stream()
5148
.filter(importTree -> importTree.is(Tree.Kind.IMPORT))
5249
.map(ImportTree.class::cast)
53-
.forEach(imports::add);
50+
.toList();
5451

5552
analyzeImportOrder(imports);
5653
}
5754

5855
private void analyzeImportOrder(List<ImportTree> imports) {
59-
if (imports.size() <= 1) {
56+
// if we don't have any module import declarations, we don't raise the issue
57+
if (imports.stream().noneMatch(ImportTree::isModule)) {
6058
return;
6159
}
6260

63-
ImportType previousType = ImportType.SENTINEL_IMPORT;
64-
for (ImportTree importTree : imports) {
65-
ImportType currentType = classifyImport(importTree);
61+
if (hasProblematicImport(imports)) {
62+
QuickFixHelper.newIssue(context)
63+
.forRule(this)
64+
.onTree(imports.get(0))
65+
.withMessage("Import declarations should be organized with module imports first, followed by grouped regular imports and grouped static imports.")
66+
.withQuickFix(() -> createQuickFix(imports))
67+
.report();
68+
}
69+
}
6670

67-
if (currentType.ordinal() < previousType.ordinal()) {
68-
String message = buildMessage(currentType, previousType);
69-
reportIssue(importTree.qualifiedIdentifier(), message);
70-
}
71+
private static JavaQuickFix createQuickFix(List<ImportTree> imports) {
72+
// Separate imports by type and determine order
73+
List<ImportTree> moduleImports = new ArrayList<>();
74+
List<ImportTree> regularImports = new ArrayList<>();
75+
List<ImportTree> staticImports = new ArrayList<>();
76+
boolean regularFirst = true;
77+
boolean orderDetermined = false;
7178

72-
previousType = currentType;
79+
for (ImportTree importTree : imports) {
80+
ImportType type = ImportType.fromTree(importTree);
81+
switch (type) {
82+
case MODULE:
83+
moduleImports.add(importTree);
84+
break;
85+
case REGULAR:
86+
regularImports.add(importTree);
87+
orderDetermined = true;
88+
break;
89+
case STATIC:
90+
staticImports.add(importTree);
91+
if (!orderDetermined) {
92+
regularFirst = false;
93+
orderDetermined = true;
94+
}
95+
break;
96+
}
7397
}
74-
}
7598

76-
private static String buildMessage(ImportType currentType, ImportType previousType) {
77-
return String.format("Reorder this %s import to come before %s imports.",
78-
currentType.getDescription(),
79-
previousType.getDescription());
99+
// Build the reorganized import section
100+
String reorganized = buildReorganizedImports(moduleImports, regularImports, staticImports, regularFirst);
101+
102+
// Create text edit to replace the entire import section
103+
ImportTree firstImport = imports.get(0);
104+
ImportTree lastImport = imports.get(imports.size() - 1);
105+
106+
return JavaQuickFix.newQuickFix("Reorganize imports")
107+
.addTextEdit(JavaTextEdit.replaceBetweenTree(firstImport, lastImport, reorganized))
108+
.build();
80109
}
81110

82-
private static ImportType classifyImport(ImportTree importTree) {
83-
boolean isWildcard = isWildcardImport(importTree);
111+
private static String buildReorganizedImports(List<ImportTree> moduleImports, List<ImportTree> regularImports,
112+
List<ImportTree> staticImports, boolean regularFirst) {
113+
StringBuilder reorganized = new StringBuilder();
84114

85-
if (importTree.isModule()) {
86-
return ImportType.MODULE_IMPORT;
115+
// Add module imports first
116+
for (ImportTree importTree : moduleImports) {
117+
reorganized.append(getImportText(importTree)).append("\n");
87118
}
88-
89-
if (importTree.isStatic()) {
90-
return isWildcard ? ImportType.STATIC_PACKAGE_IMPORT : ImportType.STATIC_SINGLE_TYPE_IMPORT;
119+
reorganized.append("\n");
120+
// Add regular and static imports in the determined order
121+
if (regularFirst) {
122+
appendImportGroup(reorganized, regularImports, !staticImports.isEmpty());
123+
appendImportGroup(reorganized, staticImports, false);
124+
} else {
125+
appendImportGroup(reorganized, staticImports, !regularImports.isEmpty());
126+
appendImportGroup(reorganized, regularImports, false);
91127
}
92128

93-
return isWildcard ? ImportType.PACKAGE_IMPORT : ImportType.SINGLE_TYPE_IMPORT;
129+
return reorganized.toString().trim();
94130
}
95131

96-
private static boolean isWildcardImport(ImportTree importTree) {
97-
return "*".equals(importTree.qualifiedIdentifier().lastToken().text());
132+
private static void appendImportGroup(StringBuilder builder, List<ImportTree> imports, boolean addBlankLineAfter) {
133+
for (ImportTree importTree : imports) {
134+
builder.append(getImportText(importTree)).append("\n");
135+
}
136+
if (addBlankLineAfter) {
137+
builder.append("\n");
138+
}
98139
}
99140

100-
enum ImportType {
101-
SENTINEL_IMPORT("sentinel"),
102-
MODULE_IMPORT("module"),
103-
PACKAGE_IMPORT("on-demand package"),
104-
SINGLE_TYPE_IMPORT("single-type"),
105-
STATIC_PACKAGE_IMPORT("static on-demand package"),
106-
STATIC_SINGLE_TYPE_IMPORT("static single-type");
141+
private static String getImportText(ImportTree importTree) {
142+
// Build the import statement from the tree structure
143+
StringBuilder sb = new StringBuilder("import ");
144+
if (importTree.isModule()) {
145+
sb.append("module ");
146+
} else if (importTree.isStatic()) {
147+
sb.append("static ");
148+
}
149+
sb.append(ExpressionsHelper.concatenate((ExpressionTree) importTree.qualifiedIdentifier())).append(";");
150+
return sb.toString();
151+
}
107152

108-
private final String description;
153+
/**
154+
* Checks if there is an import that violates the grouping rules.
155+
* Returns false if all imports are properly organized.
156+
*/
157+
@VisibleForTesting
158+
static boolean hasProblematicImport(List<ImportTree> imports) {
159+
boolean seenRegular = false;
160+
boolean seenStatic = false;
161+
boolean seenStaticAfterRegular = false;
162+
boolean seenRegularAfterStatic = false;
109163

110-
ImportType(String description) {
111-
this.description = description;
164+
for (ImportTree importTree : imports) {
165+
ImportType type = ImportType.fromTree(importTree);
166+
switch (type) {
167+
case MODULE:
168+
if (seenRegular || seenStatic) {
169+
return true;
170+
}
171+
172+
break;
173+
174+
case REGULAR:
175+
if (seenStaticAfterRegular) {
176+
//regular -> static -> regular
177+
return true;
178+
}
179+
180+
seenRegular = true;
181+
seenRegularAfterStatic |= seenStatic;
182+
break;
183+
184+
case STATIC:
185+
if (seenRegularAfterStatic) {
186+
//static -> regular -> static
187+
return true;
188+
}
189+
190+
seenStatic = true;
191+
seenStaticAfterRegular |= seenRegular;
192+
break;
193+
}
112194
}
113195

114-
public String getDescription() {
115-
return description;
196+
return false;
197+
}
198+
199+
enum ImportType {
200+
MODULE, REGULAR, STATIC;
201+
202+
static ImportType fromTree(ImportTree importTree) {
203+
if (importTree.isModule()) {
204+
return MODULE;
205+
} else if (importTree.isStatic()) {
206+
return STATIC;
207+
} else {
208+
return REGULAR;
209+
}
116210
}
117211
}
118212
}

0 commit comments

Comments
 (0)