[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview `OverrideTester` class.
|
---|
| 3 | *
|
---|
| 4 | * `OverrideTester` class handles `files` property and `excludedFiles` property
|
---|
| 5 | * of `overrides` config.
|
---|
| 6 | *
|
---|
| 7 | * It provides one method.
|
---|
| 8 | *
|
---|
| 9 | * - `test(filePath)`
|
---|
| 10 | * Test if a file path matches the pair of `files` property and
|
---|
| 11 | * `excludedFiles` property. The `filePath` argument must be an absolute
|
---|
| 12 | * path.
|
---|
| 13 | *
|
---|
| 14 | * `ConfigArrayFactory` creates `OverrideTester` objects when it processes
|
---|
| 15 | * `overrides` properties.
|
---|
| 16 | *
|
---|
| 17 | * @author Toru Nagashima <https://github.com/mysticatea>
|
---|
| 18 | */
|
---|
| 19 |
|
---|
| 20 | import assert from "assert";
|
---|
| 21 | import path from "path";
|
---|
| 22 | import util from "util";
|
---|
| 23 | import minimatch from "minimatch";
|
---|
| 24 |
|
---|
| 25 | const { Minimatch } = minimatch;
|
---|
| 26 |
|
---|
| 27 | const minimatchOpts = { dot: true, matchBase: true };
|
---|
| 28 |
|
---|
| 29 | /**
|
---|
| 30 | * @typedef {Object} Pattern
|
---|
| 31 | * @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
|
---|
| 32 | * @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
|
---|
| 33 | */
|
---|
| 34 |
|
---|
| 35 | /**
|
---|
| 36 | * Normalize a given pattern to an array.
|
---|
| 37 | * @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
|
---|
| 38 | * @returns {string[]|null} Normalized patterns.
|
---|
| 39 | * @private
|
---|
| 40 | */
|
---|
| 41 | function normalizePatterns(patterns) {
|
---|
| 42 | if (Array.isArray(patterns)) {
|
---|
| 43 | return patterns.filter(Boolean);
|
---|
| 44 | }
|
---|
| 45 | if (typeof patterns === "string" && patterns) {
|
---|
| 46 | return [patterns];
|
---|
| 47 | }
|
---|
| 48 | return [];
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | /**
|
---|
| 52 | * Create the matchers of given patterns.
|
---|
| 53 | * @param {string[]} patterns The patterns.
|
---|
| 54 | * @returns {InstanceType<Minimatch>[] | null} The matchers.
|
---|
| 55 | */
|
---|
| 56 | function toMatcher(patterns) {
|
---|
| 57 | if (patterns.length === 0) {
|
---|
| 58 | return null;
|
---|
| 59 | }
|
---|
| 60 | return patterns.map(pattern => {
|
---|
| 61 | if (/^\.[/\\]/u.test(pattern)) {
|
---|
| 62 | return new Minimatch(
|
---|
| 63 | pattern.slice(2),
|
---|
| 64 |
|
---|
| 65 | // `./*.js` should not match with `subdir/foo.js`
|
---|
| 66 | { ...minimatchOpts, matchBase: false }
|
---|
| 67 | );
|
---|
| 68 | }
|
---|
| 69 | return new Minimatch(pattern, minimatchOpts);
|
---|
| 70 | });
|
---|
| 71 | }
|
---|
| 72 |
|
---|
| 73 | /**
|
---|
| 74 | * Convert a given matcher to string.
|
---|
| 75 | * @param {Pattern} matchers The matchers.
|
---|
| 76 | * @returns {string} The string expression of the matcher.
|
---|
| 77 | */
|
---|
| 78 | function patternToJson({ includes, excludes }) {
|
---|
| 79 | return {
|
---|
| 80 | includes: includes && includes.map(m => m.pattern),
|
---|
| 81 | excludes: excludes && excludes.map(m => m.pattern)
|
---|
| 82 | };
|
---|
| 83 | }
|
---|
| 84 |
|
---|
| 85 | /**
|
---|
| 86 | * The class to test given paths are matched by the patterns.
|
---|
| 87 | */
|
---|
| 88 | class OverrideTester {
|
---|
| 89 |
|
---|
| 90 | /**
|
---|
| 91 | * Create a tester with given criteria.
|
---|
| 92 | * If there are no criteria, returns `null`.
|
---|
| 93 | * @param {string|string[]} files The glob patterns for included files.
|
---|
| 94 | * @param {string|string[]} excludedFiles The glob patterns for excluded files.
|
---|
| 95 | * @param {string} basePath The path to the base directory to test paths.
|
---|
| 96 | * @returns {OverrideTester|null} The created instance or `null`.
|
---|
| 97 | */
|
---|
| 98 | static create(files, excludedFiles, basePath) {
|
---|
| 99 | const includePatterns = normalizePatterns(files);
|
---|
| 100 | const excludePatterns = normalizePatterns(excludedFiles);
|
---|
| 101 | let endsWithWildcard = false;
|
---|
| 102 |
|
---|
| 103 | if (includePatterns.length === 0) {
|
---|
| 104 | return null;
|
---|
| 105 | }
|
---|
| 106 |
|
---|
| 107 | // Rejects absolute paths or relative paths to parents.
|
---|
| 108 | for (const pattern of includePatterns) {
|
---|
| 109 | if (path.isAbsolute(pattern) || pattern.includes("..")) {
|
---|
| 110 | throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
|
---|
| 111 | }
|
---|
| 112 | if (pattern.endsWith("*")) {
|
---|
| 113 | endsWithWildcard = true;
|
---|
| 114 | }
|
---|
| 115 | }
|
---|
| 116 | for (const pattern of excludePatterns) {
|
---|
| 117 | if (path.isAbsolute(pattern) || pattern.includes("..")) {
|
---|
| 118 | throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
|
---|
| 119 | }
|
---|
| 120 | }
|
---|
| 121 |
|
---|
| 122 | const includes = toMatcher(includePatterns);
|
---|
| 123 | const excludes = toMatcher(excludePatterns);
|
---|
| 124 |
|
---|
| 125 | return new OverrideTester(
|
---|
| 126 | [{ includes, excludes }],
|
---|
| 127 | basePath,
|
---|
| 128 | endsWithWildcard
|
---|
| 129 | );
|
---|
| 130 | }
|
---|
| 131 |
|
---|
| 132 | /**
|
---|
| 133 | * Combine two testers by logical and.
|
---|
| 134 | * If either of the testers was `null`, returns the other tester.
|
---|
| 135 | * The `basePath` property of the two must be the same value.
|
---|
| 136 | * @param {OverrideTester|null} a A tester.
|
---|
| 137 | * @param {OverrideTester|null} b Another tester.
|
---|
| 138 | * @returns {OverrideTester|null} Combined tester.
|
---|
| 139 | */
|
---|
| 140 | static and(a, b) {
|
---|
| 141 | if (!b) {
|
---|
| 142 | return a && new OverrideTester(
|
---|
| 143 | a.patterns,
|
---|
| 144 | a.basePath,
|
---|
| 145 | a.endsWithWildcard
|
---|
| 146 | );
|
---|
| 147 | }
|
---|
| 148 | if (!a) {
|
---|
| 149 | return new OverrideTester(
|
---|
| 150 | b.patterns,
|
---|
| 151 | b.basePath,
|
---|
| 152 | b.endsWithWildcard
|
---|
| 153 | );
|
---|
| 154 | }
|
---|
| 155 |
|
---|
| 156 | assert.strictEqual(a.basePath, b.basePath);
|
---|
| 157 | return new OverrideTester(
|
---|
| 158 | a.patterns.concat(b.patterns),
|
---|
| 159 | a.basePath,
|
---|
| 160 | a.endsWithWildcard || b.endsWithWildcard
|
---|
| 161 | );
|
---|
| 162 | }
|
---|
| 163 |
|
---|
| 164 | /**
|
---|
| 165 | * Initialize this instance.
|
---|
| 166 | * @param {Pattern[]} patterns The matchers.
|
---|
| 167 | * @param {string} basePath The base path.
|
---|
| 168 | * @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`.
|
---|
| 169 | */
|
---|
| 170 | constructor(patterns, basePath, endsWithWildcard = false) {
|
---|
| 171 |
|
---|
| 172 | /** @type {Pattern[]} */
|
---|
| 173 | this.patterns = patterns;
|
---|
| 174 |
|
---|
| 175 | /** @type {string} */
|
---|
| 176 | this.basePath = basePath;
|
---|
| 177 |
|
---|
| 178 | /** @type {boolean} */
|
---|
| 179 | this.endsWithWildcard = endsWithWildcard;
|
---|
| 180 | }
|
---|
| 181 |
|
---|
| 182 | /**
|
---|
| 183 | * Test if a given path is matched or not.
|
---|
| 184 | * @param {string} filePath The absolute path to the target file.
|
---|
| 185 | * @returns {boolean} `true` if the path was matched.
|
---|
| 186 | */
|
---|
| 187 | test(filePath) {
|
---|
| 188 | if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
|
---|
| 189 | throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
|
---|
| 190 | }
|
---|
| 191 | const relativePath = path.relative(this.basePath, filePath);
|
---|
| 192 |
|
---|
| 193 | return this.patterns.every(({ includes, excludes }) => (
|
---|
| 194 | (!includes || includes.some(m => m.match(relativePath))) &&
|
---|
| 195 | (!excludes || !excludes.some(m => m.match(relativePath)))
|
---|
| 196 | ));
|
---|
| 197 | }
|
---|
| 198 |
|
---|
| 199 | // eslint-disable-next-line jsdoc/require-description
|
---|
| 200 | /**
|
---|
| 201 | * @returns {Object} a JSON compatible object.
|
---|
| 202 | */
|
---|
| 203 | toJSON() {
|
---|
| 204 | if (this.patterns.length === 1) {
|
---|
| 205 | return {
|
---|
| 206 | ...patternToJson(this.patterns[0]),
|
---|
| 207 | basePath: this.basePath
|
---|
| 208 | };
|
---|
| 209 | }
|
---|
| 210 | return {
|
---|
| 211 | AND: this.patterns.map(patternToJson),
|
---|
| 212 | basePath: this.basePath
|
---|
| 213 | };
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | // eslint-disable-next-line jsdoc/require-description
|
---|
| 217 | /**
|
---|
| 218 | * @returns {Object} an object to display by `console.log()`.
|
---|
| 219 | */
|
---|
| 220 | [util.inspect.custom]() {
|
---|
| 221 | return this.toJSON();
|
---|
| 222 | }
|
---|
| 223 | }
|
---|
| 224 |
|
---|
| 225 | export { OverrideTester };
|
---|