Skip to content

Commit b444426

Browse files
committed
JS: Refactor default import interop
1 parent 0d0eaa2 commit b444426

File tree

3 files changed

+68
-9
lines changed

3 files changed

+68
-9
lines changed

javascript/ql/lib/semmle/javascript/ApiGraphs.qll

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ module API {
822822
or
823823
// special case: from `require('m')` to an export of `prop` in `m`
824824
exists(Import imp, Module m, string prop |
825-
pred = imp.getImportedModuleNode() and
825+
pred = imp.getImportedModuleNodeIfUnambiguous() and
826826
m = imp.getImportedModule() and
827827
lbl = Label::member(prop) and
828828
rhs = m.getAnExportedValue(prop)
@@ -1337,7 +1337,9 @@ module API {
13371337
result = nd.getALocalSource()
13381338
or
13391339
// additional backwards step from `require('m')` to `exports` or `module.exports` in m
1340-
exists(Import imp | imp.getImportedModuleNode() = trackDefNode(nd, t.continue()) |
1340+
exists(Import imp |
1341+
imp.getImportedModuleNodeIfUnambiguous() = trackDefNode(nd, t.continue())
1342+
|
13411343
result = DataFlow::exportsVarNode(imp.getImportedModule())
13421344
or
13431345
result = DataFlow::moduleVarNode(imp.getImportedModule()).getAPropertyRead("exports")

javascript/ql/lib/semmle/javascript/ES2015Modules.qll

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,18 @@ class ImportDeclaration extends Stmt, Import, @import_declaration {
130130

131131
override DataFlow::Node getImportedModuleNode() {
132132
// `import * as http from 'http'` or `import http from `http`'
133+
not exists(DataFlow::destructuredModuleImportNode(this)) and
133134
exists(ImportSpecifier is |
134135
is = this.getASpecifier() and
135136
result = DataFlow::valueNode(is)
136137
|
137-
is instanceof ImportNamespaceSpecifier and
138-
count(this.getASpecifier()) = 1
138+
is instanceof ImportNamespaceSpecifier
139139
or
140140
// For compatibility with the non-standard implementation of default imports,
141-
// treat default imports as namespace imports in cases where it can't cause ambiguity
142-
// between named exports and the properties of a default-exported object.
143-
not this.getImportedModule().(ES2015Module).hasBothNamedAndDefaultExports() and
144-
is.getImportedName() = "default"
141+
// treat default imports as namespace imports. In cases where it causes ambiguity
142+
// between named exports and the properties of a default-exported object, the caller
143+
// of `getImportedModuleNode()` must check `isDefaultImport()` and correct the behaviour.
144+
this.hasOnlyDefaultImport()
145145
)
146146
or
147147
// `import { createServer } from 'http'`
@@ -152,6 +152,12 @@ class ImportDeclaration extends Stmt, Import, @import_declaration {
152152
predicate isTypeOnly() { has_type_keyword(this) }
153153

154154
override string getAPrimaryQlClass() { result = "ImportDeclaration" }
155+
156+
private predicate hasOnlyDefaultImport() {
157+
unique( | | this.getASpecifier()) instanceof ImportDefaultSpecifier
158+
}
159+
160+
override predicate isDefaultImport() { this.hasOnlyDefaultImport() }
155161
}
156162

157163
/** A literal path expression appearing in an `import` declaration. */

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,58 @@ abstract class Import extends AstNode {
179179
}
180180

181181
/**
182-
* Gets the data flow node that the default import of this import is available at.
182+
* Gets the data flow node referring to imported module object.
183+
*
184+
* For example:
185+
* ```js
186+
* // ES2015 style
187+
* import * as foo from "bar"; // gets the node for `foo`
188+
* import foo from "bar"; // gets the node for `foo` (see note on default imports below)
189+
*
190+
* // CommonJS style
191+
* require("bar"); // gets the node for the `require` call
192+
*
193+
* // AMD style
194+
* define(["bar"], function(foo) { // gets the node for the `foo` parameter
195+
* })
196+
* ```
197+
*
198+
* For statements of form `import foo from "bar'`, this gives the node corresponding to `foo`.
199+
* Technically this should refer to the export binding named `"default"`, not the whole module, but for compatibility with non-standard
200+
* interpretations of default imports, this node is usually treated as also referring to the whole module.
201+
* If this behaviour is not wanted, use `isDefaultImport()` to handle that case differently.
183202
*/
184203
abstract DataFlow::Node getImportedModuleNode();
204+
205+
/**
206+
* Holds of the result of `getImportedModuleNode` actually refers to the export binding named `"default"`,
207+
* as opposed an object whose properties correspond to the export bindings of the imported module.
208+
*
209+
* For compatibility with non-standard interpretations of `default` imports, the default
210+
* import is usually returned by `getImportedModuleNode()`. If such behaviour is not wanted,
211+
* this predicate can be used to handle that case differently.
212+
*
213+
* For example, `getImportedModuleNode()` returns `foo` in both of these imports, but `isDefaultImport()`
214+
* only holds for the first one:
215+
* ```js
216+
* import foo from "bar";
217+
* import * as foo from "bar";
218+
* ```
219+
*/
220+
predicate isDefaultImport() { none() }
221+
222+
/**
223+
* Gets the same as `getImportedModuleNode()` expect this has no result for default imports when the target module
224+
* has both default and named exports.
225+
*
226+
* This is to avoid ambiguity between named export bindings and the properties of the default-exported object.
227+
*/
228+
pragma[nomagic]
229+
final DataFlow::Node getImportedModuleNodeIfUnambiguous() {
230+
if
231+
this.isDefaultImport() and
232+
this.getImportedModule().(ES2015Module).hasBothNamedAndDefaultExports()
233+
then none()
234+
else result = this.getImportedModuleNode()
235+
}
185236
}

0 commit comments

Comments
 (0)