Skip to content

Commit c96be39

Browse files
authored
Merge pull request github#15048 from hmac/hmac-model-editor-ruby-modules
Ruby: Model editor improvements
2 parents b083c35 + 22830c7 commit c96be39

File tree

9 files changed

+120
-39
lines changed

9 files changed

+120
-39
lines changed

ruby/ql/lib/codeql/ruby/ast/Module.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ class Module extends TModule {
173173
result.getParentModule() = this and
174174
result.getOwnModuleName() = name
175175
}
176+
177+
/**
178+
* Holds if this is a built-in module, e.g. `Object`.
179+
*/
180+
predicate isBuiltin() { isBuiltinModule(this) }
176181
}
177182

178183
/**

ruby/ql/lib/codeql/ruby/ast/internal/Module.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ private module Cached {
4242
result = getAnAssumedGlobalNamespacePrefix(n)
4343
}
4444

45+
cached
46+
predicate isBuiltinModule(Module m) { m = TResolved(builtin()) }
47+
4548
cached
4649
Module getSuperClass(Module cls) {
4750
cls = TResolved("Object") and result = TResolved("BasicObject")

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPublic.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,11 @@ class ModuleNode instanceof Module {
11721172
bindingset[this]
11731173
pragma[inline]
11741174
API::Node trackInstance() { result = API::Internal::getModuleInstance(this) }
1175+
1176+
/**
1177+
* Holds if this is a built-in module, e.g. `Object`.
1178+
*/
1179+
predicate isBuiltin() { super.isBuiltin() }
11751180
}
11761181

11771182
/**

ruby/ql/src/queries/modeling/internal/Types.qll

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
private import ruby
77
private import codeql.ruby.ApiGraphs
88
private import Util as Util
9+
private import codeql.ruby.ast.Module
10+
private import codeql.ruby.ast.internal.Module
911

1012
/**
1113
* Contains predicates for generating `typeModel`s that contain typing
@@ -42,5 +44,13 @@ module Types {
4244
valueHasTypeName(node.getAValueReachingSink(), type1) and
4345
Util::pathToNode(node, type2, path, true)
4446
)
47+
or
48+
// class Type2 < Type1
49+
// class Type2; include Type1
50+
exists(Module m1, Module m2 |
51+
m2.getAnImmediateAncestor() = m1 and not m2.isBuiltin() and not m1.isBuiltin()
52+
|
53+
m1.getQualifiedName() = type1 and m2.getQualifiedName() = type2 and path = ""
54+
)
4555
}
4656
}

ruby/ql/src/utils/modeleditor/FrameworkModeEndpoints.ql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
*/
88

99
import ruby
10+
import codeql.ruby.AST
1011
import ModelEditor
1112

12-
from PublicEndpointFromSource endpoint
13-
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
14-
endpoint.getParameterTypes(), endpoint.getSupportedStatus(), endpoint.getFile().getBaseName(),
13+
from Endpoint endpoint
14+
select endpoint, endpoint.getNamespace(), endpoint.getType(), endpoint.getName(),
15+
endpoint.getParameters(), endpoint.getSupportedStatus(), endpoint.getFileName(),
1516
endpoint.getSupportedType()

ruby/ql/src/utils/modeleditor/ModelEditor.qll

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,56 +25,75 @@ private predicate gemFileStep(Gem::GemSpec gem, Folder folder, int n) {
2525
}
2626

