Repository: microsoft/vscode (TypeScript)
Date: 2022-03-30 07:51:20+00:00
Task type: Bug Fix

Issue:
Glob matcher fails to check on `Promise` for expression matching
While reviewing some code in `glob.ts` I noticed potential issues:

https://github.com/microsoft/vscode/blob/ce3f26732fdce460f16a439b9bc80a70983bfbc4/src/vs/base/common/glob.ts#L582-L585

and

https://github.com/microsoft/vscode/blob/ce3f26732fdce460f16a439b9bc80a70983bfbc4/src/vs/base/common/glob.ts#L621-L624

Will wrongly stop the `for` loop when the result is a `Promise` even when that result resolves to `null`.

Would be good to have a test case for this too.


Issue discussion:
@roblourens I think you added the support for `Promise` for the expression matching via https://github.com/microsoft/vscode/commit/188ef21ab32a9eb311c9a3ce4e6dd3c49910220e, do we really still need this? I feel this makes glob matching overly complicated. Checking on usages, I think search component uses it to lazily resolve children...

Solution patch:
diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts
--- a/src/vs/base/common/glob.ts
+++ b/src/vs/base/common/glob.ts
@@ -584,15 +584,40 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse
 		}
 
 		const resultExpression: ParsedStringPattern = function (path: string, basename?: string) {
-			for (let i = 0, n = parsedPatterns.length; i < n; i++) {
+			let resultPromises: Promise<string | null>[] | undefined = undefined;
 
-				// Check if pattern matches path
+			for (let i = 0, n = parsedPatterns.length; i < n; i++) {
 				const result = parsedPatterns[i](path, basename);
-				if (result) {
-					return result;
+				if (typeof result === 'string') {
+					return result; // immediately return as soon as the first expression matches
+				}
+
+				// If the result is a promise, we have to keep it for
+				// later processing and await the result properly.
+				if (isThenable(result)) {
+					if (!resultPromises) {
+						resultPromises = [];
+					}
+
+					resultPromises.push(result);
 				}
 			}
 
+			// With result promises, we have to loop over each and
+			// await the result before we can return any result.
+			if (resultPromises) {
+				return (async () => {
+					for (const resultPromise of resultPromises) {
+						const result = await resultPromise;
+						if (typeof result === 'string') {
+							return result;
+						}
+					}
+
+					return null;
+				})();
+			}
+
 			return null;
 		};
 
@@ -611,6 +636,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse
 
 	const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) {
 		let name: string | undefined = undefined;
+		let resultPromises: Promise<string | null>[] | undefined = undefined;
 
 		for (let i = 0, n = parsedPatterns.length; i < n; i++) {
 
@@ -627,11 +653,36 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse
 			}
 
 			const result = parsedPattern(path, base, name, hasSibling);
-			if (result) {
-				return result;
+			if (typeof result === 'string') {
+				return result; // immediately return as soon as the first expression matches
+			}
+
+			// If the result is a promise, we have to keep it for
+			// later processing and await the result properly.
+			if (isThenable(result)) {
+				if (!resultPromises) {
+					resultPromises = [];
+				}
+
+				resultPromises.push(result);
 			}
 		}
 
+		// With result promises, we have to loop over each and
+		// await the result before we can return any result.
+		if (resultPromises) {
+			return (async () => {
+				for (const resultPromise of resultPromises) {
+					const result = await resultPromise;
+					if (typeof result === 'string') {
+						return result;
+					}
+				}
+
+				return null;
+			})();
+		}
+
 		return null;
 	};
 


Test patch:
diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts
--- a/src/vs/base/test/common/glob.test.ts
+++ b/src/vs/base/test/common/glob.test.ts
@@ -1099,4 +1099,19 @@ suite('Glob', () => {
 		let p = 'scheme:/**/*.md';
 		assertGlobMatch(p, URI.file('super/duper/long/some/file.md').with({ scheme: 'scheme' }).toString());
 	});
+
+	test('expression fails when siblings use promises (https://github.com/microsoft/vscode/issues/146294)', async function () {
+		let siblings = ['test.html', 'test.txt', 'test.ts'];
+		let hasSibling = (name: string) => Promise.resolve(siblings.indexOf(name) !== -1);
+
+		// { "**/*.js": { "when": "$(basename).ts" } }
+		let expression: glob.IExpression = {
+			'**/test.js': { when: '$(basename).js' },
+			'**/*.js': { when: '$(basename).ts' }
+		};
+
+		const parsedExpression = glob.parse(expression);
+
+		assert.strictEqual('**/*.js', await parsedExpression('test.js', undefined, hasSibling));
+	});
 });