diff --git a/CHANGELOG.md b/CHANGELOG.md index 2063a28..8b615f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +### Added + +- Function name extraction with support for multiple programming languages. +- Unit tests for function name extraction, covering various structures and languages. +- Frontend logic in `render/src/App.svelte` to extract and display metadata based on render type. +- Display of both CFG and function metadata in the GitHub render view, and CFG metadata in the Graph render view. + ## [0.0.16] - 2025-05-07 ### Added diff --git a/src/control-flow/cfg-c.ts b/src/control-flow/cfg-c.ts index 4d85432..59f5c5b 100644 --- a/src/control-flow/cfg-c.ts +++ b/src/control-flow/cfg-c.ts @@ -166,3 +166,17 @@ function processSwitchlike(switchSyntax: SyntaxNode, ctx: Context): BasicBlock { return blockHandler.update({ entry: headNode, exit: mergeNode }); } + +const nodeType = { + functionDefinition: "function_definition", + + // identifier lookups + identifier: "identifier", +}; + +export function extractCFunctionName(func: SyntaxNode): string | undefined { + if (func.type === nodeType.functionDefinition) { + return func.descendantsOfType(nodeType.identifier)[0]?.text; + } + return undefined; +} diff --git a/src/control-flow/cfg-cpp.ts b/src/control-flow/cfg-cpp.ts index 6b3e3df..e07909f 100644 --- a/src/control-flow/cfg-cpp.ts +++ b/src/control-flow/cfg-cpp.ts @@ -7,6 +7,7 @@ import { processReturnStatement, processThrowStatement, } from "./common-patterns.ts"; +import { extractNameByNodeType } from "./function-utils.ts"; import { type Context, GenericCFGBuilder, @@ -148,3 +149,72 @@ function processTryStatement(trySyntax: SyntaxNode, ctx: Context): BasicBlock { }); }); } + +const nodeType = { + functionDefinition: "function_definition", + lambdaExpression: "lambda_expression", + + // inline/function‑assignment cases + variableDeclaration: "variable_declaration", + initDeclarator: "init_declarator", + + // identifier lookups + identifier: "identifier", + + //Unnamed functions + anonymous: "", + + //Special cases , operator-name and destructor-name + operatorName: "operator_name", + destructorName: "destructor_name", +}; + +function findCppNameInParentHierarchy( + func: SyntaxNode, + parentType: string, + childType: string, +): string | undefined { + let parent = func.parent; + while (parent) { + if (parent.type === parentType) { + return extractNameByNodeType(parent, childType); + } + parent = parent.parent; + } + return undefined; +} + +export function extractCppFunctionName(func: SyntaxNode): string | undefined { + if (func.type === nodeType.functionDefinition) { + // Look for an operator overload (This should happen before the identifier/destructor). + const operatorNode = func.descendantsOfType(nodeType.operatorName)[0]; + if (operatorNode) return operatorNode.text; + + // else, look for the destructor name, which is a special case because identifier returns without the ~ + const destructorNode = func.descendantsOfType(nodeType.destructorName)[0]; + if (destructorNode) { + return destructorNode.text; + } + //if neither of those, look for the identifier + const idNode = func.descendantsOfType(nodeType.identifier)[0]; + if (idNode) return idNode.text; + } + if (func.type === nodeType.lambdaExpression) { + // if the lambda is assigned to a variable, return the variable name + // otherwise return "" + return ( + findCppNameInParentHierarchy( + func, + nodeType.variableDeclaration, + nodeType.identifier, + ) || + findCppNameInParentHierarchy( + func, + nodeType.initDeclarator, + nodeType.identifier, + ) || + nodeType.anonymous + ); + } + return undefined; +} diff --git a/src/control-flow/cfg-go.ts b/src/control-flow/cfg-go.ts index a27164e..7e54286 100644 --- a/src/control-flow/cfg-go.ts +++ b/src/control-flow/cfg-go.ts @@ -12,6 +12,10 @@ import { processReturnStatement, processStatementSequence, } from "./common-patterns.ts"; +import { + extractNameByNodeType, + extractTaggedValueFromTreeSitterQuery, +} from "./function-utils.ts"; import { type Context, GenericCFGBuilder, @@ -419,3 +423,75 @@ function processSwitchlike( return blockHandler.update({ entry: headNode, exit: mergeNode }); } + +const nodeType = { + // Function-related node types + functionDeclaration: "function_declaration", + methodDeclaration: "method_declaration", + funcLiteral: "func_literal", + + // identifier lookups + identifier: "identifier", + fieldIdentifier: "field_identifier", + + // Unnamed functions + anonymous: "", +}; + +const shortVarQueryAndTag = { + query: ` + (short_var_declaration + left: (expression_list + (identifier) @var.name)) + `, + tag: "var.name", +}; + +const varDeclarationQueryAndTag = { + query: ` + (var_declaration + (var_spec + (identifier) @var.name)) + `, + tag: "var.name", +}; + +const assignmentQueryAndTag = { + query: ` + (assignment_statement + left: (expression_list + (identifier) @var.name)) + `, + tag: "var.name", +}; + +export function extractGoFunctionName(func: SyntaxNode): string | undefined { + switch (func.type) { + case nodeType.functionDeclaration: + return extractNameByNodeType(func, nodeType.identifier); + case nodeType.methodDeclaration: + return extractNameByNodeType(func, nodeType.fieldIdentifier); + case nodeType.funcLiteral: + // Check if the func_literal is assigned to a variable or is a standalone function + return ( + extractTaggedValueFromTreeSitterQuery( + func, + shortVarQueryAndTag.query, + shortVarQueryAndTag.tag, + ) || + extractTaggedValueFromTreeSitterQuery( + func, + varDeclarationQueryAndTag.query, + varDeclarationQueryAndTag.tag, + ) || + extractTaggedValueFromTreeSitterQuery( + func, + assignmentQueryAndTag.query, + assignmentQueryAndTag.tag, + ) || + nodeType.anonymous + ); + default: + return undefined; + } +} diff --git a/src/control-flow/cfg-python.ts b/src/control-flow/cfg-python.ts index cf9ad0a..decffcf 100644 --- a/src/control-flow/cfg-python.ts +++ b/src/control-flow/cfg-python.ts @@ -6,6 +6,7 @@ import { forEachLoopProcessor, processStatementSequence, } from "./common-patterns.ts"; +import { extractNameByNodeType } from "./function-utils.ts"; import { type Context, GenericCFGBuilder, @@ -624,3 +625,19 @@ function processWhileStatement( return matcher.update({ entry: condBlock.entry, exit: exitNode }); } + +const nodeType = { + functionDefinition: "function_definition", + + // identifier lookups + identifier: "identifier", +}; + +export function extractPythonFunctionName( + func: SyntaxNode, +): string | undefined { + if (func.type === nodeType.functionDefinition) { + return extractNameByNodeType(func, nodeType.identifier); + } + return undefined; +} diff --git a/src/control-flow/cfg-typescript.ts b/src/control-flow/cfg-typescript.ts index ff689cf..f9c4910 100644 --- a/src/control-flow/cfg-typescript.ts +++ b/src/control-flow/cfg-typescript.ts @@ -17,6 +17,10 @@ import { processStatementSequence, processThrowStatement, } from "./common-patterns.ts"; +import { + extractNameByNodeType, + extractTaggedValueFromTreeSitterQuery, +} from "./function-utils.ts"; import { type Context, GenericCFGBuilder, @@ -319,3 +323,82 @@ function processTryStatement(trySyntax: SyntaxNode, ctx: Context): BasicBlock { }); }); } + +const nodeType = { + // function‑declaration cases + functionDeclaration: "function_declaration", + generatorFunctionDeclaration: "generator_function_declaration", + + // inline/function‑expression cases + generatorFunction: "generator_function", + arrowFunction: "arrow_function", + functionExpression: "function_expression", + + // methods + methodDefinition: "method_definition", + + // identifier lookups + identifier: "identifier", + propertyIdentifier: "property_identifier", + + // Unnamed functions + anonymous: "", +}; + +const variableDeclaratorQueryAndTag = { + query: ` + (lexical_declaration + (variable_declarator + name: (identifier) @var.name)) + `, + tag: "var.name", +}; + +/** + * Extracts the name of a TypeScript function based on its syntax node type. + * + * @param {SyntaxNode} func - The syntax node representing the TypeScript function. + * @returns {string | undefined} The function name, or `undefined` if not found. + */ +export function extractTypeScriptFunctionName( + func: SyntaxNode, +): string | undefined { + switch (func.type) { + case nodeType.functionDeclaration: + case nodeType.generatorFunctionDeclaration: + return extractNameByNodeType(func, nodeType.identifier); + + case nodeType.generatorFunction: + case nodeType.arrowFunction: + return ( + extractTaggedValueFromTreeSitterQuery( + func, + variableDeclaratorQueryAndTag.query, + variableDeclaratorQueryAndTag.tag, + ) || nodeType.anonymous + ); + + case nodeType.methodDefinition: + return extractNameByNodeType(func, nodeType.propertyIdentifier); + + case nodeType.functionExpression: { + // first check for a direct name + const optionalIdentifier = extractNameByNodeType( + func, + nodeType.identifier, + ); + if (optionalIdentifier) return optionalIdentifier; + + // otherwise fall back to a variable‑assignment name + return ( + extractTaggedValueFromTreeSitterQuery( + func, + variableDeclaratorQueryAndTag.query, + variableDeclaratorQueryAndTag.tag, + ) || nodeType.anonymous + ); + } + default: + return undefined; + } +} diff --git a/src/control-flow/function-utils.ts b/src/control-flow/function-utils.ts new file mode 100644 index 0000000..a9929a0 --- /dev/null +++ b/src/control-flow/function-utils.ts @@ -0,0 +1,83 @@ +import { Query, type Node as SyntaxNode } from "web-tree-sitter"; +import type { Language } from "./cfg"; +import { extractCFunctionName } from "./cfg-c.ts"; +import { extractCppFunctionName } from "./cfg-cpp.ts"; +import { extractGoFunctionName } from "./cfg-go.ts"; +import { extractPythonFunctionName } from "./cfg-python.ts"; +import { extractTypeScriptFunctionName } from "./cfg-typescript.ts"; + +/** + * Extracts the name of a node by searching for a child node with a specific type. + * + * @param func - The syntax node to search within. + * @param type - The type of the child node to extract the name from. + * used among all languages (mostly the easy cases of extracting the name). + */ +export function extractNameByNodeType( + func: SyntaxNode, + type: string, +): string | undefined { + return func.namedChildren.find((child) => child?.type === type)?.text; +} + +/** + * Extract a single tagged value from a syntax tree using a Tree-sitter query. + * + * @param func - The syntax node from which to extract the tree. + * @param query - The Tree-sitter query string to execute. + * @param tag - The capture tag name to filter by. + */ +export function extractTaggedValueFromTreeSitterQuery( + func: SyntaxNode, + query: string, + tag: string, +): string | undefined { + const language = func.tree.language; + const queryObj = new Query(language, query); + + const rootNode = func.tree.rootNode; + const captures = queryObj.captures(rootNode); + + const names = captures + .filter((c) => c.name === tag && c.node.text) + .map((c) => c.node.text); + + if (names.length > 1) { + return ""; + } + return names[0]; // can (and sometimes will) be undefined +} + +/** + * Extracts the name of a function based on its syntax node and language. + * + * Supports: + * - TypeScript/TSX + * - C + * - C++ + * - Python + * - Go + * + * @param func - The syntax node (The function). + * @param language - The programming language of the function. + */ +export function extractFunctionName( + func: SyntaxNode, + language: Language, +): string | undefined { + switch (language) { + case "TypeScript": + case "TSX": + return extractTypeScriptFunctionName(func); + case "C": + return extractCFunctionName(func); + case "C++": + return extractCppFunctionName(func); + case "Python": + return extractPythonFunctionName(func); + case "Go": + return extractGoFunctionName(func); + default: + return undefined; + } +} diff --git a/src/render/README.md b/src/render/README.md index 193114b..8dfc432 100644 --- a/src/render/README.md +++ b/src/render/README.md @@ -5,7 +5,9 @@ This is a frontend for rendering code from GitHub directly. Navigate to it, and use `?github=`, making sure to include the line number for your function (`#L123`) at the end of the URL (encoded as `%23`). -To choose color scheme pass `colors=light` or `colors=dark`. Default is dark. +To choose color scheme pass `colors=light` or `colors=dark`. Default is dark. + +A full example : `?github=%23L&colors=` Served at [`/render`](https://tmr232.github.io/function-graph-overview/render). diff --git a/src/render/src/App.svelte b/src/render/src/App.svelte index 7076ef7..aa625df 100644 --- a/src/render/src/App.svelte +++ b/src/render/src/App.svelte @@ -18,6 +18,7 @@ import { getLightColorList, listToScheme, } from "../../control-flow/colors"; +import { extractFunctionName } from "../../control-flow/function-utils"; import { simplifyCFG, trimFor } from "../../control-flow/graph-ops"; import { Lookup } from "../../control-flow/ranges"; import { graphToDot } from "../../control-flow/render"; @@ -27,6 +28,56 @@ import { iterFunctions, } from "../../file-parsing/vite"; +// Add state for panel and checkbox controls +let isPanelOpen = false; +let showMetadata = { + language: true, + functionName: true, + lineCount: true, + nodeCount: false, + edgeCount: false, + cyclomaticComplexity: false, +}; + +// Metadata field definitions +const metadataFields = [ + { + key: "language", + label: "Language", + value: () => functionAndCFGMetadata.functionData?.language, + }, + { + key: "functionName", + label: "Function Name", + value: () => functionAndCFGMetadata.functionData?.name, + }, + { + key: "lineCount", + label: "Line Count", + value: () => functionAndCFGMetadata.functionData?.lineCount, + }, + { + key: "nodeCount", + label: "Node Count", + value: () => functionAndCFGMetadata.cfgGraphData?.nodeCount, + }, + { + key: "edgeCount", + label: "Edge Count", + value: () => functionAndCFGMetadata.cfgGraphData?.edgeCount, + }, + { + key: "cyclomaticComplexity", + label: "Cyclomatic Complexity", + value: () => functionAndCFGMetadata.cfgGraphData?.cyclomaticComplexity, + }, +]; + +// Toggle panel open/closed +function togglePanel() { + isPanelOpen = !isPanelOpen; +} + let codeUrl: string | undefined; /** @@ -106,8 +157,10 @@ async function getFunctionByLine( function setBackgroundColor(colors: "light" | "dark") { if (colors === "dark") { document.body.style.backgroundColor = "black"; + document.body.setAttribute("data-theme", "dark"); } else { document.body.style.backgroundColor = "#ddd"; + document.body.setAttribute("data-theme", "light"); } } @@ -133,6 +186,18 @@ type Params = (GithubParams | GraphParams) & { colorScheme: ColorScheme; colors: "light" | "dark"; }; +type FunctionAndCFGMetadata = { + functionData: { + name: string; + lineCount: number; + language: Language; + }; + cfgGraphData: { + nodeCount: number; + edgeCount: number; + cyclomaticComplexity: number; + }; +}; function parseUrlSearchParams(urlSearchParams: URLSearchParams): Params { const githubUrl = urlSearchParams.get("github"); @@ -170,7 +235,9 @@ function parseUrlSearchParams(urlSearchParams: URLSearchParams): Params { }; } -async function createGitHubCFG(ghParams: GithubParams): Promise { +async function fetchFunctionAndLanguage( + ghParams: GithubParams, +): Promise<{ func: SyntaxNode; language: Language }> { const { rawUrl, line } = ghParams; const response = await fetch(rawUrl); const code = await response.text(); @@ -182,6 +249,11 @@ async function createGitHubCFG(ghParams: GithubParams): Promise { throw new Error(`Unable to find function on line ${line}`); } + return { func, language }; +} + +async function createGitHubCFG(ghParams: GithubParams): Promise { + const { func, language } = await fetchFunctionAndLanguage(ghParams); return buildCFG(func, language); } @@ -210,16 +282,50 @@ async function createCFG(params: Params): Promise { } } +let functionAndCFGMetadata: FunctionAndCFGMetadata | undefined; + +function updateMetadata(CFG: CFG, func?: SyntaxNode, language?: Language) { + // Update function metadata (GitHub only) + let name: string | undefined = undefined; + let lineCount: number | undefined = undefined; + + if (func && language) { + name = extractFunctionName(func, language); + lineCount = func.endPosition.row - func.startPosition.row + 1; + } + + // Update CFG metadata + const nodeCount: number = CFG.graph.order; + const edgeCount: number = CFG.graph.size; + const cyclomaticComplexity: number = CFG.graph.size - nodeCount + 2; // (https://en.wikipedia.org/wiki/Cyclomatic_complexity) + + return { + functionData: name && lineCount ? { name, lineCount, language } : undefined, + cfgGraphData: { + nodeCount, + edgeCount, + cyclomaticComplexity, + }, + }; +} + async function render() { try { const urlSearchParams = new URLSearchParams(window.location.search); const params = parseUrlSearchParams(urlSearchParams); setBackgroundColor(params.colors); + + const cfg = await createCFG(params); + if (params.type === "GitHub") { codeUrl = params.codeUrl; + const { func, language } = await fetchFunctionAndLanguage(params); + functionAndCFGMetadata = updateMetadata(cfg, func, language); + } else { + // Graph + functionAndCFGMetadata = updateMetadata(cfg); } - const cfg = await createCFG(params); const graphviz = await Graphviz.load(); rawSVG = graphviz.dot(graphToDot(cfg, false, params.colorScheme)); return rawSVG; @@ -289,7 +395,33 @@ onMount(() => { > + {#if functionAndCFGMetadata} + +{#if metadataFields.some(field => showMetadata[field.key] && field.value() !== undefined)} + +{/if} + + +
+

