2 * @fileoverview `OverrideTester` class.
4 * `OverrideTester` class handles `files` property and `excludedFiles` property
5 * of `overrides` config.
7 * It provides one method.
10 * Test if a file path matches the pair of `files` property and
11 * `excludedFiles` property. The `filePath` argument must be an absolute
14 * `ConfigArrayFactory` creates `OverrideTester` objects when it processes
15 * `overrides` properties.
17 * @author Toru Nagashima <https://github.com/mysticatea>
21 const assert = require("assert");
22 const path = require("path");
23 const util = require("util");
24 const { Minimatch } = require("minimatch");
25 const minimatchOpts = { dot: true, matchBase: true };
28 * @typedef {Object} Pattern
29 * @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
30 * @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
34 * Normalize a given pattern to an array.
35 * @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
36 * @returns {string[]|null} Normalized patterns.
39 function normalizePatterns(patterns) {
40 if (Array.isArray(patterns)) {
41 return patterns.filter(Boolean);
43 if (typeof patterns === "string" && patterns) {
50 * Create the matchers of given patterns.
51 * @param {string[]} patterns The patterns.
52 * @returns {InstanceType<Minimatch>[] | null} The matchers.
54 function toMatcher(patterns) {
55 if (patterns.length === 0) {
58 return patterns.map(pattern => {
59 if (/^\.[/\\]/u.test(pattern)) {
63 // `./*.js` should not match with `subdir/foo.js`
64 { ...minimatchOpts, matchBase: false }
67 return new Minimatch(pattern, minimatchOpts);
72 * Convert a given matcher to string.
73 * @param {Pattern} matchers The matchers.
74 * @returns {string} The string expression of the matcher.
76 function patternToJson({ includes, excludes }) {
78 includes: includes && includes.map(m => m.pattern),
79 excludes: excludes && excludes.map(m => m.pattern)
84 * The class to test given paths are matched by the patterns.
86 class OverrideTester {
89 * Create a tester with given criteria.
90 * If there are no criteria, returns `null`.
91 * @param {string|string[]} files The glob patterns for included files.
92 * @param {string|string[]} excludedFiles The glob patterns for excluded files.
93 * @param {string} basePath The path to the base directory to test paths.
94 * @returns {OverrideTester|null} The created instance or `null`.
96 static create(files, excludedFiles, basePath) {
97 const includePatterns = normalizePatterns(files);
98 const excludePatterns = normalizePatterns(excludedFiles);
99 let endsWithWildcard = false;
101 if (includePatterns.length === 0) {
105 // Rejects absolute paths or relative paths to parents.
106 for (const pattern of includePatterns) {
107 if (path.isAbsolute(pattern) || pattern.includes("..")) {
108 throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
110 if (pattern.endsWith("*")) {
111 endsWithWildcard = true;
114 for (const pattern of excludePatterns) {
115 if (path.isAbsolute(pattern) || pattern.includes("..")) {
116 throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
120 const includes = toMatcher(includePatterns);
121 const excludes = toMatcher(excludePatterns);
123 return new OverrideTester(
124 [{ includes, excludes }],
131 * Combine two testers by logical and.
132 * If either of the testers was `null`, returns the other tester.
133 * The `basePath` property of the two must be the same value.
134 * @param {OverrideTester|null} a A tester.
135 * @param {OverrideTester|null} b Another tester.
136 * @returns {OverrideTester|null} Combined tester.
140 return a && new OverrideTester(
147 return new OverrideTester(
154 assert.strictEqual(a.basePath, b.basePath);
155 return new OverrideTester(
156 a.patterns.concat(b.patterns),
158 a.endsWithWildcard || b.endsWithWildcard
163 * Initialize this instance.
164 * @param {Pattern[]} patterns The matchers.
165 * @param {string} basePath The base path.
166 * @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`.
168 constructor(patterns, basePath, endsWithWildcard = false) {
170 /** @type {Pattern[]} */
171 this.patterns = patterns;
173 /** @type {string} */
174 this.basePath = basePath;
176 /** @type {boolean} */
177 this.endsWithWildcard = endsWithWildcard;
181 * Test if a given path is matched or not.
182 * @param {string} filePath The absolute path to the target file.
183 * @returns {boolean} `true` if the path was matched.
186 if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
187 throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
189 const relativePath = path.relative(this.basePath, filePath);
191 return this.patterns.every(({ includes, excludes }) => (
192 (!includes || includes.some(m => m.match(relativePath))) &&
193 (!excludes || !excludes.some(m => m.match(relativePath)))
197 // eslint-disable-next-line jsdoc/require-description
199 * @returns {Object} a JSON compatible object.
202 if (this.patterns.length === 1) {
204 ...patternToJson(this.patterns[0]),
205 basePath: this.basePath
209 AND: this.patterns.map(patternToJson),
210 basePath: this.basePath
214 // eslint-disable-next-line jsdoc/require-description
216 * @returns {Object} an object to display by `console.log()`.
218 [util.inspect.custom]() {
219 return this.toJSON();
223 module.exports = { OverrideTester };