From afcc28503a2e02422a6ea88bb7af6e659058c980 Mon Sep 17 00:00:00 2001 From: David Dooling Date: Thu, 23 Apr 2020 12:37:06 -0500 Subject: [PATCH] Add pathExpressionFileMatches astUtil function [changelog:added] --- lib/tree/ast/astUtils.ts | 47 ++++++++++++++++++++++++++++++++++ test/tree/ast/astUtils.test.ts | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/lib/tree/ast/astUtils.ts b/lib/tree/ast/astUtils.ts index 1435250fd..9753f9c9e 100644 --- a/lib/tree/ast/astUtils.ts +++ b/lib/tree/ast/astUtils.ts @@ -261,6 +261,53 @@ export async function fileMatches(p: ProjectAsync, return files.filter(x => !!x); } +export interface PathExpressionFileHits { + fileHits: FileHit[]; + pathExpression: string; +} + +/** + * Iterate over provided path expression query options and return + * [[FileHit]]s for each path expression. + * @param p project + * @param peqos Array of query options + * @return hits for each file for each query option + */ +export async function pathExpressionFileMatches(p: ProjectAsync, peqos: PathExpressionQueryOptions[]): Promise { + const pefhs: PathExpressionFileHits[] = []; + const fileCache: Record = {}; + for (const peqo of peqos) { + const parsed: PathExpression = toPathExpression(peqo.pathExpression); + const parser = findParser(parsed, peqo.parseWith); + if (!parser) { + throw new Error(`Cannot find parser for path expression [${peqo.pathExpression}]: Using ${peqo.parseWith}`); + } + const matchFiles = await p.getFiles(peqo.globPatterns); + const checkFiles = matchFiles.map(f => { + if (!fileCache[f.path]) { + fileCache[f.path] = f; + } + return fileCache[f.path]; + }); + const valuesToCheckFor = literalValues(parsed); + const files: FileHit[] = []; + for (const file of checkFiles) { + const hit = await parseFile(parser, parsed, peqo.functionRegistry, p, file, + valuesToCheckFor, undefined, peqo.cacheAst !== false); + if (hit) { + files.push(hit); + } + } + if (files.length > 0) { + pefhs.push({ + pathExpression: stringify(parsed), + fileHits: files, + }); + } + } + return pefhs; +} + /** * Generator style iteration over file matches * @param p project diff --git a/test/tree/ast/astUtils.test.ts b/test/tree/ast/astUtils.test.ts index b6198bcb7..0aa59323b 100644 --- a/test/tree/ast/astUtils.test.ts +++ b/test/tree/ast/astUtils.test.ts @@ -10,6 +10,7 @@ import { literalValues, matches, matchIterator, + pathExpressionFileMatches, zapAllMatches, } from "../../../lib/tree/ast/astUtils"; import { @@ -510,6 +511,52 @@ describe("astUtils", () => { }); + describe("pathExpressionFileMatches", () => { + + it("should find matches across path expressions", async () => { + const f = `export async function foo(a: string, b: boolean, c: number): Promise { return c; }\n`; + const x = `function x(y: number): number { return y; }`; + const g = `function g(x: string): string { return x; }\n` + + `function h(y: number): number { return y; }\n`; + const p = InMemoryProject.of( + { path: "index.ts", content: `export * from "./lib/d-boon";\nexport * from "./lib/mike-watt";\n` }, + { path: "lib/d-boon.ts", content: f }, + { path: "lib/mike-watt.ts", content: x }, + { path: "lib/george-hurley.ts", content: g }, + ); + const os = [ + { + globPatterns: "**/*.ts", + pathExpression: `//FunctionDeclaration[/Identifier[@value='x']]`, + parseWith: TypeScriptES6FileParser, + }, + { + globPatterns: "lib/*.ts", + pathExpression: `//FunctionDeclaration[/Identifier[@value='g']] | //FunctionDeclaration[/Identifier[@value='h']]`, + parseWith: TypeScriptES6FileParser, + }, + ]; + const ms = await pathExpressionFileMatches(p, os); + assert(ms.length === 2); + assert(ms[0].pathExpression === "descendant-or-self::FunctionDeclaration[child::Identifier[@value='x']]"); + assert(ms[0].fileHits.length === 1); + assert(ms[0].fileHits[0].file.path === "lib/mike-watt.ts"); + assert(ms[0].fileHits[0].matches.length === 1); + assert(ms[0].fileHits[0].matches[0].$name === "FunctionDeclaration"); + assert(ms[0].fileHits[0].matches[0].$value === "function x(y: number): number { return y; }"); + assert(ms[1].pathExpression === "descendant-or-self::FunctionDeclaration[child::Identifier[@value='g']] | " + + "descendant-or-self::FunctionDeclaration[child::Identifier[@value='h']]"); + assert(ms[1].fileHits.length === 1); + assert(ms[1].fileHits[0].file.path === "lib/george-hurley.ts"); + assert(ms[1].fileHits[0].matches.length === 2); + assert(ms[1].fileHits[0].matches[0].$name === "FunctionDeclaration"); + assert(ms[1].fileHits[0].matches[0].$value === "function g(x: string): string { return x; }"); + assert(ms[1].fileHits[0].matches[1].$name === "FunctionDeclaration"); + assert(ms[1].fileHits[0].matches[1].$value === "function h(y: number): number { return y; }"); + }); + + }); + }); describe("matches in action", () => {