Skip to content

Commit b3a4e4a

Browse files
Fix crashes on enum methods, varargs calls and calls on a call result (#420)
Three crashes on ordinary Java code, met together in one real class (JabRef's `EditAction`). One commit each. ## 1. An enum with a method crashes the verifier ```java enum Action { UNDO, REDO; String getText() { return name(); } } ``` ``` NullPointerException: Cannot invoke "String.endsWith(String)" because "qualifiedName" is null at ExternalRefinementTypeChecker.visitCtMethod ``` `ExternalRefinementTypeChecker` skips classes and records (user code) but not enums, so it visits the enum's methods with no external target. **Fix:** a no-op `visitCtEnum`, as for records. ## 2. A varargs call with more arguments than parameters ```java void debug(String format, Object... args) {} logger.debug("{} and {}", a, b); // IndexOutOfBoundsException: Index 2 out of bounds for length 2 ``` `mapInvocation` and `checkParameters` index the parameters by argument position. **Fix:** both use only the arguments bound one to one to parameters; the ones collected into the varargs array are left out (an array parameter's refinement is not checked per element anyway). ## 3. A call on the result of a call without a return refinement ```java holder.get().close(); // Argument Mismatch Error: Argument '#holder_14' and its respective parameter of ghost 'Door.state1' // types are incompatible: expected Door, got Holder ``` When `get()` has no return refinement, its `TARGET` metadata is still its own receiver (`holder`), and `prepareInvocationTarget` used it as the receiver of `close()`. **Fix:** a call whose `TARGET` is not its return value (`RETURN_VAR_NAME`) gives a fresh receiver of the call's type, in an unknown state, created once per call (`Keys.RECEIVER`, as for constructor calls). Now: `found true but expected open(#fresh)`. ## Tests - `CorrectEnumWithMethod`: crashed before. - `CorrectVarargsCall` (more, fewer and an explicit array) and `ErrorVarargsCall` (a refined fixed parameter before the varargs is still checked): crashed before. - `CorrectCallResultReceiver` (a call with no precondition on a call result) and `ErrorCallResultReceiver` (reported as a state error, not an argument mismatch). - `mvn test`: 463 tests, 0 failures. Found on a real fixed bug: JabRef's Edit command calling `undo()`/`redo()` without `canUndo()`/`canRedo()` (PR JabRef/jabref#9777). We also checked 53 exercises built from real OSS bugs on main (69895fa) and with these changes: the results are identical. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d3e00be commit b3a4e4a

8 files changed

Lines changed: 143 additions & 2 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"open", "closed"})
7+
class CorrectCallResultDoor {
8+
@StateRefinement(to = "open(this)")
9+
public CorrectCallResultDoor() {}
10+
11+
@StateRefinement(from = "open(this)", to = "closed(this)")
12+
public void close() {}
13+
14+
@StateRefinement(to = "closed(this)")
15+
public void slam() {}
16+
}
17+
18+
class CorrectCallResultHolder {
19+
private final CorrectCallResultDoor door = new CorrectCallResultDoor();
20+
21+
CorrectCallResultDoor get() {
22+
return door;
23+
}
24+
}
25+
26+
public class CorrectCallResultReceiver {
27+
static void use(CorrectCallResultHolder holder) {
28+
holder.get().slam();
29+
}
30+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
enum CorrectEnumWithMethodAction {
6+
UNDO, REDO;
7+
8+
String getText() {
9+
return name();
10+
}
11+
}
12+
13+
public class CorrectEnumWithMethod {
14+
static String describe(CorrectEnumWithMethodAction action) {
15+
@Refinement("_ > 0")
16+
int width = 1;
17+
return action.getText() + width;
18+
}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
class CorrectVarargsLogger {
6+
void debug(@Refinement("_ >= 0") int level, String format, Object... args) {}
7+
}
8+
9+
public class CorrectVarargsCall {
10+
static void log(CorrectVarargsLogger logger, Object a, Object b) {
11+
logger.debug(1, "{} and {}", a, b);
12+
logger.debug(2, "nothing");
13+
logger.debug(3, "{}", new Object[] {a});
14+
}
15+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"open", "closed"})
7+
class ErrorCallResultDoor {
8+
@StateRefinement(to = "open(this)")
9+
public ErrorCallResultDoor() {}
10+
11+
@StateRefinement(from = "open(this)", to = "closed(this)")
12+
public void close() {}
13+
}
14+
15+
class ErrorCallResultHolder {
16+
private final ErrorCallResultDoor door = new ErrorCallResultDoor();
17+
18+
ErrorCallResultDoor get() {
19+
return door;
20+
}
21+
}
22+
23+
public class ErrorCallResultReceiver {
24+
static void use(ErrorCallResultHolder holder) {
25+
holder.get().close(); // Expect: State Refinement Error
26+
}
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
class ErrorVarargsLogger {
6+
void debug(@Refinement("_ >= 0") int level, String format, Object... args) {}
7+
}
8+
9+
public class ErrorVarargsCall {
10+
static void log(ErrorVarargsLogger logger, Object a, Object b) {
11+
logger.debug(-1, "{} and {}", a, b); // Expect: Refinement Error
12+
}
13+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import spoon.reflect.declaration.CtRecord;
2222
import spoon.reflect.declaration.CtElement;
2323
import spoon.reflect.declaration.CtField;
24+
import spoon.reflect.declaration.CtEnum;
2425
import spoon.reflect.declaration.CtInterface;
2526
import spoon.reflect.declaration.CtMethod;
2627
import spoon.reflect.declaration.CtParameter;
@@ -46,6 +47,11 @@ public void visitCtRecord(CtRecord ctRecord) {
4647
// records are user code, like classes: external specs are only read from interfaces
4748
}
4849

50+
@Override
51+
public <T extends Enum<?>> void visitCtEnum(CtEnum<T> ctEnum) {
52+
// enums are user code too; their methods have no external target
53+
}
54+
4955
@Override
5056
public <T> void visitCtInterface(CtInterface<T> intrface) {
5157
Optional<CtAnnotation<?>> externalRef = getExternalRefinement(intrface);

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/MethodsFunctionsChecker.java‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ private void applyReturnRefinement(CtElement invocation, RefinedFunction f, Map<
419419
private Map<String, String> mapInvocation(List<CtExpression<?>> arguments, RefinedFunction f) {
420420
Map<String, String> mapInvocation = new HashMap<>();
421421
List<Variable> functionParams = f.getArguments();
422-
for (int i = 0; i < arguments.size(); i++) {
422+
for (int i = 0; i < matchedArguments(arguments, functionParams); i++) {
423423
Variable fArg = functionParams.get(i);
424424
CtExpression<?> iArg = arguments.get(i);
425425
String invStr;
@@ -437,6 +437,16 @@ private Map<String, String> mapInvocation(List<CtExpression<?>> arguments, Refin
437437
return mapInvocation;
438438
}
439439

440+
/**
441+
* The number of arguments bound one to one to parameters. A varargs call can pass more (or fewer) arguments than
442+
* there are parameters; the ones collected into the varargs array are left out.
443+
*/
444+
private static int matchedArguments(List<CtExpression<?>> arguments, List<Variable> params) {
445+
if (arguments.size() == params.size())
446+
return arguments.size();
447+
return Math.min(arguments.size(), Math.max(params.size() - 1, 0));
448+
}
449+
440450
private String createVariableRepresentingArgument(CtExpression<?> iArg, Variable fArg) {
441451
Predicate met = (Predicate) iArg.getMetadata(Keys.REFINEMENT);
442452
if (met == null)
@@ -451,7 +461,7 @@ private String createVariableRepresentingArgument(CtExpression<?> iArg, Variable
451461
private void checkParameters(CtElement invocation, List<CtExpression<?>> arguments, RefinedFunction f,
452462
Map<String, String> map) throws LJError {
453463
List<Variable> functionParams = f.getArguments();
454-
for (int i = 0; i < arguments.size(); i++) {
464+
for (int i = 0; i < matchedArguments(arguments, functionParams); i++) {
455465
Variable fArg = functionParams.get(i);
456466
Predicate c = fArg.getMainRefinement();
457467
c = c.substituteVariable(fArg.getName(), map.get(fArg.getName()));

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/object_checkers/AuxStateHandler.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,21 @@ public static String prepareInvocationTarget(TypeChecker tc, CtElement target2,
848848
newCall.putMetadata(Keys.RECEIVER, receiver);
849849
invocation.putMetadata(Keys.TARGET, receiver);
850850
return name;
851+
} else if (target2 instanceof CtInvocation<?> call
852+
&& call.getMetadata(Keys.RECEIVER)instanceof RefinedVariable receiver) {
853+
invocation.putMetadata(Keys.TARGET, receiver);
854+
return receiver.getName();
855+
} else if (target2 instanceof CtInvocation<?> call && call.getMetadata(Keys.TARGET) != null
856+
&& !isReturnValue(call)) {
857+
// `a.get().method()` where get() has no return refinement: the call's TARGET is its own receiver (a), not
858+
// the value it returns, so the receiver is a fresh value of the call's type, in a state not known here
859+
String name = String.format(Formats.FRESH, tc.getContext().getCounter());
860+
CtTypeReference<?> type = call.getType();
861+
RefinedVariable receiver = tc.getContext().addInstanceToContext(name, type, new Predicate(), call);
862+
receiver.addSuperTypes(type.getSuperclass(), type.getSuperInterfaces());
863+
call.putMetadata(Keys.RECEIVER, receiver);
864+
invocation.putMetadata(Keys.TARGET, receiver);
865+
return name;
851866
} else if (target2.getMetadata(Keys.TARGET) != null) {
852867
VariableInstance target2Vi = (VariableInstance) target2.getMetadata(Keys.TARGET);
853868
Optional<Variable> v = target2Vi.getParent();
@@ -857,6 +872,12 @@ public static String prepareInvocationTarget(TypeChecker tc, CtElement target2,
857872
return null;
858873
}
859874

875+
/** Whether a call's TARGET metadata is the value it returns (set when its return refinement is applied). */
876+
private static boolean isReturnValue(CtInvocation<?> call) {
877+
return call.getMetadata(Keys.TARGET)instanceof VariableInstance vi
878+
&& vi.getName().equals(call.getMetadata(Keys.RETURN_VAR_NAME));
879+
}
880+
860881
static VariableInstance getTarget(CtElement invocation) {
861882
if (invocation.getMetadata(Keys.TARGET) != null) {
862883
return (VariableInstance) invocation.getMetadata(Keys.TARGET);

0 commit comments

Comments
 (0)