JS: support imports/exports for closure library code

This commit is contained in:
Asger F
2019-01-23 17:40:46 +00:00
parent 30ba7aedfe
commit 9589ccd40d
30 changed files with 299 additions and 53 deletions

View File

@@ -46,5 +46,7 @@ where
moduleExportsAssign(_, exportsVal) and moduleExportsAssign(_, exportsVal) and
// however, if there are no further uses of `exports` the assignment is useless anyway // however, if there are no further uses of `exports` the assignment is useless anyway
strictcount(exportsVar.getAnAccess()) > 1 strictcount(exportsVar.getAnAccess()) > 1
) ) and
// export assignments do work in closure modules
not assgn.getTopLevel() instanceof ClosureModule
select assgn, "Assigning to 'exports' does not export anything." select assgn, "Assigning to 'exports' does not export anything."

View File

@@ -37,39 +37,162 @@ class GoogFunctionCallStmt extends ExprStmt {
Expr getAnArgument() { result = getArgument(_) } Expr getAnArgument() { result = getArgument(_) }
} }
private abstract class GoogNamespaceRef extends ExprOrStmt {
abstract string getNamespaceId();
}
/** /**
* A call to `goog.provide`. * A call to `goog.provide`.
*/ */
class GoogProvide extends GoogFunctionCallStmt { class GoogProvide extends GoogFunctionCallStmt, GoogNamespaceRef {
GoogProvide() { getFunctionName() = "provide" } GoogProvide() { getFunctionName() = "provide" }
/** Gets the identifier of the namespace created by this call. */ /** Gets the identifier of the namespace created by this call. */
string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() } override string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
} }
/** /**
* A call to `goog.require`. * A call to `goog.require`.
*/ */
class GoogRequire extends GoogFunctionCallStmt { class GoogRequire extends GoogFunctionCall, GoogNamespaceRef {
GoogRequire() { getFunctionName() = "require" } GoogRequire() { getFunctionName() = "require" }
/** Gets the identifier of the namespace imported by this call. */ /** Gets the identifier of the namespace imported by this call. */
string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() } override string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
}
private class GoogRequireImport extends GoogRequire, Import {
/** Gets the module in which this import appears. */
override Module getEnclosingModule() { result = getTopLevel() }
/** Gets the (unresolved) path that this import refers to. */
override PathExpr getImportedPath() {
result = getArgument(0)
}
} }
/** /**
* A Closure module, that is, a toplevel that contains a call to `goog.provide` or * A call to `goog.module` or `goog.declareModuleId`.
* `goog.require`.
*/ */
class ClosureModule extends TopLevel { class GoogModuleDeclaration extends GoogFunctionCallStmt, GoogNamespaceRef {
GoogModuleDeclaration() {
getFunctionName() = "module" or
getFunctionName() = "declareModuleId"
}
/** Gets the identifier of the namespace imported by this call. */
override string getNamespaceId() { result = getArgument(0).(ConstantString).getStringValue() }
}
/**
* A module using the Closure module system, declared using `goog.module()` or `goog.declareModuleId()`.
*/
class ClosureModule extends Module {
ClosureModule() { ClosureModule() {
getAChildStmt() instanceof GoogModuleDeclaration
}
/**
* Gets the call to `goog.module()` or `goog.declareModuleId` in this module.
*/
GoogModuleDeclaration getModuleDeclaration() {
result = getAChildStmt()
}
/**
* Gets the namespace of this module.
*/
string getNamespaceId() { result = getModuleDeclaration().getNamespaceId() }
override Module getAnImportedModule() {
exists (GoogRequireImport imprt |
imprt.getEnclosingModule() = this and
result.(ClosureModule).getNamespaceId() = imprt.getNamespaceId()
)
}
/**
* Gets the top-level `exports` variable in this module, if this module is defined by
* a `good.module` call.
*
* This variable denotes the object exported from this module.
*
* Has no result for ES6 modules using `goog.declareModuleId`.
*/
Variable getExportsVariable() {
getModuleDeclaration().getFunctionName() = "module" and
result.getScope() = this.getScope() and
result.getName() = "exports"
}
override predicate exports(string name, ASTNode export) {
// exports.foo = bar
export.(AssignExpr).getLhs().(PropAccess).accesses(getExportsVariable().getAnAccess(), name)
or
// exports = { foo: bar }
exists (VarDef def |
def.getTarget() = getExportsVariable().getAReference() and
def.getSource().(ObjectExpr).getPropertyByName(name) = export
)
}
}
/**
* A global Closure script, that is, a toplevel that is executed in the global scope and
* contains a toplevel call to `goog.provide` or `goog.require`.
*/
class ClosureScript extends TopLevel {
ClosureScript() {
not this instanceof ClosureModule and
getAChildStmt() instanceof GoogProvide or getAChildStmt() instanceof GoogProvide or
getAChildStmt() instanceof GoogRequire getAChildStmt().(ExprStmt).getExpr() instanceof GoogRequire
} }
/** Gets the identifier of a namespace required by this module. */ /** Gets the identifier of a namespace required by this module. */
string getARequiredNamespace() { result = getAChildStmt().(GoogRequire).getNamespaceId() } string getARequiredNamespace() { result = getAChildStmt().(ExprStmt).getExpr().(GoogRequire).getNamespaceId() }
/** Gets the identifer of a namespace provided by this module. */ /** Gets the identifer of a namespace provided by this module. */
string getAProvidedNamespace() { result = getAChildStmt().(GoogProvide).getNamespaceId() } string getAProvidedNamespace() { result = getAChildStmt().(GoogProvide).getNamespaceId() }
} }
/**
* Holds if `name` is a closure namespace, including proper namespace prefixes.
*/
pragma[noinline]
predicate isClosureLibraryNamespacePath(string name) {
exists (string namespace | namespace = any(GoogNamespaceRef provide).getNamespaceId() |
name = namespace.substring(0, namespace.indexOf("."))
or
name = namespace
)
}
/**
* Gets the closure namespace path addressed by the given dataflow node, if any.
*/
string getClosureLibraryAccessPath(DataFlow::SourceNode node) {
isClosureLibraryNamespacePath(result) and
node = DataFlow::globalVarRef(result)
or
isClosureLibraryNamespacePath(result) and
exists (DataFlow::PropRead read | node = read |
result = getClosureLibraryAccessPath(read.getBase().getALocalSource()) + "." + read.getPropertyName()
)
or
// Associate an access path with the immediate RHS of a store on a closure namespace.
// This is to support patterns like:
// foo.bar = { baz() {} }
exists (DataFlow::PropWrite write |
node = write.getRhs() and
result = getWrittenClosureLibraryAccessPath(write)
)
or
result = node.asExpr().(GoogRequire).getNamespaceId()
}
/**
* Gets the closure namespace path written to by the given property write, if any.
*/
string getWrittenClosureLibraryAccessPath(DataFlow::PropWrite node) {
result = getClosureLibraryAccessPath(node.getBase().getALocalSource()) + "." + node.getPropertyName()
}

