Files
codeql/javascript/ql/lib/semmle/javascript/Modules.qll

237 lines
7.9 KiB
Plaintext

/**
* Provides classes for working with JavaScript modules, both
* ECMAScript 2015-style modules, and the older CommonJS and AMD-style
* modules.
*/
overlay[local?]
module;
import javascript
private import semmle.javascript.internal.CachedStages
private import semmle.javascript.internal.paths.PathExprResolver
/**
* A module, which may either be an ECMAScript 2015-style module,
* a CommonJS module, or an AMD module.
*/
abstract class Module extends TopLevel {
/** Gets the full path of the file containing this module. */
string getPath() { result = this.getFile().getAbsolutePath() }
/** Gets the short name of this module without file extension. */
string getName() { result = this.getFile().getStem() }
/** Gets an import appearing in this module. */
Import getAnImport() { result.getTopLevel() = this }
/** Gets a module from which this module imports. */
overlay[global]
Module getAnImportedModule() { result = this.getAnImport().getImportedModule() }
/** Gets a symbol exported by this module. */
overlay[global]
string getAnExportedSymbol() { exists(this.getAnExportedValue(result)) }
/**
* Get a value that is explicitly exported from this module with under `name`.
*
* Note that in some module systems (notably CommonJS and AMD)
* modules are arbitrary objects that export all their
* properties. This predicate only considers properties
* that are explicitly defined on the module object.
*
* Symbols defined in another module that are re-exported by
* this module are only sometimes considered.
*/
overlay[global]
cached
abstract DataFlow::Node getAnExportedValue(string name);
/**
* Gets a value that is exported as the whole exports object of this module.
*/
overlay[global]
cached
DataFlow::Node getABulkExportedNode() { none() } // overridden in subclasses
/**
* Gets the ES2015 `default` export from this module, or for other types of modules,
* gets a bulk exported node.
*
* This can be used to determine which value a default-import will likely refer to,
* as the interaction between different module types is not standardized.
*/
overlay[global]
DataFlow::Node getDefaultOrBulkExport() {
result = [this.getAnExportedValue("default"), this.getABulkExportedNode()]
}
/**
* Gets the root folder relative to which the given import path (which must
* appear in this module) is resolved.
*
* Each root has an associated priority, and roots with numerically smaller
* priority are preferred during import resolution.
*
* This predicate is not part of the public API, it is only exposed to allow
* overriding by subclasses.
*/
overlay[global]
deprecated predicate searchRoot(PathExpr path, Folder searchRoot, int priority) {
path.getEnclosingModule() = this and
priority = 0 and
exists(string v | v = path.getValue() |
// paths starting with a dot are resolved relative to the module's directory
if v.matches(".%")
then searchRoot = this.getFile().getParentContainer()
else
// all other paths are resolved relative to the file system root
searchRoot.getBaseName() = ""
)
}
/**
* Gets the file to which the import path `path`, which must appear in this
* module, resolves.
*
* If the path resolves to a file directly, this is the result. If the path
* resolves to a folder containing a main module (such as `index.js`), then
* that file is the result.
*/
overlay[global]
deprecated File resolve(PathExpr path) {
path.getEnclosingModule() = this and
(
// handle the case where the import path is complete
exists(Container c | c = path.resolve() |
// import may refer to a file...
result = c
or
// ...or to a directory, in which case we import index.js in that directory
result = c.(Folder).getJavaScriptFile("index")
)
or
// handle the case where the import path is missing the extension
exists(Folder f | f = path.resolveUpTo(path.getNumComponent() - 1) |
result = f.getJavaScriptFile(path.getBaseName())
or
// If a js file was not found look for a file that compiles to js
path.getExtension() = ".js" and
not exists(f.getJavaScriptFile(path.getBaseName())) and
result = f.getJavaScriptFile(path.getStem())
)
)
}
}
/**
* 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. */
abstract Module getEnclosingModule();
/** DEPRECATED. Use `getImportedPathExpr` instead. */
overlay[global]
deprecated PathExpr getImportedPath() { result = this.getImportedPathExpr() }
/** Gets the (unresolved) path that this import refers to. */
abstract Expr getImportedPathExpr();
/** Gets the imported path as a string. */
final string getImportedPathString() { result = this.getImportedPathExpr().getStringValue() }
/**
* Gets an externs module the path of this import resolves to.
*
* Any externs module whose name exactly matches the imported
* path is assumed to be a possible target of the import.
*/
overlay[global]
Module resolveExternsImport() {
result.isExterns() and result.getName() = this.getImportedPathString()
}
/**
* Gets the module the path of this import resolves to.
*/
overlay[global]
Module resolveImportedPath() { result.getFile() = this.getImportedFile() }
/**
* Gets the module the path of this import resolves to.
*/
overlay[global]
File getImportedFile() { result = ImportPathResolver::resolveExpr(this.getImportedPathExpr()) }
/**
* DEPRECATED. Use `getImportedModule()` instead.
*/
overlay[global]
deprecated Module resolveFromTypeScriptSymbol() {
exists(CanonicalName symbol |
ast_node_symbol(this, symbol) and
ast_node_symbol(result, symbol)
)
}
/**
* Gets the module this import refers to.
*
* The result is either an externs module, or an actual source module;
* in cases of ambiguity, the former are preferred. This models the runtime
* behavior of Node.js imports, which prefer core modules such as `fs` over any
* source module of the same name.
*/
overlay[global]
cached
Module getImportedModule() {
Stages::Imports::ref() and
if exists(this.resolveExternsImport())
then result = this.resolveExternsImport()
else result = this.resolveImportedPath()
}
/**
* Gets the data flow node corresponding to the imported module.
*
* For example:
* ```js
* // ES2015 style
* import * as foo from "fs"; // Gets the node for `foo`
* import { readSync } from "fs"; // Gets a node representing the destructured import
*
* // CommonJS style
* require("fs"); // Gets the return value
*
* // AMD style
* define(["fs"], function(fs) { // Gets the node for the `fs` parameter
* });
* ```
*
* For default imports, this gets two nodes: the default import node, and a node representing the imported module:
* ```js
* import foo from "fs"; // gets both `foo` and a node representing the imported module
* ```
* This behaviour is to support non-standard compilers that treat default imports
* as namespace imports. Use `getImportedModuleNodeStrict()` to avoid this behaviour in cases
* where it would cause ambiguous data flow.
*/
abstract DataFlow::Node getImportedModuleNode();
/**
* Gets the same as `getImportedModuleNode()` except ambiguous default imports are excluded
* in cases where it would cause ambiguity between named exports and properties
* of a default export.
*/
overlay[global]
final DataFlow::Node getImportedModuleNodeStrict() {
result = this.getImportedModuleNode() and
not (
result = this.(ImportDeclaration).getAmbiguousDefaultImportNode() and
this.getImportedModule().(ES2015Module).hasBothNamedAndDefaultExports()
)
}
}