JS: Path resolution for tsconfig

This commit is contained in:
Asger F
2025-04-04 00:08:18 +02:00
parent 190dfce9c5
commit 55a91ebaef
7 changed files with 349 additions and 33 deletions

View File

@@ -6,6 +6,7 @@
import javascript
private import semmle.javascript.internal.CachedStages
private import semmle.javascript.internal.PathResolution
/**
* A module, which may either be an ECMAScript 2015-style module,
@@ -139,7 +140,8 @@ abstract class Import extends AstNode {
* Gets the module the path of this import resolves to.
*/
Module resolveImportedPath() {
result.getFile() = this.getEnclosingModule().resolve(this.getImportedPath())
result.getFile() = PathResolution::resolvePathExpr(this.getImportedPath())
// result.getFile() = this.getEnclosingModule().resolve(this.getImportedPath())
}
/**

View File

@@ -0,0 +1,281 @@
private import javascript
signature module ResolvePathsSig {
/**
* Holds if `path` should be resolved to a file or folder, relative to `base`.
*/
predicate shouldResolve(Folder base, string path);
/**
* Gets an additional file or folder to consider a child of `base`.
*/
default Container getAnAdditionalChild(Container base, string name) { none() }
}
pragma[inline]
private Container getChild(Container parent, string name) {
result = parent.getFile(name)
or
result = parent.getFolder(name)
}
/**
* Provides a mechanism for resolving relative file paths.
*
* Absolute paths are not handled.
*/
module ResolvePaths<ResolvePathsSig Config> {
private import Config
private string getPathSegment(string path, int n) {
shouldResolve(_, path) and
result = path.replaceAll("\\", "/").splitAt("/", n)
}
private int getNumPathSegment(string path) {
result = strictcount(int n | exists(getPathSegment(path, n)))
}
private Container resolve(Container base, string path, int n) {
shouldResolve(base, path) and n = 0 and result = base
or
exists(Container cur, string segment |
cur = resolve(base, path, n - 1) and
segment = getPathSegment(path, n - 1)
|
result = getChild(cur, segment)
or
result = getAnAdditionalChild(cur, segment)
or
segment = [".", ""] and
result = cur
or
segment = ".." and
result = cur.getParentContainer()
)
}
/**
* Gets the file or folder that `path` resolves to when resolved from `base`.
*
* Only has results for the `base`, `path` pairs provided by `shouldResolve`
* in the instantiation of this module.
*/
Container resolve(Container base, string path) {
result = resolve(base, path, getNumPathSegment(path))
}
}
module PathResolution {
class TSConfig extends JsonObject {
TSConfig() {
this.getJsonFile().getBaseName().matches("%tsconfig%.json") and
this.isTopLevel()
}
Folder getFolder() { result = this.getJsonFile().getParentContainer() }
JsonObject getCompilerOptions() { result = this.getPropValue("compilerOptions") }
/** Gets the string value in the `extends` property. */
string getExtendsPath() { result = this.getPropStringValue("extends") }
/** Gets the file referred to by the `extends` property. */
File getExtendedFile() {
result = TSConfigResolve::resolve(this.getFolder(), this.getExtendsPath())
}
/** Gets the `TSConfig` file referred to by the `extends` property. */
TSConfig getExtendedTSConfig() { result.getJsonFile() = this.getExtendedFile() }
/** Gets the string value in the `baseUrl` property. */
string getBaseUrlPath() { result = this.getCompilerOptions().getPropStringValue("baseUrl") }
/** Gets the folder referred to by the `baseUrl` property in this file, not taking `extends` into account. */
Folder getOwnBaseUrlFolder() {
result = TSConfigResolve::resolve(this.getFolder(), this.getBaseUrlPath())
}
/** Gets the effective baseUrl folder for this tsconfig file. */
Folder getBaseUrlFolder() {
result = this.getOwnBaseUrlFolder()
or
not exists(this.getOwnBaseUrlFolder()) and
result = this.getExtendedTSConfig().getBaseUrlFolder()
}
/** Gets a path mentioned in the `include` property. */
string getAnIncludePath() {
result = this.getPropStringValue("include")
or
result = this.getPropValue("include").(JsonArray).getElementStringValue(_)
}
/**
* Gets a file or folder mentioned in the `include` property.
*
* Does not include all the files within includes directories.
*/
Container getAnIncludedBaseContainer() {
result = TSConfigResolve::resolve(this.getFolder(), this.getAnIncludePath())
or
result = this.getExtendedTSConfig().getAnIncludedBaseContainer()
}
private predicate isPrimaryTSConfig() {
this.getJsonFile().getBaseName() = "tsconfig.json"
or
// Fallback in case we can't find the primary tsconfig file
not exists(this.getFolder().getFile("tsconfig.json")) and
not this = any(TSConfig tsc).getExtendedTSConfig()
}
/**
* Gets a file or folder inside the directory tree mentioned in the `include` property.
*/
Container getAnAffectedFile() {
this.isPrimaryTSConfig() and
result = this.getAnIncludedBaseContainer()
or
result = this.getAnAffectedFile().getAChildContainer()
}
JsonObject getPathMappings() { result = this.getCompilerOptions().getPropValue("paths") }
predicate hasPathMapping(string pattern, string newPath) {
this.getPathMappings().getPropStringValue(pattern) = newPath
or
// TODO: track priority
this.getPathMappings().getPropValue(pattern).(JsonArray).getElementStringValue(_) = newPath
}
predicate hasExactPathMapping(string pattern, string newPath) {
this.hasPathMapping(pattern, newPath) and
not pattern.matches("%*%")
}
predicate hasPrefixPathMapping(string pattern, string newPath) {
this.hasPathMapping(pattern + "*", newPath + "*")
}
}
private module TSConfigResolveConfig implements ResolvePathsSig {
predicate shouldResolve(Folder base, string path) {
exists(TSConfig cfg |
base = cfg.getFolder() and
path = [cfg.getExtendsPath(), cfg.getBaseUrlPath(), cfg.getAnIncludePath()]
)
}
}
private module TSConfigResolve = ResolvePaths<TSConfigResolveConfig>;
bindingset[path]
private predicate isRelativePath(string path) { path.regexpMatch("\\.\\.?[/\\\\].*") }
private module ResolvePathMappingConfig implements ResolvePathsSig {
additional predicate shouldResolve(TSConfig cfg, Container base, string path) {
(cfg.hasExactPathMapping(_, path) or cfg.hasPrefixPathMapping(_, path)) and
if isRelativePath(path)
then base = cfg.getFolder() // relative paths are resolved relative to tsconfig.json
else base = cfg.getBaseUrlFolder() // non-relative paths are resolved relative to the baseUrl
}
predicate shouldResolve(Folder base, string path) { shouldResolve(_, base, path) }
}
private module ResolvePathMapping = ResolvePaths<ResolvePathMappingConfig>;
private Container resolvePathMapping(TSConfig cfg, string path) {
exists(Container base |
ResolvePathMappingConfig::shouldResolve(cfg, base, path) and
result = ResolvePathMapping::resolve(base, path)
)
}
private TSConfig getTSConfigFromPathExpr(PathExpr expr) {
result.getAnAffectedFile() = expr.getFile()
}
pragma[nomagic]
private predicate replacedPath1(PathExpr expr, Container base, string newPath) {
expr = any(Import imprt).getImportedPath() and
exists(TSConfig config, string value, string mappedPath |
config = getTSConfigFromPathExpr(expr).getExtendedTSConfig*() and
value = expr.getValue()
|
config.hasExactPathMapping(value, mappedPath) and
base = resolvePathMapping(config, mappedPath) and
newPath = ""
or
exists(string pattern |
config.hasPrefixPathMapping(pattern, mappedPath) and
value = pattern + any(string s) and
base = resolvePathMapping(config, mappedPath) and
newPath = value.suffix(pattern.length())
)
)
}
pragma[nomagic]
private predicate replacedPath(PathExpr expr, Container base, string newPath) {
replacedPath1(expr, base, newPath)
or
// resolve from baseUrl
expr = any(Import imprt).getImportedPath() and
not replacedPath1(expr, _, _) and
newPath = expr.getValue() and
newPath.charAt(0) != "." and
base = getTSConfigFromPathExpr(expr).getBaseUrlFolder()
}
private module ResolvePathExprConfig implements ResolvePathsSig {
additional predicate shouldResolve(PathExpr expr, Folder base, string path) {
expr = any(Import imprt).getImportedPath() and
(
replacedPath(expr, base, path)
or
not replacedPath(expr, _, _) and
isRelativePath(expr.getValue()) and
base = expr.getFile().getParentContainer() and
path = expr.getValue()
)
}
predicate shouldResolve(Folder base, string path) { shouldResolve(_, base, path) }
private predicate extensionCompilesTo(string original, string compilesTo) {
original = "ts" and
compilesTo = "js"
or
original = "tsx" and
compilesTo = ["jsx", "js"]
}
Container getAnAdditionalChild(Container base, string name) {
result = base.(Folder).getJavaScriptFile(name)
or
exists(string stem, string addedExt |
result = base.(Folder).getJavaScriptFile(stem) and
extensionCompilesTo(result.getExtension(), addedExt) and
name = result.getStem() + "." + addedExt and
not exists(base.(Folder).getFile(name))
)
}
}
private module ResolvePathExpr = ResolvePaths<ResolvePathExprConfig>;
private Container resolvePathExpr1(PathExpr expr) {
exists(Container base, string path |
ResolvePathExprConfig::shouldResolve(expr, base, path) and
result = ResolvePathExpr::resolve(base, path)
)
}
File resolvePathExpr(PathExpr expr) {
result = resolvePathExpr1(expr)
or
result = resolvePathExpr1(expr).(Folder).getJavaScriptFile("index")
}
}

