override-tester.js 6.7 KB

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