2020import java .util .Collections ;
2121import java .util .List ;
2222import 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 ;
2328import org .sonar .plugins .java .api .IssuableSubscriptionVisitor ;
24- import org .sonar .plugins .java .api .JavaVersion ;
25- import org .sonar .plugins .java .api .JavaVersionAwareVisitor ;
2629import org .sonar .plugins .java .api .tree .CompilationUnitTree ;
30+ import org .sonar .plugins .java .api .tree .ExpressionTree ;
2731import org .sonar .plugins .java .api .tree .ImportTree ;
2832import 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