diff --git a/bun.lockb b/bun.lockb index e2b0ebf8..439585a9 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/scripts/cfg-helper.ts b/scripts/cfg-helper.ts new file mode 100644 index 00000000..7fc12f80 --- /dev/null +++ b/scripts/cfg-helper.ts @@ -0,0 +1,14 @@ +import type Parser from "web-tree-sitter"; +import { type CFG, mergeNodeAttrs } from "../src/control-flow/cfg-defs.ts"; +import { type Language, newCFGBuilder } from "../src/control-flow/cfg.ts"; +import { simplifyCFG, trimFor } from "../src/control-flow/graph-ops.ts"; + +export function buildCFG(func: Parser.SyntaxNode, language: Language): CFG { + const builder = newCFGBuilder(language, { flatSwitch: true }); + + let cfg = builder.buildCFG(func); + + cfg = trimFor(cfg); + cfg = simplifyCFG(cfg, mergeNodeAttrs); + return cfg; +} diff --git a/scripts/render-function.ts b/scripts/render-function.ts index 15e5fd06..61d0fe29 100644 --- a/scripts/render-function.ts +++ b/scripts/render-function.ts @@ -2,15 +2,17 @@ import * as path from "node:path"; import { parseArgs } from "node:util"; import { Graphviz } from "@hpcc-js/wasm-graphviz"; import type Parser from "web-tree-sitter"; -import { type CFG, mergeNodeAttrs } from "../src/control-flow/cfg-defs.ts"; +import type { SyntaxNode } from "web-tree-sitter"; +import { type Language, supportedLanguages } from "../src/control-flow/cfg.ts"; import { - type Language, - newCFGBuilder, - supportedLanguages, -} from "../src/control-flow/cfg.ts"; -import { simplifyCFG, trimFor } from "../src/control-flow/graph-ops.ts"; + deserializeColorList, + getDarkColorList, + getLightColorList, + listToScheme, +} from "../src/control-flow/colors.ts"; import { graphToDot } from "../src/control-flow/render.ts"; import { getLanguage, iterFunctions } from "../src/file-parsing/bun.ts"; +import { buildCFG } from "./cfg-helper.ts"; function isLanguage(language: string): language is Language { return supportedLanguages.includes(language as Language); @@ -24,20 +26,30 @@ function normalizeFuncdef(funcdef: string): string { .trim(); } -function buildCFG(func: Parser.SyntaxNode, language: Language): CFG { - const builder = newCFGBuilder(language, { flatSwitch: true }); - - let cfg = builder.buildCFG(func); - - cfg = trimFor(cfg); - cfg = simplifyCFG(cfg, mergeNodeAttrs); - return cfg; +export function getFuncDef(sourceCode: string, func: SyntaxNode): string { + const body = func.childForFieldName("body"); + if (!body) { + throw new Error("No function body"); + } + return normalizeFuncdef(sourceCode.slice(func.startIndex, body.startIndex)); } function writeError(message: string): void { Bun.write(Bun.stderr, `${message}\n`); } +export async function getColorScheme(colors?: string) { + if (!colors || colors === "dark") { + return listToScheme(getDarkColorList()); + } + if (colors === "light") { + return listToScheme(getLightColorList()); + } + return colors + ? listToScheme(deserializeColorList(await Bun.file(colors).text())) + : undefined; +} + async function main() { process.on("SIGINT", () => { // close watcher when Ctrl-C is pressed @@ -58,6 +70,9 @@ async function main() { out: { type: "string", }, + colors: { + type: "string", + }, }, strict: true, allowPositionals: true, @@ -79,15 +94,26 @@ async function main() { const possibleMatches: { name: string; func: Parser.SyntaxNode }[] = []; const sourceCode = await Bun.file(filepath).text(); + const startIndex = Number.parseInt(functionName); + let startPosition: { row: number; column: number } | undefined; + try { + startPosition = JSON.parse(functionName); + } catch { + startPosition = undefined; + } for (const func of iterFunctions(sourceCode, language)) { - const body = func.childForFieldName("body"); - if (!body) { + let funcDef: string; + try { + funcDef = getFuncDef(sourceCode, func); + } catch { continue; } - const funcDef = normalizeFuncdef( - sourceCode.slice(func.startIndex, body.startIndex), - ); - if (funcDef.includes(functionName)) { + if ( + funcDef.includes(functionName) || + startIndex === func.startIndex || + (startPosition?.row === func.startPosition.row && + startPosition.column === func.startPosition.column) + ) { possibleMatches.push({ name: funcDef, func: func }); } } @@ -108,7 +134,10 @@ async function main() { const func: Parser.SyntaxNode = possibleMatches[0].func; const graphviz = await Graphviz.load(); const cfg = buildCFG(func, language); - const svg = graphviz.dot(graphToDot(cfg)); + + const colorScheme = await getColorScheme(values.colors); + + const svg = graphviz.dot(graphToDot(cfg, false, undefined, colorScheme)); if (values.out) { await Bun.write(values.out, svg); @@ -117,4 +146,6 @@ async function main() { } } -await main(); +if (require.main === module) { + await main(); +} diff --git a/scripts/render-graph.ts b/scripts/render-graph.ts index 622b9948..acf90d7b 100644 --- a/scripts/render-graph.ts +++ b/scripts/render-graph.ts @@ -7,14 +7,21 @@ import type { GraphNode, } from "../src/control-flow/cfg-defs.ts"; import { graphToDot } from "../src/control-flow/render.ts"; +import { getColorScheme } from "./render-function.ts"; async function main() { const { + values, positionals: [_runtime, _this, gist_url], } = parseArgs({ args: Bun.argv, strict: true, allowPositionals: true, + options: { + colors: { + type: "string", + }, + }, }); if (!gist_url) { @@ -39,11 +46,11 @@ async function main() { throw new Error("No entry found"); } const cfg: CFG = { graph, entry, offsetToNode: [] }; - const dot = graphToDot(cfg); + const colorScheme = await getColorScheme(values.colors); + const graphviz = await Graphviz.load(); - const svg = graphviz.dot(dot); + const svg = graphviz.dot(graphToDot(cfg, false, undefined, colorScheme)); console.log(svg); - // console.log(dot); } if (require.main === module) { diff --git a/scripts/scan-codebase.ts b/scripts/scan-codebase.ts index d6b452f9..e202e3b8 100644 --- a/scripts/scan-codebase.ts +++ b/scripts/scan-codebase.ts @@ -8,44 +8,121 @@ import * as path from "node:path"; */ import { parseArgs } from "node:util"; import { Glob } from "bun"; -import { newCFGBuilder } from "../src/control-flow/cfg"; import { fileTypes, getLanguage, iterFunctions, } from "../src/file-parsing/bun.ts"; +import { buildCFG } from "./cfg-helper.ts"; +import { getFuncDef } from "./render-function.ts"; -function iterSourceFiles(root: string): IterableIterator { +export function iterSourceFiles(root: string): IterableIterator { const sourceGlob = new Glob( `**/*.{${fileTypes.map(({ ext }) => ext).join(",")}}`, ); return sourceGlob.scanSync(root); } +function* iterFilenames( + root: string, + dirsToInclude: string[], +): IterableIterator { + if (dirsToInclude.length === 1 && dirsToInclude[0] === "*") { + yield* iterSourceFiles(root); + } else { + for (const dir of dirsToInclude) { + for (const filename of iterSourceFiles(path.join(root, dir))) { + // We want the path relative to the root + yield path.join(dir, filename); + } + } + } +} + +async function* iterFunctionInfo( + root: string, + filenames: IterableIterator, +): AsyncIterableIterator<{ + node_count: number; + start_position: { row: number; column: number }; + funcdef: string; + filename: string; +}> { + for (const filename of filenames) { + const code = await Bun.file(path.join(root, filename)).text(); + const language = getLanguage(filename); + for (const func of iterFunctions(code, language)) { + const cfg = buildCFG(func, language); + yield { + node_count: cfg.graph.order, + start_position: func.startPosition, + funcdef: getFuncDef(code, func), + filename: filename.replaceAll("\\", "/"), + }; + } + } +} + +async function generateIndex( + /** Project name on GitHub */ + project: string, + /** Git ref */ + ref: string, + /** Root on local filesystem */ + root: string, + /** Directories to index, relative to the root */ + dirsToInclude: string[], +) { + const filenames = iterFilenames(root, dirsToInclude); + const functions = await Array.fromAsync(iterFunctionInfo(root, filenames)); + return { + version: 1, + content: { + index_type: "github", + project, + ref, + functions, + }, + }; +} async function main() { - const { values } = parseArgs({ + const { + values, + positionals: [_runtime, _this, ...dirsToInclude], + } = parseArgs({ args: Bun.argv, options: { + project: { + type: "string", + }, + ref: { + type: "string", + }, root: { type: "string", }, + out: { + type: "string", + }, }, strict: true, allowPositionals: true, }); - const root = values.root ?? "."; + if (!values.project || !values.ref || !values.root) { + throw new Error("Missing arguments"); + } - for (const filename of iterSourceFiles(root)) { - const filepath = path.join(root, filename); - const code = await Bun.file(filepath).text(); - const language = getLanguage(filename); - for (const func of iterFunctions(code, language)) { - const builder = newCFGBuilder(language, {}); - const cfg = builder.buildCFG(func); - console.log(filepath, func.startPosition, cfg.graph.order); - } + const output = JSON.stringify( + await generateIndex(values.project, values.ref, values.root, dirsToInclude), + ); + if (values.out) { + await Bun.write(values.out, output); + } else { + await Bun.write(Bun.stdout, output); } } -await main(); +if (require.main === module) { + await main(); +}