codekingpro/portable-devtools
115k
1// synchronous utility for filtering entries and calculating subwalks
2import { GLOBSTAR } from 'minimatch';
3/**
4 * A cache of which patterns have been processed for a given Path
5 */
6export 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 */
31export 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 */
51export 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 */
88export 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