Skip to content

Commit a4fbebe

Browse files
Model the implicit close() of try-with-resources (#358)
Fixes #334. **Stacked on #352** (compliance level 17, needed to parse `try (r)`); retarget to `main` once #352 merges. ## Problem The `close()` Java inserts at the end of a try-with-resources block was never checked, so a double close or a use after the block passed verification. ## Change `RefinementTypeChecker#visitCtTryWithResource` scans resources → body → a synthesized `r.close()` per resource (reverse declaration order, as Java does) → catchers → finally. The close goes through the normal invocation check, so it works for both `@StateRefinement` classes and external refinements. Errors are reported at the resource declaration (`Res r = new Res()`). For a Java 9 resource reference they're reported at the `try (r)` header. **Spoon workaround:** Spoon 10.4.2 models `try (r)` as an *implicit* copy of `r`'s declaration, initializer included. It is also repeated once per earlier local named `r` in the file, so `try (r)` can yield `[r, r, r]`. Scanning those would re-run `new Res()` and reset the state, so implicit resources are not scanned and are closed once per name. ## Tests - `classes/try_with_resources_error`: double close (both reproducers from #334), use after the block, use in `catch`, and `try (r)` on an already-closed `r`. - `classes/try_with_resources_correct`: use inside, multiple resources, `try (r)`, catch + finally. `mvn test`: 369/369 pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e054c42 commit a4fbebe

5 files changed

Lines changed: 165 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package testSuite.classes.try_with_resources_correct;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"open", "closed"})
7+
public class Res implements AutoCloseable {
8+
@StateRefinement(to = "open(this)")
9+
public Res() {}
10+
11+
@StateRefinement(from = "open(this)")
12+
public void read() {}
13+
14+
@StateRefinement(from = "open(this)", to = "closed(this)")
15+
public void close() {}
16+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package testSuite.classes.try_with_resources_correct;
2+
3+
public class ResTest {
4+
static void useInside() {
5+
try (Res r = new Res()) {
6+
r.read();
7+
r.read();
8+
}
9+
}
10+
11+
static void multipleResources() {
12+
try (Res a = new Res(); Res b = new Res()) {
13+
a.read();
14+
b.read();
15+
}
16+
}
17+
18+
static void resourceReference() {
19+
Res r = new Res();
20+
r.read();
21+
try (r) {
22+
r.read();
23+
}
24+
}
25+
26+
static void withCatchAndFinally() {
27+
try (Res r = new Res()) {
28+
r.read();
29+
} catch (RuntimeException e) {
30+
e.getMessage();
31+
} finally {
32+
Res s = new Res();
33+
s.read();
34+
}
35+
}
36+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package testSuite.classes.try_with_resources_error;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"open", "closed"})
7+
public class Res implements AutoCloseable {
8+
@StateRefinement(to = "open(this)")
9+
public Res() {}
10+
11+
@StateRefinement(from = "open(this)")
12+
public void read() {}
13+
14+
@StateRefinement(from = "open(this)", to = "closed(this)")
15+
public void close() {}
16+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package testSuite.classes.try_with_resources_error;
2+
3+
public class ResTest {
4+
// the implicit close() at the end of the block is a second close()
5+
static void closeTwice() {
6+
try (Res r = new Res()) { // Expect: State Refinement Error
7+
r.read();
8+
r.close();
9+
}
10+
}
11+
12+
// the resource is closed after the block
13+
static void useAfter() {
14+
Res r = new Res();
15+
try (r) {
16+
r.read();
17+
}
18+
r.read(); // Expect: State Refinement Error
19+
}
20+
21+
// resources are closed before the catch block runs
22+
static void useInCatch() {
23+
Res r = new Res();
24+
try (r) {
25+
r.read();
26+
} catch (RuntimeException e) {
27+
r.read(); // Expect: State Refinement Error
28+
}
29+
}
30+
31+
// the implicit close() of an already closed resource reference
32+
static void closedBefore() {
33+
Res r = new Res();
34+
r.close();
35+
try (r) { // Expect: State Refinement Error
36+
r.hashCode();
37+
}
38+
}
39+
}

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import java.lang.annotation.Annotation;
44
import java.util.ArrayList;
55
import java.util.Arrays;
6+
import java.util.Collections;
7+
import java.util.LinkedHashMap;
68
import java.util.LinkedHashSet;
79
import java.util.List;
10+
import java.util.Map;
811
import java.util.Optional;
912
import java.util.Set;
1013

@@ -49,17 +52,22 @@
4952
import spoon.reflect.code.CtNewArray;
5053
import spoon.reflect.code.CtNewClass;
5154
import spoon.reflect.code.CtOperatorAssignment;
55+
import spoon.reflect.code.CtResource;
5256
import spoon.reflect.code.CtReturn;
5357
import spoon.reflect.code.CtStatement;
5458
import spoon.reflect.code.CtThisAccess;
5559
import spoon.reflect.code.CtThrow;
60+
import spoon.reflect.code.CtTryWithResource;
5661
import spoon.reflect.code.CtUnaryOperator;
5762
import spoon.reflect.code.CtVariableAccess;
5863
import spoon.reflect.code.CtVariableRead;
5964
import spoon.reflect.code.CtVariableWrite;
6065
import spoon.reflect.code.CtWhile;
66+
import spoon.reflect.cu.CompilationUnit;
67+
import spoon.reflect.cu.SourcePosition;
6168
import spoon.reflect.declaration.*;
6269
import spoon.reflect.factory.Factory;
70+
import spoon.reflect.reference.CtExecutableReference;
6371
import spoon.reflect.reference.CtFieldReference;
6472
import spoon.reflect.reference.CtTypeReference;
6573
import spoon.reflect.reference.CtVariableReference;
@@ -515,6 +523,56 @@ private boolean canCompleteNormally(CtStatement statement) {
515523
return true;
516524
}
517525

526+
@Override
527+
public void visitCtTryWithResource(CtTryWithResource tryWithResource) {
528+
// A resource reference (Java 9 `try (r)`) is modelled by Spoon as an implicit copy of r's declaration,
529+
// initializer included, and repeated once per earlier local with the same name, so it is not scanned (that
530+
// would re-run the initializer) and is only closed once
531+
Map<String, CtResource<?>> resources = new LinkedHashMap<>();
532+
for (CtResource<?> resource : tryWithResource.getResources()) {
533+
if (!resource.isImplicit())
534+
scan(resource);
535+
resources.put(resource.getSimpleName(), resource);
536+
}
537+
scan(tryWithResource.getBody());
538+
539+
// the resources are closed when the body ends, in reverse order, before any catch or finally block runs
540+
List<CtResource<?>> toClose = new ArrayList<>(resources.values());
541+
Collections.reverse(toClose);
542+
for (CtResource<?> resource : toClose) {
543+
SourcePosition position = resource.isImplicit() ? getHeaderPosition(tryWithResource)
544+
: resource.getPosition();
545+
scan(createImplicitClose(resource, tryWithResource, position));
546+
}
547+
scan(tryWithResource.getCatchers());
548+
scan(tryWithResource.getFinalizer());
549+
}
550+
551+
/** Position of {@code try (...)}, without the blocks */
552+
private SourcePosition getHeaderPosition(CtTryWithResource tryWithResource) {
553+
SourcePosition position = tryWithResource.getPosition();
554+
CompilationUnit cu = position.getCompilationUnit();
555+
int end = cu.getOriginalSourceCode().lastIndexOf(')', tryWithResource.getBody().getPosition().getSourceStart());
556+
if (end < position.getSourceStart())
557+
return position;
558+
return factory.Core().createSourcePosition(cu, position.getSourceStart(), end, cu.getLineSeparatorPositions());
559+
}
560+
561+
/** Builds the {@code resource.close()} that Java inserts at the end of a try-with-resources block */
562+
private CtInvocation<?> createImplicitClose(CtResource<?> resource, CtTryWithResource tryWithResource,
563+
SourcePosition position) {
564+
CtTypeReference<?> type = resource.getType();
565+
CtExecutableReference<?> close = type.getAllExecutables().stream()
566+
.filter(e -> e.getSimpleName().equals("close") && e.getParameters().isEmpty()).findFirst()
567+
.orElseGet(() -> factory.Executable().createReference(type, factory.Type().VOID_PRIMITIVE, "close"));
568+
CtExpression<?> target = factory.Code().createVariableRead(resource.getReference(), false);
569+
CtInvocation<?> invocation = factory.Code().createInvocation(target, close);
570+
invocation.setParent(tryWithResource);
571+
invocation.setPosition(position);
572+
target.setPosition(position);
573+
return invocation;
574+
}
575+
518576
@Override
519577
public void visitCtWhile(CtWhile whileLoop) {
520578
visitLoop(whileLoop, () -> {

0 commit comments

Comments
 (0)