Skip to content

Commit 9c51147

Browse files
Check calls on a constructor call whose constructor has refined parameters (#407)
Follow-up to #361 (typestate calls made directly on a constructor call). ## Problem A call made directly on `new T(args)` fails when the constructor `T(args)` has refined parameters: ```java Socket socket = new ServerSocket(8080).accept(); ``` ``` Argument Mismatch Error: Argument '#<init>_398' and its respective parameter of ghost 'java.net.ServerSocket.state1' types are incompatible: expected ServerSocket, got void ``` With refined parameters, the constructor call goes through `applyReturnRefinement`, which sets the call's `TARGET` metadata to the constructor's return-value instance (`#<init>_N`, typed `void`). `prepareInvocationTarget` checked `TARGET` before the constructor-call case, so it used that instance as the receiver. Constructors without refined parameters (`new ServerSocket().accept()`) were not affected. ## Fix - `prepareInvocationTarget` handles a constructor-call target before falling back to `TARGET` metadata. - The fresh receiver is stored under a new `Keys.RECEIVER` key on the constructor call and reused when the same invocation is prepared again (the target is prepared both for the parameter checks and for the state change), so it is created once. ## Tests - `CorrectConstructorReceiverRefinedArgument`: `new Listener(8080).accept()` where `Listener(int)` has a refined parameter and lands in `bound`. It failed before this change. - `ErrorConstructorReceiverRefinedArgument`: the same constructor followed by a call that needs `unbound` is reported. - `mvn test`: 414 tests, 0 failures. Found on a real fixed bug (a static page server whose fix was `new ServerSocket(port).accept()`). We also checked 53 exercises built from real OSS bugs: the results are identical to main's. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e0a6c81 commit 9c51147

4 files changed

Lines changed: 60 additions & 8 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
import liquidjava.specification.StateRefinement;
5+
import liquidjava.specification.StateSet;
6+
7+
@StateSet({"unbound", "bound"})
8+
class CorrectRefinedArgListener {
9+
@StateRefinement(to = "unbound(this)")
10+
public CorrectRefinedArgListener() {}
11+
12+
@StateRefinement(to = "bound(this)")
13+
public CorrectRefinedArgListener(@Refinement("0 <= _ && _ <= 65535") int port) {}
14+
15+
@StateRefinement(from = "bound(this)")
16+
public void accept() {}
17+
}
18+
19+
public class CorrectConstructorReceiverRefinedArgument {
20+
static void chained() {
21+
new CorrectRefinedArgListener(8080).accept();
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
import liquidjava.specification.StateRefinement;
5+
import liquidjava.specification.StateSet;
6+
7+
@StateSet({"unbound", "bound"})
8+
class ErrorRefinedArgListener {
9+
@StateRefinement(to = "unbound(this)")
10+
public ErrorRefinedArgListener() {}
11+
12+
@StateRefinement(to = "bound(this)")
13+
public ErrorRefinedArgListener(@Refinement("0 <= _ && _ <= 65535") int port) {}
14+
15+
@StateRefinement(from = "unbound(this)", to = "bound(this)")
16+
public void bind() {}
17+
}
18+
19+
public class ErrorConstructorReceiverRefinedArgument {
20+
static void chained() {
21+
new ErrorRefinedArgListener(8080).bind(); // Expect: State Refinement Error
22+
}
23+
}

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -744,24 +744,29 @@ public static String prepareInvocationTarget(TypeChecker tc, CtElement target2,
744744
}
745745

746746
return name;
747-
} else if (target2.getMetadata(Keys.TARGET) != null) {
748-
// invocation is in
749-
// who did put the metadata here then?
750-
VariableInstance target2Vi = (VariableInstance) target2.getMetadata(Keys.TARGET);
751-
Optional<Variable> v = target2Vi.getParent();
752-
invocation.putMetadata(Keys.TARGET, target2Vi);
753-
return v.map(Refined::getName).orElse(target2Vi.getName());
747+
} else if (target2 instanceof CtConstructorCall<?> newCall
748+
&& newCall.getMetadata(Keys.RECEIVER)instanceof RefinedVariable receiver) {
749+
// `new T(args).method()` already prepared: reuse its receiver instead of creating another one
750+
invocation.putMetadata(Keys.TARGET, receiver);
751+
return receiver.getName();
754752
} else if (target2 instanceof CtConstructorCall<?> newCall
755753
&& newCall.getMetadata(Keys.REFINEMENT)instanceof Predicate state) {
756-
// `new T(args).method()`: the receiver is a fresh object in the state the constructor gives
754+
// `new T(args).method()`: the receiver is a fresh object in the state the constructor gives. Must come
755+
// before the TARGET case: a constructor with refined parameters puts its (void) return value in TARGET
757756
String name = String.format(Formats.FRESH, tc.getContext().getCounter());
758757
CtTypeReference<?> type = newCall.getType();
759758
RefinedVariable receiver = tc.getContext().addInstanceToContext(name, type,
760759
state.substituteVariable(Keys.THIS, name).substituteVariable(Keys.WILDCARD, name), newCall);
761760
receiver.addSuperTypes(type.getSuperclass(), type.getSuperInterfaces());
762761
newCall.putMetadata(Keys.TARGET, receiver);
762+
newCall.putMetadata(Keys.RECEIVER, receiver);
763763
invocation.putMetadata(Keys.TARGET, receiver);
764764
return name;
765+
} else if (target2.getMetadata(Keys.TARGET) != null) {
766+
VariableInstance target2Vi = (VariableInstance) target2.getMetadata(Keys.TARGET);
767+
Optional<Variable> v = target2Vi.getParent();
768+
invocation.putMetadata(Keys.TARGET, target2Vi);
769+
return v.map(Refined::getName).orElse(target2Vi.getName());
765770
}
766771
return null;
767772
}

‎liquidjava-verifier/src/main/java/liquidjava/utils/constants/Keys.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public final class Keys {
44
public static final String REFINEMENT = "refinement";
55
public static final String REFINEMENT_SAT_CHECK = "refinement_sat_check";
66
public static final String TARGET = "target";
7+
public static final String RECEIVER = "receiver";
78
public static final String RETURN_VAR_NAME = "return_var_name";
89
public static final String THIS = "this";
910
public static final String WILDCARD = "_";

0 commit comments

Comments
 (0)