Skip to content

Commit 61034be

Browse files
authored
Merge pull request #1844 from asger-semmle/more-type-info
Approved by xiemaisi
2 parents 89778ef + 3186942 commit 61034be

File tree

24 files changed

+243
-69
lines changed

24 files changed

+243
-69
lines changed

change-notes/1.23/analysis-javascript.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## General improvements
44

5+
* Support for the following frameworks and libraries has been improved:
6+
- [firebase](https://www.npmjs.com/package/firebase)
7+
58
## New queries
69

710
| **Query** | **Tags** | **Purpose** |

javascript/extractor/lib/typescript/src/main.ts

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,15 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
271271
getCurrentDirectory: () => basePath,
272272
});
273273

274+
for (let typeRoot of typeRoots || []) {
275+
traverseTypeRoot(typeRoot, "");
276+
}
277+
278+
for (let sourceFile of program.getSourceFiles()) {
279+
addModuleBindingsFromModuleDeclarations(sourceFile);
280+
addModuleBindingsFromFilePath(sourceFile);
281+
}
282+
274283
/** Concatenates two imports paths. These always use `/` as path separator. */
275284
function joinModulePath(prefix: string, suffix: string) {
276285
if (prefix.length === 0) return suffix;
@@ -300,36 +309,74 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
300309
if (sourceFile == null) {
301310
continue;
302311
}
303-
let symbol = typeChecker.getSymbolAtLocation(sourceFile);
304-
if (symbol == null) continue; // Happens if the source file is not a module.
305-
306-
let canonicalSymbol = getEffectiveExportTarget(symbol); // Follow `export = X` declarations.
307-
let symbolId = state.typeTable.getSymbolId(canonicalSymbol);
308-
309-
let importPath = (child === "index.d.ts")
310-
? importPrefix
311-
: joinModulePath(importPrefix, pathlib.basename(child, ".d.ts"));
312-
313-
// Associate the module name with this symbol.
314-
state.typeTable.addModuleMapping(symbolId, importPath);
315-
316-
// Associate global variable names with this module.
317-
// For each `export as X` declaration, the global X refers to this module.
318-
// Note: the `globalExports` map is stored on the original symbol, not the target of `export=`.
319-
if (symbol.globalExports != null) {
320-
symbol.globalExports.forEach((global: ts.Symbol) => {
321-
state.typeTable.addGlobalMapping(symbolId, global.name);
322-
});
323-
}
312+
addModuleBindingFromRelativePath(sourceFile, importPrefix, child);
324313
}
325314
}
326-
for (let typeRoot of typeRoots || []) {
327-
traverseTypeRoot(typeRoot, "");
315+
316+
/**
317+
* Emits module bindings for a module with relative path `folder/baseName`.
318+
*/
319+
function addModuleBindingFromRelativePath(sourceFile: ts.SourceFile, folder: string, baseName: string) {
320+
let symbol = typeChecker.getSymbolAtLocation(sourceFile);
321+
if (symbol == null) return; // Happens if the source file is not a module.
322+
323+
let stem = getStem(baseName);
324+
let importPath = (stem === "index")
325+
? folder
326+
: joinModulePath(folder, stem);
327+
328+
let canonicalSymbol = getEffectiveExportTarget(symbol); // Follow `export = X` declarations.
329+
let symbolId = state.typeTable.getSymbolId(canonicalSymbol);
330+
331+
// Associate the module name with this symbol.
332+
state.typeTable.addModuleMapping(symbolId, importPath);
333+
334+
// Associate global variable names with this module.
335+
// For each `export as X` declaration, the global X refers to this module.
336+
// Note: the `globalExports` map is stored on the original symbol, not the target of `export=`.
337+
if (symbol.globalExports != null) {
338+
symbol.globalExports.forEach((global: ts.Symbol) => {
339+
state.typeTable.addGlobalMapping(symbolId, global.name);
340+
});
341+
}
328342
}
329343

330-
// Emit module name bindings for external module declarations, i.e: `declare module 'X' {..}`
331-
// These can generally occur anywhere; they may or may not be on the type root path.
332-
for (let sourceFile of program.getSourceFiles()) {
344+
/**
345+
* Returns the basename of `file` without its extension, while treating `.d.ts` as a
346+
* single extension.
347+
*/
348+
function getStem(file: string) {
349+
if (file.endsWith(".d.ts")) {
350+
return pathlib.basename(file, ".d.ts");
351+
}
352+
let base = pathlib.basename(file);
353+
let dot = base.lastIndexOf('.');
354+
return dot === -1 || dot === 0 ? base : base.substring(0, dot);
355+
}
356+
357+
/**
358+
* Emits module bindings for a module based on its file path.
359+
*
360+
* This looks for enclosing `node_modules` folders to determine the module name.
361+
* This is needed for modules that ship their own type definitions as opposed to having
362+
* type definitions loaded from a type root (conventionally named `@types/xxx`).
363+
*/
364+
function addModuleBindingsFromFilePath(sourceFile: ts.SourceFile) {
365+
let fullPath = sourceFile.fileName;
366+
let index = fullPath.lastIndexOf('/node_modules/');
367+
if (index === -1) return;
368+
let relativePath = fullPath.substring(index + '/node_modules/'.length);
369+
// Ignore node_modules/@types folders here as they are typically handled as type roots.
370+
if (relativePath.startsWith("@types/")) return;
371+
let { dir, base } = pathlib.parse(relativePath);
372+
addModuleBindingFromRelativePath(sourceFile, dir, base);
373+
}
374+
375+
/**
376+
* Emit module name bindings for external module declarations, i.e: `declare module 'X' {..}`
377+
* These can generally occur anywhere; they may or may not be on the type root path.
378+
*/
379+
function addModuleBindingsFromModuleDeclarations(sourceFile: ts.SourceFile) {
333380
for (let stmt of sourceFile.statements) {
334381
if (ts.isModuleDeclaration(stmt) && ts.isStringLiteral(stmt.name)) {
335382
let symbol = (stmt as any).symbol;

javascript/ql/src/semmle/javascript/CanonicalNames.qll

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ class CanonicalName extends @symbol {
3737
/**
3838
* Gets the name of the external module represented by this canonical name, if any.
3939
*/
40-
string getExternalModuleName() { symbol_module(this, result) }
40+
string getExternalModuleName() {
41+
symbol_module(this, result)
42+
or
43+
exists(PackageJSON pkg |
44+
getModule() = pkg.getMainModule() and
45+
result = pkg.getPackageName()
46+
)
47+
}
4148

4249
/**
4350
* Gets the name of the global variable represented by this canonical name, if any.

javascript/ql/src/semmle/javascript/JSON.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ class JSONValue extends @json_value, Locatable {
3232
predicate isTopLevel() { not exists(getParent()) }
3333

3434
override string toString() { json(this, _, _, _, result) }
35+
36+
/** Gets the JSON file containing this value. */
37+
File getJsonFile() {
38+
exists(Location loc |
39+
json_locations(this, loc) and
40+
result = loc.getFile()
41+
)
42+
}
3543
}
3644

3745
/**

javascript/ql/src/semmle/javascript/NPM.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ private import NodeModuleResolutionImpl
88
/** A `package.json` configuration object. */
99
class PackageJSON extends JSONObject {
1010
PackageJSON() {
11-
getFile().getBaseName() = "package.json" and
11+
getJsonFile().getBaseName() = "package.json" and
1212
isTopLevel()
1313
}
1414

@@ -274,7 +274,7 @@ class NPMPackage extends @folder {
274274
/** The `package.json` file of this package. */
275275
PackageJSON pkg;
276276

277-
NPMPackage() { pkg.getFile().getParentContainer() = this }
277+
NPMPackage() { pkg.getJsonFile().getParentContainer() = this }
278278

279279
/** Gets a textual representation of this package. */
280280
string toString() { result = this.(Folder).toString() }

javascript/ql/src/semmle/javascript/TypeScript.qll

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,14 +1550,20 @@ class ReferenceImport extends LineComment {
15501550
string getAttributeName() { result = attribute }
15511551

15521552
/**
1553+
* DEPRECATED. This is no longer supported.
1554+
*
15531555
* Gets the file referenced by this import.
15541556
*/
1555-
File getImportedFile() { none() } // Overridden in subtypes.
1557+
deprecated
1558+
File getImportedFile() { none() }
15561559

15571560
/**
1561+
* DEPRECATED. This is no longer supported.
1562+
*
15581563
* Gets the top-level of the referenced file.
15591564
*/
1560-
TopLevel getImportedTopLevel() { result.getFile() = getImportedFile() }
1565+
deprecated
1566+
TopLevel getImportedTopLevel() { none() }
15611567
}
15621568

15631569
/**
@@ -1568,24 +1574,6 @@ class ReferenceImport extends LineComment {
15681574
*/
15691575
class ReferencePathImport extends ReferenceImport {
15701576
ReferencePathImport() { attribute = "path" }
1571-
1572-
override File getImportedFile() { result = this.(PathExpr).resolve() }
1573-
}
1574-
1575-
/**
1576-
* Treats reference imports comments as path expressions without exposing
1577-
* the methods from `PathExpr` on `ReferenceImport`.
1578-
*/
1579-
private class ReferenceImportAsPathExpr extends PathExpr {
1580-
ReferenceImport reference;
1581-
1582-
ReferenceImportAsPathExpr() { this = reference }
1583-
1584-
override string getValue() { result = reference.getAttributeValue() }
1585-
1586-
override Folder getSearchRoot(int priority) {
1587-
result = reference.getFile().getParentContainer() and priority = 0
1588-
}
15891577
}
15901578

15911579
/**
@@ -1596,14 +1584,6 @@ private class ReferenceImportAsPathExpr extends PathExpr {
15961584
*/
15971585
class ReferenceTypesImport extends ReferenceImport {
15981586
ReferenceTypesImport() { attribute = "types" }
1599-
1600-
override File getImportedFile() {
1601-
result = min(Folder nodeModules, int distance |
1602-
findNodeModulesFolder(getFile().getParentContainer(), nodeModules, distance)
1603-
|
1604-
nodeModules.getFolder("@types").getFolder(value).getFile("index.d.ts") order by distance
1605-
)
1606-
}
16071587
}
16081588

16091589
/**

javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,62 @@ module DataFlow {
193193
not fun.getExit().isJoin() // can only reach exit by the return statement
194194
)
195195
}
196+
197+
/**
198+
* Gets the static type of this node as determined by the TypeScript type system.
199+
*/
200+
private Type getType() {
201+
exists(AST::ValueNode node |
202+
this = TValueNode(node) and
203+
ast_node_type(node, result)
204+
)
205+
or
206+
exists(BindingPattern pattern |
207+
this = lvalueNode(pattern) and
208+
ast_node_type(pattern, result)
209+
)
210+
or
211+
exists(MethodDefinition def |
212+
this = TThisNode(def.getInit()) and
213+
ast_node_type(def.getDeclaringClass(), result)
214+
)
215+
}
216+
217+
/**
218+
* Gets the type annotation describing the type of this node,
219+
* provided that a static type could not be found.
220+
*
221+
* Doesn't take field types and function return types into account.
222+
*/
223+
private JSDocTypeExpr getFallbackTypeAnnotation() {
224+
exists(BindingPattern pattern |
225+
this = lvalueNode(pattern) and
226+
not ast_node_type(pattern, _) and
227+
result = pattern.getTypeAnnotation()
228+
)
229+
or
230+
result = getAPredecessor().getFallbackTypeAnnotation()
231+
}
232+
233+
/**
234+
* Holds if this node is annotated with the given named type,
235+
* or is declared as a subtype thereof, or is a union or intersection containing such a type.
236+
*/
237+
predicate hasUnderlyingType(string globalName) {
238+
getType().hasUnderlyingType(globalName)
239+
or
240+
getFallbackTypeAnnotation().getAnUnderlyingType().hasQualifiedName(globalName)
241+
}
242+
243+
/**
244+
* Holds if this node is annotated with the given named type,
245+
* or is declared as a subtype thereof, or is a union or intersection containing such a type.
246+
*/
247+
predicate hasUnderlyingType(string moduleName, string typeName) {
248+
getType().hasUnderlyingType(moduleName, typeName)
249+
or
250+
getFallbackTypeAnnotation().getAnUnderlyingType().hasQualifiedName(moduleName, typeName)
251+
}
196252
}
197253

198254
/**

javascript/ql/src/semmle/javascript/frameworks/Babel.qll

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module Babel {
1111
*/
1212
class Config extends JSONObject {
1313
Config() {
14-
isTopLevel() and getFile().getBaseName().matches(".babelrc%")
14+
isTopLevel() and getJsonFile().getBaseName().matches(".babelrc%")
1515
or
1616
this = any(PackageJSON pkg).getPropValue("babel")
1717
}
@@ -34,12 +34,12 @@ module Babel {
3434
* Gets a file affected by this Babel configuration.
3535
*/
3636
Container getAContainerInScope() {
37-
result = getFile().getParentContainer()
37+
result = getJsonFile().getParentContainer()
3838
or
3939
result = getAContainerInScope().getAChildContainer() and
4040
// File-relative .babelrc search stops at any package.json or .babelrc file.
41-
not result.getAChildContainer() = any(PackageJSON pkg).getFile() and
42-
not result.getAChildContainer() = any(Config pkg).getFile()
41+
not result.getAChildContainer() = any(PackageJSON pkg).getJsonFile() and
42+
not result.getAChildContainer() = any(Config pkg).getJsonFile()
4343
}
4444

4545
/**
@@ -133,7 +133,7 @@ module Babel {
133133
/**
134134
* Gets the folder in which this configuration is located.
135135
*/
136-
Folder getFolder() { result = getFile().getParentContainer() }
136+
Folder getFolder() { result = getJsonFile().getParentContainer() }
137137
}
138138

139139
/**

javascript/ql/src/semmle/javascript/frameworks/Firebase.qll

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ module Firebase {
2727

2828
/** Gets a reference to a Firebase app created with `initializeApp`. */
2929
private DataFlow::SourceNode initApp(DataFlow::TypeTracker t) {
30-
result = firebase().getAMethodCall("initializeApp") and t.start()
30+
t.start() and
31+
result = firebase().getAMethodCall("initializeApp")
32+
or
33+
t.start() and
34+
result.hasUnderlyingType("firebase", "app.App")
3135
or
3236
exists (DataFlow::TypeTracker t2 |
3337
result = initApp(t2).track(t2, t)
@@ -48,6 +52,9 @@ module Firebase {
4852
private DataFlow::SourceNode database(DataFlow::TypeTracker t) {
4953
result = app().getAMethodCall("database") and t.start()
5054
or
55+
t.start() and
56+
result.hasUnderlyingType("firebase", "database.Database")
57+
or
5158
exists (DataFlow::TypeTracker t2 |
5259
result = database(t2).track(t2, t)
5360
)
@@ -78,6 +85,8 @@ module Firebase {
7885
)
7986
or
8087
result = snapshot().getAPropertyRead("ref")
88+
or
89+
result.hasUnderlyingType("firebase", "database.Reference")
8190
)
8291
or
8392
exists (DataFlow::TypeTracker t2 |
@@ -102,6 +111,8 @@ module Firebase {
102111
name = "orderBy" + any(string s) or
103112
name = "startAt"
104113
)
114+
or
115+
result.hasUnderlyingType("firebase", "database.Query")
105116
)
106117
or
107118
exists (DataFlow::TypeTracker t2 |
@@ -293,6 +304,8 @@ module Firebase {
293304
prop = "before" or // only defined on Change objects
294305
prop = "after"
295306
)
307+
or
308+
result.hasUnderlyingType("firebase", "database.DataSnapshot")
296309
)
297310
or
298311
promiseTaintStep(snapshot(t), result)

javascript/ql/src/semmle/javascript/frameworks/Vue.qll

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,8 @@ module Vue {
352352
}
353353

354354
private Module getModule() {
355-
exists(HTML::ScriptElement elem | elem.getFile() = file |
355+
exists(HTML::ScriptElement elem |
356+
xmlElements(elem, _, _, _, file) and // Avoid materializing all of Locatable.getFile()
356357
result.getTopLevel() = elem.getScript()
357358
)
358359
}

0 commit comments

Comments
 (0)