Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb05996
Move getAChildContainer one scope up
asgerf Apr 24, 2025
2ce01bf
Add Folder::Resolve as a generalisation of Folder::Append
asgerf Apr 24, 2025
ec9d15b
JS: Make shared Folder module visible
asgerf Apr 24, 2025
8c0b0c4
JS: Ensure json files are extracted properly in tests
asgerf Apr 24, 2025
359525b
JS: Extract more tsconfig.json patterns
asgerf Apr 29, 2025
565cb43
JS: Add test
asgerf Apr 24, 2025
17aa522
JS: Add some helpers
asgerf Apr 24, 2025
ef32a03
JS: Extract from methods from PathString into a non-abstract base class
asgerf Apr 28, 2025
59e1cbc
JS: Add tsconfig class
asgerf Apr 24, 2025
bb91df8
JS: Add helper for doing path resolution with JS rules
asgerf Apr 24, 2025
f542956
JS: Add internal extension of PackageJson class
asgerf Apr 24, 2025
ed4864e
JS: Add two more helpers to FilePath class
asgerf Apr 28, 2025
6725cb5
JS: Implement import resolution
asgerf Apr 29, 2025
e4420f6
JS: Move babel-root-import test
asgerf Apr 28, 2025
d724874
JS: Implement babel-plugin-root-import as a PathMapping
asgerf Apr 28, 2025
a195d07
JS: Resolve Angular2 templateUrl with ResolveExpr instead of PathExpr
asgerf Apr 28, 2025
c293f03
JS: Remove a dependency on getImportedPath()
asgerf Apr 28, 2025
fe055ad
JS: Use PackageJsonEx instead of resolveMainModule
asgerf Apr 28, 2025
ed2a832
JS: Deprecate PathExpr and related classes
asgerf Apr 29, 2025
be5de9c
JS: Update test output
asgerf Apr 29, 2025
5de2c93
JS: Rename getTargetFile to getImportedFile and remove its deprecated…
asgerf Apr 29, 2025
70a5ec5
JS: Add package.json files in tests relying on node_modules
asgerf Apr 29, 2025
b0f73f1
JS: Update test output now that we import .d.ts files more liberally
asgerf Apr 29, 2025
f3e0cfd
Apply suggestions from code review
asgerf May 2, 2025
5c9218f
JS: Add comment about 'path' heuristic
asgerf May 2, 2025
1f308ee
JS: Explain use of monotonicAggregates
asgerf May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions javascript/ql/lib/semmle/javascript/ES2015Modules.qll
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import javascript
private import semmle.javascript.internal.CachedStages
private import semmle.javascript.internal.paths.PathExprResolver

/**
* An ECMAScript 2015 module.
Expand Down Expand Up @@ -725,22 +726,7 @@ abstract class ReExportDeclaration extends ExportDeclaration {
cached
Module getReExportedModule() {
Stages::Imports::ref() and
result.getFile() = this.getEnclosingModule().resolve(this.getImportedPath())
or
result = this.resolveFromTypeRoot()
}

/**
* Gets a module in a `node_modules/@types/` folder that matches the imported module name.
*/
private Module resolveFromTypeRoot() {
result.getFile() =
min(TypeRootFolder typeRoot |
|
typeRoot.getModuleFile(this.getImportedPath().getStringValue())
order by
typeRoot.getSearchPriority(this.getFile().getParentContainer())
)
result.getFile() = ImportPathResolver::resolveExpr(this.getImportedPath())
}
}

Expand Down
66 changes: 7 additions & 59 deletions javascript/ql/lib/semmle/javascript/Modules.qll
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import javascript
private import semmle.javascript.internal.CachedStages
private import semmle.javascript.internal.paths.PathExprResolver

/**
* A module, which may either be an ECMAScript 2015-style module,
Expand Down Expand Up @@ -138,39 +139,17 @@ abstract class Import extends AstNode {
/**
* Gets the module the path of this import resolves to.
*/
Module resolveImportedPath() {
result.getFile() = this.getEnclosingModule().resolve(this.getImportedPath())
}
Module resolveImportedPath() { result.getFile() = this.getTargetFile() }