View File

@@ -8,22 +8,22 @@ import "../lib/index.ts"; // $ importTarget=BaseUrl/lib/index.ts
import "../lib/index.js"; // $ importTarget=BaseUrl/lib/index.ts
// Import relative to baseUrl
import "lib/file"; // $ MISSING: importTarget=BaseUrl/lib/file.ts
import "lib/file.ts"; // $ MISSING: importTarget=BaseUrl/lib/file.ts
import "lib/file.js"; // $ MISSING: importTarget=BaseUrl/lib/file.ts
import "lib"; // $ MISSING: importTarget=BaseUrl/lib/index.ts
import "lib/index"; // $ MISSING: importTarget=BaseUrl/lib/index.ts
import "lib/index.ts"; // $ MISSING: importTarget=BaseUrl/lib/index.ts
import "lib/index.js"; // $ MISSING: importTarget=BaseUrl/lib/index.ts
import "lib/file"; // $ importTarget=BaseUrl/lib/file.ts
import "lib/file.ts"; // $ importTarget=BaseUrl/lib/file.ts
import "lib/file.js"; // $ importTarget=BaseUrl/lib/file.ts
import "lib"; // $ importTarget=BaseUrl/lib/index.ts
import "lib/index"; // $ importTarget=BaseUrl/lib/index.ts
import "lib/index.ts"; // $ importTarget=BaseUrl/lib/index.ts
import "lib/index.js"; // $ importTarget=BaseUrl/lib/index.ts
// Import matching "@/*" path mapping
import "@/file"; // $ MISSING: importTarget=BaseUrl/lib/file.ts
import "@/file.ts"; // $ MISSING: importTarget=BaseUrl/lib/file.ts
import "@/file.js"; // $ MISSING: importTarget=BaseUrl/lib/file.ts
import "@/file"; // $ importTarget=BaseUrl/lib/file.ts
import "@/file.ts"; // $ importTarget=BaseUrl/lib/file.ts
import "@/file.js"; // $ importTarget=BaseUrl/lib/file.ts
import "@"; // $ MISSING: importTarget=BaseUrl/lib/nostar.ts
import "@/index"; // $ MISSING: importTarget=BaseUrl/lib/index.ts
import "@/index.ts"; // $ MISSING: importTarget=BaseUrl/lib/index.ts
import "@/index.js"; // $ MISSING: importTarget=BaseUrl/lib/index.ts
import "@/index"; // $ importTarget=BaseUrl/lib/index.ts
import "@/index.ts"; // $ importTarget=BaseUrl/lib/index.ts
import "@/index.js"; // $ importTarget=BaseUrl/lib/index.ts
// Import matching "@/*.xyz" path mapping. Note that this is not actually supported by TypeScript.
import "@/file.xyz";

