diff --git a/javascript/ql/lib/semmle/javascript/internal/PathResolution.qll b/javascript/ql/lib/semmle/javascript/internal/PathResolution.qll index 482e8dc0142..3846050520e 100644 --- a/javascript/ql/lib/semmle/javascript/internal/PathResolution.qll +++ b/javascript/ql/lib/semmle/javascript/internal/PathResolution.qll @@ -199,24 +199,38 @@ module PathResolution { expr = any(Import imprt).getImportedPath() } + // + // PACKAGE.JSON "exports" property + // private JsonValue getAPartOfExportsSection(PackageJson pkg, string matchedPath) { result = pkg.getPropValue("exports") and - matchedPath = pkg.getDeclaredPackageName() + matchedPath = "" or exists(string prop, string prevPath | result = getAPartOfExportsSection(pkg, prevPath).getPropValue(prop) and - if prop.matches("./%") - then matchedPath = prevPath + prop.suffix(1) - else matchedPath = prevPath + if prop.matches("./%") then matchedPath = prop.suffix(2) else matchedPath = prevPath ) } + private predicate packageHasExactExport(PackageJson pkg, string matchedPath, string path) { + getAPartOfExportsSection(pkg, matchedPath).getStringValue() = path and + not matchedPath.matches("%*%") + } + + private predicate packageHasPrefixExport(PackageJson pkg, string matchedPath, string path) { + getAPartOfExportsSection(pkg, matchedPath + "*").getStringValue() = path + "*" + } + private module ResolvePackageExportsConfig implements ResolvePathsSig { additional predicate shouldResolve( - PackageJson pkg, string exportedPath, Container base, string path + PackageJson pkg, string matchedPath, Container base, string path ) { - base = pkg.getJsonFile().getParentContainer() and - path = getAPartOfExportsSection(pkg, exportedPath).getStringValue() + ( + packageHasExactExport(pkg, matchedPath, path) + or + packageHasPrefixExport(pkg, matchedPath, path) + ) and + base = pkg.getJsonFile().getParentContainer() } predicate shouldResolve(Container base, string path) { shouldResolve(_, _, base, path) } @@ -224,10 +238,17 @@ module PathResolution { private module ResolvePackageExports = ResolvePaths; - private Container resolvePackageExports(PackageJson pkg, string matchedPath) { - exists(Container base, string path | - ResolvePackageExportsConfig::shouldResolve(pkg, matchedPath, base, path) and - result = ResolvePackageExports::resolve(base, path) + private Container resolvePackageExactExport(PackageJson pkg, string matchedPath) { + exists(string path | + packageHasExactExport(pkg, matchedPath, path) and + result = ResolvePackageExports::resolve(pkg.getJsonFile().getParentContainer(), path) + ) + } + + private Container resolvePackagePrefixExport(PackageJson pkg, string matchedPath) { + exists(string path | + packageHasPrefixExport(pkg, matchedPath, path) and + result = ResolvePackageExports::resolve(pkg.getJsonFile().getParentContainer(), path) ) } @@ -238,7 +259,7 @@ module PathResolution { pragma[nomagic] private predicate replacedPath1(PathExpr expr, Container base, string newPath) { - // Import path matching a "paths" passing in tsconfig.json + // Handle tsconfig mappings such as `{ "paths": { "@/*": "./src/*" }}` exists(TSConfig config, string value, string mappedPath | config = getTSConfigFromPathExpr(expr).getExtendedTSConfig*() and value = expr.getValue() @@ -255,17 +276,46 @@ module PathResolution { ) ) or - // Import path matching a path within a package, matched by an "exports" mapping in package.json - base = resolvePackageExports(_, expr.getValue()) and - newPath = "" - or - // Import path matching a non-empty path within a package, where the package has no explicit "exports" - exists(PackageJson pkg, string packageName | + // Handle imports referring to a package by name, where we have a package.json + // file for that package in the codebase. + // + // This part only handles the "exports" property of package.json, "main" and "modules" are + // handled further down because their semantics are easier to handle there. + exists(PackageJson pkg, string packageName, string remainder | packageName = getPackagePrefixFromPathExpr(expr) and pkg.getDeclaredPackageName() = packageName and + remainder = expr.getValue().suffix(packageName.length()).regexpReplaceAll("^[/\\\\]", "") + | + // "exports": { ".": "./foo.js" } + // "exports": { "./foo.js": "./foo/impl.js" } + base = resolvePackageExactExport(pkg, remainder) and + newPath = "" + or + // "exports": { "./*": "./foo/*" } + exists(string prefix | + base = resolvePackagePrefixExport(pkg, prefix) and + remainder = prefix + newPath + ) + or + // Otherwise resolve relative to the enclosing folder. + // If there is a "main" or "module" property, this is later remapped to that file in `getFileFromFolderImport` not exists(pkg.getPropValue("exports")) and - newPath = expr.getValue().suffix(packageName.length()) and - base = pkg.getJsonFile().getParentContainer() + base = pkg.getJsonFile().getParentContainer() and + newPath = remainder + ) + or + // Handle imports referring to a package by name, where we have a package.json + // file for that package in the codebase. + // + // This part only handles the "exports" property of package.json, "main" and "modules" are + // handled further down because their semantics are easier to handle there. + exists(PackageJson pkg, string packageName, string remainder, string prefix | + packageName = getPackagePrefixFromPathExpr(expr) and + pkg.getDeclaredPackageName() = packageName and + remainder = expr.getValue().suffix(packageName.length()).regexpReplaceAll("^[/\\\\]", "") and + // "exports": { "./*": "./foo/*" } + base = resolvePackagePrefixExport(pkg, prefix) and + remainder = prefix + newPath ) } diff --git a/javascript/ql/test/library-tests/PathResolution/Basic/import-packages.ts b/javascript/ql/test/library-tests/PathResolution/Basic/import-packages.ts index 15986e4f997..07854cc950a 100644 --- a/javascript/ql/test/library-tests/PathResolution/Basic/import-packages.ts +++ b/javascript/ql/test/library-tests/PathResolution/Basic/import-packages.ts @@ -8,3 +8,6 @@ import '@example/package-with-exports'; // $ importTarget=PackageWithExports/mai import '../PackageWithExports/fake-file'; // Not a valid import import '@example/package-with-exports/fake-file'; // $ importTarget=PackageWithExports/fake-file-impl.js + +import '../PackageWithExports/star/foo'; // Not a valid import +import '@example/package-with-exports/star/foo'; // $ importTarget=PackageWithExports/star-impl/foo.js diff --git a/javascript/ql/test/library-tests/PathResolution/PackageWithExports/package.json b/javascript/ql/test/library-tests/PathResolution/PackageWithExports/package.json index 51e2cf226d1..229803c3301 100644 --- a/javascript/ql/test/library-tests/PathResolution/PackageWithExports/package.json +++ b/javascript/ql/test/library-tests/PathResolution/PackageWithExports/package.json @@ -6,6 +6,9 @@ }, "./fake-file": { "default": "./fake-file-impl.js" + }, + "./star/*": { + "default": "./star-impl/*" } } } diff --git a/javascript/ql/test/library-tests/PathResolution/PackageWithExports/star-impl/foo.js b/javascript/ql/test/library-tests/PathResolution/PackageWithExports/star-impl/foo.js new file mode 100644 index 00000000000..ad1d380d6cc --- /dev/null +++ b/javascript/ql/test/library-tests/PathResolution/PackageWithExports/star-impl/foo.js @@ -0,0 +1 @@ +export const x = 1; diff --git a/javascript/ql/test/library-tests/PathResolution/test.expected b/javascript/ql/test/library-tests/PathResolution/test.expected index 75d0b000af3..4d2dc0b52b3 100644 --- a/javascript/ql/test/library-tests/PathResolution/test.expected +++ b/javascript/ql/test/library-tests/PathResolution/test.expected @@ -29,6 +29,7 @@ | Basic/import-packages.ts:6:1:6:43 | import ... -main'; | PackageWithModuleMain/main.js | | Basic/import-packages.ts:7:1:7:39 | import ... ports'; | PackageWithExports/main.js | | Basic/import-packages.ts:10:1:10:49 | import ... -file'; | PackageWithExports/fake-file-impl.js | +| Basic/import-packages.ts:13:1:13:48 | import ... r/foo'; | PackageWithExports/star-impl/foo.js | | Basic/index.ts:1:1:1:22 | import ... r/sub'; | Basic/Subdir/sub.ts | | Extended/src/main.ts:2:1:2:21 | import ... /file"; | Extended/lib/file.ts | | Extended/src/main.ts:3:1:3:24 | import ... le.ts"; | Extended/lib/file.ts |