View File

@@ -5,6 +5,7 @@
*/ */
import javascript import javascript
private import semmle.javascript.Closure
private import AbstractValuesImpl private import AbstractValuesImpl
private import semmle.javascript.dataflow.InferredTypes private import semmle.javascript.dataflow.InferredTypes
private import AbstractPropertiesImpl private import AbstractPropertiesImpl
@@ -332,3 +333,50 @@ private class AnalyzedExportAssign extends AnalyzedPropertyWrite, DataFlow::Valu
source = this source = this
} }
} }
/**
* Flow analysis for assignments to the `exports` variable in a Closure module.
*/
private class AnalyzedClosureExportAssign extends AnalyzedPropertyWrite, DataFlow::ValueNode {
ClosureModule mod;
AnalyzedClosureExportAssign() {
astNode.(AssignExpr).getLhs() = mod.getExportsVariable().getAReference()
}
override predicate writes(AbstractValue baseVal, string propName, DataFlow::AnalyzedNode source) {
baseVal = TAbstractModuleObject(astNode.getTopLevel()) and
propName = "exports" and
source = astNode.(AssignExpr).getRhs().flow()
}
}
/**
* Read of a global access path exported by a Closure library.
*
* This adds a direct flow edge to the assigned value.
*/
private class AnalyzedClosureGlobalAccessPath extends AnalyzedNode, AnalyzedPropertyRead {
string accessPath;
AnalyzedClosureGlobalAccessPath() {
accessPath = getClosureLibraryAccessPath(this)
}
override AnalyzedNode localFlowPred() {
exists (DataFlow::PropWrite write |
getWrittenClosureLibraryAccessPath(write) = accessPath and
result = write.getRhs()
)
or
result = AnalyzedNode.super.localFlowPred()
}
override predicate reads(AbstractValue base, string propName) {
exists (ClosureModule mod |
mod.getNamespaceId() = accessPath and
base = TAbstractModuleObject(mod) and
propName = "exports"
)
}
}

