Skip to content

Commit

Permalink
Merge pull request #53 from tmr232/cfgbot
Browse files Browse the repository at this point in the history
CFGBot Utils
  • Loading branch information
tmr232 authored Jan 6, 2025
2 parents c440158 + da1275f commit 59910b3
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 39 deletions.
Binary file modified bun.lockb
Binary file not shown.
14 changes: 14 additions & 0 deletions scripts/cfg-helper.ts
Original file line number Diff line number Diff line change
@@ -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;
}
75 changes: 53 additions & 22 deletions scripts/render-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -58,6 +70,9 @@ async function main() {
out: {
type: "string",
},
colors: {
type: "string",
},
},
strict: true,
allowPositionals: true,
Expand All @@ -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 });
}
}
Expand All @@ -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);
Expand All @@ -117,4 +146,6 @@ async function main() {
}
}

await main();
if (require.main === module) {
await main();
}
13 changes: 10 additions & 3 deletions scripts/render-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
105 changes: 91 additions & 14 deletions scripts/scan-codebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
export function iterSourceFiles(root: string): IterableIterator<string> {
const sourceGlob = new Glob(
`**/*.{${fileTypes.map(({ ext }) => ext).join(",")}}`,
);
return sourceGlob.scanSync(root);
}
function* iterFilenames(
root: string,
dirsToInclude: string[],
): IterableIterator<string> {
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<string>,
): 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();
}

0 comments on commit 59910b3

Please sign in to comment.