1111import spoon .reflect .declaration .CtElement ;
1212import spoon .reflect .declaration .CtField ;
1313import spoon .reflect .declaration .CtImport ;
14+ import spoon .reflect .declaration .CtImportKind ;
15+ import spoon .reflect .declaration .CtType ;
1416import spoon .reflect .reference .CtFieldReference ;
17+ import spoon .reflect .reference .CtPackageReference ;
18+ import spoon .reflect .reference .CtTypeReference ;
1519
1620/** Resolution of {@code static final} primitive/String constants used in refinement predicates and field reads. */
1721public final class StaticConstants {
@@ -28,37 +32,87 @@ public static Object resolve(CtFieldReference<?> ref) {
2832 return lit .getValue ();
2933 try {
3034 return ref .getActualField ()instanceof Field jf ? readStaticFinal (jf ) : null ;
31- } catch (Throwable ignored ) {
35+ } catch (RuntimeException | LinkageError ignored ) {
36+ // Spoon throws SpoonClassNotFoundException; reflection can throw LinkageError. Fall through.
3237 return null ;
3338 }
3439 }
3540
3641 /**
3742 * Resolve a {@code TypeName.CONST_NAME} reference (as it appears inside a refinement predicate string) via
38- * reflection. Tries {@code typeName} as-given, then {@code java.lang.}, then class names imported by
39- * {@code context}'s compilation unit (single-type and on-demand imports). {@code context} may be {@code null}.
43+ * reflection. Resolution order matches Java scoping rules: fully-qualified name → explicit imports of
44+ * {@code context}'s compilation unit (single-type and on-demand) → implicit {@code java.lang}. {@code context} may
45+ * be {@code null}.
4046 */
4147 public static Object resolve (String typeName , String constName , CtElement context ) {
4248 Object v = lookup (typeName , constName );
43- if (v == null )
44- v = lookup ("java.lang." + typeName , constName );
45- if (v != null || context == null || context .getPosition () == null || !context .getPosition ().isValidPosition ())
49+ if (v != null )
4650 return v ;
47- CtCompilationUnit cu = context .getPosition ().getCompilationUnit ();
48- if (cu == null )
51+ String fqn = findFqnInImports (typeName , constName , localImports (context ));
52+ if (fqn != null )
53+ return lookup (fqn , constName );
54+ return lookup ("java.lang." + typeName , constName );
55+ }
56+
57+ /**
58+ * Look for a candidate class in any compilation unit's imports across the same Spoon model — useful for "did you
59+ * forget an import?" hints when the user already imported {@code typeName} in another file. Returns the
60+ * fully-qualified class name of the first match, or {@code null}.
61+ */
62+ public static String findImportCandidate (String typeName , String constName , CtElement context ) {
63+ if (context == null || context .getFactory () == null )
4964 return null ;
50- for (CtImport imp : cu .getImports ()) {
51- if (imp .getReference () == null )
52- continue ;
53- String full = imp .getReference ().toString ();
54- String candidate = full .endsWith ("." + typeName ) || full .equals (typeName ) ? full
55- : full .endsWith (".*" ) ? full .substring (0 , full .length () - 1 ) + typeName : null ;
56- if (candidate != null && (v = lookup (candidate , constName )) != null )
57- return v ;
65+ for (CtCompilationUnit cu : context .getFactory ().CompilationUnit ().getMap ().values ()) {
66+ String fqn = findFqnInImports (typeName , constName , cu .getImports ());
67+ if (fqn != null )
68+ return fqn ;
69+ }
70+ return null ;
71+ }
72+
73+ /** Whether a {@link CtType} with the given simple name is present in {@code context}'s Spoon model. */
74+ public static boolean userTypeExists (String simpleName , CtElement context ) {
75+ if (context == null || context .getFactory () == null )
76+ return false ;
77+ for (CtType <?> t : context .getFactory ().Type ().getAll (true ))
78+ if (simpleName .equals (t .getSimpleName ()))
79+ return true ;
80+ return false ;
81+ }
82+
83+ private static Iterable <CtImport > localImports (CtElement context ) {
84+ if (context == null || context .getPosition () == null || !context .getPosition ().isValidPosition ())
85+ return java .util .Collections .emptyList ();
86+ CtCompilationUnit cu = context .getPosition ().getCompilationUnit ();
87+ return cu == null ? java .util .Collections .emptyList () : cu .getImports ();
88+ }
89+
90+ private static String findFqnInImports (String typeName , String constName , Iterable <CtImport > imports ) {
91+ for (CtImport imp : imports ) {
92+ String candidate = importCandidate (imp , typeName );
93+ if (candidate != null && lookup (candidate , constName ) != null )
94+ return candidate ;
5895 }
5996 return null ;
6097 }
6198
99+ private static String importCandidate (CtImport imp , String typeName ) {
100+ if (imp .getReference () == null )
101+ return null ;
102+ // Use Spoon's typed APIs so nested classes get their binary name (Map$Entry, not Map.Entry).
103+ if (imp .getImportKind () == CtImportKind .TYPE && imp .getReference ()instanceof CtTypeReference <?> tref ) {
104+ String fqn = tref .getQualifiedName ();
105+ if (fqn .equals (typeName ) || fqn .endsWith ("." + typeName ) || fqn .endsWith ("$" + typeName ))
106+ return fqn ;
107+ return null ;
108+ }
109+ if (imp .getImportKind () == CtImportKind .ALL_TYPES && imp .getReference ()instanceof CtPackageReference pref ) {
110+ String pkg = pref .getQualifiedName ();
111+ return pkg .isEmpty () ? typeName : pkg + "." + typeName ;
112+ }
113+ return null ; // FIELD / METHOD / ALL_STATIC_MEMBERS / UNRESOLVED — not relevant for type resolution.
114+ }
115+
62116 /** Wrap a resolved value as an RJ literal predicate, or {@code null} if its type is not modeled. */
63117 public static Predicate asLiteralPredicate (Object value ) {
64118 if (value instanceof Boolean )
@@ -78,26 +132,6 @@ public static Predicate asLiteralPredicate(Object value) {
78132 return null ;
79133 }
80134
81- /**
82- * Speculative scan of common JDK packages for a class named {@code typeName} that has a {@code static final}
83- * primitive/String field {@code constName}. Returns the fully-qualified class name (e.g.
84- * {@code "javax.imageio.ImageWriteParam"}) of the first match, or {@code null} if no JDK match is found. Used to
85- * suggest a missing import when {@link #resolve(String, String, CtElement)} fails.
86- */
87- public static String findJdkImportCandidate (String typeName , String constName ) {
88- for (String pkg : JDK_PACKAGE_HINTS ) {
89- String fqn = pkg + "." + typeName ;
90- if (lookup (fqn , constName ) != null )
91- return fqn ;
92- }
93- return null ;
94- }
95-
96- private static final String [] JDK_PACKAGE_HINTS = { "java.io" , "java.util" , "java.util.concurrent" ,
97- "java.util.regex" , "java.nio" , "java.nio.charset" , "java.nio.file" , "java.text" , "java.time" , "java.math" ,
98- "java.net" , "java.awt" , "java.awt.event" , "javax.swing" , "javax.imageio" , "javax.sound.sampled" ,
99- "java.security" , "java.sql" };
100-
101135 private static Object lookup (String className , String fieldName ) {
102136 try {
103137 return readStaticFinal (Class .forName (className ).getField (fieldName ));
@@ -112,7 +146,8 @@ private static Object readStaticFinal(Field jf) {
112146 try {
113147 jf .setAccessible (true );
114148 return jf .get (null );
115- } catch (Throwable ignored ) {
149+ } catch (IllegalAccessException | LinkageError ignored ) {
150+ // LinkageError covers ExceptionInInitializerError, NoClassDefFoundError, etc.
116151 return null ;
117152 }
118153 }
0 commit comments