Skip to content

Commit 35b627e

Browse files
change class names
1 parent 1df915f commit 35b627e

5 files changed

Lines changed: 128 additions & 49 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package testSuite;
2+
3+
import static java.lang.Math.PI;
4+
5+
import liquidjava.specification.Refinement;
6+
7+
/**
8+
* Locks in that the import walk ignores {@code import static} (kind {@code FIELD}) without confusing the resolver:
9+
* the file has a static import for an unrelated symbol, and a refinement that uses a regular {@code Type.CONST}
10+
* reference. The verifier must skip the static import and resolve {@code Math.PI} via the implicit {@code java.lang}
11+
* fallback (or the static-import target's class — either path is correct here).
12+
*/
13+
@SuppressWarnings("unused")
14+
public class CorrectStaticImportInPredicate {
15+
16+
static double useUnused() {
17+
return PI;
18+
}
19+
20+
static void requireBelowFour(@Refinement("_ < 4.0") double x) {
21+
}
22+
23+
public static void main(String[] args) {
24+
requireBelowFour(Math.PI);
25+
}
26+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package testSuite.classes.error_missing_import;
2+
3+
import javax.imageio.ImageWriteParam;
4+
5+
/** Sibling file that imports the class so the verifier knows where to find it. */
6+
public class Helper {
7+
public static int explicit() {
8+
return ImageWriteParam.MODE_EXPLICIT;
9+
}
10+
}

liquidjava-example/src/main/java/testSuite/ErrorMissingImportInPredicate.java renamed to liquidjava-example/src/main/java/testSuite/classes/missing_import_final_error/ClassNoImport.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
package testSuite;
1+
package testSuite.classes.missing_import_final_error;
22

33
import liquidjava.specification.Refinement;
44

55
@SuppressWarnings("unused")
6-
public class ErrorMissingImportInPredicate {
6+
public class ClassNoImport {
77

8-
// No import for javax.imageio.ImageWriteParam — the verifier should suggest the import.
8+
// No import for javax.imageio.ImageWriteParam in this file — the verifier
9+
// should suggest it because Helper.java already imports it.
910
static void requireExplicit(@Refinement("_ == ImageWriteParam.MODE_EXPLICIT") int mode) { // Error
1011
}
1112

liquidjava-verifier/src/main/java/liquidjava/rj_language/Predicate.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,16 @@ private static Expression resolveStaticFinalConstants(Expression root, CtElement
9696
e = e.substitute(en, lit.getExpression());
9797
continue;
9898
}
99-
String suggested = StaticConstants.findJdkImportCandidate(en.getTypeName(), en.getConstName());
100-
if (suggested != null) {
101-
SourcePosition pos = context == null ? null : Utils.getLJAnnotationPosition(context, en.toString());
102-
throw new liquidjava.diagnostics.errors.CustomError(String.format(
103-
"Could not resolve '%s.%s' in refinement. "
104-
+ "If you meant the static final constant, add: import %s;",
105-
en.getTypeName(), en.getConstName(), suggested), pos);
106-
}
99+
if (StaticConstants.userTypeExists(en.getTypeName(), context))
100+
continue; // likely a user-defined enum/class — let SMT translation handle it
101+
SourcePosition pos = context == null ? null : Utils.getLJAnnotationPosition(context, en.toString());
102+
String suggested = StaticConstants.findImportCandidate(en.getTypeName(), en.getConstName(), context);
103+
String hint = suggested != null ? "add: import " + suggested + ";"
104+
: "add an import for '" + en.getTypeName() + "' if it is a Java class with a static final field.";
105+
throw new liquidjava.diagnostics.errors.CustomError(
106+
String.format("Could not resolve '%s.%s' in refinement. If you meant the static final constant, %s",
107+
en.getTypeName(), en.getConstName(), hint),
108+
pos);
107109
}
108110
return e;
109111
}
@@ -115,7 +117,12 @@ private static void collectEnums(Expression e, List<Enum> out) {
115117
collectEnums(c, out);
116118
}
117119

118-
/** Create a predicate with the expression true */
120+
/**
121+
* Wrap an already-built expression in a {@link Predicate}. Unlike the string-parsing constructors, this does NOT
122+
* run static-final-constant resolution. Callers are responsible for ensuring {@code e} contains no
123+
* {@link liquidjava.rj_language.ast.Enum Enum} nodes that should be resolved to literals; in practice this is fine
124+
* for AST clones and rewrites that started life from the string constructor.
125+
*/
119126
public Predicate(Expression e) {
120127
exp = e;
121128
}

liquidjava-verifier/src/main/java/liquidjava/utils/StaticConstants.java

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
import spoon.reflect.declaration.CtElement;
1212
import spoon.reflect.declaration.CtField;
1313
import spoon.reflect.declaration.CtImport;
14+
import spoon.reflect.declaration.CtImportKind;
15+
import spoon.reflect.declaration.CtType;
1416
import 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. */
1721
public 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

Comments
 (0)