Skip to content

Suppresses RLC non-final field overwrite warning for safe constructor field initialization#7050

Open
iamsanjaymalakar wants to merge 119 commits intotypetools:masterfrom
iamsanjaymalakar:7049-dev
Open

Suppresses RLC non-final field overwrite warning for safe constructor field initialization#7050
iamsanjaymalakar wants to merge 119 commits intotypetools:masterfrom
iamsanjaymalakar:7049-dev

Conversation

@iamsanjaymalakar
Copy link
Copy Markdown
Member

@iamsanjaymalakar iamsanjaymalakar commented Apr 18, 2025

Summary by CodeRabbit

  • New Features

    • Enhanced resource leak checker to suppress false-positive warnings when a private field is assigned for the first time within a constructor.
  • Tests

    • Added comprehensive test suite covering various constructor initialization scenarios, including inline initializers, instance initializers, method calls, and conditional branching.

@iamsanjaymalakar iamsanjaymalakar self-assigned this Apr 18, 2025
Copy link
Copy Markdown
Contributor

@kelloggm kelloggm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good, just a few minor comments. I'd like to take one more look before we merge, though.

@mernst mernst changed the title Suppresses RLC non-final field overwrite warning for safe constructor… #7049 Suppresses RLC non-final field overwrite warning for safe constructor field initialization Apr 18, 2025
@mernst
Copy link
Copy Markdown
Member

mernst commented Apr 18, 2025

@iamsanjaymalakar The typecheck job is not passing.

kelloggm
kelloggm previously approved these changes Jun 6, 2025
Copy link
Copy Markdown
Contributor

@kelloggm kelloggm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I don't need to review again. Please address these comments and then request a review from Mike.

@mernst
Copy link
Copy Markdown
Member

mernst commented Jul 10, 2025

@iamsanjaymalakar Martin asked you to assign me when you had addressed his comments. Have you addressed them?

@iamsanjaymalakar iamsanjaymalakar assigned mernst and unassigned kelloggm Jul 14, 2025
@iamsanjaymalakar
Copy link
Copy Markdown
Member Author

@iamsanjaymalakar Martin asked you to assign me when you had addressed his comments. Have you addressed them?

Yes, I have. You can take a look.

Copy link
Copy Markdown
Member