Display Options

+ {#each metadataFields.filter(field => field.value() !== undefined) as { key, label }} + + {/each} +
+ {/if} +
{#await render()}

Loading code...

@@ -311,6 +443,74 @@ onMount(() => { .controls { margin: 1em; } + + .metadata { + position: fixed; + top: 4em; + right: 18.5em; + padding: 1em; + background-color: var(--metadata-bg); + transition: right 0.2s ease; + } + + .metadata:not(.panel-open) { + right: 2.5em; + } + + .metadata span { + display: block; + margin: 0.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-color); + font-size: 1em; + } + + .panel-toggle { + position: fixed; + top: 4em; + z-index: 1001; + width: 2em; + height: 4em; + background-color: var(--toggle-bg); + color: var(--toggle-color); + border: none; + font-size: 1em; + cursor: pointer; + } + + .control-panel { + position: fixed; + top: 4em; + right: -20em; + width: 18em; + padding: 1.25em; + background-color: var(--panel-bg); + color: var(--panel-text); + box-sizing: border-box; + font-size: 1em; + transition: right 0.2s ease; + } + + .control-panel.open { + right: 0; + } + + .control-panel h3 { + margin: 0 0 1.25em; + font-size: 1.5em; + color: var(--panel-heading); + } + + .control-panel label { + display: flex; + align-items: center; + gap: 0.5em; + margin-bottom: 1em; + cursor: pointer; + } + .svgContainer { display: flex; flex-direction: column; @@ -319,4 +519,24 @@ onMount(() => { width: 100dvw; height: 100dvh; } + + :global(body), :global(body[data-theme="dark"]) { + --text-color: white; + --panel-bg: rgba(30, 30, 30, 0.7); + --panel-text: white; + --panel-heading: white; + --toggle-bg: #555; + --toggle-color: white; + --metadata-bg: rgba(30, 30, 30, 0.7); + } + + :global(body[data-theme="light"]) { + --text-color: black; + --panel-bg: rgba(240, 240, 240, 0.9); + --panel-text: black; + --panel-heading: black; + --toggle-bg: #aaa; + --toggle-color: black; + --metadata-bg: rgba(240, 240, 240, 0.9); + } diff --git a/src/render/src/app.css b/src/render/src/app.css index c766c39..e56d4bc 100644 --- a/src/render/src/app.css +++ b/src/render/src/app.css @@ -1,4 +1,5 @@ body { margin: 0; background: black; + font-family: "Courier New", Courier, monospace; } diff --git a/src/test/__snapshots__/commentTest.test.ts.snap b/src/test/__snapshots__/commentTest.test.ts.snap index 4b29210..e8700c1 100644 --- a/src/test/__snapshots__/commentTest.test.ts.snap +++ b/src/test/__snapshots__/commentTest.test.ts.snap @@ -19132,4 +19132,4 @@ Lookup { }, ], } -`; +`; \ No newline at end of file diff --git a/src/test/__snapshots__/markdown-it-graphviz.test.ts.snap b/src/test/__snapshots__/markdown-it-graphviz.test.ts.snap index 18bd764..a882fb4 100644 --- a/src/test/__snapshots__/markdown-it-graphviz.test.ts.snap +++ b/src/test/__snapshots__/markdown-it-graphviz.test.ts.snap @@ -401,4 +401,4 @@ exports[`Render DOT-CFG Samples > Render CFG with clusters 1`] = `
" -`; +`; \ No newline at end of file diff --git a/src/test/functionDetails.test.ts b/src/test/functionDetails.test.ts new file mode 100644 index 0000000..19baa67 --- /dev/null +++ b/src/test/functionDetails.test.ts @@ -0,0 +1,306 @@ +import { describe, expect, test } from "vitest"; +import { extractFunctionName } from "../control-flow/function-utils.ts"; +import { iterFunctions } from "../file-parsing/bun.ts"; + +// Go Tests +describe("Go", () => { + test("function_declaration", () => { + const code = "func main(){}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; // Extract the first function node + expect(extractFunctionName(func, "Go")).toBe("main"); + }); + + test("method_declaration", () => { + const code = + "func (l *Literal) Execute(writer io.Writer, data map[string]interface{}) core.ExecuteState {}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe("Execute"); + }); + + test("func_literal with no parent", () => { + const code = "return func(x int) int {}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe(""); + }); + + test("func_literal assigned to variable", () => { + const code = "isDebugInts := func(s string) bool {}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe("isDebugInts"); + }); + + test("multiple variable names from short var declaration", () => { + const code = "y, x := func() {}, func() {}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe(""); + }); + + test("func_literal assigned to var with single identifier", () => { + const code = "var x = func() {}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe("x"); + }); + + test("func_literal assigned without var keyword", () => { + const code = "x = func() {}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe("x"); + }); + + test("func_literal assigned to multiple vars using 'var'", () => { + const code = "var x, y = func() {}, func() {}"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe(""); + }); + + test("nested function", () => { + const code = "func outer() { func inner() {} inner() }"; + const funcIterator = iterFunctions(code, "Go"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Go")).toBe("outer"); + }); + + test("nested short var", () => { + const code = `func main() { +\tvar x = func() { +\t\ty := func() {} +\t\ty() +\t} +\tx() +}`; + const funcIterator = iterFunctions(code, "Go"); + const foundNames = [...funcIterator].map((func) => + extractFunctionName(func, "Go"), + ); + const expectedNames = ["main", "x", "y"]; + expect(foundNames).toContainEqual(expectedNames); + }); +}); + +// C Tests +describe("C", () => { + test("function_definition", () => { + const code = "static void tr_where (const void caller, Dl_infoinfo) {}"; + const funcIterator = iterFunctions(code, "C"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C")).toBe("tr_where"); + }); + + test("inline function", () => { + const code = "inline int add(int a, int b) {}"; + const funcIterator = iterFunctions(code, "C"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C")).toBe("add"); + }); +}); + +// C++ Tests +describe("C++", () => { + test("function_definition", () => { + const code = "unsigned int Miner::calculate_hash_code() {}"; + const funcIterator = iterFunctions(code, "C++"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C++")).toBe("calculate_hash_code"); + }); + + test("lambda_expression - variable", () => { + const code = "std::function func2 = [&](int value) -> int {};"; + const funcIterator = iterFunctions(code, "C++"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C++")).toBe("func2"); + }); + + test("lambda_expression - Anonymous", () => { + const code = "[](int value) {};"; + const funcIterator = iterFunctions(code, "C++"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C++")).toBe(""); + }); + + test("template function", () => { + const code = "template T add(T a, T b) {}"; + const funcIterator = iterFunctions(code, "C++"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C++")).toBe("add"); + }); + + test("operator overloading", () => { + const code = "MyType operator+(const MyType& other) const {}"; + const funcIterator = iterFunctions(code, "C++"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C++")).toBe("operator+"); + }); + + test("constructor and destructor functions", () => { + const constructorCode = "MyClass() {}"; + const destructorCode = "~MyClass() {}"; + const funcIteratorConstructor = iterFunctions(constructorCode, "C++"); + const funcIteratorDestructor = iterFunctions(destructorCode, "C++"); + const constructorFunc = funcIteratorConstructor.next().value; + const destructorFunc = funcIteratorDestructor.next().value; + expect(extractFunctionName(constructorFunc, "C++")).toBe("MyClass"); + expect(extractFunctionName(destructorFunc, "C++")).toBe("~MyClass"); + }); + + test("virtual function", () => { + const code = "virtual void speak() {}"; + const funcIterator = iterFunctions(code, "C++"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C++")).toBe("speak"); + }); + + test("class nested in function", () => { + const code = ` + int square(int num) { + class X { + ~X() {}; + }; + return num * num; +}`; + const funcIterator = iterFunctions(code, "C++"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "C++")).toBe("square"); + }); + + test("anynomous lambda inside named lambda", () => { + const code = `void f() { + auto my_func = [](){ + [](){}(); + }; +}`; + const funcIterator = iterFunctions(code, "C++"); + const foundNames = [...funcIterator].map((func) => + extractFunctionName(func, "C++"), + ); + const expectedNames = ["f", "my_func", ""]; + expect(foundNames).toContainEqual(expectedNames); + }); +}); + +// Python Tests +describe("Python", () => { + test("function_definition", () => { + const code = "def now(tz: Optional[TZ_EXPR] = None) -> Arrow:"; + const funcIterator = iterFunctions(code, "Python"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Python")).toBe("now"); + }); + + test("function_definition with async", () => { + const code = "async def fetch_recent_messages(client):"; + const funcIterator = iterFunctions(code, "Python"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "Python")).toBe("fetch_recent_messages"); + }); +}); + +// TypeScript Tests +describe("TypeScript", () => { + test("function_declaration", () => { + const code = "export function getStatementHandlers(): StatementHandlers {}"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe( + "getStatementHandlers", + ); + }); + + test("arrow_function with variable", () => { + const code = "const returnInArray = (value: T): T[] => {};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("returnInArray"); + }); + + test("arrow_function with no parent", () => { + const code = "(value: T): T[] => {};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe(""); + }); + + test("arrow functions assigned to multiple variables", () => { + const code = "let x = () => {}, y = () => {};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe(""); + }); + + test("method_definition", () => { + const code = + "class SmartPhone { setPrice(smartPhonePrice: number) : void {} }"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("setPrice"); + }); + + test("function_expression with variable", () => { + const code = "const myFunction = function(name1: string): string {};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("myFunction"); + }); + + test("function_expression with direct name", () => { + const code = "const sum = function add(): number {};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("add"); + }); + + test("function_expression with no variable", () => { + const code = "function(name1: string): string {};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe(""); + }); + + test("generator_function with variable", () => { + const code = "const fn = function* (input: T): Generator {}"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("fn"); + }); + + test("generator_function with no variable", () => { + const code = "function* (input: T): Generator {}"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe(""); + }); + + test("generator_function_declaration", () => { + const code = + "function* iterTestFunctions(tree: Parser.Tree): Generator {}"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("iterTestFunctions"); + }); + test("var arrow function", () => { + const code = "var name = ()=>{};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("name"); + }); + test("global arrow function", () => { + const code = "name = ()=>{};"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe("name"); + }); + + test("constant in anonymous arrow function", () => { + const code = "()=>{ const x = 2; };"; + const funcIterator = iterFunctions(code, "TypeScript"); + const func = funcIterator.next().value; + expect(extractFunctionName(func, "TypeScript")).toBe(""); + }); +});