View File

@@ -0,0 +1,22 @@
| tests/importFromEs6.js:9:1:9:15 | es6Module.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
| tests/importFromEs6.js:10:1:10:18 | es6ModuleDefault() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
| tests/importFromEs6.js:12:1:12:16 | googModule.fun() | tests/googModule.js:4:6:4:10 | () {} |
| tests/importFromEs6.js:13:1:13:19 | googModuleDefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |
| tests/requireFromEs6.js:12:1:12:18 | globalModule.fun() | tests/globalModule.js:4:6:4:10 | () {} |
| tests/requireFromEs6.js:13:1:13:21 | globalM ... fault() | tests/globalModuleDefault.js:3:23:3:39 | function fun() {} |
| tests/requireFromEs6.js:15:1:15:15 | es6Module.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
| tests/requireFromEs6.js:16:1:16:18 | es6ModuleDefault() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
| tests/requireFromEs6.js:18:1:18:16 | googModule.fun() | tests/googModule.js:4:6:4:10 | () {} |
| tests/requireFromEs6.js:19:1:19:19 | googModuleDefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |
| tests/requireFromGlobalModule.js:10:1:10:18 | x.y.z.global.fun() | tests/globalModule.js:4:6:4:10 | () {} |
| tests/requireFromGlobalModule.js:11:1:11:21 | x.y.z.g ... fault() | tests/globalModuleDefault.js:3:23:3:39 | function fun() {} |
| tests/requireFromGlobalModule.js:13:1:13:16 | x.y.z.goog.fun() | tests/googModule.js:4:6:4:10 | () {} |
| tests/requireFromGlobalModule.js:14:1:14:19 | x.y.z.googdefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |
| tests/requireFromGlobalModule.js:16:1:16:15 | x.y.z.es6.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
| tests/requireFromGlobalModule.js:17:1:17:18 | x.y.z.es6default() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
| tests/requireFromGoogModule.js:12:1:12:18 | globalModule.fun() | tests/globalModule.js:4:6:4:10 | () {} |
| tests/requireFromGoogModule.js:13:1:13:21 | globalM ... fault() | tests/globalModuleDefault.js:3:23:3:39 | function fun() {} |
| tests/requireFromGoogModule.js:15:1:15:15 | es6Module.fun() | tests/es6Module.js:3:8:3:24 | function fun() {} |
| tests/requireFromGoogModule.js:16:1:16:18 | es6ModuleDefault() | tests/es6ModuleDefault.js:3:16:3:28 | function() {} |
| tests/requireFromGoogModule.js:18:1:18:16 | googModule.fun() | tests/googModule.js:4:6:4:10 | () {} |
| tests/requireFromGoogModule.js:19:1:19:19 | googModuleDefault() | tests/googModuleDefault.js:3:11:3:27 | function fun() {} |

View File

@@ -0,0 +1,4 @@
import javascript
from DataFlow::InvokeNode node
select node, node.getACallee()

View File

@@ -1,2 +0,0 @@
| a.js:1:1:5:1 | <toplevel> |
| b.js:1:1:3:21 | <toplevel> |

View File

@@ -1,4 +0,0 @@
import semmle.javascript.Closure
from ClosureModule cm
select cm

View File

@@ -1 +0,0 @@
| a.js:1:1:5:1 | <toplevel> | a |

View File

@@ -1,4 +0,0 @@
import semmle.javascript.Closure
from ClosureModule cm
select cm, cm.getAProvidedNamespace()

View File