View File

@@ -8,22 +8,22 @@ import "../lib/index.ts"; // $ importTarget=Extended/lib/index.ts
import "../lib/index.js"; // $ importTarget=Extended/lib/index.ts
// Import relative to baseUrl
import "lib/file"; // $ MISSING: importTarget=Extended/lib/file.ts
import "lib/file.ts"; // $ MISSING: importTarget=Extended/lib/file.ts
import "lib/file.js"; // $ MISSING: importTarget=Extended/lib/file.ts
import "lib"; // $ MISSING: importTarget=Extended/lib/index.ts
import "lib/index"; // $ MISSING: importTarget=Extended/lib/index.ts
import "lib/index.ts"; // $ MISSING: importTarget=Extended/lib/index.ts
import "lib/index.js"; // $ MISSING: importTarget=Extended/lib/index.ts
import "lib/file"; // $ importTarget=Extended/lib/file.ts
import "lib/file.ts"; // $ importTarget=Extended/lib/file.ts
import "lib/file.js"; // $ importTarget=Extended/lib/file.ts
import "lib"; // $ importTarget=Extended/lib/index.ts
import "lib/index"; // $ importTarget=Extended/lib/index.ts
import "lib/index.ts"; // $ importTarget=Extended/lib/index.ts
import "lib/index.js"; // $ importTarget=Extended/lib/index.ts
// Import matching "@/*" path mapping
import "@/file"; // $ MISSING: importTarget=Extended/lib/file.ts
import "@/file.ts"; // $ MISSING: importTarget=Extended/lib/file.ts
import "@/file.js"; // $ MISSING: importTarget=Extended/lib/file.ts
import "@/file"; // $ importTarget=Extended/lib/file.ts
import "@/file.ts"; // $ importTarget=Extended/lib/file.ts
import "@/file.js"; // $ importTarget=Extended/lib/file.ts
import "@"; // $ MISSING: importTarget=Extended/lib/nostar.ts
import "@/index"; // $ MISSING: importTarget=Extended/lib/index.ts
import "@/index.ts"; // $ MISSING: importTarget=Extended/lib/index.ts
import "@/index.js"; // $ MISSING: importTarget=Extended/lib/index.ts
import "@/index"; // $ importTarget=Extended/lib/index.ts
import "@/index.ts"; // $ importTarget=Extended/lib/index.ts
import "@/index.js"; // $ importTarget=Extended/lib/index.ts
// Import matching "@/*.xyz" path mapping. Note that this is not actually supported by TypeScript.
import "@/file.xyz";

