diff --git a/CHANGELOG.md b/CHANGELOG.md index 2063a28b..8b615f82 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/CONTRIBUTORS.md b/CONTRIBUTORS.md index c7de511c..2618ddff 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,4 +6,6 @@ The following people have contributed to the development of Function-Graph-Overv - [Tamir Bahar](https://github.com/tmr232) - [Niv Baumel](https://github.com/Nivb1569) +- [Alon Bilman](https://github.com/AlonBilman) +- [Ben Nahmias](https://github.com/Bennahmias) - [buherator](https://github.com/v-p-b) \ No newline at end of file diff --git a/src/control-flow/cfg-c.ts b/src/control-flow/cfg-c.ts index 4d854322..6e5779ff 100644 --- a/src/control-flow/cfg-c.ts +++ b/src/control-flow/cfg-c.ts @@ -20,12 +20,14 @@ import { type StatementHandlers, } from "./generic-cfg-builder.ts"; import { treeSitterNoNullNodes } from "./hacks.ts"; +import { extractCapturedTextsByCaptureName } from "./query-utils.ts"; import { buildSwitch, collectCases } from "./switch-utils.ts"; export const cLanguageDefinition = { wasmPath: treeSitterC, createCFGBuilder: createCFGBuilder, functionNodeTypes: ["function_definition"], + extractFunctionName: extractCFunctionName, }; function getChildFieldText(node: SyntaxNode, fieldName: string): string { @@ -166,3 +168,39 @@ function processSwitchlike(switchSyntax: SyntaxNode, ctx: Context): BasicBlock { return blockHandler.update({ entry: headNode, exit: mergeNode }); } + +const functionQuery = { + functionDeclarator: `(function_declarator + declarator:(identifier)@name)`, + + captureName: "name", +}; + +function getFunctionDeclarator(funcDef: SyntaxNode): SyntaxNode | null { + const body = funcDef.childForFieldName("body"); + const end = body ? body.startPosition : funcDef.endPosition; + + const nodes = funcDef.descendantsOfType( + "function_declarator", + funcDef.startPosition, + end, + ); + + const declaratorNode = nodes.find((node) => { + const decl = node?.childForFieldName("declarator"); + return decl?.type === "identifier"; + }); + + return declaratorNode ?? null; +} + +function extractCFunctionName(func: SyntaxNode): string | undefined { + const declarator = getFunctionDeclarator(func); + if (!declarator) return undefined; + + return extractCapturedTextsByCaptureName( + declarator, + functionQuery.functionDeclarator, + functionQuery.captureName, + )[0]; +} diff --git a/src/control-flow/cfg-cpp.ts b/src/control-flow/cfg-cpp.ts index 6b3e3df8..c8691dd4 100644 --- a/src/control-flow/cfg-cpp.ts +++ b/src/control-flow/cfg-cpp.ts @@ -13,11 +13,13 @@ import { type StatementHandlers, } from "./generic-cfg-builder.ts"; import { pairwise, zip } from "./itertools.ts"; +import { extractCapturedTextsByCaptureName } from "./query-utils.ts"; export const cppLanguageDefinition = { wasmPath: treeSitterCpp, createCFGBuilder: createCFGBuilder, functionNodeTypes: ["function_definition", "lambda_expression"], + extractFunctionName: extractCppFunctionName, }; export function createCFGBuilder(options: BuilderOptions): CFGBuilder { @@ -148,3 +150,113 @@ function processTryStatement(trySyntax: SyntaxNode, ctx: Context): BasicBlock { }); }); } + +const functionQuery = { + functionDeclarator: ` + (function_declarator + declarator: [ + (identifier) + (type_identifier) + (destructor_name) + (operator_name) + (operator_cast) + (field_identifier) + (qualified_identifier) + ] @name ) +`, + + initDeclarator: ` + (init_declarator + declarator: (_) @name) + `, + + captureName: "name", +}; + +const validDeclaratorTypes = new Set([ + "identifier", + "operator_name", + "operator_cast", + "destructor_name", + "qualified_identifier", + "type_identifier", + "field_identifier", +]); + +/** + * Get the function_declarator node for a function_definition. + * Uses descendantsOfType to find it directly, even if wrapped + * in pointer/reference/parenthesized declarators. + */ +function getFunctionDeclarator(funcDef: SyntaxNode): SyntaxNode | null { + const body = funcDef.childForFieldName("body"); + const end = body ? body.startPosition : funcDef.endPosition; + + const nodes = funcDef.descendantsOfType( + "function_declarator", + funcDef.startPosition, + end, + ); + + for (const node of nodes) { + const decl = node?.childForFieldName("declarator"); + if (decl && validDeclaratorTypes.has(decl.type)) { + return node; + } + } + + return null; +} + +function extractCppFunctionName(func: SyntaxNode): string | undefined { + if (func.type === "function_definition") { + const declarator = getFunctionDeclarator(func); + const name = declarator + ? extractCapturedTextsByCaptureName( + declarator, + functionQuery.functionDeclarator, + functionQuery.captureName, + )[0] + : undefined; + if (name) return name; + + //From my observations, the only functions that do not have a function_declarator child are conversion operators. + //To extract their names, I need to manipulate strings (not ideal, but it works). + const fullDeclarationName = func.childForFieldName("declarator"); + return fullDeclarationName?.text.split("(")[0]; + } + + if (func.type === "lambda_expression") { + const name = findVariableBinding(func); + return name; + } + return undefined; +} + +// Find the binding variable of a lambda function +function findVariableBinding(func: SyntaxNode): string | undefined { + const parent = func.parent; + if (!parent) return undefined; + + switch (parent.type) { + // x = -> "x" (identifier) + // x.field = -> "x.field" (field_expression) + case "assignment_expression": { + const name = parent.childForFieldName("left"); + console.log(name?.type); + return name?.type === "identifier" || name?.type === "field_expression" + ? name.text + : undefined; + } + // x = -> "x" + case "init_declarator": + return extractCapturedTextsByCaptureName( + parent, + functionQuery.initDeclarator, + functionQuery.captureName, + )[0]; + + default: + return undefined; + } +} diff --git a/src/control-flow/cfg-go.ts b/src/control-flow/cfg-go.ts index a27164ec..27278247 100644 --- a/src/control-flow/cfg-go.ts +++ b/src/control-flow/cfg-go.ts @@ -18,6 +18,7 @@ import { type StatementHandlers, } from "./generic-cfg-builder"; import { treeSitterNoNullNodes } from "./hacks.ts"; +import { extractCapturedTextsByCaptureName } from "./query-utils.ts"; import { type SwitchOptions, buildSwitch, collectCases } from "./switch-utils"; export const goLanguageDefinition = { @@ -28,6 +29,7 @@ export const goLanguageDefinition = { "method_declaration", "func_literal", ], + extractFunctionName: extractGoFunctionName, }; const processBreakStatement = labeledBreakProcessor(` @@ -419,3 +421,118 @@ function processSwitchlike( return blockHandler.update({ entry: headNode, exit: mergeNode }); } + +const functionQuery = { + functionDeclaration: `(function_declaration + name :(identifier) @name)`, + + methodDeclaration: `(method_declaration + name: (field_identifier) @name)`, + + shortVarDeclaration: `(short_var_declaration + left: (expression_list (identifier) @name))`, + + varSpec: `(var_spec + name: (identifier) @name)`, + + assignmentStatement: `(assignment_statement + left: (expression_list + [ + (identifier) @name + (selector_expression) @name + ]))`, + + captureName: "name", +}; + +// Find the variable or field name bound to a function literal +function findVariableBinding(func: SyntaxNode): string | undefined { + const parent = func.parent; + if (!parent) return undefined; + + // Walk the right-hand expression list and find the index of *this* func literal. + // I compare by node id to be safe - same node, same id. + const findFuncIndex = ( + funcNode: SyntaxNode, + right: SyntaxNode, + ): number | null => { + const index = right.namedChildren.findIndex( + (child) => child?.type === "func_literal" && child.id === funcNode.id, + ); + + if (index !== -1) { + return index; + } + return null; + }; + + // We run the left query -> get names[], locate our func literal on the right -> get index, + // then names[index] is the binding. + // If nothing matches, return undefined. + const bindFromPair = ( + node: SyntaxNode, + leftPattern: string, + rightField: "right" | "value" = "right", + ): string | undefined => { + const left = extractCapturedTextsByCaptureName( + node, + leftPattern, + functionQuery.captureName, + ); + const right = node.childForFieldName(rightField); + if (!right) return undefined; + + const bindingIndex = findFuncIndex(func, right); + if (bindingIndex !== null) { + return left[bindingIndex] ?? undefined; + } + return undefined; + }; + + switch (parent.parent?.type) { + // := short var declaration + case "short_var_declaration": + return bindFromPair( + parent.parent, + functionQuery.shortVarDeclaration, + "right", + ); + + // = plain assignment ... + case "assignment_statement": + return bindFromPair( + parent.parent, + functionQuery.assignmentStatement, + "right", + ); + + // var x, y = ..., func(){}, ... + // Same idea, but Go’s var spec uses "value". + case "var_spec": + return bindFromPair(parent.parent, functionQuery.varSpec, "value"); + + default: + return undefined; + } +} + +function extractGoFunctionName(func: SyntaxNode): string | undefined { + switch (func.type) { + case "function_declaration": + return extractCapturedTextsByCaptureName( + func, + functionQuery.functionDeclaration, + functionQuery.captureName, + )[0]; + case "method_declaration": + return extractCapturedTextsByCaptureName( + func, + functionQuery.methodDeclaration, + functionQuery.captureName, + )[0]; + case "func_literal": + return findVariableBinding(func); + default: + return undefined; + } +} diff --git a/src/control-flow/cfg-python.ts b/src/control-flow/cfg-python.ts index cf9ad0af..6f321d19 100644 --- a/src/control-flow/cfg-python.ts +++ b/src/control-flow/cfg-python.ts @@ -12,11 +12,13 @@ import { type StatementHandlers, } from "./generic-cfg-builder.ts"; import { maybe, zip } from "./itertools.ts"; +import { extractCapturedTextsByCaptureName } from "./query-utils.ts"; export const pythonLanguageDefinition = { wasmPath: treeSitterPython, createCFGBuilder: createCFGBuilder, functionNodeTypes: ["function_definition"], + extractFunctionName: extractPythonFunctionName, }; const processForStatement = forEachLoopProcessor({ query: ` @@ -624,3 +626,18 @@ function processWhileStatement( return matcher.update({ entry: condBlock.entry, exit: exitNode }); } + +const functionQuery = { + functionDefinition: `(function_definition + name :(identifier) @name)`, + + captureName: "name", +}; + +function extractPythonFunctionName(func: SyntaxNode): string | undefined { + return extractCapturedTextsByCaptureName( + func, + functionQuery.functionDefinition, + functionQuery.captureName, + )[0]; +} diff --git a/src/control-flow/cfg-typescript.ts b/src/control-flow/cfg-typescript.ts index ff689cf1..c2acf8da 100644 --- a/src/control-flow/cfg-typescript.ts +++ b/src/control-flow/cfg-typescript.ts @@ -23,6 +23,7 @@ import { type StatementHandlers, } from "./generic-cfg-builder.ts"; import { treeSitterNoNullNodes } from "./hacks.ts"; +import { extractCapturedTextsByCaptureName } from "./query-utils.ts"; import { buildSwitch, collectCases } from "./switch-utils.ts"; const typeScriptFunctionNodeTypes = [ @@ -37,11 +38,13 @@ export const typeScriptLanguageDefinition = { wasmPath: treeSitterTypeScript, createCFGBuilder: createCFGBuilder, functionNodeTypes: typeScriptFunctionNodeTypes, + extractFunctionName: extractTypeScriptFunctionName, }; export const tsxLanguageDefinition = { wasmPath: treeSitterTSX, createCFGBuilder: createCFGBuilder, functionNodeTypes: typeScriptFunctionNodeTypes, + extractFunctionName: extractTypeScriptFunctionName, }; export function createCFGBuilder(options: BuilderOptions): CFGBuilder { @@ -319,3 +322,118 @@ function processTryStatement(trySyntax: SyntaxNode, ctx: Context): BasicBlock { }); }); } + +const functionQuery = { + functionDeclaration: `(function_declaration + (identifier) @name)`, + + variableDeclaratorIdentifier: `(variable_declarator + name: (identifier) @name)`, + + methodDefinition: `(method_definition + [ + (property_identifier) @name + (computed_property_name) @name + (private_property_identifier) @name + ])`, + + functionExpression: `(function_expression + name: (identifier) @name)`, + + generatorFunction: `(generator_function + name: (identifier) @name)`, + + generatorFunctionDeclaration: `(generator_function_declaration + name: (identifier) @name)`, + + captureName: "name", +}; + +function extractTypeScriptFunctionName(func: SyntaxNode): string | undefined { + switch (func.type) { + case "function_declaration": + return extractCapturedTextsByCaptureName( + func, + functionQuery.functionDeclaration, + functionQuery.captureName, + )[0]; + + case "generator_function": { + const names = extractCapturedTextsByCaptureName( + func, + functionQuery.generatorFunction, + functionQuery.captureName, + ); + if (names.length > 0) return names[0]; + return findVariableBinding(func); + } + + case "generator_function_declaration": { + const names = extractCapturedTextsByCaptureName( + func, + functionQuery.generatorFunctionDeclaration, + functionQuery.captureName, + ); + if (names.length > 0) return names[0]; + return findVariableBinding(func); + } + + case "arrow_function": + return findVariableBinding(func); + + case "function_expression": { + const directNames = extractCapturedTextsByCaptureName( + func, + functionQuery.functionExpression, + functionQuery.captureName, + ); + if (directNames.length > 0) return directNames[0]; + return findVariableBinding(func); + } + + case "method_definition": + return extractCapturedTextsByCaptureName( + func, + functionQuery.methodDefinition, + functionQuery.captureName, + )[0]; + + default: + return undefined; + } +} + +// Find the variable, parameter, or field that this function is bound to +function findVariableBinding(func: SyntaxNode): string | undefined { + const parent = func.parent; + if (!parent) return undefined; + + switch (parent.type) { + // const/let/var x = -> "x" + case "variable_declarator": { + const name = parent.childForFieldName("name"); + return name?.type === "identifier" ? name.text : undefined; + } + // function f(x = ) {} -> "x" (default param bindings) + case "required_parameter": { + const name = parent.childForFieldName("pattern"); + return name?.type === "identifier" ? name.text : undefined; + } + // x = -> "x" + // x.field = -> "x.field" + case "assignment_expression": + case "assignment_pattern": { + const name = parent.childForFieldName("left"); + return name?.type === "identifier" || name?.type === "member_expression" + ? name.text + : undefined; + } + // class c { field = } -> "field" + case "public_field_definition": { + const name = parent.childForFieldName("name"); + return name?.type === "property_identifier" ? name.text : undefined; + } + default: + return undefined; + } +} diff --git a/src/control-flow/cfg.ts b/src/control-flow/cfg.ts index 77be576b..a877da33 100644 --- a/src/control-flow/cfg.ts +++ b/src/control-flow/cfg.ts @@ -1,3 +1,4 @@ +import type { Node as SyntaxNode } from "web-tree-sitter"; import { cLanguageDefinition } from "./cfg-c"; import { cppLanguageDefinition } from "./cfg-cpp"; import type { BuilderOptions, CFGBuilder } from "./cfg-defs"; @@ -32,6 +33,8 @@ export type LanguageDefinition = { createCFGBuilder: (options: BuilderOptions) => CFGBuilder; /** All AST nodes types representing functions */ functionNodeTypes: string[]; + /** Extract the function name from a function node */ + extractFunctionName: (node: SyntaxNode) => string | undefined; }; export const languageDefinitions: Record = { @@ -54,3 +57,10 @@ export function newCFGBuilder( ): CFGBuilder { return languageDefinitions[language].createCFGBuilder(options); } + +export function extractFunctionName( + func: SyntaxNode, + language: Language, +): string | undefined { + return languageDefinitions[language].extractFunctionName(func); +} diff --git a/src/control-flow/query-utils.ts b/src/control-flow/query-utils.ts new file mode 100644 index 00000000..0cf188ae --- /dev/null +++ b/src/control-flow/query-utils.ts @@ -0,0 +1,23 @@ +import { Query, type Node as SyntaxNode } from "web-tree-sitter"; + +/** + * Extracts the text content of syntax tree nodes captured by a Tree-sitter query. + * + * @param node - The syntax node from which to extract the tree. + * @param query - The Tree-sitter query string to execute. + * @param captureName - The capture tag name to filter by. + * + */ +export function extractCapturedTextsByCaptureName( + node: SyntaxNode, + query: string, + captureName: string, +): string[] { + const queryObj = new Query(node.tree.language, query); + const captures = queryObj.captures(node, { maxStartDepth: 1 }); + return captures + .filter((c) => c.name === captureName && c.node.text) + .map((c) => { + return c.node.text; + }); +} diff --git a/src/render/README.md b/src/render/README.md index 193114be..8dfc432e 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 7076ef72..ffa0eb1d 100644 --- a/src/render/src/App.svelte +++ b/src/render/src/App.svelte @@ -6,6 +6,7 @@ import { onMount } from "svelte"; import { type Node as SyntaxNode } from "web-tree-sitter"; import { callProcessorFor } from "../../control-flow/call-processor"; import { type Language, newCFGBuilder } from "../../control-flow/cfg"; +import { extractFunctionName } from "../../control-flow/cfg"; import { type CFG, type GraphEdge, @@ -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; /** @@ -103,11 +154,13 @@ async function getFunctionByLine( return undefined; } -function setBackgroundColor(colors: "light" | "dark") { +function applyColors(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,55 @@ 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; + let functionData: + | { name: string; lineCount: number; language: Language } + | undefined = undefined; + + if (func && language) { + name = extractFunctionName(func, language) ?? ""; + lineCount = func.endPosition.row - func.startPosition.row + 1; + functionData = { name, lineCount, language }; + } + + // Update CFG metadata + const nodeCount: number = CFG.graph.order; + const edgeCount: number = CFG.graph.size; + // https://en.wikipedia.org/wiki/Cyclomatic_complexity + const cyclomaticComplexity: number = CFG.graph.size - nodeCount + 2; + + return { + functionData, + cfgGraphData: { + nodeCount, + edgeCount, + cyclomaticComplexity, + }, + }; +} + async function render() { try { const urlSearchParams = new URLSearchParams(window.location.search); const params = parseUrlSearchParams(urlSearchParams); - setBackgroundColor(params.colors); + applyColors(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; @@ -285,11 +396,41 @@ onMount(() => { onclick={openCode} disabled={!Boolean(codeUrl)} title={Boolean(codeUrl) ? "" : "Only available for GitHub code"} - >Open Code + Open Code + + + {#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 +452,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 +528,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 c766c391..e56d4bcd 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/extractNameC.test.ts b/src/test/extractNameC.test.ts new file mode 100644 index 00000000..91b028d9 --- /dev/null +++ b/src/test/extractNameC.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, test } from "vitest"; +import { extractFunctionName } from "../control-flow/cfg.ts"; +import { iterFunctions } from "../file-parsing/bun.ts"; + +const namesFrom = (code: string) => + [...iterFunctions(code, "C")].map((f) => extractFunctionName(f, "C")); +describe("C: function name extraction", () => { + test.each([ + [ + "simple top-level function definition", + "int add(int a, int b) { return a + b; }", + ["add"], + ], + [ + "static inline function definition", + "static inline double square(double x) { return x * x; }", + ["square"], + ], + [ + "multiple functions defined on one line", + "void start() {} void stop() {} void reset() {};", + ["start", "stop", "reset"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C: (GNU extension) nested functions", () => { + test.each([ + [ + "single nested function inside another function", + ` + void outer() { + int inner(int x) { return x + 1; } + inner(5); + } + `, + ["outer", "inner"], + ], + [ + "multiple levels of nested functions", + ` + void top() { + void mid() { + void bottom() {} + void bottom2() {} + bottom(); + bottom2(); + } + mid(); + } + `, + ["top", "mid", "bottom", "bottom2"], + ], + [ + "nested functions declared inside conditional branches", + ` + void check(int flag) { + if (flag) { + void innerA() {} + innerA(); + } else { + void innerB() {} + innerB(); + } + } + `, + ["check", "innerA", "innerB"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C: functions returning function pointers", () => { + test.each([ + [ + "recurse returns its function pointer argument", + ` + int add(int a, int b) { return a + b; } + int (*recurse(int (*f)(int, int)))(int, int) { return f; } + int main(void) { return recurse(add)(2, 3); } + `, + ["add", "recurse", "main"], + ], + [ + "select_op returns one of two function pointers", + ` + int add(int a, int b) { return a + b; } + int sub(int a, int b) { return a - b; } + int (*select_op(int t))(int, int) { return t ? add : sub; } + int main(void) { return select_op(0)(5, 5); } + `, + ["add", "sub", "select_op", "main"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); diff --git a/src/test/extractNameCpp.test.ts b/src/test/extractNameCpp.test.ts new file mode 100644 index 00000000..1b1937b4 --- /dev/null +++ b/src/test/extractNameCpp.test.ts @@ -0,0 +1,382 @@ +import { describe, expect, test } from "vitest"; +import { extractFunctionName } from "../control-flow/cfg.ts"; +import { iterFunctions } from "../file-parsing/bun.ts"; + +const namesFrom = (code: string) => + [...iterFunctions(code, "C++")].map((f) => extractFunctionName(f, "C++")); + +describe("C++: basic functions & namespaces", () => { + test.each([ + [ + "free functions (qualified and unqualified)", + ` + int add(int a, int b) { return a + b; } + unsigned int Miner::calculate_hash_code() {} + `, + ["add", "Miner::calculate_hash_code"], + ], + [ + "namespaces", + ` + namespace A { + void f() {} + namespace B { + int g() { return 1; } + } + } + `, + ["f", "g"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: class/struct members & special members", () => { + test.each([ + [ + "in-class: ctor, dtor, method, static method", + ` + struct C { + C() {} + ~C() {} + void m() {} + static int sm() { return 0; } + }; + `, + ["C", "~C", "m", "sm"], + ], + [ + "out-of-class definitions (qualified)", + ` + struct A { A(); ~A(); void foo(); }; + A::A() {} + A::~A() {} + void A::foo() {} + `, + ["A::A", "A::~A", "A::foo"], + ], + [ + "local class inside function (captures local dtor)", + ` + int square(int num) { + class X { ~X() {} }; + return num * num; + } + `, + ["square", "~X"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: lambdas", () => { + test.each([ + ["assigned to variable", "auto fn = [&](int v){ return v + 1; };", ["fn"]], + ["immediately-invoked lambda", "[](){ return 42; }();", [undefined]], + [ + "flavors: generic, mutable, noexcept", + ` + auto g = [](auto x){ return x; }; + auto m = [=]() mutable { }; + auto a = []() noexcept { }; + `, + ["g", "m", "a"], + ], + [ + "nested inside named lambda", + ` + void f() { + auto my_func = [](){ [](){}(); }; + } + `, + ["f", "my_func", undefined], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: assignments & initializers", () => { + test.each([ + [ + "declaration then assignment", + ` + std::function f; + f = [](){}; + `, + ["f"], + ], + [ + "chained assignment chooses nearest binder", + ` + std::function b; + auto a = (b = [](){}); + `, + ["b"], + ], + [ + "member assignment (field_expression on LHS)", + ` + struct S { std::function fn; }; + S s; + s.fn = [](){}; + `, + ["s.fn"], + ], + [ + "static data member assignment with scope resolution", + ` + struct T { static std::function fn; }; + std::function T::fn = [](){}; + `, + ["T::fn"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: containers & expression contexts", () => { + test.each([ + [ + "initializer list (vector) with lambdas", + ` + std::vector> v{ [](){}, [](){} }; + `, + [undefined, undefined], + ], + [ + "algorithm callback", + ` + std::vector xs{1,2,3}; + std::for_each(xs.begin(), xs.end(), [](int){}); + `, + [undefined], + ], + [ + "new/sizeof/decltype with lambdas", + ` + auto p = new auto([](){}); + sizeof([](){}); + decltype([](){}) *t = nullptr; + `, + [undefined, undefined, undefined], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: templates & specialization", () => { + test.each([ + [ + "function template", + "template T add(T a, T b) { return a + b; }", + ["add"], + ], + [ + "class template method (out-of-class)", + ` + template struct Box { void put(T); }; + template void Box::put(T) {} + `, + ["Box::put"], + ], + [ + "explicit specialization (static method)", + ` + template struct A { static void m(); }; + template <> void A::m() {} + `, + ["A::m"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: control flow & ternaries with lambdas", () => { + test.each([ + [ + "if/while/switch/for", + ` + if ( [](){ return true; }() ) {} + while ( [](){ return false; }() ) {} + switch ( [](){ return 0; }() ) { default: break; } + for (int i = [](){ return 0; }(); i < 1; ++i) {} + `, + [undefined, undefined, undefined, undefined], + ], + [ + "returning a lambda from a function", + "auto factory() { return [](){}; }", + ["factory", undefined], + ], + [ + "ternary with two lambdas", + ` + bool cond = true; + auto f = cond ? [](){} : [](){}; + `, + [undefined, undefined], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: advanced declarators", () => { + test.each([ + [ + "pointer/ref layering and arrays", + ` + int*& ref_to_ptr() { static int v=42; static int* p=&v; static int*& r=p; return r; } + int** ptr_to_ptr() { static int v=5; static int* p=&v; static int** pp=&p; return pp; } + int& ref_to_array_elem() { static int arr[3]={1,2,3}; return arr[1]; } + int (* ptr_to_array())[3] { static int arr[3]={1,2,3}; return &arr; } + int (& array_ref())[3] { static int arr[3]={1,2,3}; return arr; } + `, + [ + "ref_to_ptr", + "ptr_to_ptr", + "ref_to_array_elem", + "ptr_to_array", + "array_ref", + ], + ], + [ + "func pointers/refs + arrays of func ptrs", + ` + int (* array_of_func_ptrs()[1])(int) { + static int f(int x){ return x+1; } + static int (*arr[1])(int)={f}; + return arr; + } + + int (* func_returns_funcptr())(int) { static int inner(int x){ return x; } return inner; } + int (& func_returns_funcref())(int) { static int impl(int x){ return x; } return impl; } + + int* (* ptr_to_func_ret_ptr())(int) { return nullptr; } + + int& func_ref_impl(int& x) { return x; } + int (& ref_to_func())(int&) { return func_ref_impl; } + `, + [ + "array_of_func_ptrs", + "f", + "func_returns_funcptr", + "inner", + "func_returns_funcref", + "impl", + "ptr_to_func_ret_ptr", + "func_ref_impl", + "ref_to_func", + ], + ], + [ + "pointers-to-members and plumbing", + ` + struct PmfDemo { int f(int); int val; }; + int (PmfDemo::* ptr_to_memfunc())(int) { return &PmfDemo::f; } + int PmfDemo::* ptr_to_memvar() { return &PmfDemo::val; } + + int inner_return(int){ return 1; } + int* (* ptr_to_func_ret_ptr())(int) { return &inner_return; } + + int takes_func_ptr(int (*fp)(int)) { return fp(1); } + auto auto_return_func() -> int* { static int v=0; return &v; } + + int (* complex_func(int (*f)(int)))(int) { return f; } + `, + [ + "ptr_to_memfunc", + "ptr_to_memvar", + "inner_return", + "ptr_to_func_ret_ptr", + "takes_func_ptr", + "auto_return_func", + "complex_func", + ], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: conversion operators", () => { + test.each([ + [ + "in-class to bool", + "struct C { operator bool() const {} };", + ["operator bool"], + ], + [ + "qualified out-of-class", + ` + struct Q { operator bool() const; }; + Q::operator bool() const {} + `, + ["Q::operator bool"], + ], + [ + "template-dependent target", + ` + template + struct As { operator T() const { return T{}; } }; + `, + ["operator T"], + ], + [ + "nested qualified conversion", + ` + struct Outer { struct Inner { operator long() const; }; }; + long Outer::Inner::operator long() const { return 1; } + `, + ["Outer::Inner::operator long"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: user-defined literals", () => { + test.each([ + [ + "integer UDL", + `unsigned long long operator "" _km(unsigned long long) {}`, + ['operator "" _km'], + ], + [ + "templated UDL", + ` + template + int operator "" _tag() { return sizeof...(Cs); } + `, + ['operator "" _tag'], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("C++: friend operator", () => { + test("friend operator<< (decl + def)", () => { + const code = ` + struct S { friend S& operator<<(S&, const S&); }; + S& operator<<(S&, const S&) {} + `; + expect(namesFrom(code)).toEqual(["operator<<"]); + }); +}); + +describe("C++: qualified operator definitions", () => { + test("nested class operator()", () => { + const code = ` + struct Outer { struct Fn { int operator()(int); }; }; + int Outer::Fn::operator()(int){} + `; + expect(namesFrom(code)).toEqual(["Outer::Fn::operator()"]); + }); +}); diff --git a/src/test/extractNameGo.test.ts b/src/test/extractNameGo.test.ts new file mode 100644 index 00000000..1718d72b --- /dev/null +++ b/src/test/extractNameGo.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, test } from "vitest"; +import { extractFunctionName } from "../control-flow/cfg.ts"; +import { iterFunctions } from "../file-parsing/bun.ts"; + +const namesFrom = (code: string) => + [...iterFunctions(code, "Go")].map((f) => extractFunctionName(f, "Go")); + +describe("Go: basic functions", () => { + test("named top-level functions", () => { + const code = ` + func Add(a int, b int) int { return a + b } + func greet(name string) string { return "Hello " + name } + `; + expect(namesFrom(code)).toEqual(["Add", "greet"]); + }); +}); + +describe("Go: methods with receivers", () => { + test("value and pointer receivers", () => { + const code = ` + type Point struct { X, Y int } + func (p Point) Move(dx int, dy int) { p.X += dx; p.Y += dy } + func (p *Point) Reset() { p.X = 0; p.Y = 0 } + `; + expect(namesFrom(code)).toEqual(["Move", "Reset"]); + }); +}); + +describe("Go: special & nested contexts", () => { + test.each([ + [ + "function returning another function", + ` + func makeAdder(base int) func(int) int { + return func(x int) int { return base + x } + } + `, + ["makeAdder", undefined], + ], + [ + "functions in composite literals", + ` + var handlers = []func(int){ + func(x int) {}, + func(y int) {}, + } + `, + [undefined, undefined], + ], + [ + "nested short var", + ` + func main() { + var x = func() { + y := func() {} + y() + } + x() + } + `, + ["main", "x", "y"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("Go: assignments & invocation forms", () => { + test.each([ + [ + "short var declaration (:=) with two func literals", + ` + func main() { + x := func(a int) int { return a * 2 } + y := func() {} + _ = x; _ = y + } + `, + ["main", "x", "y"], + ], + [ + "multi-var with two func literals", + ` + func main() { + var x, y = func() {}, func() {} + _ = x; _ = y + } + `, + ["main", "x", "y"], + ], + [ + "multi-var with one func literal (prefer first name)", + ` + func main() { + var a, b = func() {} + _ = a; _ = b + } + `, + ["main", "a"], + ], + [ + "var spec with explicit type + init", + ` + func main() { + var f func() = func() {} + _ = f + } + `, + ["main", "f"], + ], + [ + "selector on LHS assignment", + ` + type S struct{ fn func() } + func main() { + var s S + s.fn = func() {} + _ = s + } + `, + ["main", "s.fn"], + ], + [ + "go/defer with func literals (anonymous)", + ` + func main() { + go func() {}() + defer func() {}() + } + `, + ["main", undefined, undefined], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); diff --git a/src/test/extractNamePython.test.ts b/src/test/extractNamePython.test.ts new file mode 100644 index 00000000..e3b2ab30 --- /dev/null +++ b/src/test/extractNamePython.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "vitest"; +import { extractFunctionName } from "../control-flow/cfg.ts"; +import { iterFunctions } from "../file-parsing/bun.ts"; + +const namesFrom = (code: string) => + [...iterFunctions(code, "Python")].map((f) => + extractFunctionName(f, "Python"), + ); + +describe("Python: function definitions", () => { + test.each([ + [ + "simple top-level function definitions", + ` + def foo(): pass + def bar(x): return x + def _private(): pass + `, + ["foo", "bar", "_private"], + ], + [ + "nested functions with callbacks and returns", + ` + def outer(): + def inner(): + def deep(): + pass + return deep + def cb(): pass + run(cb) + return inner + `, + ["outer", "inner", "deep", "cb"], + ], + [ + "async function and generator function", + ` + async def a(): pass + def gen(): + yield 1 + `, + ["a", "gen"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("Python: classes and methods", () => { + test.each([ + [ + "methods of various types", + ` + class C: + def __init__(self): pass + def m(self): pass + @staticmethod + def s(): pass + @classmethod + def c(cls): pass + async def a(self): pass + `, + ["__init__", "m", "s", "c", "a"], + ], + [ + "nested classes with methods", + ` + class Outer: + def m(self): pass + class Inner: + def n(self): pass + `, + ["m", "n"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); diff --git a/src/test/extractNameTypescript.test.ts b/src/test/extractNameTypescript.test.ts new file mode 100644 index 00000000..63b0a8fc --- /dev/null +++ b/src/test/extractNameTypescript.test.ts @@ -0,0 +1,363 @@ +import { describe, expect, test } from "vitest"; +import { extractFunctionName } from "../control-flow/cfg.ts"; +import { iterFunctions } from "../file-parsing/bun.ts"; + +const namesFrom = (code: string) => + [...iterFunctions(code, "TypeScript")].map((f) => + extractFunctionName(f, "TypeScript"), + ); + +describe("TypeScript: arrow functions", () => { + test.each([ + [ + "arrow with variable binding and generic", + "const returnInArray = (value: T): T[] => {};", + ["returnInArray"], + ], + [ + "unnamed arrow with type parameters", + "(value: T): T[] => {};", + [undefined], + ], + [ + "arrow functions inside function body", + ` + function f() { + const b = n => n + 1; + const c = async () => { return 1; }; + const d = () => () => {}; + } + `, + ["f", "b", "c", "d", undefined], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("TypeScript: function declarations and expressions", () => { + test.each([ + [ + "named function expression assigned to const", + "const myFunction = function(name: string): string {};", + ["myFunction"], + ], + [ + "named function expression with inner name (alias kept)", + "const sum = function add(): number {};", + ["add"], + ], + [ + "anonymous function expression (standalone)", + "function(name: string): string {};", + [undefined], + ], + ])("expression variants: %s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); + + test.each([ + [ + "named generator function expression assigned to const", + "const fn = function* myGenerator(input: T): Generator {};", + ["myGenerator"], + ], + ["async generator declaration", "async function* stream() {}", ["stream"]], + ])("generator/async variants: %s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("TypeScript: assignments", () => { + test.each([ + [ + "simple assignment expression inside a function", + ` + function f() { + let x; + x = () => {}; + } + `, + ["f", "x"], + ], + [ + "multiple assignment expressions", + "x = () => {}, y = () => {};", + ["x", "y"], + ], + ["chained assignment expression", "let x = y = z = () => {};", ["z"]], + [ + "nested assignment with named inner function", + "let a; a = b = function inner() {};", + ["inner"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("TypeScript: IIFE patterns", () => { + test.each([ + [ + "arrow IIFE inside function body", + ` + function f() { + const g = () => {}; + (() => {})(); + } + `, + ["f", "g", undefined], + ], + ["named function-expression IIFE", "(function Boot() {})();", ["Boot"]], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("TypeScript: objects and classes", () => { + test("object literal methods and properties", () => { + const code = ` + const o = { + a: () => {}, + b() {}, + *gen() { yield 1; }, + async c() {}, + ["computed"]() {}, + "quoted": function named() {}, + [id]: function computed() {} + }; + `; + expect(namesFrom(code)).toEqual([ + undefined, + "b", + "gen", + "c", + '["computed"]', + "named", + "computed", + ]); + }); + + describe("TypeScript: class members", () => { + test.each([ + [ + "methods, fields, accessors (incl. private + arrow field)", + ` + class C { + constructor() {} + m() {} + static s() {} + async a() {} + *g() {} + field = () => {}; + #private() {} + get x() {} + set x(v: number) {} + } + `, + ["constructor", "m", "s", "a", "g", "field", "#private", "x", "x"], + ], + [ + "class expression in extends with inline base", + ` + class C extends ( () => class Base { m(){} } )() {} + `, + [undefined, "m"], + ], + [ + "member assignments", + ` + const o: any = {}; + o.x = function named() {}; + o.y = () => {}; + `, + ["named", "o.y"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); + }); +}); + +describe("TypeScript: exports", () => { + test.each([ + [ + "default export with named function", + "export default function main() {}", + ["main"], + ], + [ + "default export with anonymous function", + "export default function () {}", + [undefined], + ], + [ + "export const with arrow function", + "export const myFunc = () => {};", + ["myFunc"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("TypeScript: nesting and complex structures", () => { + test.each([ + [ + "deeply nested functions", + ` + function f() { + const myFunc = () => { + (() => { + const innerFunc = () => { + (() => {})(); + }; + function x() {} + function y() {} + })(); + }; + } + `, + ["f", "myFunc", undefined, "innerFunc", undefined, "x", "y"], + ], + [ + "nested class with methods inside function", + ` + const outer = function namedOuter() { + class A { + m() { + class B { + n() {} + } + } + } + } + `, + ["namedOuter", "m", "n"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("TypeScript: destructuring and defaults", () => { + test.each([ + [ + "default param is named function", + "function f(x = function g() {}) {}", + ["f", "g"], + ], + ["array pattern default is arrow", "const [a = () => {}] = [];", ["a"]], + ["default param is arrow", "function f(x = () => {}) {}", ["f", "x"]], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); +}); + +describe("TypeScript: expression contexts", () => { + describe("arrays and conditionals", () => { + test.each([ + [ + "array: unnamed arrow, named fn, generator", + "const arr = [() => {}, function named() {}, function* gen(){ yield 1; }];", + [undefined, "named", "gen"], + ], + [ + "ternary: unnamed arrow vs named fn", + "const f = true ? () => {} : function alt() {};", + [undefined, "alt"], + ], + [ + "nested ternary: all unnamed arrows", + "const f = cond ? (() => {}) : (other ? () => {} : () => {});", + [undefined, undefined, undefined], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); + }); + + describe("logical expressions", () => { + test("nullish coalescing with unnamed arrows", () => { + const code = "const f = (() => {}) ?? (() => {});"; + expect(namesFrom(code)).toEqual([undefined, undefined]); + }); + }); + + describe("various expressions", () => { + test.each([ + [ + "template literal with inline function", + "const str = `${function f(){}}`;", + ["f"], + ], + ["new call with function arg", "new C(function inner() {})", ["inner"]], + ["typeof function", "const t = typeof function fn() {};", ["fn"]], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); + }); + + describe("callbacks and higher-order", () => { + test.each([ + [ + "setTimeout named callback", + "setTimeout(function tick(){}, 0);", + ["tick"], + ], + ["array.map with unnamed arrow", "[1,2,3].map(n => n + 1);", [undefined]], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); + }); + + describe("static class blocks and labels", () => { + test.each([ + [ + "static block with IIFE", + ` + class C { + static { + (function init() {})(); + } + } + `, + ["init"], + ], + [ + "labeled block with named fn and unnamed arrow IIFE", + ` + label: { + function f() {} + (() => {})(); + } + `, + ["f", undefined], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); + }); + + describe("async/await and generator contexts", () => { + test.each([ + [ + "await calling named function", + "async function f() { await (function g() {})(); }", + ["f", "g"], + ], + [ + "yield calling unnamed arrow", + "function* g() { yield (() => {})(); }", + ["g", undefined], + ], + [ + "async arrow containing generator decl", + "const a = async () => { function* g() {} };", + ["a", "g"], + ], + ])("%s", (_title, code, expected) => { + expect(namesFrom(code)).toEqual(expected); + }); + }); +});