Files
codeql/javascript/ql/lib/semmle/javascript/TSConfig.qll
Asger F f3e0cfd947 Apply suggestions from code review
Co-authored-by: Erik Krogh Kristensen <erik-krogh@github.com>
2025-05-02 12:41:29 +02:00

223 lines
7.4 KiB
Plaintext

/**
* Provides a class for working with `tsconfig.json` files.
*/
private import javascript
private import semmle.javascript.internal.paths.PathMapping
/**
* A TypeScript configuration file, usually named `tsconfig.json`.
*/
class TSConfig extends JsonObject {
TSConfig() {
this.getJsonFile().getBaseName().matches("%tsconfig%.json") and
this.isTopLevel()
}
/** Gets the folder containing this file. */
Folder getFolder() { result = this.getJsonFile().getParentContainer() }
/** Gets the `compilerOptions` object. */
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 = Resolver::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 = Resolver::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 the effective baseUrl folder for this tsconfig file, or its enclosing folder if there is no baseUrl. */
Folder getBaseUrlFolderOrOwnFolder() {
result = this.getBaseUrlFolder()
or
not exists(this.getBaseUrlFolder()) and
result = this.getFolder()
}
/** 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 refenced by a path the `include` property, possibly
* inherited from an extended tsconfig file.
*
* Does not include all the files within includes directories, use `getAnIncludedContainer` for that.
*/
Container getAnIncludePathTarget() {
result = Resolver::resolve(this.getFolder(), this.getAnIncludePath())
or
not exists(this.getPropValue("include")) and
result = this.getExtendedTSConfig().getAnIncludePathTarget()
}
/**
* Gets a file or folder inside the directory tree mentioned in the `include` property.
*/
Container getAnIncludedContainer() {
result = this.getAnIncludePathTarget()
or
result = this.getAnIncludedContainer().getAChildContainer()
}
/** Gets the path mentioned in the `rootDir` property. */
string getRootDirPath() { result = this.getCompilerOptions().getPropStringValue("rootDir") }
private Container getOwnRootDir() {
result = Resolver::resolve(this.getFolder(), this.getRootDirPath())
}
/** Gets the file or folder referenced by the `rootDir` property. */
Container getRootDir() {
result = this.getOwnRootDir()
or
not exists(this.getRootDirPath()) and
result = this.getExtendedTSConfig().getOwnRootDir()
}
private string getATopLevelIncludePath() {
result = this.getAnIncludePath().(FilePath).getComponent(0)
}
private string getUniqueTopLevelIncludePath() {
result = unique( | | this.getATopLevelIncludePath())
}
/**
* Gets the folder referred to by the `rootDir` property, or if absent, an effective root dir
* derived from `include` paths.
*/
Container getEffectiveRootDir() {
result = this.getRootDir()
or
not exists(this.getRootDir()) and
(
result = this.getFolder().getFolder(this.getUniqueTopLevelIncludePath())
or
not exists(this.getUniqueTopLevelIncludePath()) and
exists(this.getATopLevelIncludePath()) and
result = this.getFolder()
or
not exists(this.getATopLevelIncludePath()) and
result = this.getExtendedTSConfig().getEffectiveRootDir()
)
}
private JsonObject getPathMappings() { result = this.getCompilerOptions().getPropValue("paths") }
/**
* Holds if this has a path mapping from `pattern` to `newPath`.
*
* For example, `"paths": { "@/*": "./src/*" }` maps the `@/*` pattern to `./src/*`.
*
* Does not include path mappings from extended tsconfig files.
*/
predicate hasPathMapping(string pattern, string newPath) {
this.getPathMappings().getPropStringValue(pattern) = newPath
or
this.getPathMappings().getPropValue(pattern).(JsonArray).getElementStringValue(_) = newPath
}
/**
* Holds if this has an exact path mapping (i.e. no wildcards) from `pattern` to `newPath`.
*
* For example, `"paths": { "@": "./src/index.ts" }` maps the `@` path to `./src/index.ts`.
*
* Does not include path mappings from extended tsconfig files.
*/
predicate hasExactPathMapping(string pattern, string newPath) {
this.hasPathMapping(pattern, newPath) and
not pattern.matches("%*%")
}
/**
* Holds if this has a path mapping from the `pattern` prefix to the `newPath` prefix.
* The trailing `*` is not included.
*
* For example, `"paths": { "@/*": "./src/*" }` maps the `@/` pattern to `./src/`.
*
* Does not include path mappings from extended tsconfig files.
*/
predicate hasPrefixPathMapping(string pattern, string newPath) {
this.hasPathMapping(pattern + "*", newPath + "*")
}
}
/** For resolving paths in a tsconfig file, except `paths` mappings. */
private module ResolverConfig implements Folder::ResolveSig {
predicate shouldResolve(Container base, string path) {
exists(TSConfig cfg |
base = cfg.getFolder() and
path =
[cfg.getExtendsPath(), cfg.getBaseUrlPath(), cfg.getRootDirPath(), cfg.getAnIncludePath()]
)
}
predicate allowGlobs() { any() } // "include" can use globs
}
private module Resolver = Folder::Resolve<ResolverConfig>;
/**
* Gets a tsconfig file to use as fallback for handling paths in `c`.
*
* This holds for files and folders where no tsconfig seems to include it,
* but it has one or more tsconfig files in parent directories.
*/
private TSConfig getFallbackTSConfig(Container c) {
not c = any(TSConfig t).getAnIncludedContainer() and
(
c = result.getFolder()
or
result = getFallbackTSConfig(c.getParentContainer())
)
}
private class TSConfigPathMapping extends PathMapping, TSConfig {
override File getAnAffectedFile() {
result = this.getAnIncludedContainer()
or
this = getFallbackTSConfig(result)
}
override predicate hasExactPathMapping(string pattern, Container newContext, string newPath) {
exists(TSConfig tsconfig |
tsconfig = this.getExtendedTSConfig*() and
tsconfig.hasExactPathMapping(pattern, newPath) and
newContext = tsconfig.getBaseUrlFolderOrOwnFolder()
)
}
override predicate hasPrefixPathMapping(string pattern, Container newContext, string newPath) {
exists(TSConfig tsconfig |
tsconfig = this.getExtendedTSConfig*() and
tsconfig.hasPrefixPathMapping(pattern, newPath) and
newContext = tsconfig.getBaseUrlFolderOrOwnFolder()
)
}
override predicate hasBaseUrl(Container base) { base = this.getBaseUrlFolder() }
}