| 1 | /**
|
|---|
| 2 | * @fileoverview `IgnorePattern` class.
|
|---|
| 3 | *
|
|---|
| 4 | * `IgnorePattern` class has the set of glob patterns and the base path.
|
|---|
| 5 | *
|
|---|
| 6 | * It provides two static methods.
|
|---|
| 7 | *
|
|---|
| 8 | * - `IgnorePattern.createDefaultIgnore(cwd)`
|
|---|
| 9 | * Create the default predicate function.
|
|---|
| 10 | * - `IgnorePattern.createIgnore(ignorePatterns)`
|
|---|
| 11 | * Create the predicate function from multiple `IgnorePattern` objects.
|
|---|
| 12 | *
|
|---|
| 13 | * It provides two properties and a method.
|
|---|
| 14 | *
|
|---|
| 15 | * - `patterns`
|
|---|
| 16 | * The glob patterns that ignore to lint.
|
|---|
| 17 | * - `basePath`
|
|---|
| 18 | * The base path of the glob patterns. If absolute paths existed in the
|
|---|
| 19 | * glob patterns, those are handled as relative paths to the base path.
|
|---|
| 20 | * - `getPatternsRelativeTo(basePath)`
|
|---|
| 21 | * Get `patterns` as modified for a given base path. It modifies the
|
|---|
| 22 | * absolute paths in the patterns as prepending the difference of two base
|
|---|
| 23 | * paths.
|
|---|
| 24 | *
|
|---|
| 25 | * `ConfigArrayFactory` creates `IgnorePattern` objects when it processes
|
|---|
| 26 | * `ignorePatterns` properties.
|
|---|
| 27 | *
|
|---|
| 28 | * @author Toru Nagashima <https://github.com/mysticatea>
|
|---|
| 29 | */
|
|---|
| 30 |
|
|---|
| 31 | //------------------------------------------------------------------------------
|
|---|
| 32 | // Requirements
|
|---|
| 33 | //------------------------------------------------------------------------------
|
|---|
| 34 |
|
|---|
| 35 | import assert from "assert";
|
|---|
| 36 | import path from "path";
|
|---|
| 37 | import ignore from "ignore";
|
|---|
| 38 | import debugOrig from "debug";
|
|---|
| 39 |
|
|---|
| 40 | const debug = debugOrig("eslintrc:ignore-pattern");
|
|---|
| 41 |
|
|---|
| 42 | /** @typedef {ReturnType<import("ignore").default>} Ignore */
|
|---|
| 43 |
|
|---|
| 44 | //------------------------------------------------------------------------------
|
|---|
| 45 | // Helpers
|
|---|
| 46 | //------------------------------------------------------------------------------
|
|---|
| 47 |
|
|---|
| 48 | /**
|
|---|
| 49 | * Get the path to the common ancestor directory of given paths.
|
|---|
| 50 | * @param {string[]} sourcePaths The paths to calculate the common ancestor.
|
|---|
| 51 | * @returns {string} The path to the common ancestor directory.
|
|---|
| 52 | */
|
|---|
| 53 | function getCommonAncestorPath(sourcePaths) {
|
|---|
| 54 | let result = sourcePaths[0];
|
|---|
| 55 |
|
|---|
| 56 | for (let i = 1; i < sourcePaths.length; ++i) {
|
|---|
| 57 | const a = result;
|
|---|
| 58 | const b = sourcePaths[i];
|
|---|
| 59 |
|
|---|
| 60 | // Set the shorter one (it's the common ancestor if one includes the other).
|
|---|
| 61 | result = a.length < b.length ? a : b;
|
|---|
| 62 |
|
|---|
| 63 | // Set the common ancestor.
|
|---|
| 64 | for (let j = 0, lastSepPos = 0; j < a.length && j < b.length; ++j) {
|
|---|
| 65 | if (a[j] !== b[j]) {
|
|---|
| 66 | result = a.slice(0, lastSepPos);
|
|---|
| 67 | break;
|
|---|
| 68 | }
|
|---|
| 69 | if (a[j] === path.sep) {
|
|---|
| 70 | lastSepPos = j;
|
|---|
| 71 | }
|
|---|
| 72 | }
|
|---|
| 73 | }
|
|---|
| 74 |
|
|---|
| 75 | let resolvedResult = result || path.sep;
|
|---|
| 76 |
|
|---|
| 77 | // if Windows common ancestor is root of drive must have trailing slash to be absolute.
|
|---|
| 78 | if (resolvedResult && resolvedResult.endsWith(":") && process.platform === "win32") {
|
|---|
| 79 | resolvedResult += path.sep;
|
|---|
| 80 | }
|
|---|
| 81 | return resolvedResult;
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | /**
|
|---|
| 85 | * Make relative path.
|
|---|
| 86 | * @param {string} from The source path to get relative path.
|
|---|
| 87 | * @param {string} to The destination path to get relative path.
|
|---|
| 88 | * @returns {string} The relative path.
|
|---|
| 89 | */
|
|---|
| 90 | function relative(from, to) {
|
|---|
| 91 | const relPath = path.relative(from, to);
|
|---|
| 92 |
|
|---|
| 93 | if (path.sep === "/") {
|
|---|
| 94 | return relPath;
|
|---|
| 95 | }
|
|---|
| 96 | return relPath.split(path.sep).join("/");
|
|---|
| 97 | }
|
|---|
| 98 |
|
|---|
| 99 | /**
|
|---|
| 100 | * Get the trailing slash if existed.
|
|---|
| 101 | * @param {string} filePath The path to check.
|
|---|
| 102 | * @returns {string} The trailing slash if existed.
|
|---|
| 103 | */
|
|---|
| 104 | function dirSuffix(filePath) {
|
|---|
| 105 | const isDir = (
|
|---|
| 106 | filePath.endsWith(path.sep) ||
|
|---|
| 107 | (process.platform === "win32" && filePath.endsWith("/"))
|
|---|
| 108 | );
|
|---|
| 109 |
|
|---|
| 110 | return isDir ? "/" : "";
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | const DefaultPatterns = Object.freeze(["/**/node_modules/*"]);
|
|---|
| 114 | const DotPatterns = Object.freeze([".*", "!.eslintrc.*", "!../"]);
|
|---|
| 115 |
|
|---|
| 116 | //------------------------------------------------------------------------------
|
|---|
| 117 | // Public
|
|---|
| 118 | //------------------------------------------------------------------------------
|
|---|
| 119 |
|
|---|
| 120 | class IgnorePattern {
|
|---|
| 121 |
|
|---|
| 122 | /**
|
|---|
| 123 | * The default patterns.
|
|---|
| 124 | * @type {string[]}
|
|---|
| 125 | */
|
|---|
| 126 | static get DefaultPatterns() {
|
|---|
| 127 | return DefaultPatterns;
|
|---|
| 128 | }
|
|---|
| 129 |
|
|---|
| 130 | /**
|
|---|
| 131 | * Create the default predicate function.
|
|---|
| 132 | * @param {string} cwd The current working directory.
|
|---|
| 133 | * @returns {((filePath:string, dot:boolean) => boolean) & {basePath:string; patterns:string[]}}
|
|---|
| 134 | * The preficate function.
|
|---|
| 135 | * The first argument is an absolute path that is checked.
|
|---|
| 136 | * The second argument is the flag to not ignore dotfiles.
|
|---|
| 137 | * If the predicate function returned `true`, it means the path should be ignored.
|
|---|
| 138 | */
|
|---|
| 139 | static createDefaultIgnore(cwd) {
|
|---|
| 140 | return this.createIgnore([new IgnorePattern(DefaultPatterns, cwd)]);
|
|---|
| 141 | }
|
|---|
| 142 |
|
|---|
| 143 | /**
|
|---|
| 144 | * Create the predicate function from multiple `IgnorePattern` objects.
|
|---|
| 145 | * @param {IgnorePattern[]} ignorePatterns The list of ignore patterns.
|
|---|
| 146 | * @returns {((filePath:string, dot?:boolean) => boolean) & {basePath:string; patterns:string[]}}
|
|---|
| 147 | * The preficate function.
|
|---|
| 148 | * The first argument is an absolute path that is checked.
|
|---|
| 149 | * The second argument is the flag to not ignore dotfiles.
|
|---|
| 150 | * If the predicate function returned `true`, it means the path should be ignored.
|
|---|
| 151 | */
|
|---|
| 152 | static createIgnore(ignorePatterns) {
|
|---|
| 153 | debug("Create with: %o", ignorePatterns);
|
|---|
| 154 |
|
|---|
| 155 | const basePath = getCommonAncestorPath(ignorePatterns.map(p => p.basePath));
|
|---|
| 156 | const patterns = [].concat(
|
|---|
| 157 | ...ignorePatterns.map(p => p.getPatternsRelativeTo(basePath))
|
|---|
| 158 | );
|
|---|
| 159 | const ig = ignore({ allowRelativePaths: true }).add([...DotPatterns, ...patterns]);
|
|---|
| 160 | const dotIg = ignore({ allowRelativePaths: true }).add(patterns);
|
|---|
| 161 |
|
|---|
| 162 | debug(" processed: %o", { basePath, patterns });
|
|---|
| 163 |
|
|---|
| 164 | return Object.assign(
|
|---|
| 165 | (filePath, dot = false) => {
|
|---|
| 166 | assert(path.isAbsolute(filePath), "'filePath' should be an absolute path.");
|
|---|
| 167 | const relPathRaw = relative(basePath, filePath);
|
|---|
| 168 | const relPath = relPathRaw && (relPathRaw + dirSuffix(filePath));
|
|---|
| 169 | const adoptedIg = dot ? dotIg : ig;
|
|---|
| 170 | const result = relPath !== "" && adoptedIg.ignores(relPath);
|
|---|
| 171 |
|
|---|
| 172 | debug("Check", { filePath, dot, relativePath: relPath, result });
|
|---|
| 173 | return result;
|
|---|
| 174 | },
|
|---|
| 175 | { basePath, patterns }
|
|---|
| 176 | );
|
|---|
| 177 | }
|
|---|
| 178 |
|
|---|
| 179 | /**
|
|---|
| 180 | * Initialize a new `IgnorePattern` instance.
|
|---|
| 181 | * @param {string[]} patterns The glob patterns that ignore to lint.
|
|---|
| 182 | * @param {string} basePath The base path of `patterns`.
|
|---|
| 183 | */
|
|---|
| 184 | constructor(patterns, basePath) {
|
|---|
| 185 | assert(path.isAbsolute(basePath), "'basePath' should be an absolute path.");
|
|---|
| 186 |
|
|---|
| 187 | /**
|
|---|
| 188 | * The glob patterns that ignore to lint.
|
|---|
| 189 | * @type {string[]}
|
|---|
| 190 | */
|
|---|
| 191 | this.patterns = patterns;
|
|---|
| 192 |
|
|---|
| 193 | /**
|
|---|
| 194 | * The base path of `patterns`.
|
|---|
| 195 | * @type {string}
|
|---|
| 196 | */
|
|---|
| 197 | this.basePath = basePath;
|
|---|
| 198 |
|
|---|
| 199 | /**
|
|---|
| 200 | * If `true` then patterns which don't start with `/` will match the paths to the outside of `basePath`. Defaults to `false`.
|
|---|
| 201 | *
|
|---|
| 202 | * It's set `true` for `.eslintignore`, `package.json`, and `--ignore-path` for backward compatibility.
|
|---|
| 203 | * It's `false` as-is for `ignorePatterns` property in config files.
|
|---|
| 204 | * @type {boolean}
|
|---|
| 205 | */
|
|---|
| 206 | this.loose = false;
|
|---|
| 207 | }
|
|---|
| 208 |
|
|---|
| 209 | /**
|
|---|
| 210 | * Get `patterns` as modified for a given base path. It modifies the
|
|---|
| 211 | * absolute paths in the patterns as prepending the difference of two base
|
|---|
| 212 | * paths.
|
|---|
| 213 | * @param {string} newBasePath The base path.
|
|---|
| 214 | * @returns {string[]} Modifired patterns.
|
|---|
| 215 | */
|
|---|
| 216 | getPatternsRelativeTo(newBasePath) {
|
|---|
| 217 | assert(path.isAbsolute(newBasePath), "'newBasePath' should be an absolute path.");
|
|---|
| 218 | const { basePath, loose, patterns } = this;
|
|---|
| 219 |
|
|---|
| 220 | if (newBasePath === basePath) {
|
|---|
| 221 | return patterns;
|
|---|
| 222 | }
|
|---|
| 223 | const prefix = `/${relative(newBasePath, basePath)}`;
|
|---|
| 224 |
|
|---|
| 225 | return patterns.map(pattern => {
|
|---|
| 226 | const negative = pattern.startsWith("!");
|
|---|
| 227 | const head = negative ? "!" : "";
|
|---|
| 228 | const body = negative ? pattern.slice(1) : pattern;
|
|---|
| 229 |
|
|---|
| 230 | if (body.startsWith("/") || body.startsWith("../")) {
|
|---|
| 231 | return `${head}${prefix}${body}`;
|
|---|
| 232 | }
|
|---|
| 233 | return loose ? pattern : `${head}${prefix}/**/${body}`;
|
|---|
| 234 | });
|
|---|
| 235 | }
|
|---|
| 236 | }
|
|---|
| 237 |
|
|---|
| 238 | export { IgnorePattern };
|
|---|