Skip to content

Commit f5279b2

Browse files
author
Max Schaefer
committed
JavaScript: Resolve AMD imports based on absolute paths if there is only a single candidate.
1 parent b29b3df commit f5279b2

File tree

14 files changed

+141
-8
lines changed

14 files changed

+141
-8
lines changed

javascript/ql/src/semmle/javascript/AMD.qll

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,59 @@ private class AmdDependencyImport extends Import {
216216
override PathExpr getImportedPath() {
217217
result = this
218218
}
219+
220+
/**
221+
* Gets a file that looks like it might be the target of this import.
222+
*
223+
* Specifically, we look for files whose absolute path ends with the imported path, possibly
224+
* adding well-known JavaScript file extensions like `.js`.
225+
*/
226+
private File guessTarget() {
227+
exists(PathString imported, string abspath, string dirname, string basename |
228+
targetCandidate(result, abspath, imported, dirname, basename)
229+
|
230+
abspath.regexpMatch(".*/\\Q" + imported + "\\E")
231+
or
232+
exists(Folder dir |
233+
// `dir` ends with the dirname of the imported path
234+
dir.getAbsolutePath().regexpMatch(".*/\\Q" + dirname + "\\E") or
235+
dirname = ""
236+
|
237+
result = dir.getJavaScriptFile(basename)
238+
)
239+
)
240+
}
241+
242+
/**
243+
* Holds if `f` is a file whose stem (that is, basename without extension) matches the imported path.
244+
*
245+
* Additionally, `abspath` is bound to the absolute path of `f`, `imported` to the imported path, and
246+
* `dirname` and `basename` to the dirname and basename (respectively) of `imported`.
247+
*/
248+
private predicate targetCandidate(
249+
File f, string abspath, PathString imported, string dirname, string basename
250+
) {
251+
imported = getImportedPath().getValue() and
252+
f.getStem() = imported.getStem() and
253+
f.getAbsolutePath() = abspath and
254+
dirname = imported.getDirName() and
255+
basename = imported.getBaseName()
256+
}
257+
258+
/**
259+
* Gets the module whose absolute path matches this import, if there is only a single such module.
260+
*/
261+
private Module resolveByAbsolutePath() {
262+
count(guessTarget()) = 1 and
263+
result.getFile() = guessTarget()
264+
}
265+
266+
override Module getImportedModule() {
267+
result = super.getImportedModule()
268+
or
269+
not exists(super.getImportedModule()) and
270+
result = resolveByAbsolutePath()
271+
}
219272
}
220273

221274
/**

javascript/ql/src/semmle/javascript/Paths.qll

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ private class ConsPath extends Path, TConsPath {
7676
override string toString() { result = pp(this) }
7777
}
7878

79+
/**
80+
* Gets a regular expression that can be used to parse slash-separated paths.
81+
*
82+
* The first capture group captures the dirname of the path, that is, everything
83+
* before the last slash, or the empty string if there isn't a slash.
84+
*
85+
* The second capture group captures the basename of the path, that is, everything
86+
* after the last slash, or the entire path if there isn't a slash.
87+
*
88+
* The third capture group captures the stem of the basename, that is, everything
89+
* before the last dot, or the entire basename if there isn't a dot.
90+
*
91+
* Finally, the fourth and fifth capture groups capture the extension of the basename,
92+
* that is, everything after the last dot. The fourth group includes the dot, the
93+
* fifth does not.
94+
*/
95+
private string pathRegex() {
96+
result = "(.*)(?:/|^)(([^/]*?)(\\.([^.]*))?)"
97+
}
98+
7999
/**
80100
* A string value that represents a (relative or absolute) file system path.
81101
*
@@ -98,7 +118,17 @@ abstract class PathString extends string {
98118
int getNumComponent() { result = count(int i | exists(getComponent(i))) }
99119

100120
/** Gets the base name of the folder or file this path refers to. */
101-
string getBaseName() { result = this.regexpCapture("(.*/|^)([^/]+)", 2) }
121+
string getBaseName() { result = this.regexpCapture(pathRegex(), 2) }
122+
123+
/**
124+
* Gets stem of the folder or file this path refers to, that is, the prefix of its base name
125+
* up to (but not including) the last dot character if there is one, or the entire
126+
* base name if there is not
127+
*/
128+
string getStem() { result = this.regexpCapture(pathRegex(), 3) }
129+
130+
/** Gets the path of the parent folder of the folder or file this path refers to. */
131+
string getDirName() { result = this.regexpCapture(pathRegex(), 1) }
102132

