Skip to content

fix: do not crash on qualified generic types with unresolved arguments in noclasspath mode#6786

Open
mvanhorn wants to merge 3 commits into
INRIA:masterfrom
mvanhorn:fix/4077-spoon-qualified-generic-noclasspath-crash
Open

fix: do not crash on qualified generic types with unresolved arguments in noclasspath mode#6786
mvanhorn wants to merge 3 commits into
INRIA:masterfrom
mvanhorn:fix/4077-spoon-qualified-generic-noclasspath-crash

Conversation

@mvanhorn

Copy link
Copy Markdown

Summary

Building a model in no-classpath mode no longer throws SpoonException: Unexpected QualifiedNameReference tokens when source code uses a qualified nested generic type whose type arguments cannot be resolved (for example java.util.AbstractMap.SimpleEntry<A, B> where A and B come from a missing dependency). handleImplicit in src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java now keeps the package reference explicit and returns when the package tokens cannot be aligned with the JDT binding, instead of falling through to the throw.

Why this matters

No-classpath mode exists precisely for analyzing code whose dependencies are absent, and this crash aborts the whole model build on a single unresolvable reference. The root cause is that JDT, lacking the type arguments, treats the declaring type as part of the package, so the Spoon-side package reference and the JDT package binding token arrays disagree in length. That mismatch is expected in this mode and not an internal error, which is what the remaining throw treated it as.

Testing

Added testQualifiedGenericTypeWithIncompleteClasspath to src/test/java/spoon/test/api/NoClasspathTest.java using the @ModelTest harness with unresolvable imports. It asserts the model builds, the local variable's type resolves to java.util.AbstractMap$SimpleEntry, and the constructor call keeps its source-form declaring type. The full NoClasspathTest class passes (7 tests) with the change.

Fixes #4077

@MartinWitt

Copy link
Copy Markdown
Collaborator

@mvanhorn looks good, can u fix the small qodana issue?

The earlier packageNames.length == off branch already returns for the equal-length case, so once execution reaches this point packageNames != null implies the lengths differ and the packageNames.length != off condition is always true.
@mvanhorn

Copy link
Copy Markdown
Author

Done — 42b8c39 drops the redundant packageNames.length != off check Qodana flagged. The earlier length == off branch already returns for the equal-length case, so by the time we reach that point packageNames != null implies the lengths differ and the condition was always true. Kept the focused NoClasspathTest (7/7), compile, and Checkstyle green locally; Qodana will confirm in CI.

//keep it explicit
return;
}
if (packageNames != null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this code only run in no classpath mode?

The previous guard returned early for any non-null packageNames when
lengths didn't match, which would have suppressed legitimate errors in
full-classpath mode. The structural signature of the no-classpath bug is
specifically that JDT folds the declaring type into the package binding's
compound name, making packageNames.length *greater than* off.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@monperrus

Copy link
Copy Markdown
Collaborator

@mvanhorn I pushed 290b0d8 to your branch to narrow the guard to packageNames.length > off. The original packageNames != null would have silently swallowed any token-count mismatch in full-classpath mode too, masking real bugs. The specific structural signature of the no-classpath case is that JDT folds the declaring type into the package compound name, making it longer than the remaining token offset — so packageNames.length > off is the precise condition. Does that match your understanding of the JDT behaviour you observed?

@mvanhorn

Copy link
Copy Markdown
Author

Thanks @monperrus — yes, that matches exactly what I observed. In the no-classpath case JDT folds the declaring type into the package compound name, so it ends up longer than the remaining token offset; packageNames.length > off is the precise structural signature. Narrowing from != null is a real improvement — the original would've masked genuine token-count mismatches in full-classpath mode. Appreciate the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Qualified generic classes with incomplete classpath crash spoon

4 participants