Skip to content

Commit e6df264

Browse files
committed
Ruby: Report module declarations to model editor
This allows us to render type relations between modules/classes, not just methods.
1 parent c1c258f commit e6df264

File tree

3 files changed

+92
-37
lines changed

3 files changed

+92
-37
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,11 @@ class ModuleNode instanceof Module {
11441144
bindingset[this]
11451145
pragma[inline]
11461146
API::Node trackInstance() { result = API::Internal::getModuleInstance(this) }
1147+
1148+
/**
1149+
* Holds if this is a built-in module, e.g. `Object`.
1150+
*/
1151+
predicate isBuiltin() { super.isBuiltin() }
11471152
}
11481153

11491154
/**

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: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ private import codeql.ruby.frameworks.core.Gem
99
private import codeql.ruby.frameworks.data.ModelsAsData
1010
private import codeql.ruby.frameworks.data.internal.ApiGraphModelsExtensions
1111
private import queries.modeling.internal.Util as Util
12+
private import codeql.util.Unit
13+
private import codeql.ruby.AST
14+
private import codeql.ruby.ast.Module
1215

1316
/** Holds if the given callable is not worth supporting. */
1417
private predicate isUninteresting(DataFlow::MethodNode c) {
@@ -26,56 +29,78 @@ private predicate gemFileStep(Gem::GemSpec gem, Folder folder, int n) {
2629
}
2730

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

34-
File getFile() { result = this.getLocation().getFile() }
44+
abstract class Endpoint instanceof AstNode {
45+
string getNamespace() { result = getNamespace(this.(AstNode).getLocation().getFile()) }
3546

36-
string getName() { result = this.getMethodName() }
47+
string getFileName() { result = this.(AstNode).getLocation().getFile().getBaseName() }
3748

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

74+
DataFlow::MethodNode getNode() { result = methodNode }
75+
76+
override string getName() { result = methodNode.getMethodName() }
77+
5278
/**
5379
* Gets the unbound type name of this endpoint.
5480
*/
55-
bindingset[this]
56-
string getTypeName() {
81+
override string getType() {
5782
result =
58-
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getMethodName()) = this)
83+
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getName()) = methodNode)
5984
.getQualifiedName() or
6085
result =
61-
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getMethodName()) = this)
86+
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getName()) = methodNode)
6287
.getQualifiedName() + "!"
6388
}
6489

6590
/**
6691
* Gets the parameter types of this endpoint.
6792
*/
68-
bindingset[this]
69-
string getParameterTypes() {
93+
override string getParameters() {
7094
// For now, return the names of postional and keyword parameters. We don't always have type information, so we can't return type names.
7195
// We don't yet handle splat params or block params.
7296
result =
7397
"(" +
7498
concat(string key, string value |
75-
value = any(int i | i.toString() = key | this.asCallable().getParameter(i)).getName()
99+
value =
100+
any(int i | i.toString() = key | methodNode.asCallable().getParameter(i)).getName()
76101
or
77102
exists(DataFlow::ParameterNode param |
78-
param = this.asCallable().getKeywordParameter(key)
103+
param = methodNode.asCallable().getKeywordParameter(key)
79104
|
80105
value = key + ":"
81106
)
@@ -90,11 +115,11 @@ class Endpoint extends DataFlow::MethodNode {
90115

91116
/** Holds if this API is a known source. */
92117
pragma[nomagic]
93-
abstract predicate isSource();
118+
predicate isSource() { this.getNode() instanceof SourceCallable }
94119

95120
/** Holds if this API is a known sink. */
96121
pragma[nomagic]
97-
abstract predicate isSink();
122+
predicate isSink() { this.getNode() instanceof SinkCallable }
98123

99124
/** Holds if this API is a known neutral. */
100125
pragma[nomagic]
@@ -108,9 +133,11 @@ class Endpoint extends DataFlow::MethodNode {
108133
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
109134
}
110135

111-
boolean getSupportedStatus() { if this.isSupported() then result = true else result = false }
136+
override boolean getSupportedStatus() {
137+
if this.isSupported() then result = true else result = false
138+
}
112139

113-
string getSupportedType() {
140+
override string getSupportedType() {
114141
this.isSink() and result = "sink"
115142
or
116143
this.isSource() and result = "source"
@@ -132,7 +159,7 @@ string methodClassification(Call method) {
132159

133160
class TestFile extends File {
134161
TestFile() {
135-
this.getRelativePath().regexpMatch(".*(test|spec).+") and
162+
this.getRelativePath().regexpMatch(".*(test|spec|examples).+") and
136163
not this.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
137164
}
138165
}
@@ -164,10 +191,32 @@ class SourceCallable extends DataFlow::CallableNode {
164191
}
165192

166193
/**
167-
* A class of effectively public callables from source code.
194+
* A module defined in source code
168195
*/
169-
class PublicEndpointFromSource extends Endpoint {
170-
override predicate isSource() { this instanceof SourceCallable }
196+
class ModuleEndpoint extends Endpoint {
197+
private DataFlow::ModuleNode moduleNode;
198+
199+
ModuleEndpoint() {
200+
this =
201+
min(AstNode n, Location loc |
202+
n = moduleNode.getADeclaration() and
203+
loc = n.getLocation()
204+
|
205+
n order by loc.getFile().getAbsolutePath(), loc.getStartLine(), loc.getStartColumn()
206+
) and
207+
not moduleNode.(Module).isBuiltin() and
208+
not moduleNode.getLocation().getFile() instanceof TestFile
209+
}
210+
211+
DataFlow::ModuleNode getNode() { result = moduleNode }
212+
213+
override string getType() { result = this.getNode().getQualifiedName() }
214+
215+
override string getName() { result = "" }
216+
217+
override string getParameters() { result = "" }
218+
219+
override boolean getSupportedStatus() { result = false }
171220

172-
override predicate isSink() { this instanceof SinkCallable }
221+
override string getSupportedType() { result = "" }
173222
}

0 commit comments

Comments
 (0)