Skip to content

Commit 674ed9b

Browse files
committed
ruby: refine query for uninitialised local variables
- there are places where uninitialised reads are intentional - there are also some places where they are impossible
1 parent 8d467c7 commit 674ed9b

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
In Ruby, raw identifiers like <code>x</code> can be both local variable accesses and method calls. It is a local variable access iff it is syntactically preceded by something that binds it (like an assignment).
8+
Consider the following example:
9+
</p>
10+
11+
<sample src="examples/UninitializedLocal.rb" />
12+
13+
<p>
14+
This will generate an alert on the last access to <code>m</code>, where it is not clear that the programmer intended to read from the local variable.
15+
</p>
16+
17+
</overview>
18+
<recommendation>
19+
20+
<p>
21+
Ensure that you check the control and data flow in the method carefully.
22+
Check that the variable reference is spelled correctly, perhaps the variable has been renamed and the reference needs to be updated.
23+
Another possibility is that an exception may be raised before the variable is assigned, in which case the read should be protected by a check for <code>nil</code>.
24+
</p>
25+
26+
</recommendation>
27+
28+
<references>
29+
30+
31+
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Dead_store">Dead store</a>.</li>
32+
33+
34+
35+
</references>
36+
</qhelp>

ruby/ql/src/queries/variables/UninitializedLocal.ql

+64-2
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,81 @@
55
* @kind problem
66
* @problem.severity error
77
* @id rb/uninitialized-local-variable
8-
* @tags reliability
8+
* @tags quality
9+
* reliability
910
* correctness
10-
* @precision low
11+
* @precision high
1112
*/
1213

1314
import codeql.ruby.AST
1415
import codeql.ruby.dataflow.SSA
16+
private import codeql.ruby.dataflow.internal.DataFlowPublic
17+
18+
predicate factor(Expr e, Expr factor) {
19+
factor = e
20+
or
21+
e.(BinaryOperation).getOperator() = ["||", "&&"] and
22+
factor = e.(BinaryOperation).getAnOperand()
23+
or
24+
e.(BinaryOperation).getOperator() = ["||", "&&"] and
25+
factor(e.(BinaryOperation).getAnOperand(), factor)
26+
}
27+
28+
predicate previousConjunct(Expr e, Expr prev) {
29+
exists(BinaryOperation b |
30+
b.getOperator() = "&&" and
31+
b.getRightOperand() = e
32+
|
33+
// 'prev' && 'e'
34+
prev = b.getLeftOperand()
35+
or
36+
// (... && 'prev') && 'e'
37+
b.getLeftOperand().(BinaryOperation).getOperator() = "&&" and
38+
prev = b.getLeftOperand().(BinaryOperation).getRightOperand()
39+
or
40+
// (subtree['prev'] && _) && 'e'
41+
b.getLeftOperand().(BinaryOperation).getOperator() = "&&" and
42+
previousConjunct(b.getLeftOperand().(BinaryOperation).getRightOperand(), prev)
43+
)
44+
}
45+
46+
Expr evaluatingMention(LocalVariableReadAccess read) {
47+
result = read
48+
or
49+
result.(AssignExpr).getLeftOperand() = read
50+
or
51+
result.(NotExpr).getOperand() = read
52+
}
1553

1654
class RelevantLocalVariableReadAccess extends LocalVariableReadAccess {
1755
RelevantLocalVariableReadAccess() {
1856
not exists(MethodCall c |
1957
c.getReceiver() = this and
2058
c.getMethodName() = "nil?"
59+
) and
60+
not exists(MethodCall c |
61+
c.getReceiver() = this and
62+
c.isSafeNavigation()
63+
) and
64+
// 'a' is fine to be uninitialised in 'a || ...'
65+
not exists(BinaryOperation b |
66+
b.getLeftOperand() = this and
67+
b.getOperator() = "||"
68+
) and
69+
// The second 'a' cannot be uninitialised in 'a && (...a...)'
70+
not exists(Expr parent |
71+
parent.getAChild*() = this and
72+
previousConjunct(parent, this.getVariable().getAnAccess())
73+
) and
74+
// Various guards
75+
not exists(ConditionalExpr c | factor(c.getCondition(), evaluatingMention(this))) and
76+
not exists(ConditionalExpr c | factor(c.getCondition(), this.getVariable().getAnAccess()) |
77+
this = c.getBranch(true).getAChild*()
78+
) and
79+
not exists(ConditionalLoop l |
80+
l.getCondition() = this.getVariable().getAnAccess() and
81+
l.entersLoopWhenConditionIs(false) and
82+
l.getAControlFlowNode().getBasicBlock().dominates(this.getAControlFlowNode().getBasicBlock())
2183
)
2284
}
2385
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
def m
2+
puts "m"
3+
end
4+
5+
def foo
6+
m # calls m above
7+
if false
8+
m = 0
9+
m # reads local variable m
10+
else
11+
end
12+
m # reads uninitialized local variable m, `nil`
13+
m2 # undefined local variable or method 'm2' for main (NameError)
14+
end

0 commit comments

Comments
 (0)