JS: Add internal extension of PackageJson class

This commit is contained in:
Asger F
2025-04-24 11:13:10 +02:00
parent bb91df8145
commit f542956f66
2 changed files with 162 additions and 21 deletions

View File

@@ -4,6 +4,7 @@
import javascript
private import NodeModuleResolutionImpl
private import semmle.javascript.internal.paths.PackageJsonEx
/** A `package.json` configuration object. */
class PackageJson extends JsonObject {
@@ -93,7 +94,10 @@ class PackageJson extends JsonObject {
* `module` paths to be exported under the relative path `"."`.
*/
string getExportedPath(string relativePath) {
result = MainModulePath::of(this, relativePath).getValue()
this.(PackageJsonEx).hasExactPathMapping(relativePath, result)
or
relativePath = "." and
result = this.(PackageJsonEx).getMainPath()
}
/** Gets the path of a command defined for this package. */
@@ -220,7 +224,7 @@ class PackageJson extends JsonObject {
/**
* Gets the main module of this package.
*/
Module getMainModule() { result = this.getExportedModule(".") }
Module getMainModule() { result.getFile() = this.(PackageJsonEx).getMainFileOrBestGuess() }
/**
* Gets the module exported under the given relative path.
@@ -228,12 +232,10 @@ class PackageJson extends JsonObject {
* The main module is considered exported under the path `"."`.
*/
Module getExportedModule(string relativePath) {
result =
min(Module m, int prio |
m.getFile() = resolveMainModule(this, prio, relativePath)
|
m order by prio
)
this.(PackageJsonEx).hasExactPathMappingTo(relativePath, result.getFile())
or
relativePath = "." and
result = this.getMainModule()
}
/**
@@ -245,19 +247,7 @@ class PackageJson extends JsonObject {
* Gets the file containing the typings of this package, which can either be from the `types` or
* `typings` field, or derived from the `main` or `module` fields.
*/
File getTypingsFile() {
result =
TypingsModulePathString::of(this).resolve(this.getFile().getParentContainer()).getContainer()
or
not exists(TypingsModulePathString::of(this)) and
exists(File mainFile |
mainFile = this.getMainModule().getFile() and
result =
mainFile
.getParentContainer()
.getFile(mainFile.getStem().regexpReplaceAll("\\.d$", "") + ".d.ts")
)
}
File getTypingsFile() { none() } // implemented in PackageJsonEx
/**
* Gets the module containing the typings of this package, which can either be from the `types` or

View File

@@ -0,0 +1,151 @@
private import javascript
private import semmle.javascript.internal.paths.JSPaths
/**
* Extension of `PackageJson` with some internal path-resolution predicates.
*/
class PackageJsonEx extends PackageJson {
private JsonValue getAPartOfExportsSection(string pattern) {
result = this.getPropValue("exports") and
pattern = ""
or
exists(string prop, string prevPath |
result = this.getAPartOfExportsSection(prevPath).getPropValue(prop) and
if prop.matches("./%") then pattern = prop.suffix(2) else pattern = prevPath
)
}
predicate hasPathMapping(string pattern, string newPath) {
this.getAPartOfExportsSection(pattern).getStringValue() = newPath
}
predicate hasExactPathMapping(string pattern, string newPath) {
this.getAPartOfExportsSection(pattern).getStringValue() = newPath and
not pattern.matches("%*%")
}
predicate hasPrefixPathMapping(string pattern, string newPath) {
this.hasPathMapping(pattern + "*", newPath + "*")
}
predicate hasExactPathMappingTo(string pattern, Container target) {
exists(string newPath |
this.hasExactPathMapping(pattern, newPath) and
target = Resolver::resolve(this.getFolder(), newPath)
)
}
predicate hasPrefixPathMappingTo(string pattern, Container target) {
exists(string newPath |
this.hasPrefixPathMapping(pattern, newPath) and
target = Resolver::resolve(this.getFolder(), newPath)
)
}
string getMainPath() { result = this.getPropStringValue(["main", "module"]) }
File getMainFile() {
exists(Container main | main = Resolver::resolve(this.getFolder(), this.getMainPath()) |
result = main
or
result = main.(Folder).getJavaScriptFileOrTypings("index")
)
}
File getMainFileOrBestGuess() {
result = this.getMainFile()
or
result = guessPackageJsonMain1(this)
or
result = guessPackageJsonMain2(this)
}
string getAPathInFilesArray() {
result = this.getPropValue("files").(JsonArray).getElementStringValue(_)
}
Container getAFileInFilesArray() {
result = Resolver::resolve(this.getFolder(), this.getAPathInFilesArray())
}
override File getTypingsFile() {
result = Resolver::resolve(this.getFolder(), this.getTypings())
or
not exists(this.getTypings()) and
exists(File mainFile |
mainFile = this.getMainFileOrBestGuess() and
result =
mainFile
.getParentContainer()
.getFile(mainFile.getStem().regexpReplaceAll("\\.d$", "") + ".d.ts")
)
}
}
private module ResolverConfig implements Folder::ResolveSig {
additional predicate shouldResolve(PackageJsonEx pkg, Container base, string path) {
base = pkg.getFolder() and
(
pkg.hasExactPathMapping(_, path)
or
pkg.hasPrefixPathMapping(_, path)
or
path = pkg.getMainPath()
or
path = pkg.getAPathInFilesArray()
or
path = pkg.getTypings()
)
}
predicate shouldResolve(Container base, string path) { shouldResolve(_, base, path) }
predicate getAnAdditionalChild = JSPaths::getAnAdditionalChild/2;
predicate isOptionalPathComponent(string segment) {
// Try to omit paths can might refer to a build format, .e.g `dist/cjs/foo.cjs` -> `src/foo.ts`
segment = ["cjs", "mjs", "js"]
}
bindingset[segment]
string rewritePathSegment(string segment) {
// Try removing anything after the first dot, such as foo.min.js -> foo (the extension is then filled in by getAdditionalChild)
result = segment.regexpReplaceAll("\\..*", "")
}
}
private module Resolver = Folder::Resolve<ResolverConfig>;
/**
* Removes the scope from a package name, e.g. `@foo/bar` -> `bar`.
*/
bindingset[name]
private string stripPackageScope(string name) { result = name.regexpReplaceAll("^@[^/]+/", "") }
private predicate isImplementationFile(File f) { not f.getBaseName().matches("%.d.ts") }
File guessPackageJsonMain1(PackageJsonEx pkg) {
not isImplementationFile(pkg.getMainFile()) and
exists(Folder folder, Folder subfolder |
folder = pkg.getFolder() and
(
subfolder = folder or
subfolder = folder.getChildContainer(getASrcFolderName()) or
subfolder =
folder
.getChildContainer(getASrcFolderName())
.(Folder)
.getChildContainer(getASrcFolderName())
)
|
result = subfolder.getJavaScriptFileOrTypings("index")
or
result = subfolder.getJavaScriptFileOrTypings(stripPackageScope(pkg.getDeclaredPackageName()))
)
}
File guessPackageJsonMain2(PackageJsonEx pkg) {
not isImplementationFile(pkg.getMainFile()) and
not isImplementationFile(guessPackageJsonMain1(pkg)) and
result = pkg.getAFileInFilesArray()
}