• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = hasWalkedCache
101            ? hasWalkedCache.copy()
102            : new HasWalkedCache();
103    }
104    processPatterns(target, patterns) {
105        this.patterns = patterns;
106        const processingSet = patterns.map(p => [target, p]);
107        // map of paths to the magic-starting subwalks they need to walk
108        // first item in patterns is the filter
109        for (let [t, pattern] of processingSet) {
110            this.hasWalkedCache.storeWalked(t, pattern);
111            const root = pattern.root();
112            const absolute = pattern.isAbsolute() && this.opts.absolute !== false;
113            // start absolute patterns at root
114            if (root) {
115                t = t.resolve(root === '/' && this.opts.root !== undefined
116                    ? this.opts.root
117                    : root);
118                const rest = pattern.rest();
119                if (!rest) {
120                    this.matches.add(t, true, false);
121                    continue;
122                }
123                else {
124                    pattern = rest;
125                }
126            }
127            if (t.isENOENT())
128                continue;
129            let p;
130            let rest;
131            let changed = false;
132            while (typeof (p = pattern.pattern()) === 'string' &&
133                (rest = pattern.rest())) {
134                const c = t.resolve(p);
135                // we can be reasonably sure that .. is a readable dir
136                if (c.isUnknown() && p !== '..')
137                    break;
138                t = c;
139                pattern = rest;
140                changed = true;
141            }
142            p = pattern.pattern();
143            rest = pattern.rest();
144            if (changed) {
145                if (this.hasWalkedCache.hasWalked(t, pattern))
146                    continue;
147                this.hasWalkedCache.storeWalked(t, pattern);
148            }
149            // now we have either a final string for a known entry,
150            // more strings for an unknown entry,
151            // or a pattern starting with magic, mounted on t.
152            if (typeof p === 'string') {
153                // must be final entry
154                if (!rest) {
155                    const ifDir = p === '..' || p === '' || p === '.';
156                    this.matches.add(t.resolve(p), absolute, ifDir);
157                }
158                else {
159                    this.subwalks.add(t, pattern);
160                }
161                continue;
162            }
163            else if (p === GLOBSTAR) {
164                // if no rest, match and subwalk pattern
165                // if rest, process rest and subwalk pattern
166                // if it's a symlink, but we didn't get here by way of a
167                // globstar match (meaning it's the first time THIS globstar
168                // has traversed a symlink), then we follow it. Otherwise, stop.
169                if (!t.isSymbolicLink() ||
170                    this.follow ||
171                    pattern.checkFollowGlobstar()) {
172                    this.subwalks.add(t, pattern);
173                }
174                const rp = rest?.pattern();
175                const rrest = rest?.rest();
176                if (!rest || ((rp === '' || rp === '.') && !rrest)) {
177                    // only HAS to be a dir if it ends in **/ or **/.
178                    // but ending in ** will match files as well.
179                    this.matches.add(t, absolute, rp === '' || rp === '.');
180                }
181                else {
182                    if (rp === '..') {
183                        // this would mean you're matching **/.. at the fs root,
184                        // and no thanks, I'm not gonna test that specific case.
185                        /* c8 ignore start */
186                        const tp = t.parent || t;
187                        /* c8 ignore stop */
188                        if (!rrest)
189                            this.matches.add(tp, absolute, true);
190                        else if (!this.hasWalkedCache.hasWalked(tp, rrest)) {
191                            this.subwalks.add(tp, rrest);
192                        }
193                    }
194                }
195            }
196            else if (p instanceof RegExp) {
197                this.subwalks.add(t, pattern);
198            }
199        }
200        return this;
201    }
202    subwalkTargets() {
203        return this.subwalks.keys();
204    }
205    child() {
206        return new Processor(this.opts, this.hasWalkedCache);
207    }
208    // return a new Processor containing the subwalks for each
209    // child entry, and a set of matches, and
210    // a hasWalkedCache that's a copy of this one
211    // then we're going to call
212    filterEntries(parent, entries) {
213        const patterns = this.subwalks.get(parent);
214        // put matches and entry walks into the results processor
215        const results = this.child();
216        for (const e of entries) {
217            for (const pattern of patterns) {
218                const absolute = pattern.isAbsolute();
219                const p = pattern.pattern();
220                const rest = pattern.rest();
221                if (p === GLOBSTAR) {
222                    results.testGlobstar(e, pattern, rest, absolute);
223                }
224                else if (p instanceof RegExp) {
225                    results.testRegExp(e, p, rest, absolute);
226                }
227                else {
228                    results.testString(e, p, rest, absolute);
229                }
230            }
231        }
232        return results;
233    }
234    testGlobstar(e, pattern, rest, absolute) {
235        if (this.dot || !e.name.startsWith('.')) {
236            if (!pattern.hasMore()) {
237                this.matches.add(e, absolute, false);
238            }
239            if (e.canReaddir()) {
240                // if we're in follow mode or it's not a symlink, just keep
241                // testing the same pattern. If there's more after the globstar,
242                // then this symlink consumes the globstar. If not, then we can
243                // follow at most ONE symlink along the way, so we mark it, which
244                // also checks to ensure that it wasn't already marked.
245                if (this.follow || !e.isSymbolicLink()) {
246                    this.subwalks.add(e, pattern);
247                }
248                else if (e.isSymbolicLink()) {
249                    if (rest && pattern.checkFollowGlobstar()) {
250                        this.subwalks.add(e, rest);
251                    }
252                    else if (pattern.markFollowGlobstar()) {
253                        this.subwalks.add(e, pattern);
254                    }
255                }
256            }
257        }
258        // if the NEXT thing matches this entry, then also add
259        // the rest.
260        if (rest) {
261            const rp = rest.pattern();
262            if (typeof rp === 'string' &&
263                // dots and empty were handled already
264                rp !== '..' &&
265                rp !== '' &&
266                rp !== '.') {
267                this.testString(e, rp, rest.rest(), absolute);
268            }
269            else if (rp === '..') {
270                /* c8 ignore start */
271                const ep = e.parent || e;
272                /* c8 ignore stop */
273                this.subwalks.add(ep, rest);
274            }
275            else if (rp instanceof RegExp) {
276                this.testRegExp(e, rp, rest.rest(), absolute);
277            }
278        }
279    }
280    testRegExp(e, p, rest, absolute) {
281        if (!p.test(e.name))
282            return;
283        if (!rest) {
284            this.matches.add(e, absolute, false);
285        }
286        else {
287            this.subwalks.add(e, rest);
288        }
289    }
290    testString(e, p, rest, absolute) {
291        // should never happen?
292        if (!e.isNamed(p))
293            return;
294        if (!rest) {
295            this.matches.add(e, absolute, false);
296        }
297        else {
298            this.subwalks.add(e, rest);
299        }
300    }
301}
302//# sourceMappingURL=processor.js.map