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