/**
* Gets a module with a `@providesModule` JSDoc tag that matches
* the imported path.
*/
private Module resolveAsProvidedModule() {
exists(JSDocTag tag |
tag.getTitle() = "providesModule" and
tag.getParent().getComment().getTopLevel() = result and
tag.getDescription().trim() = this.getImportedPath().getValue()
)
}

/**
* Gets a module in a `node_modules/@types/` folder that matches the imported module name.
* Gets the module the path of this import resolves to.
*/
private Module resolveFromTypeRoot() {
result.getFile() =
min(TypeRootFolder typeRoot |
|
typeRoot.getModuleFile(this.getImportedPath().getValue())
order by
typeRoot.getSearchPriority(this.getFile().getParentContainer())
)
}
File getTargetFile() { result = ImportPathResolver::resolveExpr(this.getImportedPath()) }

/**
* Gets the imported module, as determined by the TypeScript compiler, if any.
* DEPRECATED. Use `getImportedModule()` instead.
*/
private Module resolveFromTypeScriptSymbol() {
deprecated Module resolveFromTypeScriptSymbol() {
exists(CanonicalName symbol |
ast_node_symbol(this, symbol) and
ast_node_symbol(result, symbol)
Expand All @@ -190,42 +169,11 @@ abstract class Import extends AstNode {
Stages::Imports::ref() and
if exists(this.resolveExternsImport())
then result = this.resolveExternsImport()
else (
result = this.resolveAsProvidedModule() or
result = this.resolveImportedPath() or
result = this.resolveFromTypeRoot() or
result = this.resolveFromTypeScriptSymbol() or
result = resolveNeighbourPackage(this.getImportedPath().getValue())
)
else result = this.resolveImportedPath()
}

/**
* Gets the data flow node that the default import of this import is available at.
*/
abstract DataFlow::Node getImportedModuleNode();
}

/**
* Gets a module imported from another package in the same repository.
*
* No support for importing from folders inside the other package.
*/
private Module resolveNeighbourPackage(PathString importPath) {
exists(PackageJson json | importPath = json.getPackageName() and result = json.getMainModule())
or
exists(string package |
result.getFile().getParentContainer() = getPackageFolder(package) and
importPath = package + "/" + [result.getFile().getBaseName(), result.getFile().getStem()]
)
}

/**
* Gets the folder for a package that has name `package` according to a package.json file in the resulting folder.
*/
pragma[noinline]
private Folder getPackageFolder(string package) {
exists(PackageJson json |
json.getPackageName() = package and
result = json.getFile().getParentContainer()
)
}
42 changes: 42 additions & 0 deletions javascript/ql/lib/semmle/javascript/TSConfig.qll
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

private import javascript
private import semmle.javascript.internal.paths.PathMapping

/**
* A TypeScript configuration file, usually named `tsconfig.json`.
Expand Down Expand Up @@ -178,3 +179,44 @@ private module ResolverConfig implements Folder::ResolveSig {
}

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() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
private import javascript

/**
* A path expression that can be constant-folded by concatenating subexpressions.
*/
abstract class PathConcatenation extends Expr {
/** Gets the separator to insert between paths */
string getSeparator() { result = "" }

/** Gets the `n`th operand to concatenate. */
abstract Expr getOperand(int n);
}

private class AddExprConcatenation extends PathConcatenation, AddExpr {
override Expr getOperand(int n) {
n = 0 and result = this.getLeftOperand()
or
n = 1 and result = this.getRightOperand()
}
}

private class TemplateConcatenation extends PathConcatenation, TemplateLiteral {
override Expr getOperand(int n) { result = this.getElement(n) }
}

private class JoinCallConcatenation extends PathConcatenation, CallExpr {
JoinCallConcatenation() {
this.getReceiver().(VarAccess).getName() = "path" and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a comment that we can't use module resolution here because negative recursion, and that this approximation is probably good enough.
(But maybe a reference to which full implementation we would use, if that was possible).

this.getCalleeName() = ["join", "resolve"]
}

override Expr getOperand(int n) { result = this.getArgument(n) }

override string getSeparator() { result = "/" }
}
Loading