Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 46 additions & 23 deletions src/main/java/org/checkerframework/specimin/SpeciminRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -356,23 +356,7 @@ private static void handleUnsolvedSymbolEnumeratorResult(
Set<Path> createdDirectories,
Formatter formatter)
throws IOException {
Set<String> usedPackages = new HashSet<>();
for (CompilationUnit cu : sliceResult.solvedSlice()) {
if (cu.getPackageDeclaration().isEmpty()) {
usedPackages.add("");
continue;
}
usedPackages.add(cu.getPackageDeclaration().get().getNameAsString());
}

for (String className : enumeratorResult.classNamesToFileContent().keySet()) {
int lastDot = className.lastIndexOf('.');
if (lastDot < 0) {
usedPackages.add("");
} else {
usedPackages.add(className.substring(0, lastDot));
}
}
Set<String> usedPackagesAndClasses = getUsedPackagesAndClasses(sliceResult, enumeratorResult);

for (CompilationUnit original : sliceResult.solvedSlice()) {
if (isEmptyCompilationUnit(original)) {
Expand Down Expand Up @@ -436,7 +420,7 @@ private static void handleUnsolvedSymbolEnumeratorResult(
writer.print(
formatter.formatSourceAndFixImports(
getCompilationUnitWithUnusedWildcardImportsRemoved(
getCompilationUnitWithCommentsTrimmed(cu), usedPackages)
getCompilationUnitWithCommentsTrimmed(cu), usedPackagesAndClasses)
.toString()));
} catch (IOException | FormatterException e) {
System.out.println("failed to write output file " + targetOutputPath);
Expand Down Expand Up @@ -471,6 +455,45 @@ private static void handleUnsolvedSymbolEnumeratorResult(
}
}
}
/**
* Gets the packages and classes used in the given slice and unsolved symbol enumeration.
*
* @param sliceResult The slice
* @param enumeratorResult The unsolved enumeration
* @return The used packages and classes
*/
private static Set<String> getUsedPackagesAndClasses(
SliceResult sliceResult, UnsolvedSymbolEnumeratorResult enumeratorResult) {
Set<String> usedPackagesAndClasses = new HashSet<>();
for (CompilationUnit cu : sliceResult.solvedSlice()) {
if (cu.getPackageDeclaration().isEmpty()) {
usedPackagesAndClasses.add("");
continue;
}
usedPackagesAndClasses.add(cu.getPackageDeclaration().get().getNameAsString());

for (TypeDeclaration<?> typeDecl : cu.findAll(TypeDeclaration.class)) {
String fqn = typeDecl.getFullyQualifiedName().orElse(null);

if (fqn == null) {
continue;
}

usedPackagesAndClasses.add(fqn);
}
}

for (String className : enumeratorResult.classNamesToFileContent().keySet()) {
int lastDot = className.lastIndexOf('.');
if (lastDot < 0) {
usedPackagesAndClasses.add("");
} else {
usedPackagesAndClasses.add(className.substring(0, lastDot));
usedPackagesAndClasses.add(className);
}
}
return usedPackagesAndClasses;
}

Comment on lines +465 to +497

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix logic that skips tracking fully-qualified names in the default package.

When a compilation unit has no package declaration (the default package), the continue statement skips the inner loop, meaning no TypeDeclarations from that file are added to the set. Similarly, for class names without a dot, className itself is not added to the set.

While Java prohibits importing types from the unnamed package—which prevents this bug from affecting the wildcard import removal directly—this logic leaves the usedPackagesAndClasses set incomplete and could lead to subtle bugs if the set is ever repurposed.

🐛 Proposed fix
   private static Set<String> getUsedPackagesAndClasses(
       SliceResult sliceResult, UnsolvedSymbolEnumeratorResult enumeratorResult) {
     Set<String> usedPackagesAndClasses = new HashSet<>();
     for (CompilationUnit cu : sliceResult.solvedSlice()) {
-      if (cu.getPackageDeclaration().isEmpty()) {
-        usedPackagesAndClasses.add("");
-        continue;
-      }
-      usedPackagesAndClasses.add(cu.getPackageDeclaration().get().getNameAsString());
+      if (cu.getPackageDeclaration().isPresent()) {
+        usedPackagesAndClasses.add(cu.getPackageDeclaration().get().getNameAsString());
+      } else {
+        usedPackagesAndClasses.add("");
+      }
 
       for (TypeDeclaration<?> typeDecl : cu.findAll(TypeDeclaration.class)) {
         String fqn = typeDecl.getFullyQualifiedName().orElse(null);
 
         if (fqn == null) {
           continue;
         }
 
         usedPackagesAndClasses.add(fqn);
       }
     }
 
     for (String className : enumeratorResult.classNamesToFileContent().keySet()) {
       int lastDot = className.lastIndexOf('.');
       if (lastDot < 0) {
         usedPackagesAndClasses.add("");
       } else {
         usedPackagesAndClasses.add(className.substring(0, lastDot));
-        usedPackagesAndClasses.add(className);
       }
+      usedPackagesAndClasses.add(className);
     }
     return usedPackagesAndClasses;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static Set<String> getUsedPackagesAndClasses(
SliceResult sliceResult, UnsolvedSymbolEnumeratorResult enumeratorResult) {
Set<String> usedPackagesAndClasses = new HashSet<>();
for (CompilationUnit cu : sliceResult.solvedSlice()) {
if (cu.getPackageDeclaration().isEmpty()) {
usedPackagesAndClasses.add("");
continue;
}
usedPackagesAndClasses.add(cu.getPackageDeclaration().get().getNameAsString());
for (TypeDeclaration<?> typeDecl : cu.findAll(TypeDeclaration.class)) {
String fqn = typeDecl.getFullyQualifiedName().orElse(null);
if (fqn == null) {
continue;
}
usedPackagesAndClasses.add(fqn);
}
}
for (String className : enumeratorResult.classNamesToFileContent().keySet()) {
int lastDot = className.lastIndexOf('.');
if (lastDot < 0) {
usedPackagesAndClasses.add("");
} else {
usedPackagesAndClasses.add(className.substring(0, lastDot));
usedPackagesAndClasses.add(className);
}
}
return usedPackagesAndClasses;
}
private static Set<String> getUsedPackagesAndClasses(
SliceResult sliceResult, UnsolvedSymbolEnumeratorResult enumeratorResult) {
Set<String> usedPackagesAndClasses = new HashSet<>();
for (CompilationUnit cu : sliceResult.solvedSlice()) {
if (cu.getPackageDeclaration().isPresent()) {
usedPackagesAndClasses.add(cu.getPackageDeclaration().get().getNameAsString());
} else {
usedPackagesAndClasses.add("");
}
for (TypeDeclaration<?> typeDecl : cu.findAll(TypeDeclaration.class)) {
String fqn = typeDecl.getFullyQualifiedName().orElse(null);
if (fqn == null) {
continue;
}
usedPackagesAndClasses.add(fqn);
}
}
for (String className : enumeratorResult.classNamesToFileContent().keySet()) {
int lastDot = className.lastIndexOf('.');
if (lastDot < 0) {
usedPackagesAndClasses.add("");
} else {
usedPackagesAndClasses.add(className.substring(0, lastDot));
}
usedPackagesAndClasses.add(className);
}
return usedPackagesAndClasses;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/checkerframework/specimin/SpeciminRunner.java` around lines
459 - 491, Update getUsedPackagesAndClasses to continue processing each
compilation unit’s TypeDeclaration entries even when no package declaration
exists; record the default-package marker without returning early. In the
enumeratorResult class-name loop, also add className itself when it has no dot,
while preserving existing package and fully qualified name tracking for dotted
names.

/**
* Checks that the root directory is specified correctly, by checking that the files for the
Expand Down Expand Up @@ -576,22 +599,22 @@ private static void mapNodes(Node original, Node clone, IdentityHashMap<Node, No
* Removes all wildcard imports that are not used in the given set of package names.
*
* @param cu The CompilationUnit to process.
* @param usedPackages A set of package names that are used in the code.
* @param usedPackagesAndClasses A set of package and class names that are used in the code.
* @return The modified CompilationUnit with unused wildcard imports removed.
*/
private static CompilationUnit getCompilationUnitWithUnusedWildcardImportsRemoved(
CompilationUnit cu, Set<String> usedPackages) {
CompilationUnit cu, Set<String> usedPackagesAndClasses) {
for (ImportDeclaration decl : List.copyOf(cu.getImports())) {
if (!decl.isAsterisk()) {
continue;
}
String packageName = decl.getNameAsString();
String qualifier = decl.getNameAsString();

if (JavaLangUtils.inJdkPackage(packageName)) {
if (JavaLangUtils.inJdkPackage(qualifier)) {
continue;
}

if (!usedPackages.contains(packageName)) {
if (!usedPackagesAndClasses.contains(qualifier)) {
decl.remove();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.checkerframework.specimin;

import java.io.IOException;
import org.junit.jupiter.api.Test;

/**
* This test checks that static wildcard imports targeting classes that have used members in the
* target file are not removed.
*/
public class StaticWildcardClassImportTest {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"staticwildcardclassimport",
new String[] {"com/example/Simple.java"},
new String[] {"com.example.Simple#foo()"});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example;

public class Foo {
public static void staticFoo() {
throw new java.lang.Error();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example;

import static com.example.Foo.*;

public class Simple {
void foo() {
staticFoo();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example;

public class Foo {
public static void staticFoo() { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example;

import static com.example.Foo.*;

public class Simple {
void foo() {
staticFoo();
}
}
Loading