View File

@@ -17,13 +17,13 @@ import "lib/index.ts";
import "lib/index.js";
// Import matching "@/*" path mapping
import "@/file"; // $ MISSING: importTarget=NoBaseUrl/lib/file.ts
import "@/file.ts"; // $ MISSING: importTarget=NoBaseUrl/lib/file.ts
import "@/file.js"; // $ MISSING: importTarget=NoBaseUrl/lib/file.ts
import "@/file"; // $ importTarget=NoBaseUrl/lib/file.ts
import "@/file.ts"; // $ importTarget=NoBaseUrl/lib/file.ts
import "@/file.js"; // $ importTarget=NoBaseUrl/lib/file.ts
import "@"; // $ MISSING: importTarget=NoBaseUrl/lib/nostar.ts
import "@/index"; // $ MISSING: importTarget=NoBaseUrl/lib/index.ts
import "@/index.ts"; // $ MISSING: importTarget=NoBaseUrl/lib/index.ts
import "@/index.js"; // $ MISSING: importTarget=NoBaseUrl/lib/index.ts
import "@/index"; // $ importTarget=NoBaseUrl/lib/index.ts
import "@/index.ts"; // $ importTarget=NoBaseUrl/lib/index.ts
import "@/index.js"; // $ importTarget=NoBaseUrl/lib/index.ts
// Import matching "@/*.xyz" path mapping. Note that this is not actually supported by TypeScript.
import "@/file.xyz";

View File

