(rel: string, content: string)
| 81 | * Best-effort: returns [] on parse failure / unsupported language. |
| 82 | */ |
| 83 | export async function analyzeDataFlow(rel: string, content: string): Promise<FunctionDataFlow[]> { |
| 84 | const lang = detectLanguage(rel); |
| 85 | if (!lang) return []; |
| 86 | let parser; |
| 87 | try { parser = await getParser(lang); } catch { return []; } |
| 88 | if (!parser) return []; |
| 89 | let tree; |
| 90 | try { tree = parser.parser.parse(content); } catch { return []; } |
| 91 | |
| 92 | const results: FunctionDataFlow[] = []; |
| 93 | |
| 94 | const analyzeFunction = (fnNode: any) => { |
| 95 | const declared = new Set<string>(); |
| 96 | collectParams(fnNode, content, declared); |
| 97 | |
| 98 | // First pass: gather locally-declared names so they don't count as free. |
| 99 | const gatherDecls = (n: any) => { |
| 100 | if (!n) return; |
| 101 | if (LOCAL_DECL_RE.test(n.type)) { |
| 102 | const nameNode = n.childForFieldName?.('name') ?? n.childForFieldName?.('left'); |
| 103 | if (nameNode && (nameNode.type === 'identifier')) { |
| 104 | declared.add(content.slice(nameNode.startIndex, nameNode.endIndex)); |
| 105 | } |
| 106 | // variable_declarator with destructuring / multiple |
| 107 | for (let i = 0; i < (n.childCount ?? 0); i++) { |
| 108 | const c = n.child(i); |
| 109 | if (c?.type === 'identifier' && i === 0) { |
| 110 | declared.add(content.slice(c.startIndex, c.endIndex)); |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | // nested function params are declared in their own scope but we keep it |
| 115 | // simple: treat them as declared so we don't over-report. |
| 116 | if (FUNC_NODE_RE.test(n.type) && n !== fnNode) { |
| 117 | collectParams(n, content, declared); |
| 118 | } |
| 119 | for (let i = 0; i < (n.childCount ?? 0); i++) gatherDecls(n.child(i)); |
| 120 | }; |
| 121 | const body = fnNode.childForFieldName?.('body') ?? fnNode; |
| 122 | gatherDecls(body); |
| 123 | |
| 124 | // Second pass: count free identifiers (not declared, not a property key). |
| 125 | const freq = new Map<string, number>(); |
| 126 | const walk = (n: any) => { |
| 127 | if (!n) return; |
| 128 | if (n.type === 'identifier') { |
| 129 | const name = content.slice(n.startIndex, n.endIndex); |
| 130 | // Allow single-char identifiers — they can be real function refs (g, _, $). |
| 131 | // Only skip empty strings and pure noise: single underscores are common |
| 132 | // throwaway vars, but we keep everything else including single letters. |
| 133 | if (name.length >= 1 && name !== '_' && !declared.has(name) && !BUILTINS.has(name)) { |
| 134 | freq.set(name, (freq.get(name) ?? 0) + 1); |
| 135 | } |
| 136 | } else if (n.type === 'member_expression') { |
| 137 | // obj.property — only walk the OBJECT side (left); skip the property name (right). |
| 138 | // Field names: object (left side), property (right side = what we skip). |
| 139 | const obj = n.childForFieldName?.('object'); |
| 140 | if (obj) walk(obj); |
no test coverage detected