@@ -1 +0,0 @@
| b.js:1:1:3:21 | <toplevel> | a |

View File

@@ -1,4 +0,0 @@
import semmle.javascript.Closure
from ClosureModule cm
select cm, cm.getARequiredNamespace()

View File

@@ -1,3 +0,0 @@
| a.js:1:1:1:17 | goog.provide('a') | provide |
| b.js:1:1:1:17 | goog.require('a') | require |
| c.js:2:1:2:14 | goog.leyness() | leyness |

View File

@@ -1,4 +0,0 @@
import semmle.javascript.Closure
from GoogFunctionCall gfc
select gfc, gfc.getFunctionName()

View File

@@ -1 +0,0 @@
| a.js:1:1:1:18 | goog.provide('a'); | a |

View File

@@ -1,4 +0,0 @@
import semmle.javascript.Closure
from GoogProvide gp
select gp, gp.getNamespaceId()

View File

@@ -1 +0,0 @@
| b.js:1:1:1:18 | goog.require('a'); | a |

View File

@@ -1,4 +0,0 @@
import semmle.javascript.Closure
from GoogRequire gr
select gr, gr.getNamespaceId()

View File

@@ -1,5 +0,0 @@
goog.provide('a');
a.foo = function() {
return 42;
}

View File

@@ -1,3 +0,0 @@
goog.require('a');
console.log(a.foo());

View File

@@ -1,2 +0,0 @@
// not a Closure module
goog.leyness();

View File

@@ -0,0 +1,3 @@
goog.declareModuleId('x.y.z.es6');
export function fun() {}

View File

@@ -0,0 +1,3 @@
goog.declareModuleId('x.y.z.es6default');
export default function() {}

View File

@@ -0,0 +1,5 @@
goog.provide('x.y.z.global');
x.y.z.global = {
fun() {}
};

View File

@@ -0,0 +1,3 @@
goog.provide('x.y.z.globaldefault');
x.y.z.globaldefault = function fun() {}

View File

@@ -0,0 +1,5 @@
goog.module('x.y.z.goog');
exports = {
fun() {}
};

View File

@@ -0,0 +1,3 @@
goog.module('x.y.z.googdefault');
exports = function fun() {};

View File

@@ -0,0 +1,13 @@
// ES6 imports can import files by name, as long as they are modules
import * as googModule from './googModule';
import * as googModuleDefault from './googModuleDefault';
import * as es6Module from './es6Module';
import * as es6ModuleDefault from './es6ModuleDefault';
es6Module.fun();
es6ModuleDefault();
googModule.fun();
googModuleDefault();

View File

@@ -0,0 +1,19 @@
import * as dummy from 'dummy'; // treat as ES6 module
let globalModule = goog.require('x.y.z.global');
let globalModuleDefault = goog.require('x.y.z.globaldefault');
let es6Module = goog.require('x.y.z.es6');
let es6ModuleDefault = goog.require('x.y.z.es6default');
let googModule = goog.require('x.y.z.goog');
let googModuleDefault = goog.require('x.y.z.googdefault');
globalModule.fun();
globalModuleDefault();
es6Module.fun();
es6ModuleDefault();
googModule.fun();
googModuleDefault();

View File

@@ -0,0 +1,17 @@
goog.require('x.y.z.global');
goog.require('x.y.z.globaldefault');
goog.require('x.y.z.goog');
goog.require('x.y.z.googdefault');
goog.require('x.y.z.es6');
goog.require('x.y.z.es6default');
x.y.z.global.fun();
x.y.z.globaldefault();
x.y.z.goog.fun();
x.y.z.googdefault();
x.y.z.es6.fun();
x.y.z.es6default();

View File

@@ -0,0 +1,19 @@
goog.module('test.importer');
let globalModule = goog.require('x.y.z.global');
let globalModuleDefault = goog.require('x.y.z.globaldefault');
let es6Module = goog.require('x.y.z.es6');
let es6ModuleDefault = goog.require('x.y.z.es6default');
let googModule = goog.require('x.y.z.goog');
let googModuleDefault = goog.require('x.y.z.googdefault');
globalModule.fun();
globalModuleDefault();
es6Module.fun();
es6ModuleDefault();
googModule.fun();
googModuleDefault();