Merge pull request #1185 from xiemaisi/js/improve-amd-imports

Approved by asger-semmle
This commit is contained in:
semmle-qlci
2019-04-01 16:30:47 +01:00
committed by GitHub
33 changed files with 197 additions and 50 deletions

View File

@@ -17,6 +17,6 @@ where
not f.inExternsFile() and
f.getNumParameter() > 7 and
// exclude AMD modules
not exists(AMDModuleDefinition m | f = m.getFactoryNode().(DataFlow::FunctionNode).getAstNode())
not exists(AmdModuleDefinition m | f = m.getFactoryNode().(DataFlow::FunctionNode).getAstNode())
select f.(FirstLineOf),
capitalize(f.describe()) + " has too many parameters (" + f.getNumParameter() + ")."

View File

@@ -23,8 +23,8 @@ import javascript
* where the first argument is the module name, the second argument an
* array of dependencies, and the third argument a factory method or object.
*/
class AMDModuleDefinition extends CallExpr {
AMDModuleDefinition() {
class AmdModuleDefinition extends CallExpr {
AmdModuleDefinition() {
getParent() instanceof ExprStmt and
getCallee().(GlobalVarAccess).getName() = "define" and
exists(int n | n = getNumArgument() |
@@ -153,7 +153,7 @@ class AMDModuleDefinition extends CallExpr {
result = getModuleExpr().analyze().getAValue()
or
// explicit exports: anything assigned to `module.exports`
exists(AbstractProperty moduleExports, AMDModule m |
exists(AbstractProperty moduleExports, AmdModule m |
this = m.getDefine() and
moduleExports.getBase().(AbstractModuleObject).getModule() = m and
moduleExports.getPropertyName() = "exports"
@@ -170,10 +170,15 @@ class AMDModuleDefinition extends CallExpr {
}
}
/**
* DEPRECATED: Use `AmdModuleDefinition` instead.
*/
deprecated class AMDModuleDefinition = AmdModuleDefinition;
/** An AMD dependency, considered as a path expression. */
private class AmdDependencyPath extends PathExprCandidate {
AmdDependencyPath() {
exists(AMDModuleDefinition amd |
exists(AmdModuleDefinition amd |
this = amd.getDependencies().getAnElement() or
this = amd.getARequireCall().getAnArgument()
)
@@ -191,21 +196,83 @@ private class ConstantAmdDependencyPathElement extends PathExprInModule, Constan
* Holds if `def` is an AMD module definition in `tl` which is not
* nested inside another module definition.
*/
private predicate amdModuleTopLevel(AMDModuleDefinition def, TopLevel tl) {
private predicate amdModuleTopLevel(AmdModuleDefinition def, TopLevel tl) {
def.getTopLevel() = tl and
not def.getParent+() instanceof AMDModuleDefinition
not def.getParent+() instanceof AmdModuleDefinition
}
/**
* An AMD dependency, viewed as an import.
*/
private class AmdDependencyImport extends Import {
AmdDependencyImport() { this = any(AmdModuleDefinition def).getADependency() }
override Module getEnclosingModule() { this = result.(AmdModule).getDefine().getADependency() }
override PathExpr getImportedPath() { result = this }
/**
* Gets a file that looks like it might be the target of this import.
*
* Specifically, we look for files whose absolute path ends with the imported path, possibly
* adding well-known JavaScript file extensions like `.js`.
*/
private File guessTarget() {
exists(PathString imported, string abspath, string dirname, string basename |
targetCandidate(result, abspath, imported, dirname, basename)
|
abspath.regexpMatch(".*/\\Q" + imported + "\\E")
or
exists(Folder dir |
// `dir` ends with the dirname of the imported path
dir.getAbsolutePath().regexpMatch(".*/\\Q" + dirname + "\\E") or
dirname = ""
|
result = dir.getJavaScriptFile(basename)
)
)
}
/**
* Holds if `f` is a file whose stem (that is, basename without extension) matches the imported path.
*
* Additionally, `abspath` is bound to the absolute path of `f`, `imported` to the imported path, and
* `dirname` and `basename` to the dirname and basename (respectively) of `imported`.
*/
private predicate targetCandidate(
File f, string abspath, PathString imported, string dirname, string basename
) {
imported = getImportedPath().getValue() and
f.getStem() = imported.getStem() and
f.getAbsolutePath() = abspath and
dirname = imported.getDirName() and
basename = imported.getBaseName()
}
/**
* Gets the module whose absolute path matches this import, if there is only a single such module.
*/
private Module resolveByAbsolutePath() {
count(guessTarget()) = 1 and
result.getFile() = guessTarget()
}
override Module getImportedModule() {
result = super.getImportedModule()
or
not exists(super.getImportedModule()) and
result = resolveByAbsolutePath()
}
}
/**
* An AMD-style module.
*/
class AMDModule extends Module {
AMDModule() { strictcount(AMDModuleDefinition def | amdModuleTopLevel(def, this)) = 1 }
class AmdModule extends Module {
AmdModule() { strictcount(AmdModuleDefinition def | amdModuleTopLevel(def, this)) = 1 }
/** Gets the definition of this module. */
AMDModuleDefinition getDefine() { amdModuleTopLevel(result, this) }
override Module getAnImportedModule() { result.getFile() = resolve(getDefine().getADependency()) }
AmdModuleDefinition getDefine() { amdModuleTopLevel(result, this) }
override predicate exports(string name, ASTNode export) {
exists(DataFlow::PropWrite pwn | export = pwn.getAstNode() |
@@ -214,3 +281,8 @@ class AMDModule extends Module {
)
}
}
/**
* DEPRECATED: Use `AmdModule` instead.
*/
deprecated class AMDModule = AmdModule;

View File

@@ -19,8 +19,6 @@ class ES2015Module extends Module {
/** Gets an export declaration in this module. */
ExportDeclaration getAnExport() { result.getTopLevel() = this }
override Module getAnImportedModule() { result = getAnImport().getImportedModule() }
override predicate exports(string name, ASTNode export) {
exists(ExportDeclaration ed | ed = getAnExport() and ed = export | ed.exportsAs(_, name))
}

View File

@@ -75,7 +75,7 @@ abstract class Container extends @container {
* </table>
*/
string getBaseName() {
result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(?:\\.([^.]*))?)", 1)
result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 1)
}
/**
@@ -101,7 +101,7 @@ abstract class Container extends @container {
* <tr><td>"/tmp/x.tar.gz"</td><td>"gz"</td></tr>
* </table>
*/
string getExtension() { result = getAbsolutePath().regexpCapture(".*/([^/]*?)(\\.([^.]*))?", 3) }
string getExtension() { result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 4) }
/**
* Gets the stem of this container, that is, the prefix of its base name up to
@@ -120,7 +120,7 @@ abstract class Container extends @container {
* <tr><td>"/tmp/x.tar.gz"</td><td>"x.tar"</td></tr>
* </table>
*/
string getStem() { result = getAbsolutePath().regexpCapture(".*/([^/]*?)(?:\\.([^.]*))?", 1) }
string getStem() { result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(\\.([^.]*))?)", 2) }
/** Gets the parent container of this file or folder, if any. */
Container getParentContainer() { containerparent(result, this) }

View File

@@ -21,7 +21,7 @@ abstract class Module extends TopLevel {
Import getAnImport() { result.getTopLevel() = this }
/** Gets a module from which this module imports. */
abstract Module getAnImportedModule();
Module getAnImportedModule() { result = getAnImport().getImportedModule() }
/** Gets a symbol exported by this module. */
string getAnExportedSymbol() { exports(result, _) }
@@ -92,8 +92,8 @@ abstract class Module extends TopLevel {
}
/**
* An import in a module, which may either be an ECMAScript 2015-style
* `import` statement or a CommonJS-style `require` import.
* An import in a module, which may be an ECMAScript 2015-style
* `import` statement, a CommonJS-style `require` import, or an AMD dependency.
*/
abstract class Import extends ASTNode {
/** Gets the module in which this import appears. */

View File

@@ -21,9 +21,6 @@ class NodeModule extends Module {
/** Gets the scope induced by this module. */
override ModuleScope getScope() { result.getScopeElement() = this }
/** Gets a module imported by this module. */
override Module getAnImportedModule() { result = getAnImport().getImportedModule() }
/**
* Gets an abstract value representing one or more values that may flow
* into this module's `module.exports` property.

View File

@@ -76,6 +76,26 @@ private class ConsPath extends Path, TConsPath {
override string toString() { result = pp(this) }
}
/**
* Gets a regular expression that can be used to parse slash-separated paths.
*
* The first capture group captures the dirname of the path, that is, everything
* before the last slash, or the empty string if there isn't a slash.
*
* The second capture group captures the basename of the path, that is, everything
* after the last slash, or the entire path if there isn't a slash.
*
* The third capture group captures the stem of the basename, that is, everything
* before the last dot, or the entire basename if there isn't a dot.
*
* Finally, the fourth and fifth capture groups capture the extension of the basename,
* that is, everything after the last dot. The fourth group includes the dot, the
* fifth does not.
*/
private string pathRegex() {
result = "(.*)(?:/|^)(([^/]*?)(\\.([^.]*))?)"
}
/**
* A string value that represents a (relative or absolute) file system path.
*
@@ -98,7 +118,17 @@ abstract class PathString extends string {
int getNumComponent() { result = count(int i | exists(getComponent(i))) }
/** Gets the base name of the folder or file this path refers to. */
string getBaseName() { result = this.regexpCapture("(.*/|^)([^/]+)", 2) }
string getBaseName() { result = this.regexpCapture(pathRegex(), 2) }
/**
* Gets stem of the folder or file this path refers to, that is, the prefix of its base name
* up to (but not including) the last dot character if there is one, or the entire
* base name if there is not
*/
string getStem() { result = this.regexpCapture(pathRegex(), 3) }
/** Gets the path of the parent folder of the folder or file this path refers to. */
string getDirName() { result = this.regexpCapture(pathRegex(), 1) }
/**
* Gets the absolute path that the sub-path consisting of the first `n`

View File

@@ -472,12 +472,12 @@ module ModuleImportNode {
)
or
// declared AMD dependency
exists(AMDModuleDefinition amd |
exists(AmdModuleDefinition amd |
this = DataFlow::parameterNode(amd.getDependencyParameter(path))
)
or
// AMD require
exists(AMDModuleDefinition amd, CallExpr req |
exists(AmdModuleDefinition amd, CallExpr req |
req = amd.getARequireCall() and
this = DataFlow::valueNode(req) and
path = req.getArgument(0).(ConstantString).getStringValue()

View File

@@ -61,7 +61,7 @@ private predicate mayDynamicallyComputeExports(Module m) {
or
// AMD modules can export arbitrary objects, so an import is essentially a property read
// and hence must be considered indefinite
m instanceof AMDModule
m instanceof AmdModule
or
// `m` re-exports all exports of some other module that dynamically computes its exports
exists(BulkReExportDeclaration rexp | rexp = m.(ES2015Module).getAnExport() |
@@ -229,7 +229,7 @@ class AnalyzedExternalModuleReference extends AnalyzedPropertyRead, DataFlow::Va
* Flow analysis for AMD exports.
*/
private class AnalyzedAmdExport extends AnalyzedPropertyWrite, DataFlow::ValueNode {
AMDModule amd;
AmdModule amd;
AnalyzedAmdExport() { astNode = amd.getDefine().getModuleExpr() }
@@ -248,7 +248,7 @@ private class AnalyzedAmdImport extends AnalyzedPropertyRead, DataFlow::Node {
Module required;
AnalyzedAmdImport() {
exists(AMDModule amd, PathExpr dep, Parameter p |
exists(AmdModule amd, PathExpr dep, Parameter p |
amd.getDefine().dependencyParameter(dep, p) and
this = DataFlow::parameterNode(p) and
required.getFile() = amd.resolve(dep)

View File

@@ -180,7 +180,7 @@ private class AnalyzedAmdParameter extends AnalyzedVarDef {
AbstractValue implicitInitVal;
AnalyzedAmdParameter() {
exists(AMDModule m, AMDModuleDefinition mdef | mdef = m.getDefine() |
exists(AmdModule m, AmdModuleDefinition mdef | mdef = m.getDefine() |
this = mdef.getModuleParameter() and
implicitInitVal = TAbstractModuleObject(m)
or

View File

@@ -57,7 +57,7 @@ module TaintedPath {
ModulePathSink() {
astNode = any(Require rq).getArgument(0) or
astNode = any(ExternalModuleReference rq).getExpression() or
astNode = any(AMDModuleDefinition amd).getDependencies()
astNode = any(AmdModuleDefinition amd).getDependencies()
}
}