Skip to content

Commit 9045ecd

Browse files
Treat catch parameters as variables with unknown state (#362)
Closes #333. A catch parameter was never added to the verification context. As a result: 1. Calls on it skipped typestate checks (`x.initCause(t)` on a caught `Throwable` passed). 2. It took the state of an earlier, out-of-scope local with the same name. 3. Assigning it to another variable failed with `Variable 'e' could not be found`. **Fix** - `RefinementTypeChecker#visitCtCatchVariable` registers the catch parameter like a method parameter: a new variable whose state is unknown. - Before that, `Context#removeVarFromContext` drops any variable with the same name. Java forbids a catch parameter from shadowing a local that is in scope, so such an entry is always a stale, out-of-scope local. Fields cannot clash because they are stored under qualified `this#…` names. **Tests** `testSuite/classes/catch_parameter_error` covers the three reproducers. Each now reports the same "found true but expected noThrowable(…)" error as the method-parameter case. **Not fixed here** Reproducer 3 as written in the issue (`catch (Exception e)` assigned to a `Throwable` local) now fails with `Sorts java.lang.Throwable and java.lang.Exception are incompatible`. This bug is separate and already present on `main`: assigning a subtype to a supertype variable fails the same way when the subtype comes from a method parameter. The test therefore uses `catch (Throwable e)` for that case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ef1603d commit 9045ecd

4 files changed

Lines changed: 74 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package testSuite.classes.catch_parameter_error;
2+
3+
public class Test {
4+
static void load() throws Exception {
5+
throw new Exception("x");
6+
}
7+
8+
// the state of a catch parameter is unknown
9+
static void unknownState(Throwable t) {
10+
try {
11+
load();
12+
} catch (Throwable x) {
13+
x.initCause(t); // Expect: State Refinement Error
14+
}
15+
}
16+
17+
// the catch parameter must not take the state of an earlier local with the same name
18+
static void sameNameAsEarlierLocal() {
19+
try {
20+
load();
21+
} catch (Exception ex) {
22+
Throwable e = new Throwable("wrapped");
23+
}
24+
try {
25+
load();
26+
} catch (Throwable e) {
27+
e.initCause(new RuntimeException("more")); // Expect: State Refinement Error
28+
}
29+
}
30+
31+
// the catch parameter can be used outside the catch through another variable
32+
static void usedAfterCatch() {
33+
Throwable failure = new Throwable("none");
34+
try {
35+
load();
36+
} catch (Throwable e) {
37+
failure = e;
38+
}
39+
failure.initCause(new RuntimeException("root")); // Expect: State Refinement Error
40+
}
41+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package testSuite.classes.catch_parameter_error;
2+
3+
import liquidjava.specification.ExternalRefinementsFor;
4+
import liquidjava.specification.StateRefinement;
5+
import liquidjava.specification.StateSet;
6+
7+
@ExternalRefinementsFor("java.lang.Throwable")
8+
@StateSet({"withThrowable", "noThrowable"})
9+
public interface ThrowableRefinements {
10+
@StateRefinement(to = "noThrowable(this)")
11+
void Throwable(String message);
12+
13+
@StateRefinement(to = "withThrowable(this)")
14+
void Throwable(String message, Throwable cause);
15+
16+
@StateRefinement(from = "noThrowable(this)", to = "withThrowable(this)")
17+
Throwable initCause(Throwable cause);
18+
}

‎liquidjava-verifier/src/main/java/liquidjava/processor/context/Context.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ public RefinedVariable addVarToContext(String simpleName, CtTypeReference<?> typ
149149
return vi;
150150
}
151151

152+
public void removeVarFromContext(String name) {
153+
for (List<RefinedVariable> l : ctxVars)
154+
l.removeIf(var -> var.getName().equals(name));
155+
}
156+
152157
public RefinedVariable addInstanceToContext(String simpleName, CtTypeReference<?> type, Predicate c,
153158
CtElement element) {
154159
RefinedVariable vi = new VariableInstance(simpleName, type, c);

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import spoon.reflect.code.CtBinaryOperator;
3535
import spoon.reflect.code.CtBlock;
3636
import spoon.reflect.code.CtBreak;
37+
import spoon.reflect.code.CtCatchVariable;
3738
import spoon.reflect.code.CtConditional;
3839
import spoon.reflect.code.CtContinue;
3940
import spoon.reflect.code.CtDo;
@@ -179,6 +180,15 @@ public <T> void visitCtLocalVariable(CtLocalVariable<T> localVariable) {
179180
}
180181
}
181182

183+
@Override
184+
public <T> void visitCtCatchVariable(CtCatchVariable<T> catchVariable) {
185+
super.visitCtCatchVariable(catchVariable);
186+
// like a method parameter, the caught object is a new variable with unknown state; Java forbids it to share a
187+
// name with a local in scope, so a variable with the same name in the context is an out-of-scope local
188+
context.removeVarFromContext(catchVariable.getSimpleName());
189+
context.addVarToContext(catchVariable.getSimpleName(), catchVariable.getType(), new Predicate(), catchVariable);
190+
}
191+
182192
@Override
183193
public <T> void visitCtNewArray(CtNewArray<T> newArray) {
184194
super.visitCtNewArray(newArray);

0 commit comments

Comments
 (0)