Skip to content

Commit 9589ccd

Browse files
committed
JS: support imports/exports for closure library code
1 parent 30ba7ae commit 9589ccd

30 files changed

Lines changed: 299 additions & 53 deletions

javascript/ql/src/NodeJS/InvalidExport.ql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,7 @@ where
4646
moduleExportsAssign(_, exportsVal) and
4747
// however, if there are no further uses of `exports` the assignment is useless anyway
4848
strictcount(exportsVar.getAnAccess()) > 1
49-
)
49+
) and
50+
// export assignments do work in closure modules
51+
not assgn.getTopLevel() instanceof ClosureModule
5052
select assgn, "Assigning to 'exports' does not export anything."

javascript/ql/src/semmle/javascript/Closure.qll

Lines changed: 132 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,39 +37,162 @@ class GoogFunctionCallStmt extends ExprStmt {
3737
Expr getAnArgument() { result = getArgument(_) }
3838
}
3939

40+
private abstract class GoogNamespaceRef extends ExprOrStmt {
41+
abstract string getNamespaceId();
42+
}
43+
4044
/**
4145
* A call to `goog.provide`.
4246
*/
43-
class GoogProvide extends GoogFunctionCallStmt {
47+
class GoogProvide extends GoogFunctionCallStmt, GoogNamespaceRef {
4448
GoogProvide() { getFunctionName() = "provide" }
4549

4650
/** Gets the identifier of the namespace created by this call. */
47-
string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
51+
override string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
4852
}
4953

5054
/**
5155
* A call to `goog.require`.
5256
*/
53-
class GoogRequire extends GoogFunctionCallStmt {
57+
class GoogRequire extends GoogFunctionCall, GoogNamespaceRef {
5458
GoogRequire() { getFunctionName() = "require" }
5559

5660
/** Gets the identifier of the namespace imported by this call. */
57-
string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
61+
override string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
62+
}
63+
64+
private class GoogRequireImport extends GoogRequire, Import {
65+
/** Gets the module in which this import appears. */
66+
override Module getEnclosingModule() { result = getTopLevel() }
67+
68+
/** Gets the (unresolved) path that this import refers to. */
69+
override PathExpr getImportedPath() {
70+
result = getArgument(0)
71+
}
5872
}
5973

6074
/**
61-
* A Closure module, that is, a toplevel that contains a call to `goog.provide` or
62-
* `goog.require`.
75+
* A call to `goog.module` or `goog.declareModuleId`.
6376
*/
64-
class ClosureModule extends TopLevel {
77+
class GoogModuleDeclaration extends GoogFunctionCallStmt, GoogNamespaceRef {
78+
GoogModuleDeclaration() {
79+
getFunctionName() = "module" or
80+
getFunctionName() = "declareModuleId"
81+
}
82+
83+
/** Gets the identifier of the namespace imported by this call. */
84+
override string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
85+
}
86+
87+
/**
88+
* A module using the Closure module system, declared using `goog.module()` or `goog.declareModuleId()`.
89+
*/
90+
class ClosureModule extends Module {
6591
ClosureModule() {
92+
getAChildStmt() instanceof GoogModuleDeclaration
93+
}
94+
95+
/**
96+
* Gets the call to `goog.module()` or `goog.declareModuleId` in this module.
97+
*/
98+
GoogModuleDeclaration getModuleDeclaration() {
99+
result = getAChildStmt()
100+
}
101+
102+
/**
103+
* Gets the namespace of this module.
104+
*/
105+
string getNamespaceId() { result = getModuleDeclaration().getNamespaceId() }
106+
107+
override Module getAnImportedModule() {
108+
exists (GoogRequireImport imprt |
109+
imprt.getEnclosingModule() = this and
110+
result.(ClosureModule).getNamespaceId() = imprt.getNamespaceId()
111+
)
112+
}
113+
114+
/**
115+
* Gets the top-level `exports` variable in this module, if this module is defined by
116+
* a `good.module` call.
117+
*
118+
* This variable denotes the object exported from this module.
119+
*
120+
* Has no result for ES6 modules using `goog.declareModuleId`.
121+
*/
122+
Variable getExportsVariable() {
123+
getModuleDeclaration().getFunctionName() = "module" and
124+
result.getScope() = this.getScope() and
125+
result.getName() = "exports"
126+
}
127+
128+
override predicate exports(string name, ASTNode export) {
129+
// exports.foo = bar
130+
export.(AssignExpr).getLhs().(PropAccess).accesses(getExportsVariable().getAnAccess(), name)
131+
or
132+
// exports = { foo: bar }
133+
exists (VarDef def |
134+
def.getTarget() = getExportsVariable().getAReference() and
135+
def.getSource().(ObjectExpr).getPropertyByName(name) = export
136+
)
137+
}
138+
}
139+
140+
/**
141+
* A global Closure script, that is, a toplevel that is executed in the global scope and
142+
* contains a toplevel call to `goog.provide` or `goog.require`.
143+
*/
144+
class ClosureScript extends TopLevel {
145+
ClosureScript() {
146+
not this instanceof ClosureModule and
66147
getAChildStmt() instanceof GoogProvide or
67-
getAChildStmt() instanceof GoogRequire
148+
getAChildStmt().(ExprStmt).getExpr() instanceof GoogRequire
68149
}
69150

70151
/** Gets the identifier of a namespace required by this module. */
71-
string getARequiredNamespace() { result = getAChildStmt().(GoogRequire).getNamespaceId() }
152+
string getARequiredNamespace() { result = getAChildStmt().(ExprStmt).getExpr().(GoogRequire).getNamespaceId() }
72153

73154
/** Gets the identifer of a namespace provided by this module. */
74155
string getAProvidedNamespace() { result = getAChildStmt().(GoogProvide).getNamespaceId() }
75156
}
157+
158+
/**
159+
* Holds if `name` is a closure namespace, including proper namespace prefixes.
160+
*/
161+
pragma[noinline]
162+
predicate isClosureLibraryNamespacePath(string name) {
163+
exists (string namespace | namespace = any(GoogNamespaceRef provide).getNamespaceId() |
164+
name = namespace.substring(0, namespace.indexOf("."))
165+
or
166+
name = namespace
167+
)
168+
}
169+
170+
/**
171+
* Gets the closure namespace path addressed by the given dataflow node, if any.
172+
*/
173+
string getClosureLibraryAccessPath(DataFlow::SourceNode node) {
174+
isClosureLibraryNamespacePath(result) and
175+
node = DataFlow::globalVarRef(result)
176+
or
177+
isClosureLibraryNamespacePath(result) and
178+
exists (DataFlow::PropRead read | node = read |
179+
result = getClosureLibraryAccessPath(read.getBase().getALocalSource()) + "." + read.getPropertyName()
180+
)
181+
or
182+
// Associate an access path with the immediate RHS of a store on a closure namespace.
183+
// This is to support patterns like:
184+
// foo.bar = { baz() {} }
185+
exists (DataFlow::PropWrite write |
186+
node = write.getRhs() and
187+
result = getWrittenClosureLibraryAccessPath(write)
188+
)
189+
or
190+
result = node.asExpr().(GoogRequire).getNamespaceId()
191+
}
192+
193+
/**
194+
* Gets the closure namespace path written to by the given property write, if any.
195+
*/
196+
string getWrittenClosureLibraryAccessPath(DataFlow::PropWrite node) {
197+
result = getClosureLibraryAccessPath(node.getBase().getALocalSource()) + "." + node.getPropertyName()
198+
}

javascript/ql/src/semmle/javascript/dataflow/internal/InterModuleTypeInference.qll

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import javascript
8+
private import semmle.javascript.Closure
89
private import AbstractValuesImpl
910
private import semmle.javascript.dataflow.InferredTypes
1011
private import AbstractPropertiesImpl
@@ -332,3 +333,50 @@ private class AnalyzedExportAssign extends AnalyzedPropertyWrite, DataFlow::Valu
332333
source = this
333334
}
334335
}
336+
337+
/**
338+
* Flow analysis for assignments to the `exports` variable in a Closure module.
339+
*/
340+
private class AnalyzedClosureExportAssign extends AnalyzedPropertyWrite, DataFlow::ValueNode {
341+
ClosureModule mod;
342+
343+
AnalyzedClosureExportAssign() {
344+
astNode.(AssignExpr).getLhs() = mod.getExportsVariable().getAReference()
345+
}
346+
347+
override predicate writes(AbstractValue baseVal, string propName, DataFlow::AnalyzedNode source) {
348+
baseVal = TAbstractModuleObject(astNode.getTopLevel()) and
349+
propName = "exports" and
350+
source = astNode.(AssignExpr).getRhs().flow()
351+
}
352+
}
353+
354+
/**
355+
* Read of a global access path exported by a Closure library.
356+
*
357+
* This adds a direct flow edge to the assigned value.
358+
*/
359+
private class AnalyzedClosureGlobalAccessPath extends AnalyzedNode, AnalyzedPropertyRead {
360+
string accessPath;
361+
362+
AnalyzedClosureGlobalAccessPath() {
363+
accessPath = getClosureLibraryAccessPath(this)
364+
}
365+
366+
override AnalyzedNode localFlowPred() {
367+
exists (DataFlow::PropWrite write |
368+
getWrittenClosureLibraryAccessPath(write) = accessPath and
369+
result = write.getRhs()
370+
)
371+
or
372+
result = AnalyzedNode.super.localFlowPred()
373+
}
374+
375+
override predicate reads(AbstractValue base, string propName) {
376+
exists (ClosureModule mod |
377+
mod.getNamespaceId() = accessPath and
378+
base = TAbstractModuleObject(mod) and
379+
propName = "exports"
380+
)
381+
}
382+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
| tests/importFromEs6.js:9:1:9:15 | es6Module.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
2+
| tests/importFromEs6.js:10:1:10:18 | es6ModuleDefault() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
3+
| tests/importFromEs6.js:12:1:12:16 | googModule.fun() | tests/googModule.js:4:6:4:10 | () {} |
4+
| tests/importFromEs6.js:13:1:13:19 | googModuleDefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |
5+
| tests/requireFromEs6.js:12:1:12:18 | globalModule.fun() | tests/globalModule.js:4:6:4:10 | () {} |
6+
| tests/requireFromEs6.js:13:1:13:21 | globalM ... fault() | tests/globalModuleDefault.js:3:23:3:39 | function fun() {} |
7+
| tests/requireFromEs6.js:15:1:15:15 | es6Module.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
8+
| tests/requireFromEs6.js:16:1:16:18 | es6ModuleDefault() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
9+
| tests/requireFromEs6.js:18:1:18:16 | googModule.fun() | tests/googModule.js:4:6:4:10 | () {} |
10+
| tests/requireFromEs6.js:19:1:19:19 | googModuleDefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |
11+
| tests/requireFromGlobalModule.js:10:1:10:18 | x.y.z.global.fun() | tests/globalModule.js:4:6:4:10 | () {} |
12+
| tests/requireFromGlobalModule.js:11:1:11:21 | x.y.z.g ... fault() | tests/globalModuleDefault.js:3:23:3:39 | function fun() {} |
13+
| tests/requireFromGlobalModule.js:13:1:13:16 | x.y.z.goog.fun() | tests/googModule.js:4:6:4:10 | () {} |
14+
| tests/requireFromGlobalModule.js:14:1:14:19 | x.y.z.googdefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |
15+
| tests/requireFromGlobalModule.js:16:1:16:15 | x.y.z.es6.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
16+
| tests/requireFromGlobalModule.js:17:1:17:18 | x.y.z.es6default() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
17+
| tests/requireFromGoogModule.js:12:1:12:18 | globalModule.fun() | tests/globalModule.js:4:6:4:10 | () {} |
18+
| tests/requireFromGoogModule.js:13:1:13:21 | globalM ... fault() | tests/globalModuleDefault.js:3:23:3:39 | function fun() {} |
19+
| tests/requireFromGoogModule.js:15:1:15:15 | es6Module.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
20+
| tests/requireFromGoogModule.js:16:1:16:18 | es6ModuleDefault() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
21+
| tests/requireFromGoogModule.js:18:1:18:16 | googModule.fun() | tests/googModule.js:4:6:4:10 | () {} |
22+
| tests/requireFromGoogModule.js:19:1:19:19 | googModuleDefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
3+
from DataFlow::InvokeNode node
4+
select node, node.getACallee()

javascript/ql/test/library-tests/Closure/ClosureModule.expected

Lines changed: 0 additions & 2 deletions
This file was deleted.

javascript/ql/test/library-tests/Closure/ClosureModule.ql

Lines changed: 0 additions & 4 deletions
This file was deleted.

javascript/ql/test/library-tests/Closure/ClosureModule_getAProvidedNamespace.expected

Lines changed: 0 additions & 1 deletion
This file was deleted.

javascript/ql/test/library-tests/Closure/ClosureModule_getAProvidedNamespace.ql

Lines changed: 0 additions & 4 deletions
This file was deleted.

javascript/ql/test/library-tests/Closure/ClosureModule_getARequiredNamespace.expected

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)