|
| 1 | +private import javascript |
| 2 | +private import semmle.javascript.TSConfig |
| 3 | +private import semmle.javascript.internal.paths.PackageJsonEx |
| 4 | +private import semmle.javascript.internal.paths.JSPaths |
| 5 | + |
| 6 | +final private class FinalPathExpr = PathExpr; |
| 7 | + |
| 8 | +private class RelevantPathExpr extends FinalPathExpr { |
| 9 | + pragma[nomagic] |
| 10 | + RelevantPathExpr() { this = any(Import imprt).getImportedPath() } |
| 11 | +} |
| 12 | + |
| 13 | +/** |
| 14 | + * Gets a tsconfig file to use as fallback for handling paths in `c`. |
| 15 | + * |
| 16 | + * This holds for files and folders where no tsconfig seems to include it, |
| 17 | + * but it has one or more tsconfig files in parent directories. |
| 18 | + */ |
| 19 | +private TSConfig getFallbackTSConfig(Container c) { |
| 20 | + not c = any(TSConfig t).getAnIncludedContainer() and |
| 21 | + ( |
| 22 | + c = result.getFolder() |
| 23 | + or |
| 24 | + result = getFallbackTSConfig(c.getParentContainer()) |
| 25 | + ) |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * Gets the TSConfig file relevant for resolving `expr`. |
| 30 | + */ |
| 31 | +pragma[nomagic] |
| 32 | +private TSConfig getTSConfigFromPathExpr(RelevantPathExpr expr) { |
| 33 | + result.getAnIncludedContainer() = expr.getFile() |
| 34 | + or |
| 35 | + result = getFallbackTSConfig(expr.getFile()) |
| 36 | +} |
| 37 | + |
| 38 | +/** |
| 39 | + * Holds if `path` is relative, in the sense that it should be resolved relative to its enclosing folder. |
| 40 | + */ |
| 41 | +bindingset[path] |
| 42 | +pragma[inline_late] |
| 43 | +predicate isRelativePath(string path) { path.regexpMatch("\\.\\.?(?:[/\\\\].*)?") } |
| 44 | + |
| 45 | +/** |
| 46 | + * Gets the NPM package name from the beginning of the given import path, e.g. |
| 47 | + * gets `foo` from `foo/bar`, and `@example/foo` from `@example/foo/bar`. |
| 48 | + */ |
| 49 | +pragma[nomagic] |
| 50 | +private string getPackagePrefixFromPathExpr(RelevantPathExpr expr) { |
| 51 | + result = expr.getValue().regexpFind("^(@[^/\\\\]+[/\\\\])?[^@./\\\\][^/\\\\]*", _, _) |
| 52 | +} |
| 53 | + |
| 54 | +private Variable dirname() { result.getName() = "__dirname" } |
| 55 | + |
| 56 | +/** Holds if `add` is a relevant path expression of form `__dirname + expr`. */ |
| 57 | +private predicate prefixedByDirname(PathExpr expr) { |
| 58 | + expr = dirname().getAnAccess() |
| 59 | + or |
| 60 | + prefixedByDirname(expr.(AddExpr).getLeftOperand()) |
| 61 | + or |
| 62 | + prefixedByDirname(expr.(CallExpr).getArgument(0)) |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Holds if `expr` matches a path mapping, and should thus be resolved as `newPath` relative to `base`. |
| 67 | + */ |
| 68 | +pragma[nomagic] |
| 69 | +private predicate resolveViaPathMapping(RelevantPathExpr expr, Container base, string newPath) { |
| 70 | + // Handle tsconfig mappings such as `{ "paths": { "@/*": "./src/*" }}` |
| 71 | + exists(TSConfig config, string value | |
| 72 | + config = getTSConfigFromPathExpr(expr).getExtendedTSConfig*() and |
| 73 | + value = expr.getValue() and |
| 74 | + base = config.getBaseUrlFolderOrOwnFolder() |
| 75 | + | |
| 76 | + config.hasExactPathMapping(value, newPath) |
| 77 | + or |
| 78 | + exists(string pattern, string suffix, string mappedPath | |
| 79 | + config.hasPrefixPathMapping(pattern, mappedPath) and |
| 80 | + value = pattern + suffix and |
| 81 | + newPath = mappedPath + suffix |
| 82 | + ) |
| 83 | + ) |
| 84 | + or |
| 85 | + // Handle imports referring to a package by name, where we have a package.json |
| 86 | + // file for that package in the codebase. |
| 87 | + // |
| 88 | + // This part only handles the "exports" property of package.json. "main" and "modules" are |
| 89 | + // handled further down because their semantics are easier to handle there. |
| 90 | + exists(PackageJsonEx pkg, string packageName, string remainder | |
| 91 | + packageName = getPackagePrefixFromPathExpr(expr) and |
| 92 | + pkg.getDeclaredPackageName() = packageName and |
| 93 | + remainder = expr.getValue().suffix(packageName.length()).regexpReplaceAll("^[/\\\\]", "") |
| 94 | + | |
| 95 | + // "exports": { ".": "./foo.js" } |
| 96 | + // "exports": { "./foo.js": "./foo/impl.js" } |
| 97 | + pkg.hasExactPathMappingTo(remainder, base) and |
| 98 | + newPath = "" |
| 99 | + or |
| 100 | + // "exports": { "./*": "./foo/*" } |
| 101 | + exists(string prefix | |
| 102 | + pkg.hasPrefixPathMappingTo(prefix, base) and |
| 103 | + remainder = prefix + newPath |
| 104 | + ) |
| 105 | + ) |
| 106 | +} |
| 107 | + |
| 108 | +pragma[noopt] |
| 109 | +private predicate relativePathExpr(RelevantPathExpr expr, Container base, string path) { |
| 110 | + expr instanceof RelevantPathExpr and |
| 111 | + path = expr.getValue() and |
| 112 | + isRelativePath(path) and |
| 113 | + exists(File file | |
| 114 | + file = expr.getFile() and |
| 115 | + base = file.getParentContainer() |
| 116 | + ) |
| 117 | +} |
| 118 | + |
| 119 | +/** |
| 120 | + * Holds if `expr` should be resolved as `path` relative to `base`. |
| 121 | + */ |
| 122 | +pragma[nomagic] |
| 123 | +private predicate shouldResolve(RelevantPathExpr expr, Container base, string path) { |
| 124 | + // Relative paths are resolved from their enclosing folder |
| 125 | + relativePathExpr(expr, base, path) |
| 126 | + or |
| 127 | + // Paths prefixed by __dirname should be resolved from the root dir, because __dirname |
| 128 | + // currently has a getValue() that returns its absolute path. |
| 129 | + prefixedByDirname(expr) and |
| 130 | + not exists(base.getParentContainer()) and // get root dir |
| 131 | + path = expr.getValue() |
| 132 | + or |
| 133 | + resolveViaPathMapping(expr, base, path) |
| 134 | + or |
| 135 | + // Resolve from baseUrl of relevant tsconfig.json file |
| 136 | + path = expr.getValue() and |
| 137 | + not isRelativePath(path) and |
| 138 | + base = getTSConfigFromPathExpr(expr).getBaseUrlFolder() |
| 139 | + or |
| 140 | + // If the path starts with the name of a package, but did not match any path mapping, |
| 141 | + // resolve relative to the enclosing directory of that package. |
| 142 | + // Note that `getFileFromFolderImport` may subsequently redirect this to the package's "main", |
| 143 | + // so we don't have to deal with that here. |
| 144 | + exists(PackageJson pkg, string packageName | |
| 145 | + packageName = getPackagePrefixFromPathExpr(expr) and |
| 146 | + pkg.getDeclaredPackageName() = packageName and |
| 147 | + path = expr.getValue().suffix(packageName.length()).regexpReplaceAll("^[/\\\\]", "") and |
| 148 | + base = pkg.getFolder() |
| 149 | + ) |
| 150 | +} |
| 151 | + |
| 152 | +private module ResolverConfig implements Folder::ResolveSig { |
| 153 | + predicate shouldResolve(Container base, string path) { shouldResolve(_, base, path) } |
| 154 | + |
| 155 | + predicate getAnAdditionalChild = JSPaths::getAnAdditionalChild/2; |
| 156 | +} |
| 157 | + |
| 158 | +private module Resolver = Folder::Resolve<ResolverConfig>; |
| 159 | + |
| 160 | +private Container resolvePathExpr1(RelevantPathExpr expr) { |
| 161 | + exists(Container base, string path | |
| 162 | + shouldResolve(expr, base, path) and |
| 163 | + result = Resolver::resolve(base, path) |
| 164 | + ) |
| 165 | +} |
| 166 | + |
| 167 | +/** |
| 168 | + * Removes the scope from a package name, e.g. `@foo/bar` -> `bar`. |
| 169 | + */ |
| 170 | +bindingset[name] |
| 171 | +private string stripPackageScope(string name) { result = name.regexpReplaceAll("^@[^/]+/", "") } |
| 172 | + |
| 173 | +private File guessPackageJsonMain1(PackageJsonEx pkg) { |
| 174 | + not exists(pkg.getMainFile()) and |
| 175 | + exists(Folder folder, Folder subfolder | |
| 176 | + folder = pkg.getFolder() and |
| 177 | + ( |
| 178 | + subfolder = folder or |
| 179 | + subfolder = folder.getChildContainer(getASrcFolderName()) or |
| 180 | + subfolder = |
| 181 | + folder |
| 182 | + .getChildContainer(getASrcFolderName()) |
| 183 | + .(Folder) |
| 184 | + .getChildContainer(getASrcFolderName()) |
| 185 | + ) |
| 186 | + | |
| 187 | + result = subfolder.getJavaScriptFileOrTypings("index") |
| 188 | + or |
| 189 | + result = subfolder.getJavaScriptFileOrTypings(stripPackageScope(pkg.getDeclaredPackageName())) |
| 190 | + ) |
| 191 | +} |
| 192 | + |
| 193 | +private File guessPackageJsonMain2(PackageJsonEx pkg) { |
| 194 | + not exists(pkg.getMainFile()) and |
| 195 | + not exists(guessPackageJsonMain1(pkg)) and |
| 196 | + result = pkg.getAFileInFilesArray() |
| 197 | +} |
| 198 | + |
| 199 | +private File getFileFromFolderImport(Folder folder) { |
| 200 | + result = folder.getJavaScriptFileOrTypings("index") |
| 201 | + or |
| 202 | + // Note that unlike "exports" paths, "main" and "module" also take effect when the package |
| 203 | + // is imported via a relative path, e.g. `require("..")` targeting a folder with a package.json file. |
| 204 | + exists(PackageJsonEx pkg | pkg.getFolder() = folder | |
| 205 | + result = pkg.getMainFile() |
| 206 | + or |
| 207 | + result = guessPackageJsonMain1(pkg) |
| 208 | + or |
| 209 | + result = guessPackageJsonMain2(pkg) |
| 210 | + ) |
| 211 | +} |
| 212 | + |
| 213 | +File resolvePathExpr(PathExpr expr) { |
| 214 | + result = resolvePathExpr1(expr) |
| 215 | + or |
| 216 | + result = getFileFromFolderImport(resolvePathExpr1(expr)) |
| 217 | +} |
| 218 | + |
| 219 | +module Debug { |
| 220 | + class PathExprToDebug extends RelevantPathExpr { |
| 221 | + PathExprToDebug() { this.getValue() = "vs/nls" } |
| 222 | + } |
| 223 | + |
| 224 | + query PathExprToDebug pathExprs() { any() } |
| 225 | + |
| 226 | + query TSConfig getTSConfigFromPathExpr_(PathExprToDebug expr) { |
| 227 | + result = getTSConfigFromPathExpr(expr) |
| 228 | + } |
| 229 | + |
| 230 | + query string getPackagePrefixFromPathExpr_(PathExprToDebug expr) { |
| 231 | + result = getPackagePrefixFromPathExpr(expr) |
| 232 | + } |
| 233 | + |
| 234 | + query predicate resolveViaPathMapping_(PathExprToDebug expr, Container base, string newPath) { |
| 235 | + resolveViaPathMapping(expr, base, newPath) |
| 236 | + } |
| 237 | + |
| 238 | + query predicate shouldResolve_(PathExprToDebug expr, Container base, string newPath) { |
| 239 | + shouldResolve(expr, base, newPath) |
| 240 | + } |
| 241 | + |
| 242 | + query Container resolvePathExpr1_(PathExprToDebug expr) { result = resolvePathExpr1(expr) } |
| 243 | + |
| 244 | + query File resolvePathExpr_(PathExprToDebug expr) { result = resolvePathExpr(expr) } |
| 245 | + |
| 246 | + // Some predicates that are usually small enough that they don't need restriction |
| 247 | + query File getPackageMainFile(PackageJsonEx pkg) { result = pkg.getMainFile() } |
| 248 | + |
| 249 | + query predicate guessPackageJsonMain1_ = guessPackageJsonMain1/1; |
| 250 | + |
| 251 | + query predicate guessPackageJsonMain2_ = guessPackageJsonMain2/1; |
| 252 | + |
| 253 | + query predicate getFileFromFolderImport_ = getFileFromFolderImport/1; |
| 254 | +} |
0 commit comments