2727
/**
28-
* A callable method or accessor from either the Ruby Standard Library, a 3rd party library, or from the source.
28+
* Gets the namespace of an endpoint in `file`.
2929
*/
30-
class Endpoint extends DataFlow::MethodNode {
31-
Endpoint() { this.isPublic() and not isUninteresting(this) }
30+
string getNamespace(File file) {
31+
exists(Folder folder | folder = file.getParentContainer() |
32+
// The nearest gemspec to this endpoint, if one exists
33+
result = min(Gem::GemSpec g, int n | gemFileStep(g, folder, n) | g order by n).getName()
34+
or
35+
not gemFileStep(_, folder, _) and
36+
result = ""
37+
)
38+
}
3239

33-
File getFile() { result = this.getLocation().getFile() }
40+
abstract class Endpoint instanceof DataFlow::Node {
41+
string getNamespace() { result = getNamespace(super.getLocation().getFile()) }
3442

35-
string getName() { result = this.getMethodName() }
43+
string getFileName() { result = super.getLocation().getFile().getBaseName() }
3644

37-
/**
38-
* Gets the namespace of this endpoint.
39-
*/
40-
bindingset[this]
41-
string getNamespace() {
42-
exists(Folder folder | folder = this.getFile().getParentContainer() |
43-
// The nearest gemspec to this endpoint, if one exists
44-
result = min(Gem::GemSpec g, int n | gemFileStep(g, folder, n) | g order by n).getName()
45-
or
46-
not gemFileStep(_, folder, _) and
47-
result = ""
48-
)
45+
string toString() { result = super.toString() }
46+
47+
Location getLocation() { result = super.getLocation() }
48+
49+
abstract string getType();
50+
51+
abstract string getName();
52+
53+
abstract string getParameters();
54+
55+
abstract boolean getSupportedStatus();
56+
57+
abstract string getSupportedType();
58+
}
59+
60+
/**
61+
* A callable method or accessor from source code.
62+
*/
63+
class MethodEndpoint extends Endpoint instanceof DataFlow::MethodNode {
64+
MethodEndpoint() {
65+
this.isPublic() and
66+
not isUninteresting(this)
4967
}
5068

69+
DataFlow::MethodNode getNode() { result = this }
70+
71+
override string getName() { result = super.getMethodName() }
72+
5173
/**
5274
* Gets the unbound type name of this endpoint.
5375
*/
54-
bindingset[this]
55-
string getTypeName() {
76+
override string getType() {
5677
result =
57-
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getMethodName()) = this)
58-
.getQualifiedName() or
78+
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getName()) = this).getQualifiedName() or
5979
result =
60-
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getMethodName()) = this)
80+
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getName()) = this)
6181
.getQualifiedName() + "!"
6282
}
6383

