Skip to content

Commit 201f39e

Browse files
committed
JS: Implement import resolution
1 parent b446a00 commit 201f39e

File tree

2 files changed

+259
-31
lines changed

2 files changed

+259
-31
lines changed

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

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import javascript
88
private import semmle.javascript.internal.CachedStages
9+
private import semmle.javascript.internal.paths.PathExprResolver as PathExprResolver
910

1011
/**
1112
* A module, which may either be an ECMAScript 2015-style module,
@@ -139,7 +140,7 @@ abstract class Import extends AstNode {
139140
* Gets the module the path of this import resolves to.
140141
*/
141142
Module resolveImportedPath() {
142-
result.getFile() = this.getEnclosingModule().resolve(this.getImportedPath())
143+
result.getFile() = PathExprResolver::resolvePathExpr(this.getImportedPath())
143144
}
144145

145146
/**
@@ -168,9 +169,9 @@ abstract class Import extends AstNode {
168169
}
169170

170171
/**
171-
* Gets the imported module, as determined by the TypeScript compiler, if any.
172+
* DEPRECATED. Use `getImportedModule()` instead.
172173
*/
173-
private Module resolveFromTypeScriptSymbol() {
174+
deprecated Module resolveFromTypeScriptSymbol() {
174175
exists(CanonicalName symbol |
175176
ast_node_symbol(this, symbol) and
176177
ast_node_symbol(result, symbol)
@@ -193,9 +194,7 @@ abstract class Import extends AstNode {
193194
else (
194195
result = this.resolveAsProvidedModule() or
195196
result = this.resolveImportedPath() or
196-
result = this.resolveFromTypeRoot() or
197-
result = this.resolveFromTypeScriptSymbol() or
198-
result = resolveNeighbourPackage(this.getImportedPath().getValue())
197+
result = this.resolveFromTypeRoot()
199198
)
200199
}
201200

@@ -204,28 +203,3 @@ abstract class Import extends AstNode {
204203
*/
205204
abstract DataFlow::Node getImportedModuleNode();
206205
}
207-
208-
/**
209-
* Gets a module imported from another package in the same repository.
210-
*
211-
* No support for importing from folders inside the other package.
212-
*/
213-
private Module resolveNeighbourPackage(PathString importPath) {
214-
exists(PackageJson json | importPath = json.getPackageName() and result = json.getMainModule())
215-
or
216-
exists(string package |
217-
result.getFile().getParentContainer() = getPackageFolder(package) and
218-
importPath = package + "/" + [result.getFile().getBaseName(), result.getFile().getStem()]
219-
)
220-
}
221-
222-
/**
223-
* Gets the folder for a package that has name `package` according to a package.json file in the resulting folder.
224-
*/
225-
pragma[noinline]
226-
private Folder getPackageFolder(string package) {
227-
exists(PackageJson json |
228-
json.getPackageName() = package and
229-
result = json.getFile().getParentContainer()
230-
)
231-
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
private import javascript
2+
private import semmle.javascript.TSConfig
3+
private import semmle.javascript.internal.paths.PackageJsonEx
4+
private import semmle.javascript.internal.paths.JSPaths
5+
6+
final private class FinalPathExpr = PathExpr;
7+
8+
private class RelevantPathExpr extends FinalPathExpr {
9+
pragma[nomagic]
10+
RelevantPathExpr() { this = any(Import imprt).getImportedPath() }
11+
}
12+
13+
/**
14+
* Gets a tsconfig file to use as fallback for handling paths in `c`.
15+
*
16+
* This holds for files and folders where no tsconfig seems to include it,
17+
* but it has one or more tsconfig files in parent directories.
18+
*/
19+
private TSConfig getFallbackTSConfig(Container c) {
20+
not c = any(TSConfig t).getAnIncludedContainer() and
21+
(
22+
c = result.getFolder()
23+
or
24+
result = getFallbackTSConfig(c.getParentContainer())
25+
)
26+
}
27+
28+
/**
29+
* Gets the TSConfig file relevant for resolving `expr`.
30+
*/
31+
pragma[nomagic]
32+
private TSConfig getTSConfigFromPathExpr(RelevantPathExpr expr) {
33+
result.getAnIncludedContainer() = expr.getFile()
34+
or
35+
result = getFallbackTSConfig(expr.getFile())
36+
}
37+
38+
/**
39+
* Holds if `path` is relative, in the sense that it should be resolved relative to its enclosing folder.
40+
*/
41+
bindingset[path]
42+
pragma[inline_late]
43+
predicate isRelativePath(string path) { path.regexpMatch("\\.\\.?(?:[/\\\\].*)?") }
44+
45+
/**
46+
* Gets the NPM package name from the beginning of the given import path, e.g.
47+
* gets `foo` from `foo/bar`, and `@example/foo` from `@example/foo/bar`.
48+
*/
49+
pragma[nomagic]
50+
private string getPackagePrefixFromPathExpr(RelevantPathExpr expr) {
51+
result = expr.getValue().regexpFind("^(@[^/\\\\]+[/\\\\])?[^@./\\\\][^/\\\\]*", _, _)
52+
}
53+
54+
private Variable dirname() { result.getName() = "__dirname" }
55+
56+
/** Holds if `add` is a relevant path expression of form `__dirname + expr`. */
57+
private predicate prefixedByDirname(PathExpr expr) {
58+
expr = dirname().getAnAccess()
59+
or
60+
prefixedByDirname(expr.(AddExpr).getLeftOperand())
61+
or
62+
prefixedByDirname(expr.(CallExpr).getArgument(0))
63+
}
64+
65+
/**
66+
* Holds if `expr` matches a path mapping, and should thus be resolved as `newPath` relative to `base`.
67+
*/
68+
pragma[nomagic]
69+
private predicate resolveViaPathMapping(RelevantPathExpr expr, Container base, string newPath) {
70+
// Handle tsconfig mappings such as `{ "paths": { "@/*": "./src/*" }}`
71+
exists(TSConfig config, string value |
72+
config = getTSConfigFromPathExpr(expr).getExtendedTSConfig*() and
73+
value = expr.getValue() and
74+
base = config.getBaseUrlFolderOrOwnFolder()
75+
|
76+
config.hasExactPathMapping(value, newPath)
77+
or
78+
exists(string pattern, string suffix, string mappedPath |
79+
config.hasPrefixPathMapping(pattern, mappedPath) and
80+
value = pattern + suffix and
81+
newPath = mappedPath + suffix
82+
)
83+
)
84+
or
85+
// Handle imports referring to a package by name, where we have a package.json
86+
// file for that package in the codebase.
87+
//
88+
// This part only handles the "exports" property of package.json. "main" and "modules" are
89+
// handled further down because their semantics are easier to handle there.
90+
exists(PackageJsonEx pkg, string packageName, string remainder |
91+
packageName = getPackagePrefixFromPathExpr(expr) and
92+
pkg.getDeclaredPackageName() = packageName and
93+
remainder = expr.getValue().suffix(packageName.length()).regexpReplaceAll("^[/\\\\]", "")
94+
|
95+
// "exports": { ".": "./foo.js" }
96+
// "exports": { "./foo.js": "./foo/impl.js" }
97+
pkg.hasExactPathMappingTo(remainder, base) and
98+
newPath = ""
99+
or
100+
// "exports": { "./*": "./foo/*" }
101+
exists(string prefix |
102+
pkg.hasPrefixPathMappingTo(prefix, base) and
103+
remainder = prefix + newPath
104+
)
105+
)
106+
}
107+
108+
pragma[noopt]
109+
private predicate relativePathExpr(RelevantPathExpr expr, Container base, string path) {
110+
expr instanceof RelevantPathExpr and
111+
path = expr.getValue() and
112+
isRelativePath(path) and
113+
exists(File file |
114+
file = expr.getFile() and
115+
base = file.getParentContainer()
116+
)
117+
}
118+
119+
/**
120+
* Holds if `expr` should be resolved as `path` relative to `base`.
121+
*/
122+
pragma[nomagic]
123+
private predicate shouldResolve(RelevantPathExpr expr, Container base, string path) {
124+
// Relative paths are resolved from their enclosing folder
125+
relativePathExpr(expr, base, path)
126+
or
127+
// Paths prefixed by __dirname should be resolved from the root dir, because __dirname
128+
// currently has a getValue() that returns its absolute path.
129+
prefixedByDirname(expr) and
130+
not exists(base.getParentContainer()) and // get root dir
131+
path = expr.getValue()
132+
or
133+
resolveViaPathMapping(expr, base, path)
134+
or
135+
// Resolve from baseUrl of relevant tsconfig.json file
136+
path = expr.getValue() and
137+
not isRelativePath(path) and
138+
base = getTSConfigFromPathExpr(expr).getBaseUrlFolder()
139+
or
140+
// If the path starts with the name of a package, but did not match any path mapping,
141+
// resolve relative to the enclosing directory of that package.
142+
// Note that `getFileFromFolderImport` may subsequently redirect this to the package's "main",
143+
// so we don't have to deal with that here.
144+
exists(PackageJson pkg, string packageName |
145+
packageName = getPackagePrefixFromPathExpr(expr) and
146+
pkg.getDeclaredPackageName() = packageName and
147+
path = expr.getValue().suffix(packageName.length()).regexpReplaceAll("^[/\\\\]", "") and
148+
base = pkg.getFolder()
149+
)
150+
}
151+
152+
private module ResolverConfig implements Folder::ResolveSig {
153+
predicate shouldResolve(Container base, string path) { shouldResolve(_, base, path) }
154+
155+
predicate getAnAdditionalChild = JSPaths::getAnAdditionalChild/2;
156+
}
157+
158+
private module Resolver = Folder::Resolve<ResolverConfig>;
159+
160+
private Container resolvePathExpr1(RelevantPathExpr expr) {
161+
exists(Container base, string path |
162+
shouldResolve(expr, base, path) and
163+
result = Resolver::resolve(base, path)
164+
)
165+
}
166+
167+
/**
168+
* Removes the scope from a package name, e.g. `@foo/bar` -> `bar`.
169+
*/
170+
bindingset[name]
171+
private string stripPackageScope(string name) { result = name.regexpReplaceAll("^@[^/]+/", "") }
172+
173+
private File guessPackageJsonMain1(PackageJsonEx pkg) {
174+
not exists(pkg.getMainFile()) and
175+
exists(Folder folder, Folder subfolder |
176+
folder = pkg.getFolder() and
177+
(
178+
subfolder = folder or
179+
subfolder = folder.getChildContainer(getASrcFolderName()) or
180+
subfolder =
181+
folder
182+
.getChildContainer(getASrcFolderName())
183+
.(Folder)
184+
.getChildContainer(getASrcFolderName())
185+
)
186+
|
187+
result = subfolder.getJavaScriptFileOrTypings("index")
188+
or
189+
result = subfolder.getJavaScriptFileOrTypings(stripPackageScope(pkg.getDeclaredPackageName()))
190+
)
191+
}
192+
193+
private File guessPackageJsonMain2(PackageJsonEx pkg) {
194+
not exists(pkg.getMainFile()) and
195+
not exists(guessPackageJsonMain1(pkg)) and
196+
result = pkg.getAFileInFilesArray()
197+
}
198+
199+
private File getFileFromFolderImport(Folder folder) {
200+
result = folder.getJavaScriptFileOrTypings("index")
201+
or
202+
// Note that unlike "exports" paths, "main" and "module" also take effect when the package
203+
// is imported via a relative path, e.g. `require("..")` targeting a folder with a package.json file.
204+
exists(PackageJsonEx pkg | pkg.getFolder() = folder |
205+
result = pkg.getMainFile()
206+
or
207+
result = guessPackageJsonMain1(pkg)
208+
or
209+
result = guessPackageJsonMain2(pkg)
210+
)
211+
}
212+
213+
File resolvePathExpr(PathExpr expr) {
214+
result = resolvePathExpr1(expr)
215+
or
216+
result = getFileFromFolderImport(resolvePathExpr1(expr))
217+
}
218+
219+
module Debug {
220+
class PathExprToDebug extends RelevantPathExpr {
221+
PathExprToDebug() { this.getValue() = "vs/nls" }
222+
}
223+
224+
query PathExprToDebug pathExprs() { any() }
225+
226+
query TSConfig getTSConfigFromPathExpr_(PathExprToDebug expr) {
227+
result = getTSConfigFromPathExpr(expr)
228+
}
229+
230+
query string getPackagePrefixFromPathExpr_(PathExprToDebug expr) {
231+
result = getPackagePrefixFromPathExpr(expr)
232+
}
233+
234+
query predicate resolveViaPathMapping_(PathExprToDebug expr, Container base, string newPath) {
235+
resolveViaPathMapping(expr, base, newPath)
236+
}
237+
238+
query predicate shouldResolve_(PathExprToDebug expr, Container base, string newPath) {
239+
shouldResolve(expr, base, newPath)
240+
}
241+
242+
query Container resolvePathExpr1_(PathExprToDebug expr) { result = resolvePathExpr1(expr) }
243+
244+
query File resolvePathExpr_(PathExprToDebug expr) { result = resolvePathExpr(expr) }
245+
246+
// Some predicates that are usually small enough that they don't need restriction
247+
query File getPackageMainFile(PackageJsonEx pkg) { result = pkg.getMainFile() }
248+
249+
query predicate guessPackageJsonMain1_ = guessPackageJsonMain1/1;
250+
251+
query predicate guessPackageJsonMain2_ = guessPackageJsonMain2/1;
252+
253+
query predicate getFileFromFolderImport_ = getFileFromFolderImport/1;
254+
}

0 commit comments

Comments
 (0)