override-tester.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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. const allPatterns = includePatterns.concat(excludePatterns);
  92. if (allPatterns.length === 0) {
  93. return null;
  94. }
  95. // Rejects absolute paths or relative paths to parents.
  96. for (const pattern of allPatterns) {
  97. if (path.isAbsolute(pattern) || pattern.includes("..")) {
  98. throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
  99. }
  100. }
  101. const includes = toMatcher(includePatterns);
  102. const excludes = toMatcher(excludePatterns);
  103. return new OverrideTester([{ includes, excludes }], basePath);
  104. }
  105. /**
  106. * Combine two testers by logical and.
  107. * If either of the testers was `null`, returns the other tester.
  108. * The `basePath` property of the two must be the same value.
  109. * @param {OverrideTester|null} a A tester.
  110. * @param {OverrideTester|null} b Another tester.
  111. * @returns {OverrideTester|null} Combined tester.
  112. */
  113. static and(a, b) {
  114. if (!b) {
  115. return a && new OverrideTester(a.patterns, a.basePath);
  116. }
  117. if (!a) {
  118. return new OverrideTester(b.patterns, b.basePath);
  119. }
  120. assert.strictEqual(a.basePath, b.basePath);
  121. return new OverrideTester(a.patterns.concat(b.patterns), a.basePath);
  122. }
  123. /**
  124. * Initialize this instance.
  125. * @param {Pattern[]} patterns The matchers.
  126. * @param {string} basePath The base path.
  127. */
  128. constructor(patterns, basePath) {
  129. /** @type {Pattern[]} */
  130. this.patterns = patterns;
  131. /** @type {string} */
  132. this.basePath = basePath;
  133. }
  134. /**
  135. * Test if a given path is matched or not.
  136. * @param {string} filePath The absolute path to the target file.
  137. * @returns {boolean} `true` if the path was matched.
  138. */
  139. test(filePath) {
  140. if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
  141. throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
  142. }
  143. const relativePath = path.relative(this.basePath, filePath);
  144. return this.patterns.every(({ includes, excludes }) => (
  145. (!includes || includes.some(m => m.match(relativePath))) &&
  146. (!excludes || !excludes.some(m => m.match(relativePath)))
  147. ));
  148. }
  149. // eslint-disable-next-line jsdoc/require-description
  150. /**
  151. * @returns {Object} a JSON compatible object.
  152. */
  153. toJSON() {
  154. if (this.patterns.length === 1) {
  155. return {
  156. ...patternToJson(this.patterns[0]),
  157. basePath: this.basePath
  158. };
  159. }
  160. return {
  161. AND: this.patterns.map(patternToJson),
  162. basePath: this.basePath
  163. };
  164. }
  165. // eslint-disable-next-line jsdoc/require-description
  166. /**
  167. * @returns {Object} an object to display by `console.log()`.
  168. */
  169. [util.inspect.custom]() {
  170. return this.toJSON();
  171. }
  172. }
  173. module.exports = { OverrideTester };