Path: blob/main/extensions/copilot/src/util/vs/base/common/glob.ts
13405 views
//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode'12/*---------------------------------------------------------------------------------------------3* Copyright (c) Microsoft Corporation. All rights reserved.4* Licensed under the MIT License. See License.txt in the project root for license information.5*--------------------------------------------------------------------------------------------*/67import { equals } from './arrays';8import { isThenable } from './async';9import { CharCode } from './charCode';10import { isEqualOrParent } from './extpath';11import { LRUCache } from './map';12import { basename, extname, posix, sep } from './path';13import { isLinux } from './platform';14import { endsWithIgnoreCase, equalsIgnoreCase, escapeRegExpCharacters, ltrim } from './strings';1516export interface IRelativePattern {1718/**19* A base file path to which this pattern will be matched against relatively.20*/21readonly base: string;2223/**24* A file glob pattern like `*.{ts,js}` that will be matched on file paths25* relative to the base path.26*27* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,28* the file glob pattern will match on `index.js`.29*/30readonly pattern: string;31}3233export interface IExpression {34[pattern: string]: boolean | SiblingClause;35}3637export function getEmptyExpression(): IExpression {38return Object.create(null);39}4041interface SiblingClause {42when: string;43}4445export const GLOBSTAR = '**';46export const GLOB_SPLIT = '/';4748const PATH_REGEX = '[/\\\\]'; // any slash or backslash49const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash50const ALL_FORWARD_SLASHES = /\//g;5152function starsToRegExp(starCount: number, isLastPattern?: boolean): string {53switch (starCount) {54case 0:55return '';56case 1:57return `${NO_PATH_REGEX}*?`; // 1 star matches any number of characters except path separator (/ and \) - non greedy (?)58default:59// Matches: (Path Sep OR Path Val followed by Path Sep) 0-many times except when it's the last pattern60// in which case also matches (Path Sep followed by Path Val)61// Group is non capturing because we don't need to capture at all (?:...)62// Overall we use non-greedy matching because it could be that we match too much63return `(?:${PATH_REGEX}|${NO_PATH_REGEX}+${PATH_REGEX}${isLastPattern ? `|${PATH_REGEX}${NO_PATH_REGEX}+` : ''})*?`;64}65}6667export function splitGlobAware(pattern: string, splitChar: string): string[] {68if (!pattern) {69return [];70}7172const segments: string[] = [];7374let inBraces = false;75let inBrackets = false;7677let curVal = '';78for (const char of pattern) {79switch (char) {80case splitChar:81if (!inBraces && !inBrackets) {82segments.push(curVal);83curVal = '';8485continue;86}87break;88case '{':89inBraces = true;90break;91case '}':92inBraces = false;93break;94case '[':95inBrackets = true;96break;97case ']':98inBrackets = false;99break;100}101102curVal += char;103}104105// Tail106if (curVal) {107segments.push(curVal);108}109110return segments;111}112113function parseRegExp(pattern: string): string {114if (!pattern) {115return '';116}117118let regEx = '';119120// Split up into segments for each slash found121const segments = splitGlobAware(pattern, GLOB_SPLIT);122123// Special case where we only have globstars124if (segments.every(segment => segment === GLOBSTAR)) {125regEx = '.*';126}127128// Build regex over segments129else {130let previousSegmentWasGlobStar = false;131segments.forEach((segment, index) => {132133// Treat globstar specially134if (segment === GLOBSTAR) {135136// if we have more than one globstar after another, just ignore it137if (previousSegmentWasGlobStar) {138return;139}140141regEx += starsToRegExp(2, index === segments.length - 1);142}143144// Anything else, not globstar145else {146147// States148let inBraces = false;149let braceVal = '';150151let inBrackets = false;152let bracketVal = '';153154for (const char of segment) {155156// Support brace expansion157if (char !== '}' && inBraces) {158braceVal += char;159continue;160}161162// Support brackets163if (inBrackets && (char !== ']' || !bracketVal) /* ] is literally only allowed as first character in brackets to match it */) {164let res: string;165166// range operator167if (char === '-') {168res = char;169}170171// negation operator (only valid on first index in bracket)172else if ((char === '^' || char === '!') && !bracketVal) {173res = '^';174}175176// glob split matching is not allowed within character ranges177// see http://man7.org/linux/man-pages/man7/glob.7.html178else if (char === GLOB_SPLIT) {179res = '';180}181182// anything else gets escaped183else {184res = escapeRegExpCharacters(char);185}186187bracketVal += res;188continue;189}190191switch (char) {192case '{':193inBraces = true;194continue;195196case '[':197inBrackets = true;198continue;199200case '}': {201const choices = splitGlobAware(braceVal, ',');202203// Converts {foo,bar} => [foo|bar]204const braceRegExp = `(?:${choices.map(choice => parseRegExp(choice)).join('|')})`;205206regEx += braceRegExp;207208inBraces = false;209braceVal = '';210211break;212}213214case ']': {215regEx += ('[' + bracketVal + ']');216217inBrackets = false;218bracketVal = '';219220break;221}222223case '?':224regEx += NO_PATH_REGEX; // 1 ? matches any single character except path separator (/ and \)225continue;226227case '*':228regEx += starsToRegExp(1);229continue;230231default:232regEx += escapeRegExpCharacters(char);233}234}235236// Tail: Add the slash we had split on if there is more to237// come and the remaining pattern is not a globstar238// For example if pattern: some/**/*.js we want the "/" after239// some to be included in the RegEx to prevent a folder called240// "something" to match as well.241if (242index < segments.length - 1 && // more segments to come after this243(244segments[index + 1] !== GLOBSTAR || // next segment is not **, or...245index + 2 < segments.length // ...next segment is ** but there is more segments after that246)247) {248regEx += PATH_REGEX;249}250}251252// update globstar state253previousSegmentWasGlobStar = (segment === GLOBSTAR);254});255}256257return regEx;258}259260// regexes to check for trivial glob patterns that just check for String#endsWith261const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something262const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something263const T3 = /^{\*\*\/\*?[\w\.-]+\/?(,\*\*\/\*?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}264const T3_2 = /^{\*\*\/\*?[\w\.-]+(\/(\*\*)?)?(,\*\*\/\*?[\w\.-]+(\/(\*\*)?)?)*}$/; // Like T3, with optional trailing /**265const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else266const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else267268export type ParsedPattern = (path: string, basename?: string) => boolean;269270// The `ParsedExpression` returns a `Promise`271// iff `hasSibling` returns a `Promise`.272export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => string | null | Promise<string | null> /* the matching pattern */;273274export interface IGlobOptions {275276/**277* Simplify patterns for use as exclusion filters during278* tree traversal to skip entire subtrees. Cannot be used279* outside of a tree traversal.280*/281trimForExclusions?: boolean;282283/**284* Whether glob pattern matching should be case insensitive.285*/286ignoreCase?: boolean;287}288289interface IGlobOptionsInternal extends IGlobOptions {290equals: (a: string, b: string) => boolean;291endsWith: (str: string, candidate: string) => boolean;292isEqualOrParent: (base: string, candidate: string) => boolean;293}294295interface ParsedStringPattern {296(path: string, basename?: string): string | null | Promise<string | null> /* the matching pattern */;297basenames?: string[];298patterns?: string[];299allBasenames?: string[];300allPaths?: string[];301}302303interface ParsedExpressionPattern {304(path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | null | Promise<string | null> /* the matching pattern */;305requiresSiblings?: boolean;306allBasenames?: string[];307allPaths?: string[];308}309310const CACHE = new LRUCache<string, ParsedStringPattern>(10000); // bounded to 10000 elements311312const FALSE = function () {313return false;314};315316const NULL = function (): string | null {317return null;318};319320/**321* Check if a provided parsed pattern or expression322* is empty - hence it won't ever match anything.323*324* See {@link FALSE} and {@link NULL}.325*/326export function isEmptyPattern(pattern: ParsedPattern | ParsedExpression): pattern is (typeof FALSE | typeof NULL) {327if (pattern === FALSE) {328return true;329}330331if (pattern === NULL) {332return true;333}334335return false;336}337338function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern {339if (!arg1) {340return NULL;341}342343// Handle relative patterns344let pattern: string;345if (typeof arg1 !== 'string') {346pattern = arg1.pattern;347} else {348pattern = arg1;349}350351// Whitespace trimming352pattern = pattern.trim();353354const ignoreCase = options.ignoreCase ?? false;355const internalOptions = {356...options,357equals: ignoreCase ? equalsIgnoreCase : (a: string, b: string) => a === b,358endsWith: ignoreCase ? endsWithIgnoreCase : (str: string, candidate: string) => str.endsWith(candidate),359// TODO: the '!isLinux' part below is to keep current behavior unchanged, but it should probably be removed360// in favor of passing correct options from the caller.361isEqualOrParent: (base: string, candidate: string) => isEqualOrParent(base, candidate, !isLinux || ignoreCase)362};363364// Check cache365const patternKey = `${ignoreCase ? pattern.toLowerCase() : pattern}_${!!options.trimForExclusions}_${ignoreCase}`;366let parsedPattern = CACHE.get(patternKey);367if (parsedPattern) {368return wrapRelativePattern(parsedPattern, arg1, internalOptions);369}370371// Check for Trivials372let match: RegExpExecArray | null;373if (T1.test(pattern)) {374parsedPattern = trivia1(pattern.substring(4), pattern, internalOptions); // common pattern: **/*.txt just need endsWith check375} else if (match = T2.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/some.txt just need basename check376parsedPattern = trivia2(match[1], pattern, internalOptions);377} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}378parsedPattern = trivia3(pattern, internalOptions);379} else if (match = T4.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/something/else just need endsWith check380parsedPattern = trivia4and5(match[1].substring(1), pattern, true, internalOptions);381} else if (match = T5.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: something/else just need equals check382parsedPattern = trivia4and5(match[1], pattern, false, internalOptions);383}384385// Otherwise convert to pattern386else {387parsedPattern = toRegExp(pattern, internalOptions);388}389390// Cache391CACHE.set(patternKey, parsedPattern);392393return wrapRelativePattern(parsedPattern, arg1, internalOptions);394}395396function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string | IRelativePattern, options: IGlobOptionsInternal): ParsedStringPattern {397if (typeof arg2 === 'string') {398return parsedPattern;399}400401const wrappedPattern: ParsedStringPattern = function (path, basename) {402if (!options.isEqualOrParent(path, arg2.base)) {403// skip glob matching if `base` is not a parent of `path`404return null;405}406407// Given we have checked `base` being a parent of `path`,408// we can now remove the `base` portion of the `path`409// and only match on the remaining path components410// For that we try to extract the portion of the `path`411// that comes after the `base` portion. We have to account412// for the fact that `base` might end in a path separator413// (https://github.com/microsoft/vscode/issues/162498)414415return parsedPattern(ltrim(path.substring(arg2.base.length), sep), basename);416};417418// Make sure to preserve associated metadata419wrappedPattern.allBasenames = parsedPattern.allBasenames;420wrappedPattern.allPaths = parsedPattern.allPaths;421wrappedPattern.basenames = parsedPattern.basenames;422wrappedPattern.patterns = parsedPattern.patterns;423424return wrappedPattern;425}426427function trimForExclusions(pattern: string, options: IGlobOptions): string {428return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substring(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later429}430431// common pattern: **/*.txt just need endsWith check432function trivia1(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {433return function (path: string, basename?: string) {434return typeof path === 'string' && options.endsWith(path, base) ? pattern : null;435};436}437438// common pattern: **/some.txt just need basename check439function trivia2(base: string, pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {440const slashBase = `/${base}`;441const backslashBase = `\\${base}`;442443const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {444if (typeof path !== 'string') {445return null;446}447448if (basename) {449return options.equals(basename, base) ? pattern : null;450}451452return options.equals(path, base) || options.endsWith(path, slashBase) || options.endsWith(path, backslashBase) ? pattern : null;453};454455const basenames = [base];456parsedPattern.basenames = basenames;457parsedPattern.patterns = [pattern];458parsedPattern.allBasenames = basenames;459460return parsedPattern;461}462463// repetition of common patterns (see above) {**/*.txt,**/*.png}464function trivia3(pattern: string, options: IGlobOptionsInternal): ParsedStringPattern {465const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1)466.split(',')467.map(pattern => parsePattern(pattern, options))468.filter(pattern => pattern !== NULL), pattern);469470const patternsLength = parsedPatterns.length;471if (!patternsLength) {472return NULL;473}474475if (patternsLength === 1) {476return parsedPatterns[0];477}478479const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) {480for (let i = 0, n = parsedPatterns.length; i < n; i++) {481if (parsedPatterns[i](path, basename)) {482return pattern;483}484}485486return null;487};488489const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);490if (withBasenames) {491parsedPattern.allBasenames = withBasenames.allBasenames;492}493494const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);495if (allPaths.length) {496parsedPattern.allPaths = allPaths;497}498499return parsedPattern;500}501502// common patterns: **/something/else just need endsWith check, something/else just needs and equals check503function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean, options: IGlobOptionsInternal): ParsedStringPattern {504const usingPosixSep = sep === posix.sep;505const nativePath = usingPosixSep ? targetPath : targetPath.replace(ALL_FORWARD_SLASHES, sep);506const nativePathEnd = sep + nativePath;507const targetPathEnd = posix.sep + targetPath;508509let parsedPattern: ParsedStringPattern;510if (matchPathEnds) {511parsedPattern = function (path: string, basename?: string) {512return typeof path === 'string' && (513(options.equals(path, nativePath) || options.endsWith(path, nativePathEnd)) ||514!usingPosixSep && (options.equals(path, targetPath) || options.endsWith(path, targetPathEnd))515) ? pattern : null;516};517} else {518parsedPattern = function (path: string, basename?: string) {519return typeof path === 'string' && (options.equals(path, nativePath) || (!usingPosixSep && options.equals(path, targetPath))) ? pattern : null;520};521}522523parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath];524525return parsedPattern;526}527528function toRegExp(pattern: string, options: IGlobOptions): ParsedStringPattern {529try {530const regExp = new RegExp(`^${parseRegExp(pattern)}$`, options.ignoreCase ? 'i' : undefined);531return function (path: string) {532regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it!533534return typeof path === 'string' && regExp.test(path) ? pattern : null;535};536} catch {537return NULL;538}539}540541/**542* Simplified glob matching. Supports a subset of glob patterns:543* * `*` to match zero or more characters in a path segment544* * `?` to match on one character in a path segment545* * `**` to match any number of path segments, including none546* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)547* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)548* * `[!...]` 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`)549*/550export function match(pattern: string | IRelativePattern, path: string, options?: IGlobOptions): boolean;551export function match(expression: IExpression, path: string, options?: IGlobOptions): boolean;552export function match(arg1: string | IExpression | IRelativePattern, path: string, options?: IGlobOptions): boolean {553if (!arg1 || typeof path !== 'string') {554return false;555}556557return parse(arg1, options)(path) as boolean;558}559560/**561* Simplified glob matching. Supports a subset of glob patterns:562* * `*` to match zero or more characters in a path segment563* * `?` to match on one character in a path segment564* * `**` to match any number of path segments, including none565* * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files)566* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)567* * `[!...]` 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`)568*/569export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern;570export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression;571export function parse(arg1: string | IExpression | IRelativePattern, options?: IGlobOptions): ParsedPattern | ParsedExpression;572export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression {573if (!arg1) {574return FALSE;575}576577// Glob with String578if (typeof arg1 === 'string' || isRelativePattern(arg1)) {579const parsedPattern = parsePattern(arg1, options);580if (parsedPattern === NULL) {581return FALSE;582}583584const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[] } = function (path: string, basename?: string) {585return !!parsedPattern(path, basename);586};587588if (parsedPattern.allBasenames) {589resultPattern.allBasenames = parsedPattern.allBasenames;590}591592if (parsedPattern.allPaths) {593resultPattern.allPaths = parsedPattern.allPaths;594}595596return resultPattern;597}598599// Glob with Expression600return parsedExpression(arg1, options);601}602603export function isRelativePattern(obj: unknown): obj is IRelativePattern {604const rp = obj as IRelativePattern | undefined | null;605if (!rp) {606return false;607}608609return typeof rp.base === 'string' && typeof rp.pattern === 'string';610}611612export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {613return (<ParsedStringPattern>patternOrExpression).allBasenames || [];614}615616export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {617return (<ParsedStringPattern>patternOrExpression).allPaths || [];618}619620function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {621const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)622.map(pattern => parseExpressionPattern(pattern, expression[pattern], options))623.filter(pattern => pattern !== NULL));624625const patternsLength = parsedPatterns.length;626if (!patternsLength) {627return NULL;628}629630if (!parsedPatterns.some(parsedPattern => !!(<ParsedExpressionPattern>parsedPattern).requiresSiblings)) {631if (patternsLength === 1) {632return parsedPatterns[0] as ParsedStringPattern;633}634635const resultExpression: ParsedStringPattern = function (path: string, basename?: string) {636let resultPromises: Promise<string | null>[] | undefined = undefined;637638for (let i = 0, n = parsedPatterns.length; i < n; i++) {639const result = parsedPatterns[i](path, basename);640if (typeof result === 'string') {641return result; // immediately return as soon as the first expression matches642}643644// If the result is a promise, we have to keep it for645// later processing and await the result properly.646if (isThenable(result)) {647if (!resultPromises) {648resultPromises = [];649}650651resultPromises.push(result);652}653}654655// With result promises, we have to loop over each and656// await the result before we can return any result.657if (resultPromises) {658return (async () => {659for (const resultPromise of resultPromises) {660const result = await resultPromise;661if (typeof result === 'string') {662return result;663}664}665666return null;667})();668}669670return null;671};672673const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);674if (withBasenames) {675resultExpression.allBasenames = withBasenames.allBasenames;676}677678const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);679if (allPaths.length) {680resultExpression.allPaths = allPaths;681}682683return resultExpression;684}685686const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {687let name: string | undefined = undefined;688let resultPromises: Promise<string | null>[] | undefined = undefined;689690for (let i = 0, n = parsedPatterns.length; i < n; i++) {691692// Pattern matches path693const parsedPattern = (<ParsedExpressionPattern>parsedPatterns[i]);694if (parsedPattern.requiresSiblings && hasSibling) {695if (!base) {696base = basename(path);697}698699if (!name) {700name = base.substring(0, base.length - extname(path).length);701}702}703704const result = parsedPattern(path, base, name, hasSibling);705if (typeof result === 'string') {706return result; // immediately return as soon as the first expression matches707}708709// If the result is a promise, we have to keep it for710// later processing and await the result properly.711if (isThenable(result)) {712if (!resultPromises) {713resultPromises = [];714}715716resultPromises.push(result);717}718}719720// With result promises, we have to loop over each and721// await the result before we can return any result.722if (resultPromises) {723return (async () => {724for (const resultPromise of resultPromises) {725const result = await resultPromise;726if (typeof result === 'string') {727return result;728}729}730731return null;732})();733}734735return null;736};737738const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames);739if (withBasenames) {740resultExpression.allBasenames = withBasenames.allBasenames;741}742743const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]);744if (allPaths.length) {745resultExpression.allPaths = allPaths;746}747748return resultExpression;749}750751function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) {752if (value === false) {753return NULL; // pattern is disabled754}755756const parsedPattern = parsePattern(pattern, options);757if (parsedPattern === NULL) {758return NULL;759}760761// Expression Pattern is <boolean>762if (typeof value === 'boolean') {763return parsedPattern;764}765766// Expression Pattern is <SiblingClause>767if (value) {768const when = value.when;769if (typeof when === 'string') {770const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) => {771if (!hasSibling || !parsedPattern(path, basename)) {772return null;773}774775const clausePattern = when.replace('$(basename)', () => name!);776const matched = hasSibling(clausePattern);777return isThenable(matched) ?778matched.then(match => match ? pattern : null) :779matched ? pattern : null;780};781782result.requiresSiblings = true;783784return result;785}786}787788// Expression is anything789return parsedPattern;790}791792function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string): Array<ParsedStringPattern | ParsedExpressionPattern> {793const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(<ParsedStringPattern>parsedPattern).basenames);794if (basenamePatterns.length < 2) {795return parsedPatterns;796}797798const basenames = basenamePatterns.reduce<string[]>((all, current) => {799const basenames = (<ParsedStringPattern>current).basenames;800801return basenames ? all.concat(basenames) : all;802}, [] as string[]);803804let patterns: string[];805if (result) {806patterns = [];807808for (let i = 0, n = basenames.length; i < n; i++) {809patterns.push(result);810}811} else {812patterns = basenamePatterns.reduce((all, current) => {813const patterns = (<ParsedStringPattern>current).patterns;814815return patterns ? all.concat(patterns) : all;816}, [] as string[]);817}818819const aggregate: ParsedStringPattern = function (path: string, basename?: string) {820if (typeof path !== 'string') {821return null;822}823824if (!basename) {825let i: number;826for (i = path.length; i > 0; i--) {827const ch = path.charCodeAt(i - 1);828if (ch === CharCode.Slash || ch === CharCode.Backslash) {829break;830}831}832833basename = path.substring(i);834}835836const index = basenames.indexOf(basename);837return index !== -1 ? patterns[index] : null;838};839840aggregate.basenames = basenames;841aggregate.patterns = patterns;842aggregate.allBasenames = basenames;843844const aggregatedPatterns = parsedPatterns.filter(parsedPattern => !(<ParsedStringPattern>parsedPattern).basenames);845aggregatedPatterns.push(aggregate);846847return aggregatedPatterns;848}849850// NOTE: This is not used for actual matching, only for resetting watcher when patterns change.851// That is why it's ok to avoid case-insensitive comparison here.852export function patternsEquals(patternsA: Array<string | IRelativePattern> | undefined, patternsB: Array<string | IRelativePattern> | undefined): boolean {853return equals(patternsA, patternsB, (a, b) => {854if (typeof a === 'string' && typeof b === 'string') {855return a === b;856}857858if (typeof a !== 'string' && typeof b !== 'string') {859return a.base === b.base && a.pattern === b.pattern;860}861862return false;863});864}865866867