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
// however, if there are no further uses of `exports` the assignment is useless anyway
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."

View File

@@ -37,39 +37,162 @@ class GoogFunctionCallStmt extends ExprStmt {
Expr getAnArgument() { result = getArgument(_) }
}
private abstract class GoogNamespaceRef extends ExprOrStmt {
abstract string getNamespaceId();
}
/**
* A call to `goog.provide`.
*/
class GoogProvide extends GoogFunctionCallStmt {
class GoogProvide extends GoogFunctionCallStmt, GoogNamespaceRef {
GoogProvide() { getFunctionName() = "provide" }
/** 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`.
*/
class GoogRequire extends GoogFunctionCallStmt {
class GoogRequire extends GoogFunctionCall, GoogNamespaceRef {
GoogRequire() { getFunctionName() = "require" }
/** 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
* `goog.require`.
* A call to `goog.module` or `goog.declareModuleId`.
*/
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() {
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 GoogRequire
getAChildStmt().(ExprStmt).getExpr() instanceof GoogRequire
}
/** 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. */
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
private import semmle.javascript.Closure
private import AbstractValuesImpl
private import semmle.javascript.dataflow.InferredTypes
private import AbstractPropertiesImpl
@@ -332,3 +333,50 @@ private class AnalyzedExportAssign extends AnalyzedPropertyWrite, DataFlow::Valu
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();