Skip to content

Commit 3b126d9

Browse files
authored
Merge pull request #1488 from asger-semmle/call-graph-metric
Approved by xiemaisi
2 parents 757ec97 + ff4d6ec commit 3b126d9

File tree

5 files changed

+259
-0
lines changed

5 files changed

+259
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/**
2+
* Provides predicates for measuring the quality of the call graph, that is,
3+
* the number of calls that could be resolved to a callee.
4+
*/
5+
6+
import javascript
7+
8+
private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps
9+
private import semmle.javascript.dependencies.Dependencies
10+
private import semmle.javascript.dependencies.FrameworkLibraries
11+
private import semmle.javascript.frameworks.Testing
12+
private import DataFlow
13+
14+
/**
15+
* Gets the root folder of the snapshot.
16+
*
17+
* This is selected as the location for project-wide metrics.
18+
*/
19+
Folder projectRoot() { result.getRelativePath() = "" }
20+
21+
/** A file we ignore because it is a test file or compiled/generated/bundled code. */
22+
class IgnoredFile extends File {
23+
IgnoredFile() {
24+
any(Test t).getFile() = this
25+
or
26+
getRelativePath().regexpMatch("(?i).*/test(case)?s?/.*")
27+
or
28+
getBaseName().regexpMatch("(?i)(.*[._\\-]|^)(min|bundle|concat|spec|tests?)\\.[a-zA-Z]+")
29+
or
30+
exists(TopLevel tl | tl.getFile() = this |
31+
tl.isExterns()
32+
or
33+
tl instanceof FrameworkLibraryInstance
34+
)
35+
}
36+
}
37+
38+
/** An call site that is relevant for analysis quality. */
39+
class RelevantInvoke extends InvokeNode {
40+
RelevantInvoke() { not getFile() instanceof IgnoredFile }
41+
}
42+
43+
/**
44+
* Holds if `name` is a the name of an external module.
45+
*/
46+
predicate isExternalLibrary(string name) {
47+
// Mentioned in package.json
48+
any(Dependency dep).info(name, _) or
49+
// Node.js built-in
50+
name = "assert" or
51+
name = "async_hooks" or
52+
name = "child_process" or
53+
name = "cluster" or
54+
name = "crypto" or
55+
name = "dns" or
56+
name = "domain" or
57+
name = "events" or
58+
name = "fs" or
59+
name = "http" or
60+
name = "http2" or
61+
name = "https" or
62+
name = "inspector" or
63+
name = "net" or
64+
name = "os" or
65+
name = "path" or
66+
name = "perf_hooks" or
67+
name = "process" or
68+
name = "punycode" or
69+
name = "querystring" or
70+
name = "readline" or
71+
name = "repl" or
72+
name = "stream" or
73+
name = "string_decoder" or
74+
name = "timer" or
75+
name = "tls" or
76+
name = "trace_events" or
77+
name = "tty" or
78+
name = "dgram" or
79+
name = "url" or
80+
name = "util" or
81+
name = "v8" or
82+
name = "vm" or
83+
name = "worker_threads" or
84+
name = "zlib"
85+
}
86+
87+
/**
88+
* Holds if the global variable `name` is defined externally.
89+
*/
90+
predicate isExternalGlobal(string name) {
91+
exists(ExternalGlobalDecl decl |
92+
decl.getName() = name
93+
)
94+
or
95+
exists(Dependency dep |
96+
// If name is never assigned anywhere, and it coincides with a dependency,
97+
// it's most likely coming from there.
98+
dep.info(name, _) and
99+
not exists(Assignment assign |
100+
assign.getLhs().(GlobalVarAccess).getName() = name
101+
)
102+
)
103+
or
104+
name = "_"
105+
}
106+
107+
/**
108+
* Gets a node that was derived from an import of `moduleName`.
109+
*
110+
* This is a rough approximation as it follows all property reads, invocations,
111+
* and callbacks, so some of these might refer to internal objects.
112+
*
113+
* Additionally, we don't recognize when a project imports another file in the
114+
* same project using its module name (for example import "vscode" from inside the vscode project).
115+
*/
116+
SourceNode externalNode() {
117+
exists(string moduleName |
118+
result = moduleImport(moduleName) and
119+
isExternalLibrary(moduleName)
120+
)
121+
or
122+
exists(string name |
123+
result = globalVarRef(name) and
124+
isExternalGlobal(name)
125+
)
126+
or
127+
result = DOM::domValueRef()
128+
or
129+
result = jquery()
130+
or
131+
result = externalNode().getAPropertyRead()
132+
or
133+
result = externalNode().getAnInvocation()
134+
or
135+
result = externalNode().(InvokeNode).getCallback(_).getParameter(_)
136+
}
137+
138+
/**
139+
* Gets a data flow node that can be resolved to a callback.
140+
*
141+
* These are not part of the static call graph, but the data flow analysis can
142+
* track them, so we consider them resolved.
143+
*/
144+
SourceNode resolvableCallback() {
145+
result instanceof FunctionNode
146+
or
147+
exists(Node arg |
148+
FlowSteps::argumentPassing(_, arg, _, result) and
149+
resolvableCallback().flowsTo(arg)
150+
)
151+
}
152+
153+
/**
154+
* A call site that can be resolved to a function in the same project.
155+
*/
156+
class ResolvableCall extends RelevantInvoke {
157+
ResolvableCall() {
158+
FlowSteps::calls(this, _)
159+
or
160+
this = resolvableCallback().getAnInvocation()
161+
}
162+
}
163+
164+
/**
165+
* A call site that is believed to call an external function.
166+
*/
167+
class ExternalCall extends RelevantInvoke {
168+
ExternalCall() {
169+
not this instanceof ResolvableCall and // avoid double counting
170+
(
171+
// Call to modelled external library
172+
this = externalNode()
173+
or
174+
// 'require' call or similar
175+
this = moduleImport(_)
176+
or
177+
// Resolved to externs file
178+
exists(this.(InvokeNode).getACallee(1))
179+
or
180+
// Modelled as taint step but isn't from an NPM module, for example, `substring` or `push`.
181+
exists(TaintTracking::AdditionalTaintStep step |
182+
step.step(_, this)
183+
or
184+
step.step(this.getAnArgument(), _)
185+
)
186+
)
187+
}
188+
}
189+
190+
/**
191+
* A call site that could not be resolved.
192+
*/
193+
class UnresolvableCall extends RelevantInvoke {
194+
UnresolvableCall() {
195+
not this instanceof ResolvableCall and
196+
not this instanceof ExternalCall
197+
}
198+
}
199+
200+
/**
201+
* A call that is believed to call a function within the same project.
202+
*/
203+
class NonExternalCall extends RelevantInvoke {
204+
NonExternalCall() {
205+
not this instanceof ExternalCall
206+
}
207+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* @name Resolvable call site candidates
3+
* @description The number of non-external calls in the program.
4+
* @kind metric
5+
* @metricType project
6+
* @metricAggregate sum
7+
* @tags meta
8+
* @id js/meta/resolvable-call-candidates
9+
*/
10+
import javascript
11+
import CallGraphQuality
12+
13+
select projectRoot(), count(NonExternalCall call)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* @name Resolvable call ratio
3+
* @description The percentage of non-external calls that can be resolved to a target.
4+
* @kind metric
5+
* @metricType project
6+
* @metricAggregate sum min max avg
7+
* @tags meta
8+
* @id js/meta/resolvable-call-ratio
9+
*/
10+
import javascript
11+
import CallGraphQuality
12+
13+
select projectRoot(), 100.0 * count(ResolvableCall call) / (float) count(NonExternalCall call)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* @name Resolvable calls
3+
* @description The number of calls that could be resolved to its target.
4+
* @kind metric
5+
* @metricType project
6+
* @metricAggregate sum
7+
* @tags meta
8+
* @id js/meta/resolvable-calls
9+
*/
10+
import javascript
11+
import CallGraphQuality
12+
13+
select projectRoot(), count(ResolvableCall call)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* @name Unresolvable calls
3+
* @description The number of calls that could not be resolved to a target.
4+
* @kind metric
5+
* @metricType project
6+
* @metricAggregate sum
7+
* @tags meta
8+
* @id js/meta/unresolvable-calls
9+
*/
10+
import javascript
11+
import CallGraphQuality
12+
13+
select projectRoot(), count(UnresolvableCall call)

0 commit comments

Comments
 (0)