-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
ruby: refine rb/uninitialized-local-variable
#19205
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot wasn't able to review any files in this pull request.
Files not reviewed (1)
- ruby/ql/src/queries/variables/UninitializedLocal.ql: Language not supported
Tip: Copilot code review supports C#, Go, Java, JavaScript, Markdown, Python, Ruby and TypeScript, with more languages coming soon. Learn more
ef18662
to
48eae64
Compare
QHelp previews: ruby/ql/src/queries/variables/UninitializedLocal.qhelpPotentially uninitialized local variableIn Ruby, raw identifiers like def m
puts "m"
end
def foo
m # calls m above
if false
m = "0"
m # reads local variable m
else
end
m.strip # reads uninitialized local variable m, `nil`, and crashes
m2 # undefined local variable or method 'm2' for main (NameError)
end This will generate an alert on the last access to RecommendationEnsure that you check the control and data flow in the method carefully. Add a check for References |
48eae64
to
9530ac6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for Boolean operators seems rather complex to me. I wonder if what we are after is allowing for uninitialized accesses, when they happen in a Boolean context, and then also filter out accesses that are guarded by nil
checks, such as the implicit nil
check of a
in a && a.safe
.
end | ||
|
||
def test_guards | ||
if (a = 3 && a) #$ SPURIOUS: Alert |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This actually parses as a = (3 && a)
, so the alert is correct. If you change it to (a = 3) && a
the alert disappears.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that is right, the alert is correct.
else | ||
set_a | ||
end | ||
end until a # OK |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the reason that this is OK is because nil
is treated as false
in a Boolean context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this loop will break once a
is not nil
.
Yes, I think that is a good characterization. I will attempt to express it as two simple predicates in this vein. |
0bc476f
to
86d04bd
Compare
- there are places where uninitialised reads are intentional - there are also some places where they are impossible
86d04bd
to
8555e8c
Compare
Rewrote and rebased. Thanks for the push, it became much nicer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for rewriting the query, I agree that it makes it much nicer.
@@ -0,0 +1,4 @@ | |||
--- | |||
category: queryMetadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this file should be deleted?
// guard is `!var` | ||
guard.getAstNode().(NotExpr).getOperand() = read.getVariable().getAnAccess() and | ||
branch = false | ||
or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This disjunct can be removed; the CFG automatically handles negation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nice!
or | ||
// guard is `!var.nil?` | ||
exists(MethodCall c | guard.getAstNode().(NotExpr).getOperand() = c | | ||
c.getReceiver() = read.getVariable().getAnAccess() and | ||
c.getMethodName() = "nil?" | ||
) and | ||
branch = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This disjunct can be removed; the CFG automatically handles negation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, nice! (That does deserve repeating...)
predicate isInBooleanContext(Expr e) { | ||
e = any(ConditionalExpr c).getCondition() | ||
or | ||
e = any(ConditionalLoop l).getCondition() | ||
or | ||
e = any(LogicalAndExpr n).getAnOperand() | ||
or | ||
e = any(LogicalOrExpr n).getAnOperand() | ||
or | ||
e = any(NotExpr n).getOperand() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This predicate has a lot of overlap with the inBooleanContext
predicate from Completion.qll
. However, that predicate cannot be used directly, but it highlights some missing cases. I suggest
private predicate inBooleanContext(AstNode n) {
exists(ConditionalExpr i |
n = i.getCondition()
or
inBooleanContext(i) and
n = i.getBranch(_)
)
or
n = any(ConditionalLoop parent).getCondition()
or
n = any(InClause parent).getCondition()
or
n = any(LogicalAndExpr n).getAnOperand()
or
n = any(LogicalOrExpr n).getAnOperand()
or
n = any(NotExpr n).getOperand()
or
n = any(StmtSequence parent | inBooleanContext(parent)).getLastStmt()
or
exists(CaseExpr c, WhenClause w |
not exists(c.getValue()) and
c.getABranch() = w
|
w.getPattern(_) = n
or
w = n
)
}
The CFG handles the negation
Co-authored-by: Tom Hvitved <hvitved@github.com>
found during triage
Interviewing a Ruby developer, I learned that dealing with nil is common practice. So alerts are mostly useful, if we can point to a place where this has gone wrong.
Started experiment on suite of projects that actually contain alerts (generated with MRVA). |
This has improved autofixes I hope it also helps humans
In preparation for shipping as a quality query, this PR removes some false positives for
rb/uninitialized-local-variable
:In addition, the query will now only alert if the uninitialized read is also the receiver of a method call.
This is to account for the common practice of producing 'nil' values and deal with them later.
The query now no longer tries to highlight that a 'nil' value may have been produced, but instead tries to highlight when it may not have been correctly dealt with.
The difference in alert counts can be seen here (once that run completes).
The first three rows are
Given the small number of alerts (along with local triage) the precision has been upped to 'high'.