Skip to content

Commit 9211043

Browse files
committed
ruby: clean up logic and add test
use the CFG more than the AST
1 parent 9d81013 commit 9211043

File tree

2 files changed

+27
-12
lines changed

2 files changed

+27
-12
lines changed

ruby/ql/src/queries/performance/DatabaseQueryInLoop.ql

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import codeql.ruby.ast.internal.Constant
1414
import codeql.ruby.Concepts
1515
import codeql.ruby.frameworks.ActiveRecord
1616
private import codeql.ruby.TaintTracking
17+
private import codeql.ruby.CFG
18+
private import codeql.ruby.controlflow.internal.Guards as Guards
1719

1820
/** Gets the name of a built-in method that involves a loop operation. */
1921
string getALoopMethodName() {
@@ -36,21 +38,26 @@ class LoopingCall extends DataFlow::CallNode {
3638
predicate executesCall(DataFlow::CallNode c) { c.asExpr().getScope() = loopScope }
3739
}
3840

39-
/** Holds if `ar` influences `guard`, which may control the execution of a loop. */
40-
predicate usedInLoopControlGuard(ActiveRecordInstance ar, DataFlow::Node guard) {
41-
TaintTracking::localTaint(ar, guard) and
42-
guard = guardForLoopControl(_, _)
41+
/** Holds if `ar` influences a guard that may control the execution of a loop. */
42+
predicate usedInLoopControlGuard(ActiveRecordInstance ar) {
43+
exists(DataFlow::Node insideGuard, CfgNodes::ExprCfgNode guard |
44+
// For a guard like `cond && ar`, the whole guard will not be tainted
45+
// so we need to look at the taint of the individual parts.
46+
insideGuard.asExpr().getExpr() = guard.getExpr().getAChild*()
47+
|
48+
TaintTracking::localTaint(ar, insideGuard) and
49+
guardForLoopControl(guard, _)
50+
)
4351
}
4452

45-
/** Gets a dataflow node that is used to decide whether to break a loop. */
46-
DataFlow::Node guardForLoopControl(ConditionalExpr cond, Stmt control) {
47-
result.asExpr().getAstNode() = cond.getCondition().getAChild*() and
53+
/** Holds if `guard` controls `break` and `break` would break out of a loop. */
54+
predicate guardForLoopControl(CfgNodes::ExprCfgNode guard, CfgNodes::AstCfgNode break) {
55+
Guards::guardControlsBlock(guard, break.getBasicBlock(), _) and
4856
(
49-
control.(MethodCall).getMethodName() = "raise"
57+
break.(CfgNodes::ExprNodes::MethodCallCfgNode).getMethodName() = "raise"
5058
or
51-
control instanceof NextStmt
52-
) and
53-
control = cond.getBranch(_).getAChild()
59+
break instanceof CfgNodes::ReturningCfgNode
60+
)
5461
}
5562

5663
from LoopingCall loop, ActiveRecordModelFinderCall call
@@ -59,7 +66,7 @@ where
5966
// Disregard loops over constants
6067
not isArrayConstant(loop.getReceiver().asExpr(), _) and
6168
// Disregard cases where the looping is influenced by the query result
62-
not usedInLoopControlGuard(call, _) and
69+
not usedInLoopControlGuard(call) and
6370
// Only report calls that are likely to be expensive
6471
not call.getMethodName() in ["new", "create"]
6572
select call,

ruby/ql/test/query-tests/performance/DatabaseQueryInLoop/DatabaseQueryInLoop.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ def test
4242
end
4343
end
4444

45+
# more complicated condition
46+
names.map do |name|
47+
user = User.where(login: name).pluck(:id).first
48+
unless cond && user
49+
raise Error.new("User '#{name}' not found")
50+
end
51+
end
52+
4553
# skipping through the loop when users are not relevant
4654
names.map do |name|
4755
user = User.where(login: name).pluck(:id).first

0 commit comments

Comments
 (0)