6484
/**
6585
* Gets the parameter types of this endpoint.
6686
*/
67-
bindingset[this]
68-
string getParameterTypes() {
69-
// For now, return the names of postional and keyword parameters. We don't always have type information, so we can't return type names.
87+
override string getParameters() {
88+
// For now, return the names of positional and keyword parameters. We don't always have type information, so we can't return type names.
7089
// We don't yet handle splat params or block params.
7190
result =
7291
"(" +
7392
concat(string key, string value |
74-
value = any(int i | i.toString() = key | this.asCallable().getParameter(i)).getName()
93+
value = any(int i | i.toString() = key | super.asCallable().getParameter(i)).getName()
7594
or
7695
exists(DataFlow::ParameterNode param |
77-
param = this.asCallable().getKeywordParameter(key)
96+
param = super.asCallable().getKeywordParameter(key)
7897
|
7998
value = key + ":"
8099
)
@@ -89,11 +108,11 @@ class Endpoint extends DataFlow::MethodNode {
89108

90109
/** Holds if this API is a known source. */
91110
pragma[nomagic]
92-
abstract predicate isSource();
111+
predicate isSource() { this.getNode() instanceof SourceCallable }
93112

94113
/** Holds if this API is a known sink. */
95114
pragma[nomagic]
96-
abstract predicate isSink();
115+
predicate isSink() { this.getNode() instanceof SinkCallable }
97116

98117
/** Holds if this API is a known neutral. */
99118
pragma[nomagic]
@@ -107,9 +126,11 @@ class Endpoint extends DataFlow::MethodNode {
107126
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
108127
}
109128

110-
boolean getSupportedStatus() { if this.isSupported() then result = true else result = false }
129+
override boolean getSupportedStatus() {
130+
if this.isSupported() then result = true else result = false
131+
}
111132

112-
string getSupportedType() {
133+
override string getSupportedType() {
113134
this.isSink() and result = "sink"
114135
or
115136
this.isSource() and result = "source"
@@ -131,7 +152,7 @@ string methodClassification(Call method) {
131152

132153
class TestFile extends File {
133154
TestFile() {
134-
this.getRelativePath().regexpMatch(".*(test|spec).+") and
155+
this.getRelativePath().regexpMatch(".*(test|spec|examples).+") and
135156
not this.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
136157
}
137158
}
@@ -163,10 +184,32 @@ class SourceCallable extends DataFlow::CallableNode {
163184
}
164185

165186
/**
166-
* A class of effectively public callables from source code.
187+
* A module defined in source code
167188
*/
168-
class PublicEndpointFromSource extends Endpoint {
169-
override predicate isSource() { this instanceof SourceCallable }
189+
class ModuleEndpoint extends Endpoint {
190+
private DataFlow::ModuleNode moduleNode;
191+
192+
ModuleEndpoint() {
193+
this =
194+
min(DataFlow::Node n, Location loc |
195+
n.asExpr().getExpr() = moduleNode.getADeclaration() and
196+
loc = n.getLocation()
197+
|
198+
n order by loc.getFile().getAbsolutePath(), loc.getStartLine(), loc.getStartColumn()
199+
) and
200+
not moduleNode.(Module).isBuiltin() and
201+
not moduleNode.getLocation().getFile() instanceof TestFile
202+
}
203+
204+
DataFlow::ModuleNode getNode() { result = moduleNode }
205+
206+
override string getType() { result = this.getNode().getQualifiedName() }
207+
208+
override string getName() { result = "" }
209+
210+
override string getParameters() { result = "" }
211+
212+
override boolean getSupportedStatus() { result = false }
170213

171-
override predicate isSink() { this instanceof SinkCallable }
214+
override string getSupportedType() { result = "" }
172215
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
| lib/module.rb:1:1:7:3 | M1 | mylib | M1 | | | false | module.rb | |
12
| lib/module.rb:2:3:3:5 | foo | mylib | M1 | foo | (x,y) | false | module.rb | |
23
| lib/module.rb:5:3:6:5 | self_foo | mylib | M1! | self_foo | (x,y) | false | module.rb | |
4+
| lib/mylib.rb:3:1:27:3 | A | mylib | A | | | false | mylib.rb | |
35
| lib/mylib.rb:4:3:5:5 | foo | mylib | A | foo | (x,y,key1:) | false | mylib.rb | |
46
| lib/mylib.rb:7:3:8:5 | bar | mylib | A | bar | (x) | false | mylib.rb | |
57
| lib/mylib.rb:10:3:11:5 | self_foo | mylib | A! | self_foo | (x,y) | false | mylib.rb | |
8+
| lib/mylib.rb:18:3:26:5 | ANested | mylib | A::ANested | | | false | mylib.rb | |
69
| lib/mylib.rb:19:5:20:7 | foo | mylib | A::ANested | foo | (x,y) | false | mylib.rb | |
10+
| lib/other.rb:3:1:8:3 | B | mylib | B | | | false | other.rb | |
711
| lib/other.rb:6:3:7:5 | foo | mylib | B | foo | (x,y) | false | other.rb | |
12+
| lib/other.rb:10:1:12:3 | C | mylib | C | | | false | other.rb | |
13+
| other_lib/lib/other_gem.rb:1:1:6:3 | OtherLib | other-lib | OtherLib | | | false | other_gem.rb | |
14+
| other_lib/lib/other_gem.rb:2:5:5:7 | A | other-lib | OtherLib::A | | | false | other_gem.rb | |
815
| other_lib/lib/other_gem.rb:3:9:4:11 | foo | other-lib | OtherLib::A | foo | (x,y) | false | other_gem.rb | |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
sourceModel
2+
sinkModel
3+
typeVariableModel
4+
typeModel
5+
| M1 | B | |
6+
summaryModel
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/modeling/GenerateModel.ql

0 commit comments

Comments
 (0)