Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/util/vs/base/common/glob.ts
13405 views
1
//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode'
2
3
/*---------------------------------------------------------------------------------------------
4
* Copyright (c) Microsoft Corporation. All rights reserved.
5
* Licensed under the MIT License. See License.txt in the project root for license information.
6
*--------------------------------------------------------------------------------------------*/
7
8
import { equals } from './arrays';
9
import { isThenable } from './async';
10
import { CharCode } from './charCode';
11
import { isEqualOrParent } from './extpath';
12
import { LRUCache } from './map';
13
import { basename, extname, posix, sep } from './path';
14
import { isLinux } from './platform';
15
import { endsWithIgnoreCase, equalsIgnoreCase, escapeRegExpCharacters, ltrim } from './strings';
16
17
export interface IRelativePattern {
18
19
/**
20
* A base file path to which this pattern will be matched against relatively.
21
*/
22
readonly base: string;
23
24
/**
25
* A file glob pattern like `*.{ts,js}` that will be matched on file paths
26
* relative to the base path.
27
*
28
* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
29
* the file glob pattern will match on `index.js`.
30
*/
31
readonly pattern: string;
32
}
33
34
export interface IExpression {
35
[pattern: string]: boolean | SiblingClause;
36
}
37
38
export function getEmptyExpression(): IExpression {
39
return Object.create(null);
40
}
41
42
interface SiblingClause {
43
when: string;
44
}
45
46
export const GLOBSTAR = '**';
47
export const GLOB_SPLIT = '/';
48
49
const PATH_REGEX = '[/\\\\]'; // any slash or backslash
50
const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash
51
const ALL_FORWARD_SLASHES = /\//g;
52
53
function starsToRegExp(starCount: number, isLastPattern?: boolean): string {
54
switch (starCount) {
55
case 0:
56
return '';
57
case 1:
58
return `${NO_PATH_REGEX}*?`; // 1 star matches any number of characters except path separator (/ and \) - non greedy (?)
59
default:
60
// Matches: (Path Sep OR Path Val followed by Path Sep) 0-many times except when it's the last pattern
61
// in which case also matches (Path Sep followed by Path Val)
62
// Group is non capturing because we don't need to capture at all (?:...)
63
// Overall we use non-greedy matching because it could be that we match too much
64
return `(?:${PATH_REGEX}|${NO_PATH_REGEX}+${PATH_REGEX}${isLastPattern ? `|${PATH_REGEX}${NO_PATH_REGEX}+` : ''})*?`;
65
}
66
}
67
68
export function splitGlobAware(pattern: string, splitChar: string): string[] {
69
if (!pattern) {
70
return [];
71
}
72
73
const segments: string[] = [];
74
75
let inBraces = false;
76
let inBrackets = false;
77
78
let curVal = '';
79
for (const char of pattern) {
80
switch (char) {
81
case splitChar:
82
if (!inBraces && !inBrackets) {
83
segments.push(curVal);
84
curVal = '';
85
86
continue;
87
}
88
break;
89
case '{':
90
inBraces = true;
91
break;
92
case '}':
93
inBraces = false;
94
break;
95
case '[':
96
inBrackets = true;
97
break;
98
case ']':
99
inBrackets = false;
100
break;
101
}
102
103
curVal += char;
104
}
105
106
// Tail
107
if (curVal) {
108
segments.push(curVal);
109
}
110
111
return segments;
112
}
113
114
function parseRegExp(pattern: string): string {
115
if (!pattern) {
116
return '';
117
}
118
119
let regEx = '';
120
121
// Split up into segments for each slash found
122
const segments = splitGlobAware(pattern, GLOB_SPLIT);
123
124
// Special case where we only have globstars
125
if (segments.every(segment => segment === GLOBSTAR)) {
126
regEx = '.*';
127
}
128
129
// Build regex over segments
130
else {
131
let previousSegmentWasGlobStar = false;
132
segments.forEach((segment, index) => {
133
134
// Treat globstar specially
135
if (segment === GLOBSTAR) {
136
137
// if we have more than one globstar after another, just ignore it
138
if (previousSegmentWasGlobStar) {
139
return;
140
}
141
142
regEx += starsToRegExp(2, index === segments.length - 1);
143
}
144
145
// Anything else, not globstar
146
else {
147
148
// States
149
let inBraces = false;
150
let braceVal = '';
151
152
let inBrackets = false;
153
let bracketVal = '';
154
155
for (const char of segment) {
156
157
// Support brace expansion
158
if (char !== '}' && inBraces) {
159
braceVal += char;
160
continue;
161
}
162
163
// Support brackets
164
if (inBrackets && (char !== ']' || !bracketVal) /* ] is literally only allowed as first character in brackets to match it */) {
165
let res: string;
166
167
// range operator
168
if (char === '-') {
169
res = char;
170
}
171
172
// negation operator (only valid on first index in bracket)
173
else if ((char === '^' || char === '!') && !bracketVal) {
174
res = '^';
175
}
176
177
// glob split matching is not allowed within character ranges
178
// see http://man7.org/linux/man-pages/man7/glob.7.html
179
else if (char === GLOB_SPLIT) {
180
res = '';
181
}
182
183
// anything else gets escaped
184
else {
185
res = escapeRegExpCharacters(char);
186
}
187
188
bracketVal += res;
189
continue;
190
}
191
192
switch (char) {
193
case '{':
194
inBraces = true;
195
continue;
196
197
case '[':
198
inBrackets = true;
199
continue;
200
201
case '}': {
202
const choices = splitGlobAware(braceVal, ',');
203
204
// Converts {foo,bar} => [foo|bar]
205
const braceRegExp = `(?:${choices.map(choice => parseRegExp(choice)).join('|')})`;
206
207
regEx += braceRegExp;
208
209
inBraces = false;
210
braceVal = '';
211
212
break;
213
}
214
215
case ']': {
216
regEx += ('[' + bracketVal + ']');
217
218
inBrackets = false;
219
bracketVal = '';
220
221
break;
222
}
223
224
case '?':
225
regEx += NO_PATH_REGEX; // 1 ? matches any single character except path separator (/ and \)
226
continue;
227
228
case '*':
229
regEx += starsToRegExp(1);
230
continue;
231
232
default:
233
regEx += escapeRegExpCharacters(char);
234
}
235
}
236
237
// Tail: Add the slash we had split on if there is more to
238
// come and the remaining pattern is not a globstar
239
// For example if pattern: some/**/*.js we want the "/" after
240
// some to be included in the RegEx to prevent a folder called
241
// "something" to match as well.
242
if (
243
index < segments.length - 1 && // more segments to come after this
244
(
245
segments[index + 1] !== GLOBSTAR || // next segment is not **, or...
246
index + 2 < segments.length // ...next segment is ** but there is more segments after that
247
)
248
) {
249
regEx += PATH_REGEX;
250
}
251
}
252
253
// update globstar state
254
previousSegmentWasGlobStar = (segment === GLOBSTAR);
255
});
256
}
257
258
return regEx;
259
}
260
261
// regexes to check for trivial glob patterns that just check for String#endsWith
262
const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
263
const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something
264
const T3 = /^{\*\*\/\*?[\w\.-]+\/?(,\*\*\/\*?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}
265
const T3_2 = /^{\*\*\/\*?[\w\.-]+(\/(\*\*)?)?(,\*\*\/\*?[\w\.-]+(\/(\*\*)?)?)*}$/; // Like T3, with optional trailing /**
266
const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else
267
const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else
268
269
export type ParsedPattern = (path: string, basename?: string) => boolean;
270
271
// The `ParsedExpression` returns a `Promise`
272
// iff `hasSibling` returns a `Promise`.
273
export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => string | null | Promise<string | null> /* the matching pattern */;
274
275
export interface IGlobOptions {
276
277
/**
278
* Simplify patterns for use as exclusion filters during
279
* tree traversal to skip entire subtrees. Cannot be used
280
* outside of a tree traversal.
281
*/
282
trimForExclusions?: boolean;
283
284
/**
285
* Whether glob pattern matching should be case insensitive.
286
*/
287
ignoreCase?: boolean;
288
}
289
290
interface IGlobOptionsInternal extends IGlobOptions {
291
equals: (a: string, b: string) => boolean;
292
endsWith: (str: string, candidate: string) => boolean;
293
isEqualOrParent: (base: string, candidate: string) => boolean;
294
}
295
296
interface ParsedStringPattern {
297
(path: string, basename?: string): string | null | Promise<string | null> /* the matching pattern */;
298
basenames?: string[];
299
patterns?: string[];
300
allBasenames?: string[];
301
allPaths?: string[];
302
}
303
304
interface ParsedExpressionPattern {
305
(path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | null | Promise<string | null> /* the matching pattern */;
306
requiresSiblings?: boolean;
307
allBasenames?: string[];
308
allPaths?: string[];
309
}
310
311
const CACHE = new LRUCache<string, ParsedStringPattern>(10000); // bounded to 10000 elements
312
313
const FALSE = function () {
314
return false;
315
};
316
317
const NULL = function (): string | null {
318
return null;
319
};
320
321
/**
322
* Check if a provided parsed pattern or expression
323
* is empty - hence it won't ever match anything.
324
*
325
* See {@link FALSE} and {@link NULL}.
326
*/
327
export function isEmptyPattern(pattern: ParsedPattern | ParsedExpression): pattern is (typeof FALSE | typeof NULL) {
328
if (pattern === FALSE) {
329
return true;
330
}
331
332
if (pattern === NULL) {
333
return true;
334
}
335
336
return false;
337
}
338
339
function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern {
340
if (!arg1) {
341
return NULL;
342
}
343
344
// Handle relative patterns
345
let pattern: string;
346
if (typeof arg1 !== 'string') {
347
pattern = arg1.pattern;
348
} else {
349
pattern = arg1;
350
}
351
352
// Whitespace trimming
353
pattern = pattern.trim();
354
355
const ignoreCase = options.ignoreCase ?? false;
356
const internalOptions = {
357
...options,
358
equals: ignoreCase ? equalsIgnoreCase : (a: string, b: string) => a === b,
359
endsWith: ignoreCase ? endsWithIgnoreCase : (str: string, candidate: string) => str.endsWith(candidate),
360
// TODO: the '!isLinux' part below is to keep current behavior unchanged, but it should probably be removed
361
// in favor of passing correct options from the caller.
362
isEqualOrParent: (base: string, candidate: string) => isEqualOrParent(base, candidate, !isLinux || ignoreCase)
363
};
364
365
// Check cache
366
const patternKey = `${ignoreCase ? pattern.toLowerCase() : pattern}_${!!options.trimForExclusions}_${ignoreCase}`;
367
let parsedPattern = CACHE.get(patternKey);
368
if (parsedPattern) {
369
return wrapRelativePattern(parsedPattern, arg1, internalOptions);
370
}
371
372
// Check for Trivials
373
let match: RegExpExecArray | null;
374
if (T1.test(pattern)) {
375
parsedPattern = trivia1(pattern.substring(4), pattern, internalOptions); // common pattern: **/*.txt just need endsWith check
376
} else if (match = T2.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/some.txt just need basename check
377
parsedPattern = trivia2(match[1], pattern, internalOptions);
378
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
379
parsedPattern = trivia3(pattern, internalOptions);
380
} else if (match = T4.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/something/else just need endsWith check
381
parsedPattern = trivia4and5(match[1].substring(1), pattern, true, internalOptions);
382
} else if (match = T5.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: something/else just need equals check
383
parsedPattern = trivia4and5(match[1], pattern, false, internalOptions);
384
}
385
386
// Otherwise convert to pattern
387
else {
388
parsedPattern = toRegExp(pattern, internalOptions);
389
}
390
391
// Cache
392
CACHE.set(patternKey, parsedPattern);
393
394
return wrapRelativePattern(parsedPattern, arg1, internalOptions);
395
}
396
397
function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | IRelativePattern, options: IGlobOptionsInternal): ParsedStringPattern {
398
if (typeof arg2 === 'string') {
399
return parsedPattern;
400
}
401
402
const wrappedPattern: ParsedStringPattern = function (path, basename) {
403
if (!options.isEqualOrParent(path, arg2.base)) {
404
// skip glob matching if `base` is not a parent of `path`
405
return null;
406
}
407
408
// Given we have checked `base` being a parent of `path`,
409
// we can now remove the `base` portion of the `path`
410
// and only match on the remaining path components
411
// For that we try to extract the portion of the `path`
412
// that comes after the `base` portion. We have to account
413
// for the fact that `base` might end in a path separator
414
// (https://github.com/microsoft/vscode/issues/162498)
415
416
return parsedPattern(ltrim(path.substring(arg2.base.length), sep), basename);
417
};
418
419
// Make sure to preserve associated metadata
420
wrappedPattern.allBasenames = parsedPattern.allBasenames;
421
wrappedPattern.allPaths = parsedPattern.allPaths;
422
wrappedPattern.basenames = parsedPattern.basenames;
423
wrappedPattern.patterns = parsedPattern.patterns;
424
425
return wrappedPattern;
426
}
427
428
function trimForExclusions(pattern: string, options: IGlobOptions): string {
429
return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substring(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later
430
}
431
432
// common pattern: **/*.txt just need endsWith check
433
function trivia1(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
434
return function (path: string, basename?: string) {
435
return typeof path === 'string' && options.endsWith(path, base) ? pattern : null;
436
};
437
}
438
439
// common pattern: **/some.txt just need basename check
440
function trivia2(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
441
const slashBase = `/${base}`;
442
const backslashBase = `\\${base}`;
443
444
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
445
if (typeof path !== 'string') {
446
return null;
447
}
448
449
if (basename) {
450
return options.equals(basename, base) ? pattern : null;
451
}
452
453
return options.equals(path, base) || options.endsWith(path, slashBase) || options.endsWith(path, backslashBase) ? pattern : null;
454
};
455
456
const basenames = [base];
457
parsedPattern.basenames = basenames;
458
parsedPattern.patterns = [pattern];
459
parsedPattern.allBasenames = basenames;
460
461
return parsedPattern;
462
}
463
464
// repetition of common patterns (see above) {**/*.txt,**/*.png}
465
function trivia3(pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {
466
const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1)
467
.split(',')
468
.map(pattern => parsePattern(pattern, options))
469
.filter(pattern => pattern !== NULL), pattern);
470
471
const patternsLength = parsedPatterns.length;
472
if (!patternsLength) {
473
return NULL;
474
}
475
476
if (patternsLength === 1) {
477
return parsedPatterns[0];
478
}
479
480
const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {
481
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
482
if (parsedPatterns[i](path, basename)) {
483
return pattern;
484
}
485
}
486
487
return null;
488
};
489
490
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
491
if (withBasenames) {
492
parsedPattern.allBasenames = withBasenames.allBasenames;
493
}
494
495
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
496
if (allPaths.length) {
497
parsedPattern.allPaths = allPaths;
498
}
499
500
return parsedPattern;
501
}
502
503
// common patterns: **/something/else just need endsWith check, something/else just needs and equals check
504
function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean, options: IGlobOptionsInternal): ParsedStringPattern {
505
const usingPosixSep = sep === posix.sep;
506
const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, sep);
507
const nativePathEnd = sep + nativePath;
508
const targetPathEnd = posix.sep + targetPath;
509
510
let parsedPattern: ParsedStringPattern;
511
if (matchPathEnds) {
512
parsedPattern = function (path: string, basename?: string) {
513
return typeof path === 'string' && (
514
(options.equals(path, nativePath) || options.endsWith(path, nativePathEnd)) ||
515
!usingPosixSep && (options.equals(path, targetPath) || options.endsWith(path, targetPathEnd))
516
) ? pattern : null;
517
};
518
} else {
519
parsedPattern = function (path: string, basename?: string) {
520
return typeof path === 'string' && (options.equals(path, nativePath) || (!usingPosixSep && options.equals(path, targetPath))) ? pattern : null;
521
};
522
}
523
524
parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath];
525
526
return parsedPattern;
527
}
528
529
function toRegExp(pattern: string, options: IGlobOptions): ParsedStringPattern {
530
try {
531
const regExp = new RegExp(`^${parseRegExp(pattern)}$`, options.ignoreCase ? 'i' : undefined);
532
return function (path: string) {
533
regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it!
534
535
return typeof path === 'string' && regExp.test(path) ? pattern : null;
536
};
537
} catch {
538
return NULL;
539
}
540
}
541
542
/**
543
* Simplified glob matching. Supports a subset of glob patterns:
544
* * `*` to match zero or more characters in a path segment
545
* * `?` to match on one character in a path segment
546
* * `**` to match any number of path segments, including none
547
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
548
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
549
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
550
*/
551
export function match(pattern: string | IRelativePattern, path: string, options?: IGlobOptions): boolean;
552
export function match(expression: IExpression, path: string, options?: IGlobOptions): boolean;
553
export function match(arg1: string | IExpression | IRelativePattern, path: string, options?: IGlobOptions): boolean {
554
if (!arg1 || typeof path !== 'string') {
555
return false;
556
}
557
558
return parse(arg1, options)(path) as boolean;
559
}
560
561
/**
562
* Simplified glob matching. Supports a subset of glob patterns:
563
* * `*` to match zero or more characters in a path segment
564
* * `?` to match on one character in a path segment
565
* * `**` to match any number of path segments, including none
566
* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)
567
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
568
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
569
*/
570
export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern;
571
export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression;
572
export function parse(arg1: string | IExpression | IRelativePattern, options?: IGlobOptions): ParsedPattern | ParsedExpression;
573
export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression {
574
if (!arg1) {
575
return FALSE;
576
}
577
578
// Glob with String
579
if (typeof arg1 === 'string' || isRelativePattern(arg1)) {
580
const parsedPattern = parsePattern(arg1, options);
581
if (parsedPattern === NULL) {
582
return FALSE;
583
}
584
585
const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[] } = function (path: string, basename?: string) {
586
return !!parsedPattern(path, basename);
587
};
588
589
if (parsedPattern.allBasenames) {
590
resultPattern.allBasenames = parsedPattern.allBasenames;
591
}
592
593
if (parsedPattern.allPaths) {
594
resultPattern.allPaths = parsedPattern.allPaths;
595
}
596
597
return resultPattern;
598
}
599
600
// Glob with Expression
601
return parsedExpression(arg1, options);
602
}
603
604
export function isRelativePattern(obj: unknown): obj is IRelativePattern {
605
const rp = obj as IRelativePattern | undefined | null;
606
if (!rp) {
607
return false;
608
}
609
610
return typeof rp.base === 'string' && typeof rp.pattern === 'string';
611
}
612
613
export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
614
return (<ParsedStringPattern>patternOrExpression).allBasenames || [];
615
}
616
617
export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
618
return (<ParsedStringPattern>patternOrExpression).allPaths || [];
619
}
620
621
function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {
622
const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)
623
.map(pattern => parseExpressionPattern(pattern, expression[pattern], options))
624
.filter(pattern => pattern !== NULL));
625
626
const patternsLength = parsedPatterns.length;
627
if (!patternsLength) {
628
return NULL;
629
}
630
631
if (!parsedPatterns.some(parsedPattern => !!(<ParsedExpressionPattern>parsedPattern).requiresSiblings)) {
632
if (patternsLength === 1) {
633
return parsedPatterns[0] as ParsedStringPattern;
634
}
635
636
const resultExpression: ParsedStringPattern = function (path: string, basename?: string) {
637
let resultPromises: Promise<string | null>[] | undefined = undefined;
638
639
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
640
const result = parsedPatterns[i](path, basename);
641
if (typeof result === 'string') {
642
return result; // immediately return as soon as the first expression matches
643
}
644
645
// If the result is a promise, we have to keep it for
646
// later processing and await the result properly.
647
if (isThenable(result)) {
648
if (!resultPromises) {
649
resultPromises = [];
650
}
651
652
resultPromises.push(result);
653
}
654
}
655
656
// With result promises, we have to loop over each and
657
// await the result before we can return any result.
658
if (resultPromises) {
659
return (async () => {
660
for (const resultPromise of resultPromises) {
661
const result = await resultPromise;
662
if (typeof result === 'string') {
663
return result;
664
}
665
}
666
667
return null;
668
})();
669
}
670
671
return null;
672
};
673
674
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
675
if (withBasenames) {
676
resultExpression.allBasenames = withBasenames.allBasenames;
677
}
678
679
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
680
if (allPaths.length) {
681
resultExpression.allPaths = allPaths;
682
}
683
684
return resultExpression;
685
}
686
687
const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {
688
let name: string | undefined = undefined;
689
let resultPromises: Promise<string | null>[] | undefined = undefined;
690
691
for (let i = 0, n = parsedPatterns.length; i < n; i++) {
692
693
// Pattern matches path
694
const parsedPattern = (<ParsedExpressionPattern>parsedPatterns[i]);
695
if (parsedPattern.requiresSiblings && hasSibling) {
696
if (!base) {
697
base = basename(path);
698
}
699
700
if (!name) {
701
name = base.substring(0, base.length - extname(path).length);
702
}
703
}
704
705
const result = parsedPattern(path, base, name, hasSibling);
706
if (typeof result === 'string') {
707
return result; // immediately return as soon as the first expression matches
708
}
709
710
// If the result is a promise, we have to keep it for
711
// later processing and await the result properly.
712
if (isThenable(result)) {
713
if (!resultPromises) {
714
resultPromises = [];
715
}
716
717
resultPromises.push(result);
718
}
719
}
720
721
// With result promises, we have to loop over each and
722
// await the result before we can return any result.
723
if (resultPromises) {
724
return (async () => {
725
for (const resultPromise of resultPromises) {
726
const result = await resultPromise;
727
if (typeof result === 'string') {
728
return result;
729
}
730
}
731
732
return null;
733
})();
734
}
735
736
return null;
737
};
738
739
const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);
740
if (withBasenames) {
741
resultExpression.allBasenames = withBasenames.allBasenames;
742
}
743
744
const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);
745
if (allPaths.length) {
746
resultExpression.allPaths = allPaths;
747
}
748
749
return resultExpression;
750
}
751
752
function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) {
753
if (value === false) {
754
return NULL; // pattern is disabled
755
}
756
757
const parsedPattern = parsePattern(pattern, options);
758
if (parsedPattern === NULL) {
759
return NULL;
760
}
761
762
// Expression Pattern is <boolean>
763
if (typeof value === 'boolean') {
764
return parsedPattern;
765
}
766
767
// Expression Pattern is <SiblingClause>
768
if (value) {
769
const when = value.when;
770
if (typeof when === 'string') {
771
const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => {
772
if (!hasSibling || !parsedPattern(path, basename)) {
773
return null;
774
}
775
776
const clausePattern = when.replace('$(basename)', () => name!);
777
const matched = hasSibling(clausePattern);
778
return isThenable(matched) ?
779
matched.then(match => match ? pattern : null) :
780
matched ? pattern : null;
781
};
782
783
result.requiresSiblings = true;
784
785
return result;
786
}
787
}
788
789
// Expression is anything
790
return parsedPattern;
791
}
792
793
function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string): Array<ParsedStringPattern | ParsedExpressionPattern> {
794
const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(<ParsedStringPattern>parsedPattern).basenames);
795
if (basenamePatterns.length < 2) {
796
return parsedPatterns;
797
}
798
799
const basenames = basenamePatterns.reduce<string[]>((all, current) => {
800
const basenames = (<ParsedStringPattern>current).basenames;
801
802
return basenames ? all.concat(basenames) : all;
803
}, [] as string[]);
804
805
let patterns: string[];
806
if (result) {
807
patterns = [];
808
809
for (let i = 0, n = basenames.length; i < n; i++) {
810
patterns.push(result);
811
}
812
} else {
813
patterns = basenamePatterns.reduce((all, current) => {
814
const patterns = (<ParsedStringPattern>current).patterns;
815
816
return patterns ? all.concat(patterns) : all;
817
}, [] as string[]);
818
}
819
820
const aggregate: ParsedStringPattern = function (path: string, basename?: string) {
821
if (typeof path !== 'string') {
822
return null;
823
}
824
825
if (!basename) {
826
let i: number;
827
for (i = path.length; i > 0; i--) {
828
const ch = path.charCodeAt(i - 1);
829
if (ch === CharCode.Slash || ch === CharCode.Backslash) {
830
break;
831
}
832
}
833
834
basename = path.substring(i);
835
}
836
837
const index = basenames.indexOf(basename);
838
return index !== -1 ? patterns[index] : null;
839
};
840
841
aggregate.basenames = basenames;
842
aggregate.patterns = patterns;
843
aggregate.allBasenames = basenames;
844
845
const aggregatedPatterns = parsedPatterns.filter(parsedPattern => !(<ParsedStringPattern>parsedPattern).basenames);
846
aggregatedPatterns.push(aggregate);
847
848
return aggregatedPatterns;
849
}
850
851
// NOTE: This is not used for actual matching, only for resetting watcher when patterns change.
852
// That is why it's ok to avoid case-insensitive comparison here.
853
export function patternsEquals(patternsA: Array<string | IRelativePattern> | undefined, patternsB: Array<string | IRelativePattern> | undefined): boolean {
854
return equals(patternsA, patternsB, (a, b) => {
855
if (typeof a === 'string' && typeof b === 'string') {
856
return a === b;
857
}
858
859
if (typeof a !== 'string' && typeof b !== 'string') {
860
return a.base === b.base && a.pattern === b.pattern;
861
}
862
863
return false;
864
});
865
}
866
867