Merge remote-tracking branch 'origin/main' into fix/kotlin-property-initializer-walk

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
danusha2345
2026-09-08 18:55:08 +03:00
10 changed files with 456 additions and 23 deletions
+2
View File
@@ -222,6 +222,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Kotlin `init { }` blocks and destructuring declarations no longer swallow their code: `init { val cfg = load() }` and `val (a, b) = makePair()` contributed no call edge at all, and now attribute to the enclosing class or file.
- A Kotlin property's accessor body now belongs to the property whichever line it is written on, instead of being dropped (same line) or handed to the enclosing class (own line).
- Kotlin properties that hold a lambda, a SAM callback or an anonymous object — `private val frameListener = CameraFrameListener { … }`, the way Android and MSDK callbacks are almost always declared — now contribute call edges. Previously everything inside such an initializer was dropped, so a function reached only through one of these callbacks looked like it had no callers at all and its blast radius came back far too small. Delegated properties (`by lazy { … }`) and plain initializers (`val x = compute()`) were affected the same way and are fixed too. Re-index with `codegraph index -f` after upgrading to pick up the new edges.
- 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, #1650)
- **Functions bound with `const` inside another function are symbols now.** `const handleClear = () => {…}` inside a React component — every handler that skips `useCallback` — was invisible to `callers`, `callees` and impact, answering "Symbol not found" exactly the way a function with no callers would. It is indexed like its module-level twin, contained by the enclosing function, with its own calls. Re-index after upgrading. (#1669)
- `codegraph callers`, `callees`, and `query` now clearly report when their result limit hides additional matches, including exact totals in callers/callees JSON output; the `codegraph_callers` and `codegraph_callees` MCP answers carry the same "showing N of M" note. (#1639, #1674)
- CommonJS controllers written as `exports.getItems = async (req, res) => {…}` or `module.exports.x = function () {…}` are now indexed as exported functions, so `node`, `callers` and impact find every Express handler in that style and the calls inside them belong to the handler instead of the file. Re-index JavaScript projects after upgrading. (#1675)
@@ -229,6 +230,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- **A C macro call written with designated initializers no longer swallows every function after it.** Betaflight resets each config struct with `RESET_CONFIG(type, dst, .field = value, …)`, a shape the C grammar cannot parse; past a hundred or so fields its error recovery ran the enclosing function to the end of the file, the next function vanished from the index and every later one was filed under the first, where name matching then treated it as an unreachable closure. The argument list of such a call is now blanked before parsing, offsets kept, so the file's functions come out with their real extents. On that tree 45 functions in `pid.c` alone moved back to top level and their 117 callers resolve at exact-match confidence. Re-index after upgrading. (#1729)
- A method called on the result of another call — `d.setdefault(k, []).append(v)`, `make().run()` — no longer produces a call edge to an unrelated top-level function that merely shares the name, in Python and JavaScript/TypeScript. The receiver is kept so the inner call still resolves; the outer method stays unresolved rather than guessed. Re-index after upgrading. (#1683, #1681)
- A Python call through an imported project module whose name collides with a builtin collection method — `ledger.append(row)` after `from . import ledger` — is no longer dropped as `list.append`. The builtin-method filter now lets the receiver through when it is an imported module that resolves to a file in the project, so `resolveViaImport` can attach the real edge; a stdlib/PyPI receiver (`os.remove`) still produces none. Re-index after upgrading. (#1681, via #1704)
- Python method calls on module-scope builtin collections no longer create false calls or file dependencies to unrelated project methods with the same name; re-index after upgrading. (#1652)
- **A definition its language makes file-local no longer captures calls from other files.** A C `static` in another source file (`.c`/`.cc`/… — not a header's `static inline`, which is textually included), a Kotlin/Java/C#/Swift/Scala/Dart/PHP `private` member, a Go unexported name in another package, and a Rust non-`pub` item outside its module subtree cannot be what a name in another file means, but name matching accepted them whenever the names agreed: an Android `editor.apply()` onto an unrelated class's `private fun apply`, a JavaScript `fail(...)` onto a Go `func fail`, a Rust `.count()` onto a private `fn count` in another crate, and C USB helpers onto a `static` in a `.c` they never link. Such a target is now declined after the whole name-matching pipeline settles — the reference stays unresolved rather than falling through to a fuzzy namesake. Same-file definitions, a child Rust module reaching its ancestors' private items, and Rust `impl Trait for Type` methods stay resolvable. Re-index after upgrading. (#1730, #1731)
- **A binding in a module that exports nothing is no longer a cross-file target.** On vite, every `import { defineConfig } from 'vite'` across the playground resolved onto a `const vite = await createServer(…)` sitting at module scope in `playground/ssr-html/test-stacktrace.js` — a file with an import and no export, so that binding is reachable from nowhere but itself. Name matching commits as soon as one candidate survives, and nothing asked whether an import could reach the survivor; that one binding took 157 edges. A JS/TS file holding an `import` and no export of any kind now offers its locals to no other file. Classic scripts, CommonJS (including `exports["x"] = …`), a later `export { … }`, and names contributed through `declare global` are all unaffected. Across vite this removed 320 wrong edges and added 18, each addition a reference that was previously ambiguous rather than newly invented. Re-index after upgrading. (#1719)
- **A bare call inside a JavaScript or TypeScript method no longer resolves to the method itself.** When a method and a module-scope function share a name, `serialize(this.raw)` written inside `Record.serialize` means the function, but the nearest same-named definition won the tie and the graph recorded the method calling itself. A call written without a receiver can never reach a method in JS/TS, so methods are no longer candidates for it; `this.serialize()` and `other.serialize()` resolve as before. (#1714)
+52
View File
@@ -9052,6 +9052,58 @@ function M:send(data) return self end
const send = methods.find((m) => m.name === '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', () => {
+10 -1
View File
@@ -24,7 +24,7 @@ local function localFn(...)
return select("#", ...)
end
-- doc for anonAssigned (variable, initializer invisible)
-- doc for anonAssigned (function named from its local binding)
local anonAssigned = function(v)
return hidden(v)
end
@@ -68,6 +68,15 @@ M.assigned = function(z)
return topFn(z)
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 }
local tbl = { cb = topFn, [1] = localFn, nested = { deep_cb = topFn } }
+16
View File
@@ -127,6 +127,22 @@ describe.skipIf(!kernelBuilt)('kernel Lua/Luau extraction parity', () => {
// lua functions carry NO isExported (undefined — not false).
const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'topFn');
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.
const v = result.nodes.find((n) => n.kind === 'variable' && n.name === 'core');
expect(v?.isExported).toBe(false);
@@ -0,0 +1,106 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const collections = [
{ name: 'dict_literal', value: '{"answer": "42"}', method: 'get' },
{ name: 'empty_dict', value: '{}', method: 'get' },
{ name: 'dict_constructor', value: 'dict()', method: 'get' },
{ name: 'list_literal', value: '[1]', method: 'append' },
{ name: 'empty_list', value: '[]', method: 'append' },
{ name: 'list_constructor', value: 'list()', method: 'append' },
{ name: 'set_literal', value: '{1}', method: 'add' },
{ name: 'set_constructor', value: 'set()', method: 'add' },
{ name: 'tuple_literal', value: '(1,)', method: 'index' },
{ name: 'empty_tuple', value: '()', method: 'index' },
{ name: 'tuple_constructor', value: 'tuple()', method: 'index' },
{ name: 'frozenset_constructor', value: 'frozenset()', method: 'union' },
];
let dir: string;
let cg: CodeGraph;
beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1652-'));
fs.writeFileSync(path.join(dir, 'settings.py'), `DEFAULTS = {"answer": "42"}
def read_setting(name):
return DEFAULTS.get(name, None)
`);
fs.writeFileSync(path.join(dir, 'cache.py'), `class LRUCache:
def __init__(self):
self._store = {}
def get(self, key):
return self._store.get(key)
class ProjectCollection:
def append(self, item):
pass
def add(self, item):
pass
def index(self, item):
return 0
def union(self, item):
return self
`);
for (const { name, value, method } of collections) {
// Capitalizing lRUCache matches a real class. Its name must not override
// the same-file binding's collection initializer (#1652).
fs.writeFileSync(path.join(dir, `${name}.py`), `lRUCache = ${value}
def use_${name}(item):
return lRUCache.${method}(item)
`);
}
fs.writeFileSync(path.join(dir, 'unknown.py'), `UNKNOWN = load_defaults()
def read_unknown(name):
return UNKNOWN.get(name)
`);
fs.writeFileSync(path.join(dir, 'client.py'), `from cache import LRUCache
def read_cache(lRUCache: LRUCache, name):
return lRUCache.get(name)
`);
cg = await CodeGraph.init(dir, { index: true });
});
afterAll(() => {
cg?.destroy();
fs.rmSync(dir, { recursive: true, force: true });
});
function expectNoMethodCall(callerName: string, file: string, methodName: string) {
const caller = cg.getNodesByName(callerName).find((n) => n.kind === 'function' && n.filePath === file);
const method = cg.getNodesByName(methodName).find((n) => n.kind === 'method' && n.filePath === 'cache.py');
expect(caller).toBeDefined();
expect(method).toBeDefined();
expect(cg.getCallers(method!.id).map(({ node }) => node.id)).not.toContain(caller!.id);
expect(cg.getCallees(caller!.id).map(({ node }) => node.id)).not.toContain(method!.id);
}
describe('Python module-scope collection methods (#1652)', () => {
it('does not connect DEFAULTS.get to the unrelated LRUCache.get method', () => {
expectNoMethodCall('read_setting', 'settings.py', 'get');
});
it.each(collections)('keeps $name ($method) external even when the receiver resembles a class', ({ name, method }) => {
expectNoMethodCall(`use_${name}`, `${name}.py`, method);
});
it('does not treat an unrelated variable as evidence of a project class', () => {
expectNoMethodCall('read_unknown', 'unknown.py', 'get');
});
it('preserves real instance calls despite same-named collections in other files', () => {
const caller = cg.getNodesByName('read_cache').find((n) => n.kind === 'function')!;
const method = cg.getNodesByName('get').find((n) => n.kind === 'method' && n.filePath === 'cache.py')!;
expect(caller).toBeDefined();
expect(method).toBeDefined();
expect(cg.getCallees(caller.id).map(({ node }) => node.id)).toContain(method.id);
expect(cg.getCallers(method.id).map(({ node }) => node.id)).toContain(caller.id);
});
});
+26
View File
@@ -2055,6 +2055,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)', () => {
// 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
+132 -12
View File
@@ -468,7 +468,7 @@ impl<'t> Walker<'t> {
}
// plain path returns false → children re-visited (the
// typeof(require(...)) alias+import pair rides this).
} else if kind == "variable_declaration" {
} else if matches!(kind, "variable_declaration" | "assignment_statement") {
self.extract_variable(node);
// Initializer subtrees are never walked — candidates only.
self.scan_fn_ref_subtree(node, 0);
@@ -578,21 +578,38 @@ impl<'t> Walker<'t> {
}
None => Vec::new(),
};
let names: Vec<Node<'t>> = match var_list {
let targets: Vec<Node<'t>> = match var_list {
Some(vl) => {
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(),
};
for (i, name_node) in names.iter().enumerate() {
let name = self.text(*name_node);
if name.is_empty() {
for (i, name_node) in targets.iter().enumerate() {
let Some((name, receiver, full_name)) = self.lua_assignment_target(*name_node) else {
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;
}
// Positional value pairing; a missing value → NO signature key.
let signature = values.get(i).map(|v| util::init_signature(self.text(*v)));
let name = name.to_string();
let signature = value.map(|v| util::init_signature(self.text(v)));
self.create_node(
"variable",
&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 --------
/// Returns skipChildren (always false on the plain path).
@@ -790,13 +911,12 @@ impl<'t> Walker<'t> {
return;
}
// Halt at nested function definitions (their bodies are walked — and
// attributed — by extractFunction). function_definition (anonymous)
// is deliberately NOT in the halt list — the scan descends into
// anonymous initializer bodies, attributing candidates to the file.
// attributed — by extractFunction). Lua function_definition values are
// now extracted from their assignment target and must stop this scan too.
if depth > 0
&& matches!(
node.kind(),
"function_declaration" | "arrow_function" | "function_expression"
"function_declaration" | "function_definition" | "arrow_function" | "function_expression"
| "lambda_literal" | "lambda_expression"
)
{
+4 -1
View File
@@ -75,7 +75,10 @@ export const luaExtractor: LanguageExtractor = {
typeAliasTypes: [],
importTypes: [], // `require` is a function_call — handled in visitNode below
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',
bodyField: 'body',
paramsField: 'parameters',
+91 -5
View File
@@ -648,6 +648,7 @@ export class TreeSitterExtractor {
const nodeType = node.type;
if (depth > 0 && (
this.extractor?.functionTypes.includes(nodeType) ||
((this.language === 'lua' || this.language === 'luau') && nodeType === 'function_definition') ||
nodeType === 'arrow_function' ||
nodeType === 'function_expression' ||
nodeType === 'lambda_literal' ||
@@ -2938,14 +2939,28 @@ export class TreeSitterExtractor {
const varList = assign.namedChildren.find((c) => c.type === 'variable_list');
const exprList = assign.namedChildren.find((c) => c.type === 'expression_list');
const values = exprList ? exprList.namedChildren : [];
const names = varList ? varList.namedChildren.filter((c) => c.type === 'identifier') : [];
names.forEach((nameNode, i) => {
const name = getNodeText(nameNode, this.source);
if (!name) return;
const targets = varList ? varList.namedChildren : [];
targets.forEach((nameNode, 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 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') {
// C: a `declaration` node's name nests inside the `declarator` field —
@@ -3044,6 +3059,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).
* For languages like Go, resolveTypeAliasKind detects when the type_spec
+17 -4
View File
@@ -2104,8 +2104,22 @@ export class ReferenceResolver {
// project dependency, not `list.append` (#1681). Without this, the
// qualified ref never reaches resolveViaImport / resolvePythonModuleMember.
if (PYTHON_BUILT_IN_METHODS.has(method)) {
// A module-scope collection binding is stronger evidence than a
// coincidentally matching class name (#1652). Only use this file's
// binding: an unrelated module may reuse the receiver for a collection.
const isCollection = this.context.getNodesByName(receiver).some((node) =>
node.language === 'python' && node.filePath === ref.filePath &&
(node.kind === 'variable' || node.kind === 'constant') &&
node.qualifiedName === receiver &&
/^=\s*(?:[\[{]|(?:dict|list|set|tuple|frozenset)\s*\(|\(\s*\)|\([^()]*,)/.test(node.signature ?? '')
);
if (isCollection) return true;
const capitalized = receiver.charAt(0).toUpperCase() + receiver.slice(1);
const isKnownClass = this.knownNames?.has(capitalized) ?? false;
const isKnownClass = this.context.getNodesByName(capitalized).some((node) =>
node.language === 'python' &&
(node.kind === 'class' || node.kind === 'struct' || node.kind === 'interface')
);
const isProjectModule =
!isKnownClass && this.isPythonProjectModule(ref, receiver);
if (!isKnownClass && !isProjectModule) {
@@ -2116,9 +2130,8 @@ export class ReferenceResolver {
// A bare name colliding with a builtin method (index, get, update, count…)
// is only a builtin when NOTHING in the codebase declares it. A declared
// symbol with that exact name — e.g. a Flask/FastAPI view `def index()` or
// `def get()` — is a real reference target. Mirrors the knownNames guard on
// the dotted branch above; without it, every handler named after a builtin
// method silently loses its route→handler edge.
// `def get()` — is a real reference target. Without this guard, every
// handler named after a builtin method silently loses its route→handler edge.
if (PYTHON_BUILT_IN_METHODS.has(name) && !this.knownNames?.has(name)) {
return true;
}