Skip to content

Commit fb1d04e

Browse files
committed
JS: Factor out tsconfig path mapping support
1 parent 0789cdc commit fb1d04e

File tree

2 files changed

+85
-34
lines changed

2 files changed

+85
-34
lines changed

javascript/ql/lib/semmle/javascript/internal/paths/PathExprResolver.qll

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
private import javascript
2-
private import semmle.javascript.TSConfig
32
private import semmle.javascript.internal.paths.PackageJsonEx
43
private import semmle.javascript.internal.paths.JSPaths
4+
private import semmle.javascript.internal.paths.PathMapping
55
private import semmle.javascript.internal.paths.PathConcatenation
66
private import semmle.javascript.dataflow.internal.DataFlowNode
77

@@ -81,28 +81,11 @@ module ResolveExpr<exprSig/1 shouldResolveExpr> {
8181
}
8282

8383
/**
84-
* Gets a tsconfig file to use as fallback for handling paths in `c`.
85-
*
86-
* This holds for files and folders where no tsconfig seems to include it,
87-
* but it has one or more tsconfig files in parent directories.
88-
*/
89-
private TSConfig getFallbackTSConfig(Container c) {
90-
not c = any(TSConfig t).getAnIncludedContainer() and
91-
(
92-
c = result.getFolder()
93-
or
94-
result = getFallbackTSConfig(c.getParentContainer())
95-
)
96-
}
97-
98-
/**
99-
* Gets the TSConfig file relevant for resolving `expr`.
84+
* Gets a path mapping relevant for resolving `expr`.
10085
*/
10186
pragma[nomagic]
102-
private TSConfig getTSConfigFromPathExpr(RelevantExpr expr) {
103-
result.getAnIncludedContainer() = expr.getFile()
104-
or
105-
result = getFallbackTSConfig(expr.getFile())
87+
private PathMapping getAPathMappingFromPathExpr(RelevantExpr expr) {
88+
result.getAnAffectedFile() = expr.getFile()
10689
}
10790

10891
/**
@@ -126,23 +109,23 @@ module ResolveExpr<exprSig/1 shouldResolveExpr> {
126109
*/
127110
pragma[nomagic]
128111
private predicate resolveViaPathMapping(RelevantExpr expr, Container base, string newPath) {
129-
// Handle tsconfig mappings such as `{ "paths": { "@/*": "./src/*" }}`
130-
exists(TSConfig config, string value |
131-
config = getTSConfigFromPathExpr(expr).getExtendedTSConfig*() and
132-
value = expr.getValue() and
133-
base = config.getBaseUrlFolderOrOwnFolder()
112+
// Handle path mappings such as `{ "paths": { "@/*": "./src/*" }}` in a tsconfig.json file
113+
exists(PathMapping mapping, string value |
114+
mapping = getAPathMappingFromPathExpr(expr) and
115+
value = expr.getValue()
134116
|
135-
config.hasExactPathMapping(value, newPath)
117+
mapping.hasExactPathMapping(value, base, newPath)
136118
or
137119
exists(string pattern, string suffix, string mappedPath |
138-
config.hasPrefixPathMapping(pattern, mappedPath) and
120+
mapping.hasPrefixPathMapping(pattern, base, mappedPath) and
139121
value = pattern + suffix and
140122
newPath = mappedPath + suffix
141123
)
142124
)
143125
or
144126
// Handle imports referring to a package by name, where we have a package.json
145-
// file for that package in the codebase.
127+
// file for that package in the codebase. This is treated separately from PathMapping for performance
128+
// reasons, as there can be a large number of packages which affect all files in the project.
146129
//
147130
// This part only handles the "exports" property of package.json. "main" and "modules" are
148131
// handled further down because their semantics are easier to handle there.
@@ -197,7 +180,7 @@ module ResolveExpr<exprSig/1 shouldResolveExpr> {
197180
// Resolve from baseUrl of relevant tsconfig.json file
198181
path = expr.getValue() and
199182
not isRelativePath(path) and
200-
base = getTSConfigFromPathExpr(expr).getBaseUrlFolder()
183+
getAPathMappingFromPathExpr(expr).hasBaseUrl(base)
201184
or
202185
// If the path starts with the name of a package, but did not match any path mapping,
203186
// resolve relative to the enclosing directory of that package.
@@ -242,10 +225,6 @@ module ResolveExpr<exprSig/1 shouldResolveExpr> {
242225

243226
query PathExprToDebug pathExprs() { any() }
244227

245-
query TSConfig getTSConfigFromPathExpr_(PathExprToDebug expr) {
246-
result = getTSConfigFromPathExpr(expr)
247-
}
248-
249228
query string getPackagePrefixFromPathExpr_(PathExprToDebug expr) {
250229
result = getPackagePrefixFromPathExpr(expr)
251230
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Provides an extensible mechanism for modeling path mappings.
3+
*/
4+
5+
private import javascript
6+
private import semmle.javascript.TSConfig
7+
8+
/**
9+
* A configuration entity, such as a `tsconfig.json` file, which may provide path mappings.
10+
*/
11+
abstract class PathMapping extends Locatable {
12+
/**
13+
* Gets a file affected by this path mapping.
14+
*/
15+
abstract File getAnAffectedFile();
16+
17+
/**
18+
* Holds if imports paths exactly matching `pattern` should be redirected to `newPath`
19+
* resolved relative to `newContext`.
20+
*/
21+
abstract predicate hasExactPathMapping(string pattern, Container newContext, string newPath);
22+
23+
/**
24+
* Holds if imports paths starting with `pattern` should have the matched prefix replaced by `newPath`
25+
* and then resolved relative to `newContext`.
26+
*/
27+
abstract predicate hasPrefixPathMapping(string pattern, Container newContext, string newPath);
28+
29+
/** Holds if non-relative paths in affected files should be resolved relative to `base`. */
30+
abstract predicate hasBaseUrl(Container base);
31+
}
32+
33+
/**
34+
* Gets a tsconfig file to use as fallback for handling paths in `c`.
35+
*
36+
* This holds for files and folders where no tsconfig seems to include it,
37+
* but it has one or more tsconfig files in parent directories.
38+
*/
39+
private TSConfig getFallbackTSConfig(Container c) {
40+
not c = any(TSConfig t).getAnIncludedContainer() and
41+
(
42+
c = result.getFolder()
43+
or
44+
result = getFallbackTSConfig(c.getParentContainer())
45+
)
46+
}
47+
48+
private class TSConfigPathMapping extends PathMapping, TSConfig {
49+
override File getAnAffectedFile() {
50+
result = this.getAnIncludedContainer()
51+
or
52+
this = getFallbackTSConfig(result)
53+
}
54+
55+
override predicate hasExactPathMapping(string pattern, Container newContext, string newPath) {
56+
exists(TSConfig tsconfig |
57+
tsconfig = this.getExtendedTSConfig*() and
58+
tsconfig.hasExactPathMapping(pattern, newPath) and
59+
newContext = tsconfig.getBaseUrlFolderOrOwnFolder()
60+
)
61+
}
62+
63+
override predicate hasPrefixPathMapping(string pattern, Container newContext, string newPath) {
64+
exists(TSConfig tsconfig |
65+
tsconfig = this.getExtendedTSConfig*() and
66+
tsconfig.hasPrefixPathMapping(pattern, newPath) and
67+
newContext = tsconfig.getBaseUrlFolderOrOwnFolder()
68+
)
69+
}
70+
71+
override predicate hasBaseUrl(Container base) { base = this.getBaseUrlFolder() }
72+
}

0 commit comments

Comments
 (0)