Skip to content

Commit f542956

Browse files
committed
JS: Add internal extension of PackageJson class
1 parent bb91df8 commit f542956

File tree

2 files changed

+162
-21
lines changed

2 files changed

+162
-21
lines changed

javascript/ql/lib/semmle/javascript/NPM.qll

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import javascript
66
private import NodeModuleResolutionImpl
7+
private import semmle.javascript.internal.paths.PackageJsonEx
78

89
/** A `package.json` configuration object. */
910
class PackageJson extends JsonObject {
@@ -93,7 +94,10 @@ class PackageJson extends JsonObject {
9394
* `module` paths to be exported under the relative path `"."`.
9495
*/
9596
string getExportedPath(string relativePath) {
96-
result = MainModulePath::of(this, relativePath).getValue()
97+
this.(PackageJsonEx).hasExactPathMapping(relativePath, result)
98+
or
99+
relativePath = "." and
100+
result = this.(PackageJsonEx).getMainPath()
97101
}
98102

99103
/** Gets the path of a command defined for this package. */
@@ -220,20 +224,18 @@ class PackageJson extends JsonObject {
220224
/**
221225
* Gets the main module of this package.
222226
*/
223-
Module getMainModule() { result = this.getExportedModule(".") }
227+
Module getMainModule() { result.getFile() = this.(PackageJsonEx).getMainFileOrBestGuess() }
224228

225229
/**
226230
* Gets the module exported under the given relative path.
227231
*
228232
* The main module is considered exported under the path `"."`.
229233
*/
230234
Module getExportedModule(string relativePath) {
231-
result =
232-
min(Module m, int prio |
233-
m.getFile() = resolveMainModule(this, prio, relativePath)
234-
|
235-
m order by prio
236-
)
235+
this.(PackageJsonEx).hasExactPathMappingTo(relativePath, result.getFile())
236+
or
237+
relativePath = "." and
238+
result = this.getMainModule()
237239
}
238240

239241
/**
@@ -245,19 +247,7 @@ class PackageJson extends JsonObject {
245247
* Gets the file containing the typings of this package, which can either be from the `types` or
246248
* `typings` field, or derived from the `main` or `module` fields.
247249
*/
248-
File getTypingsFile() {
249-
result =
250-
TypingsModulePathString::of(this).resolve(this.getFile().getParentContainer()).getContainer()
251-
or
252-
not exists(TypingsModulePathString::of(this)) and
253-
exists(File mainFile |
254-
mainFile = this.getMainModule().getFile() and
255-
result =
256-
mainFile
257-
.getParentContainer()
258-
.getFile(mainFile.getStem().regexpReplaceAll("\\.d$", "") + ".d.ts")
259-
)
260-
}
250+
File getTypingsFile() { none() } // implemented in PackageJsonEx
261251

262252
/**
263253
* Gets the module containing the typings of this package, which can either be from the `types` or
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
private import javascript
2+
private import semmle.javascript.internal.paths.JSPaths
3+
4+
/**
5+
* Extension of `PackageJson` with some internal path-resolution predicates.
6+
*/
7+
class PackageJsonEx extends PackageJson {
8+
private JsonValue getAPartOfExportsSection(string pattern) {
9+
result = this.getPropValue("exports") and
10+
pattern = ""
11+
or
12+
exists(string prop, string prevPath |
13+
result = this.getAPartOfExportsSection(prevPath).getPropValue(prop) and
14+
if prop.matches("./%") then pattern = prop.suffix(2) else pattern = prevPath
15+
)
16+
}
17+
18+
predicate hasPathMapping(string pattern, string newPath) {
19+
this.getAPartOfExportsSection(pattern).getStringValue() = newPath
20+
}
21+
22+
predicate hasExactPathMapping(string pattern, string newPath) {
23+
this.getAPartOfExportsSection(pattern).getStringValue() = newPath and
24+
not pattern.matches("%*%")
25+
}
26+
27+
predicate hasPrefixPathMapping(string pattern, string newPath) {
28+
this.hasPathMapping(pattern + "*", newPath + "*")
29+
}
30+
31+
predicate hasExactPathMappingTo(string pattern, Container target) {
32+
exists(string newPath |
33+
this.hasExactPathMapping(pattern, newPath) and
34+
target = Resolver::resolve(this.getFolder(), newPath)
35+
)
36+
}
37+
38+
predicate hasPrefixPathMappingTo(string pattern, Container target) {
39+
exists(string newPath |
40+
this.hasPrefixPathMapping(pattern, newPath) and
41+
target = Resolver::resolve(this.getFolder(), newPath)
42+
)
43+
}
44+
45+
string getMainPath() { result = this.getPropStringValue(["main", "module"]) }
46+
47+
File getMainFile() {
48+
exists(Container main | main = Resolver::resolve(this.getFolder(), this.getMainPath()) |
49+
result = main
50+
or
51+
result = main.(Folder).getJavaScriptFileOrTypings("index")
52+
)
53+
}
54+
55+
File getMainFileOrBestGuess() {
56+
result = this.getMainFile()
57+
or
58+
result = guessPackageJsonMain1(this)
59+
or
60+
result = guessPackageJsonMain2(this)
61+
}
62+
63+
string getAPathInFilesArray() {
64+
result = this.getPropValue("files").(JsonArray).getElementStringValue(_)
65+
}
66+
67+
Container getAFileInFilesArray() {
68+
result = Resolver::resolve(this.getFolder(), this.getAPathInFilesArray())
69+
}
70+
71+
override File getTypingsFile() {
72+
result = Resolver::resolve(this.getFolder(), this.getTypings())
73+
or
74+
not exists(this.getTypings()) and
75+
exists(File mainFile |
76+
mainFile = this.getMainFileOrBestGuess() and
77+
result =
78+
mainFile
79+
.getParentContainer()
80+
.getFile(mainFile.getStem().regexpReplaceAll("\\.d$", "") + ".d.ts")
81+
)
82+
}
83+
}
84+
85+
private module ResolverConfig implements Folder::ResolveSig {
86+
additional predicate shouldResolve(PackageJsonEx pkg, Container base, string path) {
87+
base = pkg.getFolder() and
88+
(
89+
pkg.hasExactPathMapping(_, path)
90+
or
91+
pkg.hasPrefixPathMapping(_, path)
92+
or
93+
path = pkg.getMainPath()
94+
or
95+
path = pkg.getAPathInFilesArray()
96+
or
97+
path = pkg.getTypings()
98+
)
99+
}
100+
101+
predicate shouldResolve(Container base, string path) { shouldResolve(_, base, path) }
102+
103+
predicate getAnAdditionalChild = JSPaths::getAnAdditionalChild/2;
104+
105+
predicate isOptionalPathComponent(string segment) {
106+
// Try to omit paths can might refer to a build format, .e.g `dist/cjs/foo.cjs` -> `src/foo.ts`
107+
segment = ["cjs", "mjs", "js"]
108+
}
109+
110+
bindingset[segment]
111+
string rewritePathSegment(string segment) {
112+
// Try removing anything after the first dot, such as foo.min.js -> foo (the extension is then filled in by getAdditionalChild)
113+
result = segment.regexpReplaceAll("\\..*", "")
114+
}
115+
}
116+
117+
private module Resolver = Folder::Resolve<ResolverConfig>;
118+
119+
/**
120+
* Removes the scope from a package name, e.g. `@foo/bar` -> `bar`.
121+
*/
122+
bindingset[name]
123+
private string stripPackageScope(string name) { result = name.regexpReplaceAll("^@[^/]+/", "") }
124+
125+
private predicate isImplementationFile(File f) { not f.getBaseName().matches("%.d.ts") }
126+
127+
File guessPackageJsonMain1(PackageJsonEx pkg) {
128+
not isImplementationFile(pkg.getMainFile()) and
129+
exists(Folder folder, Folder subfolder |
130+
folder = pkg.getFolder() and
131+
(
132+
subfolder = folder or
133+
subfolder = folder.getChildContainer(getASrcFolderName()) or
134+
subfolder =
135+
folder
136+
.getChildContainer(getASrcFolderName())
137+
.(Folder)
138+
.getChildContainer(getASrcFolderName())
139+
)
140+
|
141+
result = subfolder.getJavaScriptFileOrTypings("index")
142+
or
143+
result = subfolder.getJavaScriptFileOrTypings(stripPackageScope(pkg.getDeclaredPackageName()))
144+
)
145+
}
146+
147+
File guessPackageJsonMain2(PackageJsonEx pkg) {
148+
not isImplementationFile(pkg.getMainFile()) and
149+
not isImplementationFile(guessPackageJsonMain1(pkg)) and
150+
result = pkg.getAFileInFilesArray()
151+
}

0 commit comments

Comments
 (0)