|
|
/** * @fileoverview `OverrideTester` class. * * `OverrideTester` class handles `files` property and `excludedFiles` property * of `overrides` config. * * It provides one method. * * - `test(filePath)` * Test if a file path matches the pair of `files` property and * `excludedFiles` property. The `filePath` argument must be an absolute * path. * * `ConfigArrayFactory` creates `OverrideTester` objects when it processes * `overrides` properties. * * @author Toru Nagashima <https://github.com/mysticatea>
*/ "use strict";
const assert = require("assert"); const path = require("path"); const util = require("util"); const { Minimatch } = require("minimatch"); const minimatchOpts = { dot: true, matchBase: true };
/** * @typedef {Object} Pattern * @property {InstanceType<Minimatch>[] | null} includes The positive matchers. * @property {InstanceType<Minimatch>[] | null} excludes The negative matchers. */
/** * Normalize a given pattern to an array. * @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns. * @returns {string[]|null} Normalized patterns. * @private */ function normalizePatterns(patterns) { if (Array.isArray(patterns)) { return patterns.filter(Boolean); } if (typeof patterns === "string" && patterns) { return [patterns]; } return []; }
/** * Create the matchers of given patterns. * @param {string[]} patterns The patterns. * @returns {InstanceType<Minimatch>[] | null} The matchers. */ function toMatcher(patterns) { if (patterns.length === 0) { return null; } return patterns.map(pattern => { if (/^\.[/\\]/u.test(pattern)) { return new Minimatch( pattern.slice(2),
// `./*.js` should not match with `subdir/foo.js`
{ ...minimatchOpts, matchBase: false } ); } return new Minimatch(pattern, minimatchOpts); }); }
/** * Convert a given matcher to string. * @param {Pattern} matchers The matchers. * @returns {string} The string expression of the matcher. */ function patternToJson({ includes, excludes }) { return { includes: includes && includes.map(m => m.pattern), excludes: excludes && excludes.map(m => m.pattern) }; }
/** * The class to test given paths are matched by the patterns. */ class OverrideTester {
/** * Create a tester with given criteria. * If there are no criteria, returns `null`. * @param {string|string[]} files The glob patterns for included files. * @param {string|string[]} excludedFiles The glob patterns for excluded files. * @param {string} basePath The path to the base directory to test paths. * @returns {OverrideTester|null} The created instance or `null`. */ static create(files, excludedFiles, basePath) { const includePatterns = normalizePatterns(files); const excludePatterns = normalizePatterns(excludedFiles); let endsWithWildcard = false;
if (includePatterns.length === 0) { return null; }
// Rejects absolute paths or relative paths to parents.
for (const pattern of includePatterns) { if (path.isAbsolute(pattern) || pattern.includes("..")) { throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); } if (pattern.endsWith("*")) { endsWithWildcard = true; } } for (const pattern of excludePatterns) { if (path.isAbsolute(pattern) || pattern.includes("..")) { throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); } }
const includes = toMatcher(includePatterns); const excludes = toMatcher(excludePatterns);
return new OverrideTester( [{ includes, excludes }], basePath, endsWithWildcard ); }
/** * Combine two testers by logical and. * If either of the testers was `null`, returns the other tester. * The `basePath` property of the two must be the same value. * @param {OverrideTester|null} a A tester. * @param {OverrideTester|null} b Another tester. * @returns {OverrideTester|null} Combined tester. */ static and(a, b) { if (!b) { return a && new OverrideTester( a.patterns, a.basePath, a.endsWithWildcard ); } if (!a) { return new OverrideTester( b.patterns, b.basePath, b.endsWithWildcard ); }
assert.strictEqual(a.basePath, b.basePath); return new OverrideTester( a.patterns.concat(b.patterns), a.basePath, a.endsWithWildcard || b.endsWithWildcard ); }
/** * Initialize this instance. * @param {Pattern[]} patterns The matchers. * @param {string} basePath The base path. * @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`. */ constructor(patterns, basePath, endsWithWildcard = false) {
/** @type {Pattern[]} */ this.patterns = patterns;
/** @type {string} */ this.basePath = basePath;
/** @type {boolean} */ this.endsWithWildcard = endsWithWildcard; }
/** * Test if a given path is matched or not. * @param {string} filePath The absolute path to the target file. * @returns {boolean} `true` if the path was matched. */ test(filePath) { if (typeof filePath !== "string" || !path.isAbsolute(filePath)) { throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`); } const relativePath = path.relative(this.basePath, filePath);
return this.patterns.every(({ includes, excludes }) => ( (!includes || includes.some(m => m.match(relativePath))) && (!excludes || !excludes.some(m => m.match(relativePath))) )); }
// eslint-disable-next-line jsdoc/require-description
/** * @returns {Object} a JSON compatible object. */ toJSON() { if (this.patterns.length === 1) { return { ...patternToJson(this.patterns[0]), basePath: this.basePath }; } return { AND: this.patterns.map(patternToJson), basePath: this.basePath }; }
// eslint-disable-next-line jsdoc/require-description
/** * @returns {Object} an object to display by `console.log()`. */ [util.inspect.custom]() { return this.toJSON(); } }
module.exports = { OverrideTester };
|