mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
fix(lua): extract assigned function expressions
This commit is contained in:
@@ -12,6 +12,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Lua and Luau function expressions assigned to locals, table members, or keyed table fields are now indexed as callable nodes. Calls from `local f = function() ... end`, `M.f = function() ... end`, and callback tables such as `M.handlers = { onClick = function() ... end }` are attributed to the named function or method instead of collapsing onto the file node, so callers and impact no longer omit these handlers. Re-index after upgrading. (#1616)
|
||||||
|
|
||||||
## [1.6.0] - 2026-08-26
|
## [1.6.0] - 2026-08-26
|
||||||
|
|
||||||
|
|||||||
@@ -8486,6 +8486,58 @@ function M:send(data) return self end
|
|||||||
const send = methods.find((m) => m.name === 'send');
|
const send = methods.find((m) => m.name === 'send');
|
||||||
expect(send?.qualifiedName).toBe('M::send');
|
expect(send?.qualifiedName).toBe('M::send');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should name function expressions from local, member, and table-field bindings', () => {
|
||||||
|
const code = `
|
||||||
|
local function helper() return 1 end
|
||||||
|
local localFn = function() return helper() end
|
||||||
|
local M = {
|
||||||
|
callbacks = {
|
||||||
|
onStart = function() return helper() end,
|
||||||
|
["onStop"] = function() return helper() end,
|
||||||
|
[DYNAMIC] = function() return helper() end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
M.assignedFn = function() return helper() end
|
||||||
|
M["bracketFn"] = function() return helper() end
|
||||||
|
localFn()
|
||||||
|
`;
|
||||||
|
const result = extractFromSource('handlers.lua', code);
|
||||||
|
const localFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'localFn');
|
||||||
|
const assignedFn = result.nodes.find(
|
||||||
|
(n) => n.kind === 'method' && n.qualifiedName === 'M::assignedFn'
|
||||||
|
);
|
||||||
|
const onStart = result.nodes.find(
|
||||||
|
(n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::onStart'
|
||||||
|
);
|
||||||
|
const onStop = result.nodes.find(
|
||||||
|
(n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::onStop'
|
||||||
|
);
|
||||||
|
const bracketFn = result.nodes.find(
|
||||||
|
(n) => n.kind === 'method' && n.qualifiedName === 'M::bracketFn'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(localFn).toBeDefined();
|
||||||
|
expect(assignedFn).toBeDefined();
|
||||||
|
expect(onStart).toBeDefined();
|
||||||
|
expect(onStop).toBeDefined();
|
||||||
|
expect(bracketFn).toBeDefined();
|
||||||
|
expect(result.nodes.some((n) => n.name === 'DYNAMIC')).toBe(false);
|
||||||
|
expect(result.nodes.some((n) => n.kind === 'variable' && n.name === 'localFn')).toBe(false);
|
||||||
|
|
||||||
|
for (const callable of [localFn, assignedFn, onStart, onStop, bracketFn]) {
|
||||||
|
expect(
|
||||||
|
result.unresolvedReferences.some(
|
||||||
|
(r) => r.fromNodeId === callable!.id && r.referenceKind === 'calls' && r.referenceName === 'helper'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
result.unresolvedReferences.some(
|
||||||
|
(r) => r.referenceKind === 'calls' && r.referenceName === 'localFn'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Variable extraction', () => {
|
describe('Variable extraction', () => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ local function localFn(...)
|
|||||||
return select("#", ...)
|
return select("#", ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- doc for anonAssigned (variable, initializer invisible)
|
-- doc for anonAssigned (function named from its local binding)
|
||||||
local anonAssigned = function(v)
|
local anonAssigned = function(v)
|
||||||
return hidden(v)
|
return hidden(v)
|
||||||
end
|
end
|
||||||
@@ -68,6 +68,15 @@ M.assigned = function(z)
|
|||||||
return topFn(z)
|
return topFn(z)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M.callbacks = {
|
||||||
|
on_start = function()
|
||||||
|
return topFn(17)
|
||||||
|
end,
|
||||||
|
["on_stop"] = function()
|
||||||
|
return topFn(18)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
M.handlers = { on_start = topFn, on_stop = localFn, skipped = missing }
|
M.handlers = { on_start = topFn, on_stop = localFn, skipped = missing }
|
||||||
local tbl = { cb = topFn, [1] = localFn, nested = { deep_cb = topFn } }
|
local tbl = { cb = topFn, [1] = localFn, nested = { deep_cb = topFn } }
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,22 @@ describe.skipIf(!kernelBuilt)('kernel Lua/Luau extraction parity', () => {
|
|||||||
// lua functions carry NO isExported (undefined — not false).
|
// lua functions carry NO isExported (undefined — not false).
|
||||||
const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'topFn');
|
const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'topFn');
|
||||||
expect(fn?.isExported).toBeUndefined();
|
expect(fn?.isExported).toBeUndefined();
|
||||||
|
expect(result.nodes.some((n) => n.kind === 'function' && n.name === 'anonAssigned')).toBe(true);
|
||||||
|
expect(result.nodes.some((n) => n.kind === 'method' && n.qualifiedName === 'M::assigned')).toBe(true);
|
||||||
|
expect(
|
||||||
|
result.nodes.some((n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::on_start')
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
result.nodes.some((n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::on_stop')
|
||||||
|
).toBe(true);
|
||||||
|
for (const qualifiedName of ['M::assigned', 'M.callbacks::on_start', 'M.callbacks::on_stop']) {
|
||||||
|
const callable = result.nodes.find((n) => n.qualifiedName === qualifiedName)!;
|
||||||
|
expect(
|
||||||
|
refs.some(
|
||||||
|
(r) => r.fromNodeId === callable.id && r.referenceKind === 'calls' && r.referenceName === 'topFn'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
// variables DO carry isExported === false.
|
// variables DO carry isExported === false.
|
||||||
const v = result.nodes.find((n) => n.kind === 'variable' && n.name === 'core');
|
const v = result.nodes.find((n) => n.kind === 'variable' && n.name === 'core');
|
||||||
expect(v?.isExported).toBe(false);
|
expect(v?.isExported).toBe(false);
|
||||||
|
|||||||
@@ -2001,6 +2001,32 @@ func main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Lua function-expression resolution (#1616)', () => {
|
||||||
|
it('attributes helper calls to each assigned callable instead of the file node', async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'util.lua'),
|
||||||
|
`util = {}\nfunction util.helper() return 1 end\nreturn util\n`
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempDir, 'handlers.lua'),
|
||||||
|
`local M = {}\nfunction M.namedFn() return util.helper() end\nM.assignedFn = function() return util.helper() end\nM.callbacks = { onStart = function() return util.helper() end }\nreturn M\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
cg = await CodeGraph.init(tempDir, { index: true });
|
||||||
|
cg.resolveReferences();
|
||||||
|
|
||||||
|
const helper = cg
|
||||||
|
.getNodesByKind('method')
|
||||||
|
.find((n) => n.qualifiedName === 'util::helper');
|
||||||
|
expect(helper).toBeDefined();
|
||||||
|
const callers = cg.getCallers(helper!.id).map((c) => c.node);
|
||||||
|
expect(callers.some((n) => n.qualifiedName === 'M::namedFn')).toBe(true);
|
||||||
|
expect(callers.some((n) => n.qualifiedName === 'M::assignedFn')).toBe(true);
|
||||||
|
expect(callers.some((n) => n.qualifiedName === 'M.callbacks::onStart')).toBe(true);
|
||||||
|
expect(callers.some((n) => n.kind === 'file' && n.filePath === 'handlers.lua')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Watchdog-safe resolution on collision-heavy repos (#1122)', () => {
|
describe('Watchdog-safe resolution on collision-heavy repos (#1122)', () => {
|
||||||
// On a large Java-style repo, per-ref resolution cost is unbounded in the
|
// On a large Java-style repo, per-ref resolution cost is unbounded in the
|
||||||
// worst case (a colliding method name whose candidate set misses the LRU
|
// worst case (a colliding method name whose candidate set misses the LRU
|
||||||
|
|||||||
+132
-12
@@ -468,7 +468,7 @@ impl<'t> Walker<'t> {
|
|||||||
}
|
}
|
||||||
// plain path returns false → children re-visited (the
|
// plain path returns false → children re-visited (the
|
||||||
// typeof(require(...)) alias+import pair rides this).
|
// typeof(require(...)) alias+import pair rides this).
|
||||||
} else if kind == "variable_declaration" {
|
} else if matches!(kind, "variable_declaration" | "assignment_statement") {
|
||||||
self.extract_variable(node);
|
self.extract_variable(node);
|
||||||
// Initializer subtrees are never walked — candidates only.
|
// Initializer subtrees are never walked — candidates only.
|
||||||
self.scan_fn_ref_subtree(node, 0);
|
self.scan_fn_ref_subtree(node, 0);
|
||||||
@@ -578,21 +578,38 @@ impl<'t> Walker<'t> {
|
|||||||
}
|
}
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
};
|
};
|
||||||
let names: Vec<Node<'t>> = match var_list {
|
let targets: Vec<Node<'t>> = match var_list {
|
||||||
Some(vl) => {
|
Some(vl) => {
|
||||||
let mut c = vl.walk();
|
let mut c = vl.walk();
|
||||||
vl.named_children(&mut c).filter(|n| n.kind() == "identifier").collect()
|
vl.named_children(&mut c).collect()
|
||||||
}
|
}
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
};
|
};
|
||||||
for (i, name_node) in names.iter().enumerate() {
|
for (i, name_node) in targets.iter().enumerate() {
|
||||||
let name = self.text(*name_node);
|
let Some((name, receiver, full_name)) = self.lua_assignment_target(*name_node) else {
|
||||||
if name.is_empty() {
|
continue;
|
||||||
|
};
|
||||||
|
let value = values.get(i).copied();
|
||||||
|
if let Some(value) = value {
|
||||||
|
if value.kind() == "function_definition" {
|
||||||
|
self.extract_lua_function_value(
|
||||||
|
value,
|
||||||
|
name,
|
||||||
|
receiver,
|
||||||
|
docstring.clone(),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if value.kind() == "table_constructor" {
|
||||||
|
self.extract_lua_table_functions(value, full_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dotted assignments update table members, not standalone vars.
|
||||||
|
if receiver.is_some() || node.kind() == "assignment_statement" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Positional value pairing; a missing value → NO signature key.
|
// Positional value pairing; a missing value → NO signature key.
|
||||||
let signature = values.get(i).map(|v| util::init_signature(self.text(*v)));
|
let signature = value.map(|v| util::init_signature(self.text(v)));
|
||||||
let name = name.to_string();
|
|
||||||
self.create_node(
|
self.create_node(
|
||||||
"variable",
|
"variable",
|
||||||
&name,
|
&name,
|
||||||
@@ -607,6 +624,110 @@ impl<'t> Walker<'t> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lua_assignment_target(&self, node: Node<'t>) -> Option<(String, Option<String>, String)> {
|
||||||
|
if node.kind() == "identifier" {
|
||||||
|
let name = self.text(node).trim().to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some((name.clone(), None, name));
|
||||||
|
}
|
||||||
|
if !matches!(
|
||||||
|
node.kind(),
|
||||||
|
"dot_index_expression" | "method_index_expression" | "bracket_index_expression"
|
||||||
|
) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let table = node.child_by_field_name("table")?;
|
||||||
|
let field = node
|
||||||
|
.child_by_field_name("field")
|
||||||
|
.or_else(|| node.child_by_field_name("method"))?;
|
||||||
|
let receiver = self.text(table).trim().to_string();
|
||||||
|
let name = self.lua_static_field_name(field, node.kind() == "bracket_index_expression");
|
||||||
|
if receiver.is_empty() || name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let full_name = format!("{receiver}.{name}");
|
||||||
|
Some((name, Some(receiver), full_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_static_field_name(&self, node: Node<'t>, bracketed: bool) -> String {
|
||||||
|
if node.kind() == "identifier" {
|
||||||
|
return if bracketed {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
self.text(node).trim().to_string()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if node.kind() == "string" {
|
||||||
|
let mut cursor = node.walk();
|
||||||
|
for child in node.named_children(&mut cursor) {
|
||||||
|
if child.kind() == "string_content" {
|
||||||
|
return self.text(child).trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_lua_function_value(
|
||||||
|
&mut self,
|
||||||
|
node: Node<'t>,
|
||||||
|
name: String,
|
||||||
|
receiver: Option<String>,
|
||||||
|
docstring: Option<String>,
|
||||||
|
) {
|
||||||
|
let signature = self.signature_of(node);
|
||||||
|
let (kind, qualified_name_override, is_exported) = match receiver {
|
||||||
|
Some(receiver) => (
|
||||||
|
"method",
|
||||||
|
Some(format!("{receiver}::{name}")),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
None => ("function", None, self.is_exported_of(node)),
|
||||||
|
};
|
||||||
|
let row = self.create_node(
|
||||||
|
kind,
|
||||||
|
&name,
|
||||||
|
node,
|
||||||
|
Extra {
|
||||||
|
docstring,
|
||||||
|
signature,
|
||||||
|
qualified_name_override,
|
||||||
|
is_exported,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let Some(row) = row else { return };
|
||||||
|
self.stack.push(Scope { row, kind, name });
|
||||||
|
if let Some(body) = node.child_by_field_name("body") {
|
||||||
|
self.visit_body(body);
|
||||||
|
}
|
||||||
|
self.stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_lua_table_functions(&mut self, table: Node<'t>, receiver: String) {
|
||||||
|
let mut cursor = table.walk();
|
||||||
|
let fields: Vec<Node<'t>> = table.named_children(&mut cursor).collect();
|
||||||
|
for field in fields {
|
||||||
|
if field.kind() != "field" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(name_node) = field.child_by_field_name("name") else { continue };
|
||||||
|
let Some(value) = field.child_by_field_name("value") else { continue };
|
||||||
|
let bracketed = self.text(field).trim_start().starts_with('[');
|
||||||
|
let name = self.lua_static_field_name(name_node, bracketed);
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if value.kind() == "function_definition" {
|
||||||
|
self.extract_lua_function_value(value, name, Some(receiver.clone()), None);
|
||||||
|
} else if value.kind() == "table_constructor" {
|
||||||
|
self.extract_lua_table_functions(value, format!("{receiver}.{name}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- extractTypeAlias (2890; plain path 2967-2991) — luau only --------
|
// --- extractTypeAlias (2890; plain path 2967-2991) — luau only --------
|
||||||
|
|
||||||
/// Returns skipChildren (always false on the plain path).
|
/// Returns skipChildren (always false on the plain path).
|
||||||
@@ -790,13 +911,12 @@ impl<'t> Walker<'t> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Halt at nested function definitions (their bodies are walked — and
|
// Halt at nested function definitions (their bodies are walked — and
|
||||||
// attributed — by extractFunction). function_definition (anonymous)
|
// attributed — by extractFunction). Lua function_definition values are
|
||||||
// is deliberately NOT in the halt list — the scan descends into
|
// now extracted from their assignment target and must stop this scan too.
|
||||||
// anonymous initializer bodies, attributing candidates to the file.
|
|
||||||
if depth > 0
|
if depth > 0
|
||||||
&& matches!(
|
&& matches!(
|
||||||
node.kind(),
|
node.kind(),
|
||||||
"function_declaration" | "arrow_function" | "function_expression"
|
"function_declaration" | "function_definition" | "arrow_function" | "function_expression"
|
||||||
| "lambda_literal" | "lambda_expression"
|
| "lambda_literal" | "lambda_expression"
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,4 +21,4 @@
|
|||||||
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
|
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
|
||||||
* in the product is load-bearing").
|
* in the product is load-bearing").
|
||||||
*/
|
*/
|
||||||
export const EXTRACTION_VERSION = 25;
|
export const EXTRACTION_VERSION = 26;
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ export const luaExtractor: LanguageExtractor = {
|
|||||||
typeAliasTypes: [],
|
typeAliasTypes: [],
|
||||||
importTypes: [], // `require` is a function_call — handled in visitNode below
|
importTypes: [], // `require` is a function_call — handled in visitNode below
|
||||||
callTypes: ['function_call'],
|
callTypes: ['function_call'],
|
||||||
variableTypes: ['variable_declaration'], // see the `lua` branch in extractVariable
|
// Top-level assignments can introduce module members just as declarations do:
|
||||||
|
// `M.run = function() ... end`. The Lua branch in extractVariable ignores
|
||||||
|
// non-callable member assignments, but extracts function-valued targets.
|
||||||
|
variableTypes: ['variable_declaration', 'assignment_statement'],
|
||||||
nameField: 'name',
|
nameField: 'name',
|
||||||
bodyField: 'body',
|
bodyField: 'body',
|
||||||
paramsField: 'parameters',
|
paramsField: 'parameters',
|
||||||
|
|||||||
@@ -606,6 +606,7 @@ export class TreeSitterExtractor {
|
|||||||
const nodeType = node.type;
|
const nodeType = node.type;
|
||||||
if (depth > 0 && (
|
if (depth > 0 && (
|
||||||
this.extractor?.functionTypes.includes(nodeType) ||
|
this.extractor?.functionTypes.includes(nodeType) ||
|
||||||
|
((this.language === 'lua' || this.language === 'luau') && nodeType === 'function_definition') ||
|
||||||
nodeType === 'arrow_function' ||
|
nodeType === 'arrow_function' ||
|
||||||
nodeType === 'function_expression' ||
|
nodeType === 'function_expression' ||
|
||||||
nodeType === 'lambda_literal' ||
|
nodeType === 'lambda_literal' ||
|
||||||
@@ -2817,14 +2818,28 @@ export class TreeSitterExtractor {
|
|||||||
const varList = assign.namedChildren.find((c) => c.type === 'variable_list');
|
const varList = assign.namedChildren.find((c) => c.type === 'variable_list');
|
||||||
const exprList = assign.namedChildren.find((c) => c.type === 'expression_list');
|
const exprList = assign.namedChildren.find((c) => c.type === 'expression_list');
|
||||||
const values = exprList ? exprList.namedChildren : [];
|
const values = exprList ? exprList.namedChildren : [];
|
||||||
const names = varList ? varList.namedChildren.filter((c) => c.type === 'identifier') : [];
|
const targets = varList ? varList.namedChildren : [];
|
||||||
names.forEach((nameNode, i) => {
|
targets.forEach((nameNode, i) => {
|
||||||
const name = getNodeText(nameNode, this.source);
|
|
||||||
if (!name) return;
|
|
||||||
const valueNode = values[i];
|
const valueNode = values[i];
|
||||||
|
const target = this.luaAssignmentTarget(nameNode);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
if (valueNode?.type === 'function_definition') {
|
||||||
|
this.extractLuaFunctionValue(valueNode, target.name, target.receiver, docstring);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueNode?.type === 'table_constructor') {
|
||||||
|
this.extractLuaTableFunctions(valueNode, target.fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A dotted assignment updates a table member; it is not a standalone
|
||||||
|
// variable node. Function-valued members were handled above.
|
||||||
|
if (target.receiver || node.type === 'assignment_statement') return;
|
||||||
|
|
||||||
const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined;
|
const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined;
|
||||||
const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
|
const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
|
||||||
this.createNode(kind, name, nameNode, { docstring, signature: initSignature, isExported });
|
this.createNode(kind, target.name, nameNode, { docstring, signature: initSignature, isExported });
|
||||||
});
|
});
|
||||||
} else if (this.language === 'c') {
|
} else if (this.language === 'c') {
|
||||||
// C: a `declaration` node's name nests inside the `declarator` field —
|
// C: a `declaration` node's name nests inside the `declarator` field —
|
||||||
@@ -2904,6 +2919,77 @@ export class TreeSitterExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve a Lua assignment target into its callable name and optional table receiver. */
|
||||||
|
private luaAssignmentTarget(node: SyntaxNode): { name: string; receiver?: string; fullName: string } | null {
|
||||||
|
if (node.type === 'identifier') {
|
||||||
|
const name = getNodeText(node, this.source).trim();
|
||||||
|
return name ? { name, fullName: name } : null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
node.type !== 'dot_index_expression' &&
|
||||||
|
node.type !== 'method_index_expression' &&
|
||||||
|
node.type !== 'bracket_index_expression'
|
||||||
|
) return null;
|
||||||
|
const table = getChildByField(node, 'table');
|
||||||
|
const field = getChildByField(node, 'field') ?? getChildByField(node, 'method');
|
||||||
|
if (!table || !field) return null;
|
||||||
|
const receiver = getNodeText(table, this.source).trim();
|
||||||
|
const name = this.luaStaticFieldName(field, node.type === 'bracket_index_expression');
|
||||||
|
if (!receiver || !name) return null;
|
||||||
|
return { name, receiver, fullName: `${receiver}.${name}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A statically-known Lua field name; dynamic bracket keys are not callable identities. */
|
||||||
|
private luaStaticFieldName(node: SyntaxNode, bracketed: boolean): string {
|
||||||
|
if (node.type === 'identifier') {
|
||||||
|
return bracketed ? '' : getNodeText(node, this.source).trim();
|
||||||
|
}
|
||||||
|
if (node.type === 'string') {
|
||||||
|
const content = node.namedChildren.find((child) => child.type === 'string_content');
|
||||||
|
return content ? getNodeText(content, this.source).trim() : '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract an anonymous Lua function using the name supplied by its assignment target. */
|
||||||
|
private extractLuaFunctionValue(
|
||||||
|
node: SyntaxNode,
|
||||||
|
name: string,
|
||||||
|
receiver?: string,
|
||||||
|
docstring?: string
|
||||||
|
): void {
|
||||||
|
if (!this.extractor) return;
|
||||||
|
const signature = this.extractor.getSignature?.(node, this.source);
|
||||||
|
const extra: Partial<Node> = { docstring, signature };
|
||||||
|
if (receiver) extra.qualifiedName = this.composeReceiverQualifiedName(receiver, name);
|
||||||
|
else extra.isExported = this.extractor.isExported?.(node, this.source);
|
||||||
|
|
||||||
|
const functionNode = this.createNode(receiver ? 'method' : 'function', name, node, extra);
|
||||||
|
if (!functionNode) return;
|
||||||
|
this.nodeStack.push(functionNode.id);
|
||||||
|
const body = getChildByField(node, this.extractor.bodyField);
|
||||||
|
if (body) this.visitFunctionBody(body, functionNode.id);
|
||||||
|
this.nodeStack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract function-valued keyed fields from a Lua table, including nested tables. */
|
||||||
|
private extractLuaTableFunctions(table: SyntaxNode, receiver: string): void {
|
||||||
|
for (const field of table.namedChildren) {
|
||||||
|
if (field.type !== 'field') continue;
|
||||||
|
const nameNode = getChildByField(field, 'name');
|
||||||
|
const valueNode = getChildByField(field, 'value');
|
||||||
|
if (!nameNode || !valueNode) continue;
|
||||||
|
const bracketed = getNodeText(field, this.source).trimStart().startsWith('[');
|
||||||
|
const name = this.luaStaticFieldName(nameNode, bracketed);
|
||||||
|
if (!name) continue;
|
||||||
|
if (valueNode.type === 'function_definition') {
|
||||||
|
this.extractLuaFunctionValue(valueNode, name, receiver);
|
||||||
|
} else if (valueNode.type === 'table_constructor') {
|
||||||
|
this.extractLuaTableFunctions(valueNode, `${receiver}.${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract a type alias (e.g. `export type X = ...` in TypeScript).
|
* Extract a type alias (e.g. `export type X = ...` in TypeScript).
|
||||||
* For languages like Go, resolveTypeAliasKind detects when the type_spec
|
* For languages like Go, resolveTypeAliasKind detects when the type_spec
|
||||||
|
|||||||
Reference in New Issue
Block a user