Skip to content

Commit a300eed

Browse files
committed
ruby: first sketches
1 parent 6b182c5 commit a300eed

File tree

1 file changed

+49
-0
lines changed

1 file changed

+49
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @id dsl/could-be-batched
3+
* @kind problem
4+
* @problem.severity info
5+
* @problem.type CodeSmell
6+
* @problem.description Use `ActiveRecord::Relation#in_batches` to process records in batches.
7+
* @problem.links https://api.rubyonrails.org/classes/ActiveRecord/Batches/ClassMethods.html#method-i-in_batches
8+
*/
9+
10+
import ruby
11+
// private import codeql.ruby.CFG
12+
import codeql.ruby.Concepts
13+
import codeql.ruby.frameworks.ActiveRecord
14+
15+
// collection.each { |item| expensiveCall(item) }
16+
predicate expensiveCall(DataFlow::CallNode c) { c instanceof SqlExecution }
17+
18+
string loopMethodName() {
19+
// TODO: check these
20+
result in ["each", "map", "foreach", "flat_map", "do"]
21+
}
22+
23+
class LoopingCall extends DataFlow::CallNode {
24+
DataFlow::CallableNode loopBlock;
25+
26+
LoopingCall() {
27+
this.getMethodName() = loopMethodName() and loopBlock = this.getBlock().asCallable()
28+
}
29+
30+
DataFlow::CallableNode getLoopBlock() { result = loopBlock }
31+
}
32+
33+
predicate happensInLoop(DataFlow::CallNode e) {
34+
any(LoopingCall loop).getLoopBlock().asCallableAstNode() = e.asExpr().getScope()
35+
}
36+
37+
// Active Record
38+
/** A call to e.g. `user.update_attribute(name, "foo")` */
39+
private class LoadElementsCall extends ActiveRecordInstanceMethodCall {
40+
LoadElementsCall() { this.getMethodName() in ["latest", "find_by", "where"] }
41+
}
42+
43+
// from LoopingCall loopCall
44+
// select loopCall, loopCall.getLoopBlock()
45+
from DataFlow::CallNode call
46+
where
47+
happensInLoop(call) and //and expensiveCall(call)
48+
call instanceof LoadElementsCall
49+
select call, "This call happens in a loop"

0 commit comments

Comments
 (0)