Add pi-tool-display (compact tool rendering, adaptive diffs) + pi-lsp-extension (LSP diagnostics/hover/definitions) — vendored from npm 0.5.0 / 1.3.0
This commit is contained in:
296
extensions/pi-lsp-extension/test-structural-search.ts
Normal file
296
extensions/pi-lsp-extension/test-structural-search.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Tests for structural search & rewrite: pattern-compiler, search-engine, rewrite-engine.
|
||||
*
|
||||
* Run: npx tsx test-structural-search.ts
|
||||
*/
|
||||
|
||||
import { TreeSitterManager } from "./src/tree-sitter/parser-manager.js";
|
||||
import { compilePattern, type CompiledPattern } from "./src/tree-sitter/pattern-compiler.js";
|
||||
import { searchFiles, collectFilesByLanguage } from "./src/tree-sitter/search-engine.js";
|
||||
import { computeRewrites, applyRewrites, substituteCaptures } from "./src/tree-sitter/rewrite-engine.js";
|
||||
import { writeFileSync, mkdirSync, rmSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition: boolean, label: string) {
|
||||
if (condition) {
|
||||
console.log(` ✅ ${label}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ❌ ${label}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_DIR = resolve("/tmp/structural-search-test");
|
||||
|
||||
async function main() {
|
||||
const mgr = new TreeSitterManager();
|
||||
await mgr.init();
|
||||
|
||||
// Set up test fixtures
|
||||
try { rmSync(TEST_DIR, { recursive: true }); } catch {}
|
||||
mkdirSync(resolve(TEST_DIR, "src"), { recursive: true });
|
||||
|
||||
writeFileSync(resolve(TEST_DIR, "src/app.ts"), `
|
||||
import { readFile } from "fs";
|
||||
|
||||
function processData(data: string): string {
|
||||
console.log("processing", data);
|
||||
const result = data.trim();
|
||||
console.log("done");
|
||||
return result;
|
||||
}
|
||||
|
||||
function helper() {
|
||||
var x = 10;
|
||||
var y = "hello";
|
||||
const z = 42;
|
||||
}
|
||||
|
||||
class UserService {
|
||||
async getUser(id: string) {
|
||||
const user = await this.db.find(id);
|
||||
return user;
|
||||
}
|
||||
|
||||
async deleteUser(id: string) {
|
||||
console.log("deleting", id);
|
||||
await this.db.remove(id);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
writeFileSync(resolve(TEST_DIR, "src/utils.ts"), `
|
||||
export function formatDate(d: Date): string {
|
||||
console.log("formatting date");
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export function parseDate(s: string): Date {
|
||||
return new Date(s);
|
||||
}
|
||||
|
||||
export const doSomething = (a: number, b: number) => {
|
||||
var result = a + b;
|
||||
return result;
|
||||
};
|
||||
`);
|
||||
|
||||
// ── 1. Pattern compiler ──
|
||||
console.log("\n🔧 Pattern Compiler");
|
||||
|
||||
// Simple identifier pattern
|
||||
const p1 = await compilePattern("console.log($ARG)", "typescript", mgr);
|
||||
assert(p1.root.kind === "literal", "console.log($ARG) compiles to literal root");
|
||||
assert(p1.metavars.includes("ARG"), "captures $ARG metavar");
|
||||
assert(p1.languageId === "typescript", "language is typescript");
|
||||
|
||||
// Variadic pattern
|
||||
const p2 = await compilePattern("console.log($$$ARGS)", "typescript", mgr);
|
||||
assert(p2.metavars.includes("ARGS"), "captures $$$ARGS variadic metavar");
|
||||
|
||||
// Variable declaration pattern
|
||||
const p3 = await compilePattern("var $N = $V", "typescript", mgr);
|
||||
assert(p3.metavars.includes("N"), "captures $N");
|
||||
assert(p3.metavars.includes("V"), "captures $V");
|
||||
|
||||
// Pattern with no metavars
|
||||
const p4 = await compilePattern("return result", "typescript", mgr);
|
||||
assert(p4.metavars.length === 0, "no metavars in literal pattern");
|
||||
|
||||
// Bad pattern should throw
|
||||
let threw = false;
|
||||
try {
|
||||
await compilePattern("{{{{invalid}}}", "typescript", mgr);
|
||||
} catch {
|
||||
threw = true;
|
||||
}
|
||||
assert(threw, "invalid pattern throws error");
|
||||
|
||||
// ── 2. Search engine — basic matching ──
|
||||
console.log("\n🔍 Search Engine — basic matching");
|
||||
|
||||
// Find console.log calls with single arg
|
||||
const compiled1 = await compilePattern("console.log($ARG)", "typescript", mgr);
|
||||
const matches1 = await searchFiles(compiled1, TEST_DIR, mgr);
|
||||
console.log(` Found ${matches1.length} console.log($ARG) matches`);
|
||||
assert(matches1.length > 0, "found console.log($ARG) matches");
|
||||
// Check captures
|
||||
const firstMatch = matches1[0];
|
||||
assert("ARG" in firstMatch.captures, "match has $ARG capture");
|
||||
console.log(` First $ARG = "${firstMatch.captures.ARG}"`);
|
||||
|
||||
// Find var declarations
|
||||
const compiled3 = await compilePattern("var $N = $V", "typescript", mgr);
|
||||
const matches3 = await searchFiles(compiled3, TEST_DIR, mgr);
|
||||
console.log(` Found ${matches3.length} var $N = $V matches`);
|
||||
assert(matches3.length >= 3, "found at least 3 var declarations");
|
||||
// Check that captures are different
|
||||
const varNames = matches3.map(m => m.captures.N);
|
||||
console.log(` Variable names: ${varNames.join(", ")}`);
|
||||
assert(varNames.includes("x"), 'found var x');
|
||||
assert(varNames.includes("y"), 'found var y');
|
||||
|
||||
// ── 3. Search engine — variadic matching ──
|
||||
console.log("\n🔍 Search Engine — variadic matching");
|
||||
|
||||
// Find console.log with any number of args
|
||||
const compiled2 = await compilePattern("console.log($$$ARGS)", "typescript", mgr);
|
||||
const matches2 = await searchFiles(compiled2, TEST_DIR, mgr);
|
||||
console.log(` Found ${matches2.length} console.log($$$ARGS) matches`);
|
||||
assert(matches2.length >= 4, "found variadic console.log matches (at least 4)");
|
||||
|
||||
// ── 4. Search engine — file scoping ──
|
||||
console.log("\n🔍 Search Engine — file scoping");
|
||||
|
||||
const scopedMatches = await searchFiles(compiled1, TEST_DIR, mgr, {
|
||||
path: "src/utils.ts",
|
||||
});
|
||||
console.log(` Found ${scopedMatches.length} matches in utils.ts only`);
|
||||
assert(scopedMatches.length >= 1, "found matches in scoped file");
|
||||
assert(scopedMatches.every(m => m.file.endsWith("utils.ts")), "all matches in utils.ts");
|
||||
|
||||
// ── 5. Search engine — max results ──
|
||||
console.log("\n🔍 Search Engine — max results");
|
||||
|
||||
const limitedMatches = await searchFiles(compiled2, TEST_DIR, mgr, { maxResults: 2 });
|
||||
assert(limitedMatches.length <= 2, `max_results respected (got ${limitedMatches.length})`);
|
||||
|
||||
// ── 6. File collection ──
|
||||
console.log("\n📂 File Collection");
|
||||
|
||||
const tsFiles = await collectFilesByLanguage(TEST_DIR, "typescript", mgr);
|
||||
assert(tsFiles.length === 2, `found 2 TypeScript files (got ${tsFiles.length})`);
|
||||
assert(tsFiles.some(f => f.endsWith("app.ts")), "includes app.ts");
|
||||
assert(tsFiles.some(f => f.endsWith("utils.ts")), "includes utils.ts");
|
||||
|
||||
// Skips node_modules
|
||||
mkdirSync(resolve(TEST_DIR, "node_modules/pkg"), { recursive: true });
|
||||
writeFileSync(resolve(TEST_DIR, "node_modules/pkg/index.ts"), "export const x = 1;");
|
||||
const tsFiles2 = await collectFilesByLanguage(TEST_DIR, "typescript", mgr);
|
||||
assert(tsFiles2.length === 2, `still 2 files (node_modules skipped, got ${tsFiles2.length})`);
|
||||
|
||||
// ── 7. Rewrite engine — substituteCaptures ──
|
||||
console.log("\n✏️ Rewrite Engine — substituteCaptures");
|
||||
|
||||
assert(
|
||||
substituteCaptures("const $N = $V", { N: "x", V: "42" }) === "const x = 42",
|
||||
"basic substitution works",
|
||||
);
|
||||
assert(
|
||||
substituteCaptures("$$$ARGS", { ARGS: "a, b, c" }) === "a, b, c",
|
||||
"variadic substitution works",
|
||||
);
|
||||
assert(
|
||||
substituteCaptures("$UNKNOWN stays", {}) === "$UNKNOWN stays",
|
||||
"unknown metavar left as-is",
|
||||
);
|
||||
|
||||
// ── 8. Rewrite engine — computeRewrites (dry run) ──
|
||||
console.log("\n✏️ Rewrite Engine — computeRewrites (dry run)");
|
||||
|
||||
const varMatches = await searchFiles(compiled3, TEST_DIR, mgr);
|
||||
const rewrites = computeRewrites(varMatches, "const $N = $V");
|
||||
assert(rewrites.length === varMatches.length, "one rewrite per match");
|
||||
for (const r of rewrites) {
|
||||
assert(r.before.startsWith("var "), `before starts with "var" (got: ${r.before})`);
|
||||
assert(r.after.startsWith("const "), `after starts with "const" (got: ${r.after})`);
|
||||
console.log(` ${r.file.split("/").pop()}:${r.line} — ${r.before} → ${r.after}`);
|
||||
}
|
||||
|
||||
// ── 9. Rewrite engine — applyRewrites ──
|
||||
console.log("\n✏️ Rewrite Engine — applyRewrites");
|
||||
|
||||
// Work on a copy to not mess up other tests
|
||||
const rewriteDir = resolve(TEST_DIR, "rewrite-test");
|
||||
mkdirSync(rewriteDir, { recursive: true });
|
||||
writeFileSync(resolve(rewriteDir, "test.ts"), `
|
||||
function example() {
|
||||
var a = 1;
|
||||
var b = "hello";
|
||||
const c = true;
|
||||
var d = [];
|
||||
}
|
||||
`);
|
||||
|
||||
const varPattern = await compilePattern("var $N = $V", "typescript", mgr);
|
||||
const varResults = await searchFiles(varPattern, rewriteDir, mgr);
|
||||
console.log(` Found ${varResults.length} var declarations to rewrite`);
|
||||
assert(varResults.length === 3, `found 3 var declarations (got ${varResults.length})`);
|
||||
|
||||
const result = await applyRewrites(varResults, "const $N = $V");
|
||||
assert(result.filesModified === 1, `modified 1 file (got ${result.filesModified})`);
|
||||
assert(result.changes.length === 3, `3 changes (got ${result.changes.length})`);
|
||||
|
||||
// Verify the file was actually rewritten
|
||||
const rewritten = readFileSync(resolve(rewriteDir, "test.ts"), "utf-8");
|
||||
assert(!rewritten.includes("var a"), "var a replaced");
|
||||
assert(!rewritten.includes("var b"), "var b replaced");
|
||||
assert(!rewritten.includes("var d"), "var d replaced");
|
||||
assert(rewritten.includes("const a = 1"), "const a = 1 present");
|
||||
assert(rewritten.includes('const b = "hello"'), 'const b = "hello" present');
|
||||
assert(rewritten.includes("const d = []"), "const d = [] present");
|
||||
assert(rewritten.includes("const c = true"), "original const c preserved");
|
||||
console.log(" Rewritten file content:");
|
||||
console.log(rewritten.split("\n").map(l => ` ${l}`).join("\n"));
|
||||
|
||||
// ── 10. Rewrite — no-op when before === after ──
|
||||
console.log("\n✏️ Rewrite Engine — no-op detection");
|
||||
|
||||
const constPattern = await compilePattern("const $N = $V", "typescript", mgr);
|
||||
const constMatches = await searchFiles(constPattern, rewriteDir, mgr);
|
||||
const noopResult = await applyRewrites(constMatches, "const $N = $V");
|
||||
assert(noopResult.filesModified === 0, "no files modified when replacement matches original");
|
||||
|
||||
// ── 11. Cross-language — Python ──
|
||||
console.log("\n🐍 Cross-language — Python");
|
||||
|
||||
mkdirSync(resolve(TEST_DIR, "py"), { recursive: true });
|
||||
writeFileSync(resolve(TEST_DIR, "py/main.py"), `
|
||||
def process(data):
|
||||
print("start")
|
||||
result = data.strip()
|
||||
print("done")
|
||||
return result
|
||||
|
||||
def helper():
|
||||
print("helper called")
|
||||
`);
|
||||
|
||||
const pyPattern = await compilePattern("print($ARG)", "python", mgr);
|
||||
const pyMatches = await searchFiles(pyPattern, TEST_DIR, mgr, { path: "py" });
|
||||
console.log(` Found ${pyMatches.length} print($ARG) matches in Python`);
|
||||
assert(pyMatches.length >= 3, "found Python print matches");
|
||||
assert(pyMatches.every(m => m.file.endsWith(".py")), "all matches are Python files");
|
||||
|
||||
// ── 12. Search — return value structure ──
|
||||
console.log("\n📋 Search — result structure");
|
||||
|
||||
const singleMatch = (await searchFiles(compiled1, TEST_DIR, mgr, { maxResults: 1 }))[0];
|
||||
assert(typeof singleMatch.file === "string", "match.file is string");
|
||||
assert(typeof singleMatch.line === "number" && singleMatch.line > 0, "match.line is positive number");
|
||||
assert(typeof singleMatch.column === "number" && singleMatch.column > 0, "match.column is positive number");
|
||||
assert(typeof singleMatch.matchedText === "string", "match.matchedText is string");
|
||||
assert(typeof singleMatch.startIndex === "number", "match.startIndex exists");
|
||||
assert(typeof singleMatch.endIndex === "number", "match.endIndex exists");
|
||||
assert(singleMatch.endIndex > singleMatch.startIndex, "endIndex > startIndex");
|
||||
assert(typeof singleMatch.captures === "object", "match.captures is object");
|
||||
|
||||
// ── Cleanup ──
|
||||
try { rmSync(TEST_DIR, { recursive: true }); } catch {}
|
||||
mgr.shutdown();
|
||||
|
||||
// ── Summary ──
|
||||
console.log(`\n${"═".repeat(50)}`);
|
||||
console.log(` ${passed} passed, ${failed} failed`);
|
||||
console.log(`${"═".repeat(50)}\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user