@@ -5,6 +5,19 @@
| BaseUrl/src/main.ts:6:1:6:22 | import ... index"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:7:1:7:25 | import ... ex.ts"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:8:1:8:25 | import ... ex.js"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:11:1:11:18 | import "lib/file"; | BaseUrl/lib/file.ts |
| BaseUrl/src/main.ts:12:1:12:21 | import ... le.ts"; | BaseUrl/lib/file.ts |
| BaseUrl/src/main.ts:13:1:13:21 | import ... le.js"; | BaseUrl/lib/file.ts |
| BaseUrl/src/main.ts:14:1:14:13 | import "lib"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:15:1:15:19 | import "lib/index"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:16:1:16:22 | import ... ex.ts"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:17:1:17:22 | import ... ex.js"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:20:1:20:16 | import "@/file"; | BaseUrl/lib/file.ts |
| BaseUrl/src/main.ts:21:1:21:19 | import "@/file.ts"; | BaseUrl/lib/file.ts |
| BaseUrl/src/main.ts:22:1:22:19 | import "@/file.js"; | BaseUrl/lib/file.ts |
| BaseUrl/src/main.ts:24:1:24:17 | import "@/index"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:25:1:25:20 | import "@/index.ts"; | BaseUrl/lib/index.ts |
| BaseUrl/src/main.ts:26:1:26:20 | import "@/index.js"; | BaseUrl/lib/index.ts |
| Extended/src/main.ts:2:1:2:21 | import ... /file"; | Extended/lib/file.ts |
| Extended/src/main.ts:3:1:3:24 | import ... le.ts"; | Extended/lib/file.ts |
| Extended/src/main.ts:4:1:4:24 | import ... le.js"; | Extended/lib/file.ts |
@@ -12,6 +25,19 @@
| Extended/src/main.ts:6:1:6:22 | import ... index"; | Extended/lib/index.ts |
| Extended/src/main.ts:7:1:7:25 | import ... ex.ts"; | Extended/lib/index.ts |
| Extended/src/main.ts:8:1:8:25 | import ... ex.js"; | Extended/lib/index.ts |
| Extended/src/main.ts:11:1:11:18 | import "lib/file"; | Extended/lib/file.ts |
| Extended/src/main.ts:12:1:12:21 | import ... le.ts"; | Extended/lib/file.ts |
| Extended/src/main.ts:13:1:13:21 | import ... le.js"; | Extended/lib/file.ts |
| Extended/src/main.ts:14:1:14:13 | import "lib"; | Extended/lib/index.ts |
| Extended/src/main.ts:15:1:15:19 | import "lib/index"; | Extended/lib/index.ts |
| Extended/src/main.ts:16:1:16:22 | import ... ex.ts"; | Extended/lib/index.ts |
| Extended/src/main.ts:17:1:17:22 | import ... ex.js"; | Extended/lib/index.ts |
| Extended/src/main.ts:20:1:20:16 | import "@/file"; | Extended/lib/file.ts |
| Extended/src/main.ts:21:1:21:19 | import "@/file.ts"; | Extended/lib/file.ts |
| Extended/src/main.ts:22:1:22:19 | import "@/file.js"; | Extended/lib/file.ts |
| Extended/src/main.ts:24:1:24:17 | import "@/index"; | Extended/lib/index.ts |
| Extended/src/main.ts:25:1:25:20 | import "@/index.ts"; | Extended/lib/index.ts |
| Extended/src/main.ts:26:1:26:20 | import "@/index.js"; | Extended/lib/index.ts |
| NoBaseUrl/src/main.ts:2:1:2:21 | import ... /file"; | NoBaseUrl/lib/file.ts |
| NoBaseUrl/src/main.ts:3:1:3:24 | import ... le.ts"; | NoBaseUrl/lib/file.ts |
| NoBaseUrl/src/main.ts:4:1:4:24 | import ... le.js"; | NoBaseUrl/lib/file.ts |
@@ -19,3 +45,9 @@
| NoBaseUrl/src/main.ts:6:1:6:22 | import ... index"; | NoBaseUrl/lib/index.ts |
| NoBaseUrl/src/main.ts:7:1:7:25 | import ... ex.ts"; | NoBaseUrl/lib/index.ts |
| NoBaseUrl/src/main.ts:8:1:8:25 | import ... ex.js"; | NoBaseUrl/lib/index.ts |
| NoBaseUrl/src/main.ts:20:1:20:16 | import "@/file"; | NoBaseUrl/lib/file.ts |
| NoBaseUrl/src/main.ts:21:1:21:19 | import "@/file.ts"; | NoBaseUrl/lib/file.ts |
| NoBaseUrl/src/main.ts:22:1:22:19 | import "@/file.js"; | NoBaseUrl/lib/file.ts |
| NoBaseUrl/src/main.ts:24:1:24:17 | import "@/index"; | NoBaseUrl/lib/index.ts |
| NoBaseUrl/src/main.ts:25:1:25:20 | import "@/index.ts"; | NoBaseUrl/lib/index.ts |
| NoBaseUrl/src/main.ts:26:1:26:20 | import "@/index.js"; | NoBaseUrl/lib/index.ts |

View File

@@ -1,4 +1,5 @@
import javascript
import semmle.javascript.internal.PathResolution
query predicate importTarget(Import imprt, string value) {
imprt.getImportedModule().getFile().getRelativePath() = value