Skip to content

Commit eaf3aa7

Browse files
authored
Merge pull request #10036 from asgerf/js/exports-handling
JS: More precise handling of "exports"
2 parents 1645165 + 3c41f28 commit eaf3aa7

File tree

3 files changed

+58
-12
lines changed

3 files changed

+58
-12
lines changed

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,23 @@ class PackageJson extends JsonObject {
5050
/** Gets a file for this package. */
5151
string getAFile() { result = this.getFiles().getElementStringValue(_) }
5252

53-
/** Gets the main module of this package. */
54-
string getMain() { result = MainModulePath::of(this).getValue() }
53+
/**
54+
* Gets the main module of this package.
55+
*
56+
* This can be given by the `main` or `module` property, or via the
57+
* `exports` property with the relative path `"."`.
58+
*/
59+
string getMain() { result = this.getExportedPath(".") }
60+
61+
/**
62+
* Gets the path to the file exported with the given relative path.
63+
*
64+
* This can be given by the `exports` property, but also considers `main` and
65+
* `module` paths to be exported under the relative path `"."`.
66+
*/
67+
string getExportedPath(string relativePath) {
68+
result = MainModulePath::of(this, relativePath).getValue()
69+
}
5570

5671
/** Gets the path of a command defined for this package. */
5772
string getBin(string cmd) {
@@ -181,6 +196,18 @@ class PackageJson extends JsonObject {
181196
result = min(Module m, int prio | m.getFile() = resolveMainModule(this, prio) | m order by prio)
182197
}
183198

199+
/**
200+
* Gets the module exported under the given relative path.
201+
*
202+
* The main module is considered exported under the path `"."`.
203+
*/
204+
Module getExportedModule(string relativePath) {
205+
relativePath = "." and
206+
result = this.getMainModule()
207+
or
208+
result.getFile() = MainModulePath::of(this, relativePath).resolve()
209+
}
210+
184211
/**
185212
* Gets the `types` or `typings` field of this package.
186213
*/

javascript/ql/lib/semmle/javascript/NodeModuleResolutionImpl.qll

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private string getStem(string name) { result = name.regexpCapture("(.+?)(?:\\.([
9393
* Gets the main module described by `pkg` with the given `priority`.
9494
*/
9595
File resolveMainModule(PackageJson pkg, int priority) {
96-
exists(PathExpr main | main = MainModulePath::of(pkg) |
96+
exists(PathExpr main | main = MainModulePath::of(pkg, ".") |
9797
result = main.resolve() and priority = 0
9898
or
9999
result = tryExtensions(main.resolve(), "index", priority)
@@ -142,21 +142,36 @@ File resolveMainModule(PackageJson pkg, int priority) {
142142
private string getASrcFolderName() { result = ["ts", "js", "src", "lib"] }
143143

144144
/**
145-
* A JSON string in a `package.json` file specifying the path of the main
146-
* module of the package.
145+
* A JSON string in a `package.json` file specifying the path of one of the exported
146+
* modules of the package.
147147
*/
148148
class MainModulePath extends PathExpr, @json_string {
149149
PackageJson pkg;
150+
string relativePath;
150151

151152
MainModulePath() {
153+
relativePath = "." and
152154
this = pkg.getPropValue(["main", "module"])
153155
or
154-
this = getAJsonChild*(pkg.getPropValue("exports"))
156+
// { "exports": "path" } is sugar for { "exports": { ".": "path" }}
157+
relativePath = "." and
158+
this = pkg.getPropValue("exports")
159+
or
160+
exists(JsonValue val | val = pkg.getPropValue("exports").getPropValue(relativePath) |
161+
// Either specified directly as a string: { "./path": "./path.js" }
162+
this = val
163+
or
164+
// Or by module type: { "./path": { "require": "./path.cjs", ... }}
165+
this = val.getPropValue(_)
166+
)
155167
}
156168

157169
/** Gets the `package.json` file in which this path occurs. */
158170
PackageJson getPackageJson() { result = pkg }
159171

172+
/** Gets the relative path under which this is exported, usually starting with a `.`. */
173+
string getRelativePath() { result = relativePath }
174+
160175
/** DEPRECATED: Alias for getPackageJson */
161176
deprecated PackageJSON getPackageJSON() { result = getPackageJson() }
162177

@@ -168,11 +183,15 @@ class MainModulePath extends PathExpr, @json_string {
168183
}
169184
}
170185

171-
/** Gets the value of a property from the JSON object `obj`. */
172-
private JsonValue getAJsonChild(JsonObject obj) { result = obj.getPropValue(_) }
173-
174186
module MainModulePath {
175-
MainModulePath of(PackageJson pkg) { result.getPackageJson() = pkg }
187+
/** Gets the path to the main entry point of `pkg`. */
188+
MainModulePath of(PackageJson pkg) { result = of(pkg, ".") }
189+
190+
/** Gets the path to the file exported from `pkg` as `relativePath`. */
191+
MainModulePath of(PackageJson pkg, string relativePath) {
192+
result.getPackageJson() = pkg and
193+
result.getRelativePath() = relativePath
194+
}
176195
}
177196

178197
/**
@@ -185,7 +204,7 @@ private class FilesPath extends PathExpr, @json_string {
185204

186205
FilesPath() {
187206
this = pkg.getPropValue("files").(JsonArray).getElementValue(_) and
188-
not exists(MainModulePath::of(pkg))
207+
not exists(MainModulePath::of(pkg, _))
189208
}
190209

191210
/** Gets the `package.json` file in which this path occurs. */

javascript/ql/lib/semmle/javascript/PackageExports.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private import NodeModuleResolutionImpl as NodeModule
5252
private DataFlow::Node getAValueExportedByPackage() {
5353
// The base case, an export from a named `package.json` file.
5454
result =
55-
getAnExportFromModule(any(PackageJson pack | exists(pack.getPackageName())).getMainModule())
55+
getAnExportFromModule(any(PackageJson pack | exists(pack.getPackageName())).getExportedModule(_))
5656
or
5757
// module.exports.bar.baz = result;
5858
exists(DataFlow::PropWrite write |

0 commit comments

Comments
 (0)