103133
/**
104134
* Gets the absolute path that the sub-path consisting of the first `n`
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
| a.js:1:1:3:3 | <toplevel> | a.js:1:1:3:2 | define( ... 2 };\\n}) |
22
| dir/b.js:1:1:3:3 | <toplevel> | dir/b.js:1:1:3:2 | define( ... : 42\\n}) |
3+
| lib/a.js:1:1:3:3 | <toplevel> | lib/a.js:1:1:3:2 | define( ... 2 };\\n}) |
4+
| lib/foo.js:1:1:4:0 | <toplevel> | lib/foo.js:1:1:3:2 | define( ... : 23\\n}) |
5+
| lib/nested/a.js:1:1:3:3 | <toplevel> | lib/nested/a.js:1:1:3:2 | define( ... 2 };\\n}) |
36
| tst2.js:1:1:3:3 | <toplevel> | tst2.js:1:1:3:2 | define( ... 42;\\n}) |
47
| tst3.js:1:1:3:3 | <toplevel> | tst3.js:1:1:3:2 | define( ... 42;\\n}) |
8+
| tst4.js:1:1:11:3 | <toplevel> | tst4.js:1:1:11:2 | define( ... };\\n}) |
59
| tst.js:1:1:6:3 | <toplevel> | tst.js:1:1:6:2 | define( ... };\\n}) |
610
| umd.js:1:1:14:4 | <toplevel> | umd.js:4:9:4:43 | define( ... actory) |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
| a.js:1:1:3:2 | define( ... 2 };\\n}) | a.js:1:8:3:1 | functio ... 42 };\\n} |
22
| dir/b.js:1:1:3:2 | define( ... : 42\\n}) | dir/b.js:1:8:3:1 | {\\n bar: 42\\n} |
3+
| lib/a.js:1:1:3:2 | define( ... 2 };\\n}) | lib/a.js:1:8:3:1 | functio ... 42 };\\n} |
4+
| lib/foo.js:1:1:3:2 | define( ... : 23\\n}) | lib/foo.js:1:8:3:1 | {\\n foo: 23\\n} |
5+
| lib/nested/a.js:1:1:3:2 | define( ... 2 };\\n}) | lib/nested/a.js:1:8:3:1 | functio ... 42 };\\n} |
36
| tst2.js:1:1:3:2 | define( ... 42;\\n}) | tst2.js:1:21:3:1 | functio ... = 42;\\n} |
47
| tst3.js:1:1:3:2 | define( ... 42;\\n}) | tst3.js:1:8:3:1 | functio ... = 42;\\n} |
8+
| tst4.js:1:1:11:2 | define( ... };\\n}) | tst4.js:6:11:11:1 | functio ... };\\n} |
59
| tst.js:1:1:6:2 | define( ... };\\n}) | tst.js:1:28:6:1 | functio ... };\\n} |
610
| umd.js:4:9:4:43 | define( ... actory) | umd.js:9:9:14:1 | functio ... };\\n} |

javascript/ql/test/library-tests/AMD/AmdModuleDependencies.expected

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
| tst2.js:1:1:3:2 | define( ... 42;\\n}) | tst2.js:1:9:1:17 | 'exports' |
22
| tst3.js:1:1:3:2 | define( ... 42;\\n}) | tst3.js:2:21:2:25 | './a' |
3+
| tst4.js:1:1:11:2 | define( ... };\\n}) | tst4.js:2:9:2:14 | 'a.js' |
4+
| tst4.js:1:1:11:2 | define( ... };\\n}) | tst4.js:3:9:3:13 | 'foo' |
5+
| tst4.js:1:1:11:2 | define( ... };\\n}) | tst4.js:4:9:4:18 | 'nested/a' |
6+
| tst4.js:1:1:11:2 | define( ... };\\n}) | tst4.js:5:9:5:20 | 'lib/foo.js' |
37
| tst.js:1:1:6:2 | define( ... };\\n}) | tst.js:1:9:1:13 | './a' |
48
| tst.js:1:1:6:2 | define( ... };\\n}) | tst.js:1:16:1:24 | './dir/b' |
59
| umd.js:4:9:4:43 | define( ... actory) | umd.js:4:17:4:21 | './a' |

javascript/ql/test/library-tests/AMD/AmdModuleExportedSymbol.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
| a.js:1:1:3:3 | <toplevel> | foo |
22
| dir/b.js:1:1:3:3 | <toplevel> | bar |
3+
| lib/a.js:1:1:3:3 | <toplevel> | foo |
4+
| lib/foo.js:1:1:4:0 | <toplevel> | foo |
5+
| lib/nested/a.js:1:1:3:3 | <toplevel> | foo |
36
| tst2.js:1:1:3:3 | <toplevel> | foo |
47
| tst3.js:1:1:3:3 | <toplevel> | foo |
8+
| tst4.js:1:1:11:3 | <toplevel> | bar |
9+
| tst4.js:1:1:11:3 | <toplevel> | foo |
510
| tst.js:1:1:6:3 | <toplevel> | bar |
611
| tst.js:1:1:6:3 | <toplevel> | foo |
712
| umd.js:1:1:14:4 | <toplevel> | bar |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
| a.js:1:1:3:2 | define( ... 2 };\\n}) | a.js:2:12:2:22 | { foo: 42 } | a.js:2:12:2:22 | { foo: 42 } |
22
| dir/b.js:1:1:3:2 | define( ... : 42\\n}) | dir/b.js:1:8:3:1 | {\\n bar: 42\\n} | dir/b.js:1:8:3:1 | {\\n bar: 42\\n} |
3+
| lib/a.js:1:1:3:2 | define( ... 2 };\\n}) | lib/a.js:2:12:2:22 | { foo: 42 } | lib/a.js:2:12:2:22 | { foo: 42 } |
4+
| lib/foo.js:1:1:3:2 | define( ... : 23\\n}) | lib/foo.js:1:8:3:1 | {\\n foo: 23\\n} | lib/foo.js:1:8:3:1 | {\\n foo: 23\\n} |
5+
| lib/nested/a.js:1:1:3:2 | define( ... 2 };\\n}) | lib/nested/a.js:2:12:2:22 | { foo: 42 } | lib/nested/a.js:2:12:2:22 | { foo: 42 } |
6+
| tst4.js:1:1:11:2 | define( ... };\\n}) | tst4.js:7:12:10:5 | {\\n ... r\\n } | tst4.js:7:12:10:5 | {\\n ... r\\n } |
37
| tst.js:1:1:6:2 | define( ... };\\n}) | tst.js:2:12:5:5 | {\\n ... r\\n } | tst.js:2:12:5:5 | {\\n ... r\\n } |
48
| umd.js:4:9:4:43 | define( ... actory) | umd.js:10:12:13:5 | {\\n ... r\\n } | umd.js:10:12:13:5 | {\\n ... r\\n } |
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
| tst3.js:1:1:3:3 | <toplevel> | a.js:1:1:3:3 | <toplevel> |
2-
| tst.js:1:1:6:3 | <toplevel> | a.js:1:1:3:3 | <toplevel> |
3-
| tst.js:1:1:6:3 | <toplevel> | dir/b.js:1:1:3:3 | <toplevel> |
4-
| umd.js:1:1:14:4 | <toplevel> | a.js:1:1:3:3 | <toplevel> |
5-
| umd.js:1:1:14:4 | <toplevel> | dir/b.js:1:1:3:3 | <toplevel> |
1+
| tst3.js:1:1:3:3 | <toplevel> | tst3.js:2:21:2:25 | './a' | a.js:1:1:3:3 | <toplevel> |
2+
| tst4.js:1:1:11:3 | <toplevel> | tst4.js:3:9:3:13 | 'foo' | lib/foo.js:1:1:4:0 | <toplevel> |
3+
| tst4.js:1:1:11:3 | <toplevel> | tst4.js:4:9:4:18 | 'nested/a' | lib/nested/a.js:1:1:3:3 | <toplevel> |
4+
| tst4.js:1:1:11:3 | <toplevel> | tst4.js:5:9:5:20 | 'lib/foo.js' | lib/foo.js:1:1:4:0 | <toplevel> |
5+
| tst.js:1:1:6:3 | <toplevel> | tst.js:1:9:1:13 | './a' | a.js:1:1:3:3 | <toplevel> |
6+
| tst.js:1:1:6:3 | <toplevel> | tst.js:1:16:1:24 | './dir/b' | dir/b.js:1:1:3:3 | <toplevel> |
7+
| umd.js:1:1:14:4 | <toplevel> | umd.js:4:17:4:21 | './a' | a.js:1:1:3:3 | <toplevel> |
8+
| umd.js:1:1:14:4 | <toplevel> | umd.js:4:24:4:32 | './dir/b' | dir/b.js:1:1:3:3 | <toplevel> |
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import javascript
22

3-
from AmdModule m
4-
select m, m.getAnImportedModule()
3+
from AmdModule m, Import i
4+
where i = m.getAnImport()
5+
select m, i, i.getImportedModule()

javascript/ql/test/library-tests/AMD/AmdModule_exports.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
| a.js:1:1:3:3 | <toplevel> | foo | a.js:2:14:2:20 | foo: 42 |
22
| dir/b.js:1:1:3:3 | <toplevel> | bar | dir/b.js:2:5:2:11 | bar: 42 |
3+
| lib/a.js:1:1:3:3 | <toplevel> | foo | lib/a.js:2:14:2:20 | foo: 42 |
4+
| lib/foo.js:1:1:4:0 | <toplevel> | foo | lib/foo.js:2:5:2:11 | foo: 23 |
5+
| lib/nested/a.js:1:1:3:3 | <toplevel> | foo | lib/nested/a.js:2:14:2:20 | foo: 42 |
36
| tst2.js:1:1:3:3 | <toplevel> | foo | tst2.js:2:5:2:15 | exports.foo |
47
| tst3.js:1:1:3:3 | <toplevel> | foo | tst3.js:2:29:2:39 | exports.foo |
8+
| tst4.js:1:1:11:3 | <toplevel> | bar | tst4.js:9:9:9:18 | bar: b.bar |
9+
| tst4.js:1:1:11:3 | <toplevel> | foo | tst4.js:8:9:8:18 | foo: a.foo |
510
| tst.js:1:1:6:3 | <toplevel> | bar | tst.js:4:9:4:18 | bar: b.bar |
611
| tst.js:1:1:6:3 | <toplevel> | foo | tst.js:3:9:3:18 | foo: a.foo |
712
| umd.js:1:1:14:4 | <toplevel> | bar | umd.js:11:9:11:18 | bar: a.foo |

0 commit comments

Comments
 (0)