Skip to content

Commit 207ed1e

Browse files
committed
JS: Add query for measuring call graph quality
1 parent 53d4b2d commit 207ed1e

File tree

1 file changed

+198
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)