mirror of
https://github.com/github/codeql.git
synced 2025-12-20 10:46:30 +01:00
QL code and tests for C#/C++/JavaScript.
This commit is contained in:
47
javascript/ql/src/NodeJS/CyclicImport.qhelp
Normal file
47
javascript/ql/src/NodeJS/CyclicImport.qhelp
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Module imports in node.js can be cyclic, that is, a module can (directly or indirectly) import itself.
|
||||
In order to prevent an infinite loop, such cyclic imports return incomplete copies of the loaded
|
||||
module, which do not yet have all exported members available. Such incomplete modules are difficult
|
||||
to work with, and cyclic dependencies in general make the code hard to maintain and understand.
|
||||
Consequently, cyclic imports should be avoided.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Refactor the involved modules to break the dependency cycle.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
In the following example, module <code>a.js</code> depends on module <code>b.js</code>, which in turn
|
||||
depends on module <code>a.js</code>, so they each import the other module, leading to an import cycle.
|
||||
</p>
|
||||
|
||||
<sample src="examples/CyclicImport.js" />
|
||||
|
||||
<p>
|
||||
The cyclic dependency can be fixed by moving <code>firstName</code> into module <code>b.js</code>,
|
||||
so that it no longer depends on <code>a.js</code>:
|
||||
</p>
|
||||
|
||||
<sample src="examples/CyclicImportGood.js" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
|
||||
<li>Brad Harris: <a href="http://selfcontained.us/2012/05/08/node-js-circular-dependencies/">node.js and circular dependencies</a>.</li>
|
||||
<li>Node.js Manual: <a href="http://nodejs.org/api/modules.html#modules_cycles">Modules</a>.</li>
|
||||
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
88
javascript/ql/src/NodeJS/CyclicImport.ql
Normal file
88
javascript/ql/src/NodeJS/CyclicImport.ql
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @name Cyclic module import
|
||||
* @description If a module indirectly imports itself, some modules involved in the import cycle may end up
|
||||
* with partially loaded dependencies. This is error-prone and should be avoided.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/node/cyclic-import
|
||||
* @tags reliability
|
||||
* maintainability
|
||||
* frameworks/node.js
|
||||
* @precision medium
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Gets the shortest suffix of the path of `c1` that differs from the
|
||||
* corresponding suffix of `c2`; if that suffix is a proper suffix, it is
|
||||
* additionally prefixed with `.../`.
|
||||
*/
|
||||
string ppDiff(Container c1, Container c2) {
|
||||
relatedAncestors(c1, c2) and
|
||||
if c1.getBaseName() = c2.getBaseName() then
|
||||
result = ppDiff(c1.getParentContainer(), c2.getParentContainer()) + "/" + c1.getBaseName()
|
||||
else if not exists(c1.getParentContainer()) or
|
||||
sourceLocationPrefix(c1.getParentContainer().getAbsolutePath()) then
|
||||
result = "/" + c1.getBaseName()
|
||||
else
|
||||
result = ".../" + c1.getBaseName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `c1` and `c2` are related modules (as determined by predicate
|
||||
* `relatedModules`), or (transitive) parent folders of such modules.
|
||||
*
|
||||
* This predicate is used to restrict the domain of `ppDiff`.
|
||||
*/
|
||||
predicate relatedAncestors(Container c1, Container c2) {
|
||||
exists (NodeModule m, NodeModule n | relatedModules(m, n) |
|
||||
c1 = m.getFile() and c2 = n.getFile()
|
||||
) or
|
||||
relatedAncestors(c1.(Folder).getAChildContainer(), c2.(Folder).getAChildContainer())
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pretty-printed name for `m` that distinguishes it from `other`:
|
||||
* this is simply the name of `m` it is different from the name of `other`,
|
||||
* or else a suffix of the path of `m` that is different from `other` as
|
||||
* computed by `ppDiff`.
|
||||
*/
|
||||
string pp(NodeModule m, NodeModule other) {
|
||||
relatedModules(m, other) and
|
||||
if m.getName() = other.getName() and m != other then
|
||||
result = ppDiff(m.getFile(), other.getFile())
|
||||
else
|
||||
result = m.getName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `m` imports `n` or vice versa.
|
||||
*
|
||||
* This predicate is used to restrict the domain of `pp`.
|
||||
*/
|
||||
predicate relatedModules(NodeModule m, NodeModule n) {
|
||||
n = m.getAnImportedModule() or m = n.getAnImportedModule()
|
||||
}
|
||||
|
||||
from NodeModule m, Require r, NodeModule imported, string msg,
|
||||
ASTNode linktarget, string linktext
|
||||
where r = m.getAnImport() and
|
||||
imported = r.getImportedModule() and
|
||||
if imported = m then
|
||||
// set linktarget and linktext to dummy values in this case
|
||||
(msg = "directly imports itself" and linktarget = m and linktext = "")
|
||||
else
|
||||
// find an import in `imported` that (directly or indirectly) imports `m`
|
||||
exists (Require r2, Module imported2 | r2 = imported.getAnImport() and imported2 = r2.getImportedModule() |
|
||||
imported2.getAnImportedModule*() = m and
|
||||
msg = "imports module " + pp(imported, m) + ", which in turn $@ it" and
|
||||
linktarget = r2 and
|
||||
// check whether it is a direct or indirect import
|
||||
(if imported2 = m then
|
||||
linktext = "imports"
|
||||
else
|
||||
// only report indirect imports if there is no direct import
|
||||
(linktext = "indirectly imports" and not imported.getAnImportedModule() = m))
|
||||
)
|
||||
select r, "Module " + pp(m, imported) + " " + msg + ".", linktarget, linktext
|
||||
47
javascript/ql/src/NodeJS/DubiousImport.qhelp
Normal file
47
javascript/ql/src/NodeJS/DubiousImport.qhelp
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Since JavaScript is a dynamically typed language, module imports in node.js are not statically checked
|
||||
for correctness: calls to <code>require</code> simply return an object containing all the exports of
|
||||
the imported module, and accessing a member that was not, in fact, exported, yields
|
||||
<code>undefined</code>. This is most likely unintentional and usually indicates a bug.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Examine the import in question and determine the correct name of the symbol to import.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
In the following example, module <code>point.js</code> exports the function <code>Point</code> by
|
||||
assigning it to <code>module.exports</code>. The client module <code>client.js</code> tries to
|
||||
import it by reading from the <code>Point</code> property, but since this property does not exist
|
||||
the result will be <code>undefined</code>, and the <code>new</code> invocation will fail.
|
||||
</p>
|
||||
|
||||
<sample src="examples/DubiousImport.js" />
|
||||
|
||||
<p>
|
||||
Instead of reading the <code>Point</code> property, <code>client.js</code> should directly use
|
||||
the result of the <code>require</code> call:
|
||||
</p>
|
||||
|
||||
<sample src="examples/DubiousImportGood.js" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
|
||||
<li>Node.js Manual: <a href="http://nodejs.org/api/modules.html">Modules</a>.</li>
|
||||
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
68
javascript/ql/src/NodeJS/DubiousImport.ql
Normal file
68
javascript/ql/src/NodeJS/DubiousImport.ql
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @name Dubious import
|
||||
* @description Importing a symbol from a module that does not export it most likely indicates a bug.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/node/import-without-export
|
||||
* @tags reliability
|
||||
* maintainability
|
||||
* frameworks/node.js
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/** Holds if `m` is likely to have exports that are not picked up by the analysis. */
|
||||
predicate hasUntrackedExports(NodeModule m) {
|
||||
// look for assignments of the form `module.exports[p] = ...`, where we cannot
|
||||
// determine the name of the exported property being assigned
|
||||
exists (DataFlow::PropWrite pwn |
|
||||
pwn.getBase().analyze().getAValue() = m.getAModuleExportsValue() and
|
||||
not exists(pwn.getPropertyName())
|
||||
)
|
||||
or
|
||||
// look for assignments of the form `module.exports = exp` where `exp` is indefinite
|
||||
exists (AbstractModuleObject am, AnalyzedPropertyWrite apw, DataFlow::AnalyzedNode exp |
|
||||
am.getModule() = m and
|
||||
apw.writes(am, "exports", exp) and
|
||||
exp.getAValue().isIndefinite(_)
|
||||
)
|
||||
or
|
||||
// look for function calls of the form `f(module.exports)`
|
||||
exists (InvokeExpr invk |
|
||||
invk.getAnArgument().analyze().getAValue() = m.getAModuleExportsValue()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is an assignment anywhere defining `prop` on the result of
|
||||
* a `require` import of module `m`.
|
||||
*/
|
||||
predicate propDefinedOnRequire(NodeModule m, string prop) {
|
||||
exists (DataFlow::ModuleImportNode imp |
|
||||
imp.asExpr().(Require).getImportedModule() = m and
|
||||
exists(imp.getAPropertyWrite(prop))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the base expression of `pacc` could refer to the result of
|
||||
* a `require` import of module `m`.
|
||||
*/
|
||||
predicate propAccessOn(PropAccess pacc, NodeModule m) {
|
||||
exists (DataFlow::ModuleImportNode imp |
|
||||
imp.asExpr().(Require).getImportedModule() = m and
|
||||
imp.flowsToExpr(pacc.getBase())
|
||||
)
|
||||
}
|
||||
|
||||
from NodeModule m, PropAccess pacc, string prop
|
||||
where propAccessOn(pacc, m) and count(NodeModule mm | propAccessOn(pacc, mm)) = 1 and
|
||||
prop = pacc.getPropertyName() and
|
||||
// m doesn't export 'prop'
|
||||
not prop = m.getAnExportedSymbol() and
|
||||
// 'prop' isn't otherwise defined on m
|
||||
not propDefinedOnRequire(m, prop) and
|
||||
// m doesn't use complicated exports
|
||||
not hasUntrackedExports(m)
|
||||
select pacc, "Module $@ does not export symbol " + prop + ".", m, m.getName()
|
||||
48
javascript/ql/src/NodeJS/InvalidExport.qhelp
Normal file
48
javascript/ql/src/NodeJS/InvalidExport.qhelp
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Node.js modules that only export a single value commonly do so by assigning it directly to the
|
||||
<code>module.exports</code> property. A common mistake is to assign it to the <code>exports</code>
|
||||
variable instead, but this simply overwrites the value of <code>exports</code> without affecting
|
||||
the value of <code>module.exports</code>, and does not lead to anything being exported.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Rewrite the assignment to assign to <code>module.exports</code> instead.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
In the following example, module <code>point.js</code> attempts to export the function <code>Point</code>
|
||||
by assigning it to <code>exports</code>. As explained above, this does not work as expected: after
|
||||
the assignment, the <code>exports</code> <i>variable</i> will contain a reference to <code>Point</code>, but
|
||||
the <code>module.exports</code> <i>property</i> still contains a reference to an empty object.
|
||||
Consequently, the client code in <code>client.js</code> will fail, since it attempts to call an object as
|
||||
a constructor.
|
||||
</p>
|
||||
|
||||
<sample src="examples/InvalidExport.js" />
|
||||
|
||||
<p>
|
||||
Instead of assigning to <code>exports</code>, <code>point.js</code> should assign to <code>module.exports</code>:
|
||||
</p>
|
||||
|
||||
<sample src="examples/InvalidExportGood.js" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
|
||||
<li>Node.js Manual: <a href="http://nodejs.org/api/modules.html#modules_exports_alias">exports alias</a>.</li>
|
||||
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
49
javascript/ql/src/NodeJS/InvalidExport.ql
Normal file
49
javascript/ql/src/NodeJS/InvalidExport.ql
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @name Assignment to exports variable
|
||||
* @description Assigning to the special 'exports' variable only overwrites its value and does not export
|
||||
* anything. Such an assignment is hence most likely unintentional.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/node/assignment-to-exports-variable
|
||||
* @tags maintainability
|
||||
* frameworks/node.js
|
||||
* external/cwe/cwe-563
|
||||
* @precision very-high
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Holds if `assign` assigns the value of `nd` to `exportsVar`, which is an `exports` variable
|
||||
*/
|
||||
predicate exportsAssign(Assignment assgn, Variable exportsVar, DataFlow::Node nd) {
|
||||
exists (NodeModule m |
|
||||
exportsVar = m.getScope().getVariable("exports") and
|
||||
assgn.getLhs() = exportsVar.getAnAccess() and
|
||||
nd = assgn.getRhs().flow()
|
||||
)
|
||||
or
|
||||
exportsAssign(assgn, exportsVar, nd.getASuccessor())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `pw` assigns the value of `nd` to `module.exports`.
|
||||
*/
|
||||
predicate moduleExportsAssign(DataFlow::PropWrite pw, DataFlow::Node nd) {
|
||||
pw.getBase().asExpr() instanceof ModuleAccess and
|
||||
pw.getPropertyName() = "exports" and
|
||||
nd = pw.getRhs()
|
||||
or
|
||||
moduleExportsAssign(pw, nd.getASuccessor())
|
||||
}
|
||||
|
||||
from Assignment assgn, Variable exportsVar, DataFlow::Node exportsVal
|
||||
where exportsAssign(assgn, exportsVar, exportsVal) and
|
||||
not exists(exportsVal.getAPredecessor()) and
|
||||
not (
|
||||
// this is OK if `exportsVal` flows into `module.exports`
|
||||
moduleExportsAssign(_, exportsVal) and
|
||||
// however, if there are no further uses of `exports` the assignment is useless anyway
|
||||
strictcount (exportsVar.getAnAccess()) > 1
|
||||
)
|
||||
select assgn, "Assigning to 'exports' does not export anything."
|
||||
48
javascript/ql/src/NodeJS/MissingExports.qhelp
Normal file
48
javascript/ql/src/NodeJS/MissingExports.qhelp
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Referencing an otherwise undeclared global variable in a module that exports
|
||||
a definition of the same name is confusing and may indicate a bug.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
If the global variable reference is intentional, consider adding a JSLint
|
||||
<code>/*global ...*/</code> directive or an externs declaration to declare the variable.
|
||||
</p>
|
||||
<p>
|
||||
If the global variable reference is unintentional, qualifying the reference
|
||||
with <code>exports</code> will make it refer to the exported definition instead.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
In the following example, the module exports two functions <code>checkOne</code>
|
||||
and <code>checkList</code>. The latter is also stored in a variable of the same name
|
||||
that is local to the module, but the former is not. Hence the call <code>checkOne(xs[i])</code>
|
||||
on line 7 does not refer to the function defined on line 1, but to an otherwise undeclared
|
||||
global variable also called <code>checkOne</code>.
|
||||
</p>
|
||||
|
||||
<sample src="examples/MissingExports.js" />
|
||||
|
||||
<p>
|
||||
Assuming that the intention is to call the <code>checkOne</code> function defined on line 1,
|
||||
the call should be qualified with <code>exports</code> like this:
|
||||
</p>
|
||||
|
||||
<sample src="examples/MissingExportsGood.js" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Node.js: <a href="https://nodejs.org/api/modules.html">Modules</a>.</li>
|
||||
<li>JSLint Help: <a href="http://www.jslint.com/help.html">JSLint Directives</a>.</li>
|
||||
<li>Closure Compiler: <a href="https://developers.google.com/closure/compiler/docs/api-tutorial3">Advanced Compilation and Externs</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
43
javascript/ql/src/NodeJS/MissingExports.ql
Normal file
43
javascript/ql/src/NodeJS/MissingExports.ql
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @name Missing exports qualifier
|
||||
* @description Referencing an undeclared global variable in a module that exports
|
||||
* a definition of the same name is confusing and may indicate a bug.
|
||||
* @kind problem
|
||||
* @problem.severity error
|
||||
* @id js/node/missing-exports-qualifier
|
||||
* @tags maintainability
|
||||
* frameworks/node.js
|
||||
* @precision high
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/** Holds if variable `v` is assigned somewhere in module `m`. */
|
||||
predicate definedInModule(GlobalVariable v, NodeModule m) {
|
||||
exists (LValue def |
|
||||
def.getTopLevel() = m and
|
||||
def.(Expr).accessesGlobal(v.getName())
|
||||
)
|
||||
}
|
||||
|
||||
from NodeModule m, GlobalVariable f, InvokeExpr invk, ASTNode export, GlobalVarAccess acc
|
||||
where m.exports(f.getName(), export) and
|
||||
acc = f.getAnAccess() and
|
||||
invk.getCallee() = acc and
|
||||
invk.getTopLevel() = m and
|
||||
|
||||
// don't flag if the variable is defined in the same module
|
||||
not definedInModule(f, m) and
|
||||
|
||||
// don't flag if there is a linter directive declaring the variable
|
||||
not exists (Linting::GlobalDeclaration glob |
|
||||
glob.declaresGlobalForAccess(acc)
|
||||
) and
|
||||
|
||||
// don't flag if there is an externs declaration for the variable
|
||||
not exists (ExternalGlobalDecl egd | egd.getName() = f.getName()) and
|
||||
|
||||
// don't flag if the invocation could refer to a property introduced by `with`
|
||||
not exists (WithStmt with | with.mayAffect(invk.getCallee()))
|
||||
select invk, "'" + f.getName() + "' references an undeclared global variable, "
|
||||
+ "not the variable exported $@.", export, "here"
|
||||
55
javascript/ql/src/NodeJS/UnresolvableImport.qhelp
Normal file
55
javascript/ql/src/NodeJS/UnresolvableImport.qhelp
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Node.js modules can be imported either directly by specifying a file or folder, or indirectly
|
||||
by specifying a module name, which will be looked up in a <code>node_modules</code> folder.
|
||||
In the latter case, care should be taken that the imported module is either bundled with
|
||||
the code that uses it, or declared as a dependency in the <code>package.json</code> file
|
||||
to ensure that the import does not fail at runtime.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Declare the dependency in the <code>package.json</code> file or include an externs file
|
||||
for it during extraction.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Externs files for all the core packages of Node.js are available in the
|
||||
<code>tool/data/externs/nodejs</code> directory of the distribution, and are included
|
||||
by default for projects created using bootstrap.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
In the following example, the <code>package.json</code> file specifies no dependencies,
|
||||
but <code>index.js</code> imports <code>acorn</code>. This import will fail unless a copy
|
||||
of <code>acorn</code> happens to be installed (for instance in the user's <code>node_modules</code>
|
||||
folder). On the other hand, the import of <code>fs</code> is unproblematic, since
|
||||
<code>fs</code> is a standard module that is included with every Node.js installation.
|
||||
</p>
|
||||
|
||||
<sample src="examples/UnresolvableImport.js" />
|
||||
|
||||
<p>
|
||||
The dependency on <code>acorn</code> should be declared in the <code>package.json</code> file:
|
||||
</p>
|
||||
|
||||
<sample src="examples/UnresolvableImportGood.js" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
|
||||
<li>Node.js Manual: <a href="https://nodejs.org/api/modules.html#modules_all_together">Module resolution algorithm</a>.</li>
|
||||
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
49
javascript/ql/src/NodeJS/UnresolvableImport.ql
Normal file
49
javascript/ql/src/NodeJS/UnresolvableImport.ql
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @name Unresolvable import
|
||||
* @description An import that cannot be resolved to a module will
|
||||
* cause an exception at runtime.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/node/unresolvable-import
|
||||
* @tags maintainability
|
||||
* frameworks/node.js
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Gets the `package.json` of the nearest enclosing NPM package to which
|
||||
* file `f` belongs.
|
||||
*/
|
||||
PackageJSON getClosestPackageJSON(Folder f) {
|
||||
result = f.(NPMPackage).getPackageJSON() or
|
||||
not f instanceof NPMPackage and result = getClosestPackageJSON(f.getParentContainer())
|
||||
}
|
||||
|
||||
from Require r, string path, string mod
|
||||
where path = r.getImportedPath().getValue() and
|
||||
|
||||
// the imported module is the initial segment of the path, up to
|
||||
// `/` or the end of the string, whichever comes first; we exclude
|
||||
// local paths starting with `.` or `/`, since they might refer to files
|
||||
// downloaded or generated during the build
|
||||
mod = path.regexpCapture("([^./][^/]*)(/.*|$)", 1) and
|
||||
|
||||
// exclude WebPack/Require.js loaders
|
||||
not mod.matches("%!%") and
|
||||
|
||||
// import cannot be resolved statically
|
||||
not exists (r.getImportedModule()) and
|
||||
|
||||
// no enclosing NPM package declares a dependency on `mod`
|
||||
forex (NPMPackage pkg, PackageJSON pkgJSON |
|
||||
pkg.getAModule() = r.getTopLevel() and pkgJSON = pkg.getPackageJSON() |
|
||||
not pkgJSON.declaresDependency(mod, _) and
|
||||
not pkgJSON.getPeerDependencies().getADependency(mod, _) and
|
||||
// exclude packages depending on `fbjs`, which automatically pulls in many otherwise
|
||||
// undeclared dependencies
|
||||
not pkgJSON.declaresDependency("fbjs", _)
|
||||
)
|
||||
select r, "Module " + mod + " cannot be resolved, and is not declared as a dependency in $@.",
|
||||
getClosestPackageJSON(r.getFile().getParentContainer()), "package.json"
|
||||
73
javascript/ql/src/NodeJS/UnusedDependency.qhelp
Normal file
73
javascript/ql/src/NodeJS/UnusedDependency.qhelp
Normal file
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Specifying an unused dependency in <code>package.json</code> may make packages harder to install.
|
||||
The unused dependency will still be downloaded by npm, and if it conflicts with another package
|
||||
installation will become difficult or impossible.
|
||||
</p>
|
||||
<p>
|
||||
Dependencies on packages that are only used during development (such as testing frameworks or
|
||||
linters) should be listed under <code>devDependencies</code> rather than <code>dependencies</code>,
|
||||
since they are not required for deployment.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Remove the unused dependency.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
In the following example, the <code>package.json</code> file specifies dependencies on both
|
||||
<code>acorn</code> and <code>esprima</code>, but in fact only <code>acorn</code> is used.
|
||||
The dependency on <code>esprima</code> can simply be removed.
|
||||
</p>
|
||||
|
||||
<sample src="examples/UnusedDependency.js" />
|
||||
|
||||
<p>
|
||||
As another example, the following <code>package.json</code> file specifies a dependency on
|
||||
<code>eslint-plugin-babel</code>, a plugin for a popular linter:
|
||||
</p>
|
||||
|
||||
<sample language="javascript">
|
||||
{
|
||||
"name": "another-example-package",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"eslint-plugin-babel": "3.3.0"
|
||||
}
|
||||
}
|
||||
</sample>
|
||||
|
||||
<p>
|
||||
Since this dependency is only used during development, it should instead be listed under
|
||||
<code>devDependencies</code>:
|
||||
</p>
|
||||
|
||||
<sample language="javascript">
|
||||
{
|
||||
"name": "another-example-package",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"eslint-plugin-babel": "3.3.0"
|
||||
}
|
||||
}
|
||||
</sample>
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
|
||||
<li>NPM Manual: <a href="https://www.npmjs.org/doc/files/package.json.html">package.json</a>.</li>
|
||||
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
110
javascript/ql/src/NodeJS/UnusedDependency.ql
Normal file
110
javascript/ql/src/NodeJS/UnusedDependency.ql
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @name Unused npm dependency
|
||||
* @description If unnecessary package dependencies are included in package.json, the
|
||||
* package will become harder to install.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/node/unused-npm-dependency
|
||||
* @tags maintainability
|
||||
* frameworks/node.js
|
||||
* @precision medium
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Holds if the NPM package `pkg` declares a dependency on package `name`,
|
||||
* and `dep` is the corresponding declaration in the `package.json` file.
|
||||
*/
|
||||
predicate declaresDependency(NPMPackage pkg, string name, JSONValue dep) {
|
||||
dep = pkg.getPackageJSON().getDependencies().getPropValue(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a path expression in a module belonging to `pkg`.
|
||||
*/
|
||||
PathExpr getAPathExpr(NPMPackage pkg) {
|
||||
result.getEnclosingModule() = pkg.getAModule()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a URL-valued attribute in a module or HTML file belonging to `pkg`.
|
||||
*/
|
||||
DOM::AttributeDefinition getAURLAttribute(NPMPackage pkg) {
|
||||
result.getFile() = pkg.getAFile() and
|
||||
DOM::isUrlValuedAttribute(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of a script in the 'scripts' object of `pkg`.
|
||||
* The script makes use of a declared `dependency` of `pkg`.
|
||||
*/
|
||||
string getPackageScriptNameWithDependency(NPMPackage pkg, string dependency){
|
||||
exists (JSONObject scriptsObject, string scriptName, string script |
|
||||
declaresDependency(pkg, dependency, _) and
|
||||
scriptsObject = pkg.getPackageJSON().getPropValue("scripts") and
|
||||
script = scriptsObject.getPropStringValue(scriptName) and
|
||||
script.regexpMatch(".*\\b\\Q" + dependency + "\\E\\b.*") and
|
||||
result = scriptName
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the NPM package `pkg` declares a dependency on package `name`,
|
||||
* and uses it at least once.
|
||||
*/
|
||||
predicate usesDependency(NPMPackage pkg, string name) {
|
||||
declaresDependency(pkg, name, _) and
|
||||
(
|
||||
// there is a path expression (e.g., in a `require` or `import`) that
|
||||
// references `pkg`
|
||||
exists (PathExpr path | path = getAPathExpr(pkg) |
|
||||
// check whether the path is `name` or starts with `name/`, ignoring a prefix that ends with '!' (example: "scriptloader!moment")
|
||||
path.getValue().regexpMatch("(.*!)?\\Q" + name + "\\E(/.*)?")
|
||||
)
|
||||
or
|
||||
// there is an HTML URL attribute that may reference `pkg`
|
||||
exists (DOM::AttributeDefinition attr | attr = getAURLAttribute(pkg) |
|
||||
// check whether the URL contains `node_modules/name`
|
||||
attr.getStringValue().regexpMatch(".*\\bnode_modules/\\Q" + name + "\\E(/.*)?")
|
||||
)
|
||||
or
|
||||
// there is a reference in a package.json white-listed script
|
||||
exists (string packageScriptName |
|
||||
packageScriptName = getPackageScriptNameWithDependency(pkg, name) |
|
||||
packageScriptName = "preinstall" or
|
||||
packageScriptName = "install" or
|
||||
packageScriptName = "postinstall"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `pkg` implicitly requires module `name`.
|
||||
*
|
||||
* Currently, the only implicit requires that are recognized are Express
|
||||
* view engine definitions, which (may) implicitly require the specified
|
||||
* engine as a module.
|
||||
*/
|
||||
predicate implicitRequire(NPMPackage pkg, string name) {
|
||||
// look for Express `set('view engine', ...)` calls
|
||||
exists (MethodCallExpr setViewEngine, string engine |
|
||||
Express::appCreation().flowsToExpr(setViewEngine.getReceiver()) and
|
||||
setViewEngine.getMethodName() = "set" and
|
||||
setViewEngine.getArgument(0).getStringValue() = "view engine" and
|
||||
setViewEngine.getArgument(1).getStringValue() = engine and
|
||||
setViewEngine.getTopLevel() = pkg.getAModule() |
|
||||
// chop off leading dot, if any
|
||||
if engine.matches(".%") then
|
||||
name = engine.suffix(1)
|
||||
else
|
||||
name = engine
|
||||
)
|
||||
}
|
||||
|
||||
from NPMPackage pkg, string name, JSONValue dep
|
||||
where exists (pkg.getAModule()) and
|
||||
declaresDependency(pkg, name, dep) and
|
||||
not usesDependency(pkg, name) and
|
||||
not implicitRequire(pkg, name)
|
||||
select dep, "Unused dependency '" + name + "'."
|
||||
17
javascript/ql/src/NodeJS/examples/CyclicImport.js
Normal file
17
javascript/ql/src/NodeJS/examples/CyclicImport.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// a.js
|
||||
var b = require('./b');
|
||||
|
||||
var title = "Ms";
|
||||
|
||||
function example() {
|
||||
return title + " " + b.fullName;
|
||||
}
|
||||
|
||||
exports.firstName = "Ada";
|
||||
|
||||
// b.js
|
||||
var a = require('./a');
|
||||
|
||||
var lastName = "Lovelace";
|
||||
|
||||
exports.fullName = a.firstName + " " + lastName;
|
||||
14
javascript/ql/src/NodeJS/examples/CyclicImportGood.js
Normal file
14
javascript/ql/src/NodeJS/examples/CyclicImportGood.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// a.js
|
||||
var b = require('./b');
|
||||
|
||||
var title = "Ms";
|
||||
|
||||
function example() {
|
||||
return title + " " + b.fullName;
|
||||
}
|
||||
|
||||
// b.js
|
||||
var firstName = "Ada",
|
||||
lastName = "Lovelace";
|
||||
|
||||
exports.fullName = firstName + " " + lastName;
|
||||
17
javascript/ql/src/NodeJS/examples/DubiousImport.js
Normal file
17
javascript/ql/src/NodeJS/examples/DubiousImport.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// point.js
|
||||
function Point(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
Point.prototype.distance = function() {
|
||||
return Math.sqrt(this.x*this.x+this.y*this.y);
|
||||
};
|
||||
|
||||
module.exports = Point;
|
||||
|
||||
// client.js
|
||||
var Point = require('./point').Point;
|
||||
|
||||
var pyth = new Point(3, 4);
|
||||
console.log(pyth.distance());
|
||||
17
javascript/ql/src/NodeJS/examples/DubiousImportGood.js
Normal file
17
javascript/ql/src/NodeJS/examples/DubiousImportGood.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// point.js
|
||||
function Point(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
Point.prototype.distance = function() {
|
||||
return Math.sqrt(this.x*this.x+this.y*this.y);
|
||||
};
|
||||
|
||||
module.exports = Point;
|
||||
|
||||
// client.js
|
||||
var Point = require('./point');
|
||||
|
||||
var pyth = new Point(3, 4);
|
||||
console.log(pyth.distance());
|
||||
17
javascript/ql/src/NodeJS/examples/InvalidExport.js
Normal file
17
javascript/ql/src/NodeJS/examples/InvalidExport.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// point.js
|
||||
function Point(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
Point.prototype.distance = function() {
|
||||
return Math.sqrt(this.x*this.x+this.y*this.y);
|
||||
};
|
||||
|
||||
exports = Point;
|
||||
|
||||
// client.js
|
||||
var Point = require('./point');
|
||||
|
||||
var pyth = new Point(3, 4);
|
||||
console.log(pyth.distance());
|
||||
17
javascript/ql/src/NodeJS/examples/InvalidExportGood.js
Normal file
17
javascript/ql/src/NodeJS/examples/InvalidExportGood.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// point.js
|
||||
function Point(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
Point.prototype.distance = function() {
|
||||
return Math.sqrt(this.x*this.x+this.y*this.y);
|
||||
};
|
||||
|
||||
module.exports = Point;
|
||||
|
||||
// client.js
|
||||
var Point = require('./point');
|
||||
|
||||
var pyth = new Point(3, 4);
|
||||
console.log(pyth.distance());
|
||||
8
javascript/ql/src/NodeJS/examples/MissingExports.js
Normal file
8
javascript/ql/src/NodeJS/examples/MissingExports.js
Normal file
@@ -0,0 +1,8 @@
|
||||
exports.checkOne = function(x) {
|
||||
if (!x) throw new Error();
|
||||
};
|
||||
|
||||
var checkList = exports.checkList = function(xs) {
|
||||
for (var i=0; i<xs.length; ++i)
|
||||
checkOne(xs[i]);
|
||||
};
|
||||
8
javascript/ql/src/NodeJS/examples/MissingExportsGood.js
Normal file
8
javascript/ql/src/NodeJS/examples/MissingExportsGood.js
Normal file
@@ -0,0 +1,8 @@
|
||||
exports.checkOne = function(x) {
|
||||
if (!x) throw new Error();
|
||||
};
|
||||
|
||||
var checkList = exports.checkList = function(xs) {
|
||||
for (var i=0; i<xs.length; ++i)
|
||||
exports.checkOne(xs[i]);
|
||||
};
|
||||
10
javascript/ql/src/NodeJS/examples/UnresolvableImport.js
Normal file
10
javascript/ql/src/NodeJS/examples/UnresolvableImport.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// package.json
|
||||
{
|
||||
"name": "example-package",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
|
||||
// index.js
|
||||
var acorn = require('acorn'),
|
||||
fs = require('fs');
|
||||
acorn.parse(fs.readFileSync('tst.js'), 'utf-8');
|
||||
13
javascript/ql/src/NodeJS/examples/UnresolvableImportGood.js
Normal file
13
javascript/ql/src/NodeJS/examples/UnresolvableImportGood.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// package.json
|
||||
{
|
||||
"name": "example-package",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"acorn": "*"
|
||||
}
|
||||
}
|
||||
|
||||
// index.js
|
||||
var acorn = require('acorn'),
|
||||
fs = require('fs');
|
||||
acorn.parse(fs.readFileSync('tst.js'), 'utf-8');
|
||||
14
javascript/ql/src/NodeJS/examples/UnusedDependency.js
Normal file
14
javascript/ql/src/NodeJS/examples/UnusedDependency.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// package.json
|
||||
{
|
||||
"name": "example-package",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"acorn": "*",
|
||||
"esprima": "~2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
// index.js
|
||||
var acorn = require('acorn'),
|
||||
fs = require('fs');
|
||||
acorn.parse(fs.readFileSync('tst.js'), 'utf-8');
|
||||
Reference in New Issue
Block a user