@mernst mernst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clarify some documentation, and update the code if necessary. (I didn't read the code because there is no point in doing so until the documentation is clear.)

@mernst mernst assigned iamsanjaymalakar and unassigned mernst Jul 14, 2025

public FirstAssignmentInConditional(boolean b) {
try {
if (b) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how it would turn into a CFG dependency.

try {
if (b) {
// ::error: (required.method.not.called)
s = new FileInputStream("test1.txt"); // false positive: first write in this branch
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

@mernst
Copy link
Copy Markdown
Member

mernst commented Apr 14, 2026

@iamsanjaymalakar When you address a code review comment, please mark it as "resolved" so that we know its status and it does not clutter this code review webpage.

}
} else if (stmt instanceof TryTree tryTree) {

// finally introduces ordering across try/catch that requires CFG reasoning
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it require CFG reasoning? Here is a simple suggestion:
If the finally block cannot assign to the field, then it need not cause the whole scan to return REASSIGNMENT.

Another way to say this is that you might as well scan the finally block, as it is run at the end.
In a regular statement block, whether the second statement is executed depends on whether the first throws an exception, but that does not prevent your algorithm from scanning it.

If no previous code (in the try body or in any catch) can assign the field, then an assignment in the finally block can be the first assignment. So move handling of the finally block to the end of the handling of TryTree, and scan it.

}

// try-with-resources evaluates resource initializers before the try body. Modeling those
// effects would require extra handling, so reject.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it require extra handling? I think it is enough to scan the variable declarations, one after another, just as the code scans variable declarations in any other block.

}

// If any catch assigns the field, then initialization is path-dependent (try vs catch).
// Without control-flow reasoning, conservatively reject.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is control-flow reasoning (as opposed to tree path reasoning) required? If the body of the try definitely does not assign the variable, then each catch can be processed independently, and the assignment may be the first. If the body of the try may assign the variable, then there is already an answer before getting to the catch blocks. More generally, process the try block in execution order (which is what comments claim about the algorithm anyway).

Regarding the note about "initialization if path-dependent": that is also true for if statements and many other constructs, but that fact does not prevent analysis of those constructs. This file is only trying to ask: if statement S is executed, then is it the first assignment to the field?

Comment on lines +241 to +253

boolean targetInThen = containsTargetAssignment(thenStmt, targetAssignment);
boolean targetInElse = containsTargetAssignment(elseStmt, targetAssignment);

// If the target assignment is in one branch, only that branch is on the path to the
// target assignment. Return the result after scanning that branch.
if (targetInThen) {
return scanForFirstWrite(List.of(thenStmt), targetAssignment, targetField, cmAtf);
}
if (targetInElse) {
return scanForFirstWrite(List.of(elseStmt), targetAssignment, targetField, cmAtf);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the need for any of this code.
The below code processes the then and the else.
It can return the following result:

  • If either the then or the else is FIRST_ASSIGNMENT, that is the outcome of the if statement.
  • If either the then or the else is REASSIGNMENT, that is the outcome of the if statement.
  • Otherwise, both are UNASSIGNED, so that is the outcome of the if statement.

}
return FirstWriteScanResult.REASSIGNMENT;
} else {
// Any other statement kind requires control-flow-aware reasoning that this helper does not
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with this claim about control-flow-aware reasoning. For example, an AssertTree consists of two expressions that can be processed one after the other.

EmptyStatementTree, LabeledStatementTree, and ReturnTree, are also easy to handle.

If you want to say that you are all other statement kinds because you don't want to put in the work, that is OK (and I agree that some of them are more trouble than they are worth or require control-flow reasoning), but please don't make blanket statements that are not true.

* not handle field initializers, static or instance initializer blocks, or statement-level
* control-flow constructs.
*
* <p>If it is used on an unsupported fragment, the result is conservative only with respect to
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "unsupported fragment" mean an unsupported Expression type or an unsupported expression context?

// - current assignment → first write → FIRST_ASSIGNMENT
// - earlier assignment → not first → REASSIGNMENT
// TODO: Must scan the RHS, to catch a nested assignment to the same field within the RHS,
// e.g., this.f = (this.f = new Foo()).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO is done, is it not?

@Override
public FirstWriteScanResult visitMethodInvocation(MethodInvocationTree node, Void p) {
// Treat any side-effecting call before the target assignment as possibly assigning the field.
// An explicit super(...) call is also allowed,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly discuss whether this is an unsoundness. (I think it is.)

*/
private abstract static class BooleanShortCircuitScanner extends TreeScanner<Boolean, Void> {

/** Do not instantiate. */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is instantiated, so the comment is wrong. (Or if I am wrong about that, add a throw statement to the body.)

@FindDistinct Tree targetAssignment,
@FindDistinct VariableElement targetField,
RLCCalledMethodsAnnotatedTypeFactory cmAtf) {
return new ExpressionFirstWriteScanner(targetAssignment, targetField, cmAtf).scan(root, null);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every call to scanForFirstWrite(ExpressionTree, ...) creates a new ExpressionFirstWriteScanner. That seems wasteful. Furthermore, it highlights a difference between the way that statements are handled and how expressions are handled. Unify them. Here are two possibilities:

  • eliminate ExpressionFirstWriteScanner and make top-level private method scanForFirstWrite handle all Trees (including both statement trees and expression trees) by calling itself recursively, or
  • eliminate top-level private method scanForFirstWrite and expand ExpressionFirstWriteScanner into FirstWriteScanner that handles all Treess (including both statement trees and expression trees).

Either of those approaches can work. Given your current code structure, you may find the first one simpler to implement.

@mernst mernst removed their assignment Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants