processor.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. // synchronous utility for filtering entries and calculating subwalks
  2. import { GLOBSTAR } from 'minimatch';
  3. /**
  4. * A cache of which patterns have been processed for a given Path
  5. */
  6. export class HasWalkedCache {
  7. store;
  8. constructor(store = new Map()) {
  9. this.store = store;
  10. }
  11. copy() {
  12. return new HasWalkedCache(new Map(this.store));
  13. }
  14. hasWalked(target, pattern) {
  15. return this.store.get(target.fullpath())?.has(pattern.globString());
  16. }
  17. storeWalked(target, pattern) {
  18. const fullpath = target.fullpath();
  19. const cached = this.store.get(fullpath);
  20. if (cached)
  21. cached.add(pattern.globString());
  22. else
  23. this.store.set(fullpath, new Set([pattern.globString()]));
  24. }
  25. }
  26. /**
  27. * A record of which paths have been matched in a given walk step,
  28. * and whether they only are considered a match if they are a directory,
  29. * and whether their absolute or relative path should be returned.
  30. */
  31. export class MatchRecord {
  32. store = new Map();
  33. add(target, absolute, ifDir) {
  34. const n = (absolute ? 2 : 0) | (ifDir ? 1 : 0);
  35. const current = this.store.get(target);
  36. this.store.set(target, current === undefined ? n : n & current);
  37. }
  38. // match, absolute, ifdir
  39. entries() {
  40. return [...this.store.entries()].map(([path, n]) => [
  41. path,
  42. !!(n & 2),
  43. !!(n & 1),
  44. ]);
  45. }
  46. }
  47. /**
  48. * A collection of patterns that must be processed in a subsequent step
  49. * for a given path.
  50. */
  51. export class SubWalks {
  52. store = new Map();
  53. add(target, pattern) {
  54. if (!target.canReaddir()) {
  55. return;
  56. }
  57. const subs = this.store.get(target);
  58. if (subs) {
  59. if (!subs.find(p => p.globString() === pattern.globString())) {
  60. subs.push(pattern);
  61. }
  62. }
  63. else
  64. this.store.set(target, [pattern]);
  65. }
  66. get(target) {
  67. const subs = this.store.get(target);
  68. /* c8 ignore start */
  69. if (!subs) {
  70. throw new Error('attempting to walk unknown path');
  71. }
  72. /* c8 ignore stop */
  73. return subs;
  74. }
  75. entries() {
  76. return this.keys().map(k => [k, this.store.get(k)]);
  77. }
  78. keys() {
  79. return [...this.store.keys()].filter(t => t.canReaddir());
  80. }
  81. }
  82. /**
  83. * The class that processes patterns for a given path.
  84. *
  85. * Handles child entry filtering, and determining whether a path's
  86. * directory contents must be read.
  87. */
  88. export class Processor {
  89. hasWalkedCache;
  90. matches = new MatchRecord();
  91. subwalks = new SubWalks();
  92. patterns;
  93. follow;
  94. dot;
  95. opts;
  96. constructor(opts, hasWalkedCache) {
  97. this.opts = opts;
  98. this.follow = !!opts.follow;
  99. this.dot = !!opts.dot;
  100. this.hasWalkedCache =
  101. hasWalkedCache ? hasWalkedCache.copy() : new HasWalkedCache();
  102. }
  103. processPatterns(target, patterns) {
  104. this.patterns = patterns;
  105. const processingSet = patterns.map(p => [target, p]);
  106. // map of paths to the magic-starting subwalks they need to walk
  107. // first item in patterns is the filter
  108. for (let [t, pattern] of processingSet) {
  109. this.hasWalkedCache.storeWalked(t, pattern);
  110. const root = pattern.root();
  111. const absolute = pattern.isAbsolute() && this.opts.absolute !== false;
  112. // start absolute patterns at root
  113. if (root) {
  114. t = t.resolve(root === '/' && this.opts.root !== undefined ?
  115. this.opts.root
  116. : root);
  117. const rest = pattern.rest();
  118. if (!rest) {
  119. this.matches.add(t, true, false);
  120. continue;
  121. }
  122. else {
  123. pattern = rest;
  124. }
  125. }
  126. if (t.isENOENT())
  127. continue;
  128. let p;
  129. let rest;
  130. let changed = false;
  131. while (typeof (p = pattern.pattern()) === 'string' &&
  132. (rest = pattern.rest())) {
  133. const c = t.resolve(p);
  134. t = c;
  135. pattern = rest;
  136. changed = true;
  137. }
  138. p = pattern.pattern();
  139. rest = pattern.rest();
  140. if (changed) {
  141. if (this.hasWalkedCache.hasWalked(t, pattern))
  142. continue;
  143. this.hasWalkedCache.storeWalked(t, pattern);
  144. }
  145. // now we have either a final string for a known entry,
  146. // more strings for an unknown entry,
  147. // or a pattern starting with magic, mounted on t.
  148. if (typeof p === 'string') {
  149. // must not be final entry, otherwise we would have
  150. // concatenated it earlier.
  151. const ifDir = p === '..' || p === '' || p === '.';
  152. this.matches.add(t.resolve(p), absolute, ifDir);
  153. continue;
  154. }
  155. else if (p === GLOBSTAR) {
  156. // if no rest, match and subwalk pattern
  157. // if rest, process rest and subwalk pattern
  158. // if it's a symlink, but we didn't get here by way of a
  159. // globstar match (meaning it's the first time THIS globstar
  160. // has traversed a symlink), then we follow it. Otherwise, stop.
  161. if (!t.isSymbolicLink() ||
  162. this.follow ||
  163. pattern.checkFollowGlobstar()) {
  164. this.subwalks.add(t, pattern);
  165. }
  166. const rp = rest?.pattern();
  167. const rrest = rest?.rest();
  168. if (!rest || ((rp === '' || rp === '.') && !rrest)) {
  169. // only HAS to be a dir if it ends in **/ or **/.
  170. // but ending in ** will match files as well.
  171. this.matches.add(t, absolute, rp === '' || rp === '.');
  172. }
  173. else {
  174. if (rp === '..') {
  175. // this would mean you're matching **/.. at the fs root,
  176. // and no thanks, I'm not gonna test that specific case.
  177. /* c8 ignore start */
  178. const tp = t.parent || t;
  179. /* c8 ignore stop */
  180. if (!rrest)
  181. this.matches.add(tp, absolute, true);
  182. else if (!this.hasWalkedCache.hasWalked(tp, rrest)) {
  183. this.subwalks.add(tp, rrest);
  184. }
  185. }
  186. }
  187. }
  188. else if (p instanceof RegExp) {
  189. this.subwalks.add(t, pattern);
  190. }
  191. }
  192. return this;
  193. }
  194. subwalkTargets() {
  195. return this.subwalks.keys();
  196. }
  197. child() {
  198. return new Processor(this.opts, this.hasWalkedCache);
  199. }
  200. // return a new Processor containing the subwalks for each
  201. // child entry, and a set of matches, and
  202. // a hasWalkedCache that's a copy of this one
  203. // then we're going to call
  204. filterEntries(parent, entries) {
  205. const patterns = this.subwalks.get(parent);
  206. // put matches and entry walks into the results processor
  207. const results = this.child();
  208. for (const e of entries) {
  209. for (const pattern of patterns) {
  210. const absolute = pattern.isAbsolute();
  211. const p = pattern.pattern();
  212. const rest = pattern.rest();
  213. if (p === GLOBSTAR) {
  214. results.testGlobstar(e, pattern, rest, absolute);
  215. }
  216. else if (p instanceof RegExp) {
  217. results.testRegExp(e, p, rest, absolute);
  218. }
  219. else {
  220. results.testString(e, p, rest, absolute);
  221. }
  222. }
  223. }
  224. return results;
  225. }
  226. testGlobstar(e, pattern, rest, absolute) {
  227. if (this.dot || !e.name.startsWith('.')) {
  228. if (!pattern.hasMore()) {
  229. this.matches.add(e, absolute, false);
  230. }
  231. if (e.canReaddir()) {
  232. // if we're in follow mode or it's not a symlink, just keep
  233. // testing the same pattern. If there's more after the globstar,
  234. // then this symlink consumes the globstar. If not, then we can
  235. // follow at most ONE symlink along the way, so we mark it, which
  236. // also checks to ensure that it wasn't already marked.
  237. if (this.follow || !e.isSymbolicLink()) {
  238. this.subwalks.add(e, pattern);
  239. }
  240. else if (e.isSymbolicLink()) {
  241. if (rest && pattern.checkFollowGlobstar()) {
  242. this.subwalks.add(e, rest);
  243. }
  244. else if (pattern.markFollowGlobstar()) {
  245. this.subwalks.add(e, pattern);
  246. }
  247. }
  248. }
  249. }
  250. // if the NEXT thing matches this entry, then also add
  251. // the rest.
  252. if (rest) {
  253. const rp = rest.pattern();
  254. if (typeof rp === 'string' &&
  255. // dots and empty were handled already
  256. rp !== '..' &&
  257. rp !== '' &&
  258. rp !== '.') {
  259. this.testString(e, rp, rest.rest(), absolute);
  260. }
  261. else if (rp === '..') {
  262. /* c8 ignore start */
  263. const ep = e.parent || e;
  264. /* c8 ignore stop */
  265. this.subwalks.add(ep, rest);
  266. }
  267. else if (rp instanceof RegExp) {
  268. this.testRegExp(e, rp, rest.rest(), absolute);
  269. }
  270. }
  271. }
  272. testRegExp(e, p, rest, absolute) {
  273. if (!p.test(e.name))
  274. return;
  275. if (!rest) {
  276. this.matches.add(e, absolute, false);
  277. }
  278. else {
  279. this.subwalks.add(e, rest);
  280. }
  281. }
  282. testString(e, p, rest, absolute) {
  283. // should never happen?
  284. if (!e.isNamed(p))
  285. return;
  286. if (!rest) {
  287. this.matches.add(e, absolute, false);
  288. }
  289. else {
  290. this.subwalks.add(e, rest);
  291. }
  292. }
  293. }
  294. //# sourceMappingURL=processor.js.map