override-tester.js 7.1 KB

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