-
Notifications
You must be signed in to change notification settings - Fork 437
Suppresses RLC non-final field overwrite warning for safe constructor field initialization #7050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 82 commits
158e737
325e983
4cb90d7
9d4d403
c524542
a9a1e84
dbd6447
29071ca
14aebc4
87b838c
8ad095b
fa2aef4
96c47f8
9a70147
4010022
4765ecf
143f3f4
b11cd19
dda12eb
d78354e
81c8cab
f3b9022
0d53a06
07617a7
c188d0f
7f4360a
7eb84c4
827e573
80d52c2
a5d1f60
786d477
5d9d51a
46687a1
679cbdf
000a833
2d9e299
d55afd1
93210dd
04d72ba
2d1691a
43490e6
2429215
33f4adf
caa85b1
a7eb9a8
df08e91
a891075
f74552f
17468f6
9bd6d38
aa0e9e9
4d93d90
b6a6daf
48ea240
219c5b5
7b7f3f9
1d0fdae
11ebf6c
705019a
80f0d1f
6aa49ee
efc7b7c
af3ca85
80e4041
ece1b44
6c806fb
b9af79d
6a3e7d0
52f6d69
869bb89
57985ba
481e926
4babfbd
7668ccc
cb5f029
7305395
224f485
775a2db
f26dca7
8e127b5
29b6694
e3f1c5d
b278f86
d6b1e99
9b70eec
4d88f93
3e5fcd2
f9b8a87
db0b98b
4fccac1
96dee63
f5420f7
1bc4989
27703ee
f55e800
a4b23a5
e9fcc45
1f04475
544c7f8
05247d8
013cc24
9764391
b5cd39c
38ef995
038ae36
6bdd2e9
8f1a689
3476b4d
6a62d27
54448fd
e2b0f40
131101b
0868839
6b01f8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package org.checkerframework.checker.test.junit; | ||
|
|
||
| import java.io.File; | ||
| import java.util.List; | ||
| import org.checkerframework.checker.resourceleak.ResourceLeakChecker; | ||
| import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; | ||
| import org.junit.runners.Parameterized.Parameters; | ||
|
|
||
| /** | ||
| * Tests for validating safe suppression of resource leak warnings when a private field is | ||
| * initialized for the first time inside a constructor. | ||
| * | ||
| * <p>These tests check that the checker allows first-time constructor-based assignments (when safe) | ||
| * and continues to report reassignments or leaks in all other cases (e.g., after method calls, | ||
| * initializer blocks, etc.). | ||
| */ | ||
| public class ResourceLeakFirstInitConstructorTest extends CheckerFrameworkPerDirectoryTest { | ||
| public ResourceLeakFirstInitConstructorTest(List<File> testFiles) { | ||
| super( | ||
| testFiles, | ||
| ResourceLeakChecker.class, | ||
| "resourceleak-firstinitconstructor", | ||
| "-AwarnUnneededSuppressions", | ||
| "-encoding", | ||
| "UTF-8"); | ||
| } | ||
|
|
||
| @Parameters | ||
| public static String[] getTestDirs() { | ||
| return new String[] {"resourceleak-firstinitconstructor"}; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Test: Field is initialized in one constructor and reassigned in another via this() chaining. | ||
| // Expected: Warning in constructor and open() due to reassignments. | ||
|
|
||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class ConstructorChainingLeak { | ||
| private @Owning FileInputStream s; | ||
|
|
||
| public ConstructorChainingLeak() throws Exception { | ||
| this(42); | ||
| // :: error: [required.method.not.called] | ||
| s = new FileInputStream("test.txt"); | ||
| } | ||
|
|
||
| private ConstructorChainingLeak(int x) throws Exception { | ||
| s = new FileInputStream("test.txt"); | ||
| } | ||
|
|
||
| // :: error: [missing.creates.mustcall.for] | ||
| public void open() { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| s = new FileInputStream("test.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| @EnsuresCalledMethods(value = "this.s", methods = "close") | ||
| public void close() { | ||
| try { | ||
| s.close(); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Test: Field is explicitly initialized to null and assigned in constructor. | ||
| // Expected: No warning in constructor, warning in open(). | ||
|
|
||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class ExplicitNullInitializer { | ||
| private @Owning FileInputStream s = null; | ||
|
|
||
| public ExplicitNullInitializer() { | ||
| try { | ||
| s = new FileInputStream("test.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| // :: error: [missing.creates.mustcall.for] | ||
| public void open() { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| s = new FileInputStream("test.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| @EnsuresCalledMethods(value = "this.s", methods = "close") | ||
| public void close() { | ||
| try { | ||
| s.close(); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class FinalField { | ||
| private int i; | ||
|
|
||
| private final @Owning FileInputStream s; | ||
|
|
||
| public FinalField() throws Exception { | ||
| havoc(); | ||
| s = new FileInputStream("test.txt"); | ||
| } | ||
|
|
||
| void havoc() { | ||
| i++; | ||
| } | ||
|
|
||
| @EnsuresCalledMethods(value = "this.s", methods = "close") | ||
| public void close() { | ||
| try { | ||
| s.close(); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Demonstrates conservative behavior for assignments inside conditionals. | ||
| // | ||
| // Because the current analysis does not reason about control-flow branches, | ||
| // each assignment inside an if/else is treated as a potential re-assignment | ||
| // rather than the first write. As a result, every write below is reported, | ||
| // even though each branch actually assigns the field only once. | ||
| // | ||
| // A CFG-aware implementation would recognize that only one branch executes | ||
| // and would not warn on any of these assignments. | ||
|
|
||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class FirstAssignmentInConditional { | ||
| private @Owning FileInputStream s; | ||
|
|
||
| public FirstAssignmentInConditional(boolean b) { | ||
| try { | ||
| if (b) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be straightforward to handle this case. Override
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could special-case this if/else, but I chose not to: any branching is now a barrier and we bail out, because partial branch handling might quickly turns into CFG dependency.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see how it would turn into a CFG dependency. |
||
| // ::error: [required.method.not.called] | ||
| s = new FileInputStream("test1.txt"); // false positive: first write in this branch | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is the first assignment, I would expect that it is permitted even though there would be an error reported at the other assignment. This is based on the comment above "checks if {
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, agreed. The earlier wording was misleading because it suggested “first write to the field” in a semantic/control-flow sense. I updated the implementation and comments to make the policy explicit: this is an AST-only, conservative check that only recognizes a “first write” when the assignment occurs in the constructor’s straight-line initialization prefix (statements that execute in order without branching/looping). If an if (or other branching/looping construct) occurs before the target assignment, the analysis bails out and returns false. Under that policy, an assignment inside an if branch is intentionally not treated as “first,” even if it is the first write along that branch, because proving that requires control-flow reasoning.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see that handling branches requires control-flow reasoning or a control-flow graph. It can be done purely (and simply) in the AST. If you don't want to do it, that is one thing, but please provide the correct rationale. I think the wording you are looking for is "lexically first"; "AST-only" does not convey anything to me. |
||
| } else { | ||
| // ::error: [required.method.not.called] | ||
| s = new FileInputStream("test2.txt"); // false positive: first write in this branch | ||
| } | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| public FirstAssignmentInConditional(boolean b1, boolean b2) { | ||
| try { | ||
| if (b1) { | ||
| if (b2) { | ||
| // ::error: [required.method.not.called] | ||
| s = new FileInputStream("test1.txt"); // false positive | ||
| } else { | ||
| // ::error: [required.method.not.called] | ||
| s = new FileInputStream("test2.txt"); // false positive | ||
| } | ||
| } else { | ||
| if (b2) { | ||
| // ::error: [required.method.not.called] | ||
| s = new FileInputStream("test1.txt"); // false positive | ||
| } else { | ||
| // ::error: [required.method.not.called] | ||
| s = new FileInputStream("test2.txt"); // false positive | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
mernst marked this conversation as resolved.
|
||
|
|
||
| @EnsuresCalledMethods(value = "this.s", methods = "close") | ||
| public void close() { | ||
| try { | ||
| s.close(); | ||
| } catch (Exception e) { | ||
| // ignore | ||
| } | ||
| } | ||
|
mernst marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Test: Field has no initializer and is first assigned in constructor in a try-catch block. | ||
| // Expected: No warning in constructor, warning in open(). | ||
|
|
||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class FirstAssignmentInConstructor { | ||
| private @Owning FileInputStream s; | ||
|
|
||
| public FirstAssignmentInConstructor() { | ||
| try { | ||
| s = new FileInputStream("test.txt"); // no warning | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| // :: error: [missing.creates.mustcall.for] | ||
| public void open() { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| s = new FileInputStream("test.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| @EnsuresCalledMethods(value = "this.s", methods = "close") | ||
| public void close() { | ||
| try { | ||
| s.close(); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Test: Field has no initializer and is first assigned in constructor, but in a constructor block. | ||
| // Expected: No warning in the first assignment in the constructor block, warning in any later | ||
| // assignments in the constructor and in open(). | ||
|
|
||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class FirstAssignmentInConstructorBlock { | ||
| private @Owning FileInputStream s; | ||
|
|
||
| static FileInputStream s2; | ||
| static FileInputStream s3; | ||
|
|
||
| public FirstAssignmentInConstructorBlock() { | ||
| s = s2; | ||
| } | ||
|
|
||
| public FirstAssignmentInConstructorBlock(boolean b) { | ||
| { | ||
| s = s2; | ||
| } | ||
| } | ||
|
|
||
| public FirstAssignmentInConstructorBlock(int i) { | ||
| { | ||
| s = s2; | ||
| } | ||
| // :: error: [required.method.not.called] | ||
| s = s2; | ||
| } | ||
|
|
||
| public FirstAssignmentInConstructorBlock(float f) { | ||
| s = s2; | ||
| { | ||
| // :: error: [required.method.not.called] | ||
| s = s2; | ||
| } | ||
| } | ||
|
|
||
| public FirstAssignmentInConstructorBlock(byte b) { | ||
| { | ||
| s = s3; | ||
| } | ||
| // :: error: [required.method.not.called] | ||
| s = s2; | ||
| } | ||
|
|
||
| public FirstAssignmentInConstructorBlock(char c) { | ||
| s = s2; | ||
| { | ||
| // :: error: [required.method.not.called] | ||
| s = s3; | ||
| } | ||
| } | ||
|
|
||
| // :: error: [missing.creates.mustcall.for] | ||
| public void open() { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| s = new FileInputStream("test.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| @EnsuresCalledMethods(value = "this.s", methods = "close") | ||
| public void close() { | ||
| try { | ||
| s.close(); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // Test: Field has a non-null inline initializer and is reassigned in constructor and open(). | ||
| // Expected: Warning in constructor and in open(). | ||
|
|
||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class InlineInitializerLeak { | ||
| private @Owning FileInputStream s = new FileInputStream("test.txt"); | ||
|
|
||
| public InlineInitializerLeak() throws Exception { | ||
| // :: error: [required.method.not.called] | ||
| s = new FileInputStream("test.txt"); | ||
| } | ||
|
|
||
| // :: error: [missing.creates.mustcall.for] | ||
| public void open() { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| s = new FileInputStream("test.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| @EnsuresCalledMethods(value = "this.s", methods = "close") | ||
| public void close() { | ||
| try { | ||
| s.close(); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Test: Field is assigned in an instance initializer block and reassigned later. | ||
| // Expected: Warning in initializer block, constructor and open(). | ||
|
|
||
| import java.io.FileInputStream; | ||
| import org.checkerframework.checker.calledmethods.qual.*; | ||
| import org.checkerframework.checker.mustcall.qual.*; | ||
|
|
||
| @InheritableMustCall({"close"}) | ||
| class InstanceInitializerBlockLeak { | ||
| private @Owning FileInputStream f; | ||
|
|
||
| { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| f = new FileInputStream("file.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| public InstanceInitializerBlockLeak() { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| f = new FileInputStream("file.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| // :: error: [missing.creates.mustcall.for] | ||
| public void open() { | ||
| try { | ||
| // :: error: [required.method.not.called] | ||
| f = new FileInputStream("file.txt"); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
|
|
||
| @EnsuresCalledMethods(value = "this.f", methods = "close") | ||
| public void close() { | ||
| try { | ||
| f.close(); | ||
| } catch (Exception e) { | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.