Skip to content

Commit de661a6

Browse files
authored
Merge pull request #134 from AlonBilman/issue#94
Adding function information to the /render page
2 parents 10fb77e + 2330f49 commit de661a6

17 files changed

Lines changed: 1741 additions & 6 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Function name extraction with support for multiple programming languages.
12+
- Unit tests for function name extraction, covering various structures and languages.
13+
- Frontend logic in `render/src/App.svelte` to extract and display metadata based on render type.
14+
- Display of both CFG and function metadata in the GitHub render view, and CFG metadata in the Graph render view.
15+
916
## [0.0.16] - 2025-05-07
1017

1118
### Added

CONTRIBUTORS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ The following people have contributed to the development of Function-Graph-Overv
66

77
- [Tamir Bahar](https://github.com/tmr232)
88
- [Niv Baumel](https://github.com/Nivb1569)
9+
- [Alon Bilman](https://github.com/AlonBilman)
10+
- [Ben Nahmias](https://github.com/Bennahmias)
911
- [buherator](https://github.com/v-p-b)

src/control-flow/cfg-c.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import {
2020
type StatementHandlers,
2121
} from "./generic-cfg-builder.ts";
2222
import { treeSitterNoNullNodes } from "./hacks.ts";
23+
import { extractCapturedTextsByCaptureName } from "./query-utils.ts";
2324
import { buildSwitch, collectCases } from "./switch-utils.ts";
2425

2526
export const cLanguageDefinition = {
2627
wasmPath: treeSitterC,
2728
createCFGBuilder: createCFGBuilder,
2829
functionNodeTypes: ["function_definition"],
30+
extractFunctionName: extractCFunctionName,
2931
};
3032

3133
function getChildFieldText(node: SyntaxNode, fieldName: string): string {
@@ -166,3 +168,39 @@ function processSwitchlike(switchSyntax: SyntaxNode, ctx: Context): BasicBlock {
166168

167169
return blockHandler.update({ entry: headNode, exit: mergeNode });
168170
}
171+
172+
const functionQuery = {
173+
functionDeclarator: `(function_declarator
174+
declarator:(identifier)@name)`,
175+
176+
captureName: "name",
177+
};
178+
179+
function getFunctionDeclarator(funcDef: SyntaxNode): SyntaxNode | null {
180+
const body = funcDef.childForFieldName("body");
181+
const end = body ? body.startPosition : funcDef.endPosition;
182+
183+
const nodes = funcDef.descendantsOfType(
184+
"function_declarator",
185+
funcDef.startPosition,
186+
end,
187+
);
188+
189+
const declaratorNode = nodes.find((node) => {
190+
const decl = node?.childForFieldName("declarator");
191+
return decl?.type === "identifier";
192+
});
193+
194+
return declaratorNode ?? null;
195+
}
196+
197+
function extractCFunctionName(func: SyntaxNode): string | undefined {
198+
const declarator = getFunctionDeclarator(func);
199+
if (!declarator) return undefined;
200+
201+
return extractCapturedTextsByCaptureName(
202+
declarator,
203+
functionQuery.functionDeclarator,
204+
functionQuery.captureName,
205+
)[0];
206+
}

src/control-flow/cfg-cpp.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import {
1313
type StatementHandlers,
1414
} from "./generic-cfg-builder.ts";
1515
import { pairwise, zip } from "./itertools.ts";
16+
import { extractCapturedTextsByCaptureName } from "./query-utils.ts";
1617

1718
export const cppLanguageDefinition = {
1819
wasmPath: treeSitterCpp,
1920
createCFGBuilder: createCFGBuilder,
2021
functionNodeTypes: ["function_definition", "lambda_expression"],
22+
extractFunctionName: extractCppFunctionName,
2123
};
2224

2325
export function createCFGBuilder(options: BuilderOptions): CFGBuilder {
@@ -148,3 +150,113 @@ function processTryStatement(trySyntax: SyntaxNode, ctx: Context): BasicBlock {
148150
});
149151
});
150152
}
153+
154+
const functionQuery = {
155+
functionDeclarator: `
156+
(function_declarator
157+
declarator: [
158+
(identifier)
159+
(type_identifier)
160+
(destructor_name)
161+
(operator_name)
162+
(operator_cast)
163+
(field_identifier)
164+
(qualified_identifier)
165+
] @name )
166+
`,
167+
168+
initDeclarator: `
169+
(init_declarator
170+
declarator: (_) @name)
171+
`,
172+
173+
captureName: "name",
174+
};
175+
176+
const validDeclaratorTypes = new Set([
177+
"identifier",
178+
"operator_name",
179+
"operator_cast",
180+
"destructor_name",
181+
"qualified_identifier",
182+
"type_identifier",
183+
"field_identifier",
184+
]);
185+
186+
/**
187+
* Get the function_declarator node for a function_definition.
188+
* Uses descendantsOfType to find it directly, even if wrapped
189+
* in pointer/reference/parenthesized declarators.
190+
*/
191+
function getFunctionDeclarator(funcDef: SyntaxNode): SyntaxNode | null {
192+
const body = funcDef.childForFieldName("body");
193+
const end = body ? body.startPosition : funcDef.endPosition;
194+
195+
const nodes = funcDef.descendantsOfType(
196+
"function_declarator",
197+
funcDef.startPosition,
198+
end,
199+
);
200+
201+
for (const node of nodes) {
202+
const decl = node?.childForFieldName("declarator");
203+
if (decl && validDeclaratorTypes.has(decl.type)) {
204+
return node;
205+
}
206+
}
207+
208+
return null;
209+
}
210+
211+
function extractCppFunctionName(func: SyntaxNode): string | undefined {
212+
if (func.type === "function_definition") {
213+
const declarator = getFunctionDeclarator(func);
214+
const name = declarator
215+
? extractCapturedTextsByCaptureName(
216+
declarator,
217+
functionQuery.functionDeclarator,
218+
functionQuery.captureName,
219+
)[0]
220+
: undefined;
221+
if (name) return name;
222+
223+
//From my observations, the only functions that do not have a function_declarator child are conversion operators.
224+
//To extract their names, I need to manipulate strings (not ideal, but it works).
225+
const fullDeclarationName = func.childForFieldName("declarator");
226+
return fullDeclarationName?.text.split("(")[0];
227+
}
228+
229+
if (func.type === "lambda_expression") {
230+
const name = findVariableBinding(func);
231+
return name;
232+
}
233+
return undefined;
234+
}
235+
236+
// Find the binding variable of a lambda function
237+
function findVariableBinding(func: SyntaxNode): string | undefined {
238+
const parent = func.parent;
239+
if (!parent) return undefined;
240+
241+
switch (parent.type) {
242+
// x = <func> -> "x" (identifier)
243+
// x.field = <func> -> "x.field" (field_expression)
244+
case "assignment_expression": {
245+
const name = parent.childForFieldName("left");
246+
console.log(name?.type);
247+
return name?.type === "identifier" || name?.type === "field_expression"
248+
? name.text
249+
: undefined;
250+
}
251+
// <type> x = <func> -> "x"
252+
case "init_declarator":
253+
return extractCapturedTextsByCaptureName(
254+
parent,
255+
functionQuery.initDeclarator,
256+
functionQuery.captureName,
257+
)[0];
258+
259+
default:
260+
return undefined;
261+
}
262+
}

src/control-flow/cfg-go.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type StatementHandlers,
1919
} from "./generic-cfg-builder";
2020
import { treeSitterNoNullNodes } from "./hacks.ts";
21+
import { extractCapturedTextsByCaptureName } from "./query-utils.ts";
2122
import { type SwitchOptions, buildSwitch, collectCases } from "./switch-utils";
2223

2324
export const goLanguageDefinition = {
@@ -28,6 +29,7 @@ export const goLanguageDefinition = {
2829
"method_declaration",
2930
"func_literal",
3031
],
32+
extractFunctionName: extractGoFunctionName,
3133
};
3234

3335
const processBreakStatement = labeledBreakProcessor(`
@@ -419,3 +421,118 @@ function processSwitchlike(
419421

420422
return blockHandler.update({ entry: headNode, exit: mergeNode });
421423
}
424+
425+
const functionQuery = {
426+
functionDeclaration: `(function_declaration
427+
name :(identifier) @name)`,
428+
429+
methodDeclaration: `(method_declaration
430+
name: (field_identifier) @name)`,
431+
432+
shortVarDeclaration: `(short_var_declaration
433+
left: (expression_list (identifier) @name))`,
434+
435+
varSpec: `(var_spec
436+
name: (identifier) @name)`,
437+
438+
assignmentStatement: `(assignment_statement
439+
left: (expression_list
440+
[
441+
(identifier) @name
442+
(selector_expression) @name
443+
]))`,
444+
445+
captureName: "name",
446+
};
447+
448+
// Find the variable or field name bound to a function literal
449+
function findVariableBinding(func: SyntaxNode): string | undefined {
450+
const parent = func.parent;
451+
if (!parent) return undefined;
452+
453+
// Walk the right-hand expression list and find the index of *this* func literal.
454+
// I compare by node id to be safe - same node, same id.
455+
const findFuncIndex = (
456+
funcNode: SyntaxNode,
457+
right: SyntaxNode,
458+
): number | null => {
459+
const index = right.namedChildren.findIndex(
460+
(child) => child?.type === "func_literal" && child.id === funcNode.id,
461+
);
462+
463+
if (index !== -1) {
464+
return index;
465+
}
466+
return null;
467+
};
468+
469+
// We run the left query -> get names[], locate our func literal on the right -> get index,
470+
// then names[index] is the binding.
471+
// If nothing matches, return undefined.
472+
const bindFromPair = (
473+
node: SyntaxNode,
474+
leftPattern: string,
475+
rightField: "right" | "value" = "right",
476+
): string | undefined => {
477+
const left = extractCapturedTextsByCaptureName(
478+
node,
479+
leftPattern,
480+
functionQuery.captureName,
481+
);
482+
const right = node.childForFieldName(rightField);
483+
if (!right) return undefined;
484+
485+
const bindingIndex = findFuncIndex(func, right);
486+
if (bindingIndex !== null) {
487+
return left[bindingIndex] ?? undefined;
488+
}
489+
return undefined;
490+
};
491+
492+
switch (parent.parent?.type) {
493+
// := short var declaration
494+
case "short_var_declaration":
495+
return bindFromPair(
496+
parent.parent,
497+
functionQuery.shortVarDeclaration,
498+
"right",
499+
);
500+
501+
// = plain assignment ...
502+
case "assignment_statement":
503+
return bindFromPair(
504+
parent.parent,
505+
functionQuery.assignmentStatement,
506+
"right",
507+
);
508+
509+
// var x, y = ..., func(){}, ...
510+
// Same idea, but Go’s var spec uses "value".
511+
case "var_spec":
512+
return bindFromPair(parent.parent, functionQuery.varSpec, "value");
513+
514+
default:
515+
return undefined;
516+
}
517+
}
518+
519+
function extractGoFunctionName(func: SyntaxNode): string | undefined {
520+
switch (func.type) {
521+
case "function_declaration":
522+
return extractCapturedTextsByCaptureName(
523+
func,
524+
functionQuery.functionDeclaration,
525+
functionQuery.captureName,
526+
)[0];
527+
case "method_declaration":
528+
return extractCapturedTextsByCaptureName(
529+
func,
530+
functionQuery.methodDeclaration,
531+
functionQuery.captureName,
532+
)[0];
533+
case "func_literal":
534+
return findVariableBinding(func);
535+
default:
536+
return undefined;
537+
}
538+
}

src/control-flow/cfg-python.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import {
1212
type StatementHandlers,
1313
} from "./generic-cfg-builder.ts";
1414
import { maybe, zip } from "./itertools.ts";
15+
import { extractCapturedTextsByCaptureName } from "./query-utils.ts";
1516

1617
export const pythonLanguageDefinition = {
1718
wasmPath: treeSitterPython,
1819
createCFGBuilder: createCFGBuilder,
1920
functionNodeTypes: ["function_definition"],
21+
extractFunctionName: extractPythonFunctionName,
2022
};
2123
const processForStatement = forEachLoopProcessor({
2224
query: `
@@ -624,3 +626,18 @@ function processWhileStatement(
624626

625627
return matcher.update({ entry: condBlock.entry, exit: exitNode });
626628
}
629+
630+
const functionQuery = {
631+
functionDefinition: `(function_definition
632+
name :(identifier) @name)`,
633+
634+
captureName: "name",
635+
};
636+
637+
function extractPythonFunctionName(func: SyntaxNode): string | undefined {
638+
return extractCapturedTextsByCaptureName(
639+
func,
640+
functionQuery.functionDefinition,
641+
functionQuery.captureName,
642+
)[0];
643+
}

0 commit comments

Comments
 (0)