mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 06:53:52 +08:00
fix(extraction): a function in an initializer is referenced once, by what the initializer declares (#2743)
PRs #693 and #1802 walk a declaration's initializer under the symbol it declares, so `const routes = { home }` references `home` from `routes`. The dispatcher's declaration-wide function-as-value scan then took the same values again for the enclosing file (or class). Flush-time dedup keys on the source, so both stayed: a second reference to the same function from the same spot. Each initializer walk now records what it walked, and the scan skips it. PR #2738 added that mechanism for component wrappers, and C#, Scala and Dart already had their own. The newly covered paths: - TS/JS module-level constants and variables, owned object literals included (#2300), and TS/JS class properties; - Python assignments, Go var/const specs, Rust const/static items; - Java fields, and Kotlin properties (walkInitializer takes the property's id, so their walk is unchanged). Mirrored in the kernel: tsjs, python, go, rustlang, java and kotlin. References no initializer walk takes are unchanged: a module-level call argument `register(fn)`, C-family tables, Rust's `static CB: fn() = handler` (the declaration's own value, which the scan still reads). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ae0fc631e0
commit
51be28d15b
@@ -269,6 +269,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In TypeScript, a type written with a qualifier, like `React.ReactNode`, `Y.Doc`, `TChat.ChatMessage` or `EQueryType.CLICKHOUSE`, now links to the member of what the qualifier names: a type from a package, like React's, no longer links to a class or component of your own that shares its name, and a member of an enum or a namespace your project declares links to that member instead of to another enum's or a same-named component. Re-index TypeScript projects after upgrading.
|
||||
- In JavaScript and TypeScript, an import written from the app's root with a leading slash, like bigbluebutton's `import Icon from '/imports/ui/components/common/icon/component'`, now links to the file it names when the app's `tsconfig.json` or `jsconfig.json` maps it, as `"paths": { "/*": ["*"] }` does. Before, such an import was taken for a package, so the module looked unused and every name it imports linked by name instead, often to the wrong one of several components of the same name, or to nothing. Re-index projects that import this way after upgrading.
|
||||
- In JavaScript and TypeScript, a call whose callee is not a name, like an immediately invoked function `(() => { … })()`, a curried call such as `connect(mapState)(View)`, `handlers[type]()` or `(0, fn)()`, no longer stores the callee's whole source text as the name of a reference that nothing in the project can answer. A single immediately invoked function could leave its entire body in the index that way. The calls inside such a callee, and the inner call of a curried one, are still recorded. A call through parentheses, a cast or a non-null assertion, like `(fn)()`, `(fn as F)()` or `fn!()`, now links to `fn` exactly as `fn()` does, so a parameter named `fn` no longer links to a same-named property elsewhere in the project. In TypeScript, a call with type arguments behind an operator, like `!settings.get<boolean>('X')` or `void apiRequest<Status>()`, now links to what it calls, as `await load<T>()` already did.
|
||||
- In JavaScript, TypeScript, Python, Go, Rust, Java and Kotlin, a function named in the value of a top-level constant or variable, or of a class field, like `const routes = { home }`, `HANDLERS = {"home": home}`, a Go `map[string]func()` table, a Rust `static` list of functions, `static handlers = { click: onClick }`, a Java `List.of(Registry::home)` field or a Kotlin `listOf(::home)` property, is now referenced once, by the constant, variable, field or property that holds it. The file or class around it no longer also shows up as a second reference from the same place, so a function's callers and impact list each table once.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public class TortureService extends BaseService implements Runnable, AutoCloseab
|
||||
}
|
||||
};
|
||||
private final Runnable fieldMethodRef = TortureService::compute;
|
||||
private static final List<Runnable> FIELD_TABLE = List.of(TortureService::compute);
|
||||
|
||||
/** Ctor javadoc. */
|
||||
public TortureService(List<String> names) {
|
||||
|
||||
@@ -14,6 +14,7 @@ const arrow = (x) => legacyHelper(x, 1);
|
||||
|
||||
class Widget extends EventEmitter {
|
||||
static registry = new Map();
|
||||
static hooks = { tick: legacyHelper };
|
||||
#privateField = 1;
|
||||
label = 'w';
|
||||
onTick = () => {
|
||||
@@ -78,6 +79,7 @@ export default {
|
||||
// leaked to the FILE node; a non-exported object literal was skipped outright.
|
||||
const eagerConfig = loadConfig();
|
||||
const handlerMap = { onSave: () => persist(eagerConfig), onLoad: loadConfig() };
|
||||
const registry = { legacyHelper, run: arrow, nested: [legacyHelper] };
|
||||
const lazyList = [() => persist(eagerConfig)];
|
||||
// --- CommonJS export assignments (#1675) -----------------------------------
|
||||
exports.getItems = async (req, res) => { res.json(await findItems()); };
|
||||
|
||||
@@ -52,6 +52,7 @@ val withGetter: Int
|
||||
get() = 42
|
||||
val initLambda: () -> Unit = { caller() }
|
||||
val initSam = Runnable { caller() }
|
||||
val handlerList = listOf(::caller)
|
||||
val initObject = object : Runnable {
|
||||
override fun run() {
|
||||
caller()
|
||||
@@ -60,6 +61,7 @@ val initObject = object : Runnable {
|
||||
|
||||
class WidgetK(val size: Int, private var name: String = defaultName()) {
|
||||
val area: Int = size * size
|
||||
val hooks = listOf(::caller)
|
||||
var label: String? = null
|
||||
val computed: Int
|
||||
get() = size * 2
|
||||
|
||||
@@ -305,6 +305,8 @@ routes![top_level_h];
|
||||
const INIT_CONST: usize = compute_len();
|
||||
static INIT_LAZY: Lazy<Cfg> = Lazy::new(|| build_cfg());
|
||||
static INIT_ALIAS: fn() = free_fn;
|
||||
// A table the walk above takes for its symbol, and the scan no longer for the file.
|
||||
static INIT_TABLE: &[fn()] = &[handler, handler2];
|
||||
|
||||
pub union Reg {
|
||||
pub raw: u32,
|
||||
|
||||
@@ -23,6 +23,7 @@ var oldVar = 'x';
|
||||
@scoped.Registry<Config>('name')
|
||||
export abstract class BaseService extends EventTarget implements Disposable, Serializable {
|
||||
static instances = 0;
|
||||
static handlers = { refresh: helperFn, table: CONFIG_TABLE };
|
||||
private readonly cache: Map<string, Config> = new Map();
|
||||
public fonts: FontConfig;
|
||||
count = 0;
|
||||
@@ -189,6 +190,7 @@ export const actionsMap = {
|
||||
update(input: UpdateInput) {
|
||||
return persistExisting(input);
|
||||
},
|
||||
fallback: helperFn,
|
||||
};
|
||||
|
||||
// Zustand-style store through middleware wrappers.
|
||||
|
||||
@@ -779,12 +779,11 @@ describe('Function-as-value capture (#756)', () => {
|
||||
|
||||
// The DRF wiring: get_serializer_class → the imported serializer class,
|
||||
// via `return` — the issue's headline gap. The module-level registry
|
||||
// dict rides BOTH the assigned name (the initializer walk, #693) and the
|
||||
// file node (the dispatcher's own scan, which runs either way).
|
||||
// dict rides the assigned name (the initializer walk, #693), and not the
|
||||
// file node as well: the dispatcher's scan skips what that walk took.
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'OrgSerializerFull'))).toEqual([
|
||||
'SERIALIZER_REGISTRY',
|
||||
'get_serializer_class',
|
||||
'views.py',
|
||||
]);
|
||||
// Second branch return + a module-level call argument.
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'OrgSerializerBrief'))).toEqual([
|
||||
@@ -1581,4 +1580,72 @@ func Unknown(obj interface{}) { Submit(obj.Fetch) }
|
||||
cg.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// An initializer walked under the symbol it declares (#693) takes its
|
||||
// function values for that symbol. The declaration-wide scan took each one
|
||||
// again for the enclosing file or class: a second reference to the same
|
||||
// function from the same spot (TS/JS constants and class fields, Python,
|
||||
// Go, Rust, Java fields, Kotlin properties). Each function here is
|
||||
// referenced only from an initializer, so a second source shows.
|
||||
it('INITIALIZER WALKS: a function in an initializer is referenced once, by what it declares', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-init-'));
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'app.ts'),
|
||||
[
|
||||
'export function tsHome() {}',
|
||||
'export function tsAbout() {}',
|
||||
'export function tsClick() {}',
|
||||
'export function tsBoot() {}',
|
||||
'function register(fn: () => void) {}',
|
||||
'const routes = { tsHome, about: tsAbout, list: [tsHome] };',
|
||||
'const api = { load() {}, tsAbout };',
|
||||
'export class Widget {',
|
||||
' static handlers = { click: tsClick };',
|
||||
'}',
|
||||
'register(tsBoot);',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'views.py'),
|
||||
['def py_home():', ' pass', '', 'HANDLERS = {"home": py_home}', 'ROUTES = [py_home]'].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'handlers.go'),
|
||||
['package main', '', 'func goHome() {}', '', 'var handlers = map[string]func(){"home": goHome}'].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'lib.rs'),
|
||||
['fn rust_home() {}', 'static HANDLERS: &[fn()] = &[rust_home];'].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'Registry.java'),
|
||||
[
|
||||
'class Registry {',
|
||||
' static void javaHome() {}',
|
||||
' static final java.util.List<Runnable> TABLE = java.util.List.of(Registry::javaHome);',
|
||||
'}',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'handlers.kt'),
|
||||
['fun ktHome() {}', 'val ktHandlers = listOf(::ktHome)', 'class KtWidget {', ' val hooks = listOf(::ktHome)', '}'].join('\n')
|
||||
);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir);
|
||||
try {
|
||||
await cg.indexAll();
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsHome'))).toEqual(['routes']);
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsAbout'))).toEqual(['api', 'routes']);
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsClick'))).toEqual(['handlers']);
|
||||
// A call argument at module scope is the file's, as before.
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'tsBoot'))).toEqual(['app.ts']);
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'py_home'))).toEqual(['HANDLERS', 'ROUTES']);
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'goHome'))).toEqual(['handlers']);
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'rust_home'))).toEqual(['HANDLERS']);
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'javaHome'))).toEqual(['TABLE']);
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'ktHome'))).toEqual(['hooks', 'ktHandlers']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -428,8 +428,10 @@ impl<'t> Walker<'t> {
|
||||
} else if matches!(kind, "var_declaration" | "short_var_declaration" | "const_declaration")
|
||||
&& !self.inside_class_like()
|
||||
{
|
||||
self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
// The initializers extract_variable walked under their specs'
|
||||
// symbols took their own candidates; the scan covers the rest.
|
||||
let walked = self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0, &walked);
|
||||
skip_children = true;
|
||||
} else if kind == "import_declaration" {
|
||||
self.extract_import(node);
|
||||
@@ -678,7 +680,10 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
/// extractVariable's Go branch: var/const specs + short_var_declaration.
|
||||
fn extract_variable(&mut self, node: Node<'t>) {
|
||||
/// Returns the initializers it walked under their specs' symbols, for the
|
||||
/// declaration scan to skip.
|
||||
fn extract_variable(&mut self, node: Node<'t>) -> Vec<usize> {
|
||||
let mut walked = Vec::new();
|
||||
let docstring = self.docstring_of(node);
|
||||
let is_const_decl = node.kind() == "const_declaration";
|
||||
|
||||
@@ -726,6 +731,7 @@ impl<'t> Walker<'t> {
|
||||
self.stack.push(Scope { row, kind: "variable", name });
|
||||
self.visit_function_body(value_field);
|
||||
self.stack.pop();
|
||||
walked.push(value_field.id());
|
||||
} else {
|
||||
self.visit_function_body(value_field);
|
||||
}
|
||||
@@ -756,6 +762,7 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
}
|
||||
walked
|
||||
}
|
||||
|
||||
/// extractImport's Go branch: one import node + ref per import_spec.
|
||||
@@ -1241,11 +1248,15 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
|
||||
stack_guard!();
|
||||
if depth > 12 {
|
||||
return;
|
||||
}
|
||||
// Subtrees the body walker has already been through.
|
||||
if walked.contains(&node.id()) {
|
||||
return;
|
||||
}
|
||||
if depth > 0
|
||||
&& matches!(
|
||||
node.kind(),
|
||||
@@ -1258,7 +1269,7 @@ impl<'t> Walker<'t> {
|
||||
self.maybe_capture_fn_refs(node);
|
||||
for i in 0..node.named_child_count() {
|
||||
if let Some(c) = node.named_child(i) {
|
||||
self.scan_fn_ref_subtree(c, depth + 1);
|
||||
self.scan_fn_ref_subtree(c, depth + 1, walked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,12 +507,14 @@ impl<'t> Walker<'t> {
|
||||
self.extract_enum(node);
|
||||
skip_children = true;
|
||||
} else if kind == "field_declaration" && self.inside_class_like() {
|
||||
self.extract_field(node);
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
// The initializers extract_field walked under their fields took
|
||||
// their own candidates; the scan covers the rest.
|
||||
let walked = self.extract_field(node);
|
||||
self.scan_fn_ref_subtree(node, 0, &walked);
|
||||
skip_children = true;
|
||||
} else if kind == "local_variable_declaration" && !self.inside_class_like() {
|
||||
self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
self.scan_fn_ref_subtree(node, 0, &[]);
|
||||
skip_children = true;
|
||||
} else if kind == "import_declaration" {
|
||||
self.extract_import(node);
|
||||
@@ -726,8 +728,11 @@ impl<'t> Walker<'t> {
|
||||
self.stack.pop();
|
||||
}
|
||||
|
||||
/// extractField — each declarator becomes a field/constant node.
|
||||
fn extract_field(&mut self, node: Node<'t>) {
|
||||
/// extractField — each declarator becomes a field/constant node. Returns
|
||||
/// the initializers walked under their fields, for the declaration scan to
|
||||
/// skip.
|
||||
fn extract_field(&mut self, node: Node<'t>) -> Vec<usize> {
|
||||
let mut walked = Vec::new();
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let visibility = self.visibility_of(node);
|
||||
let is_static = Some(self.is_static(node));
|
||||
@@ -786,6 +791,7 @@ impl<'t> Walker<'t> {
|
||||
self.stack.push(Scope { row, kind: field_kind, name: name.clone() });
|
||||
self.visit_function_body(value);
|
||||
self.stack.pop();
|
||||
walked.push(value.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -805,6 +811,7 @@ impl<'t> Walker<'t> {
|
||||
);
|
||||
}
|
||||
}
|
||||
walked
|
||||
}
|
||||
|
||||
/// extractVariable's generic fallback (top-level locals — rare in Java).
|
||||
@@ -1230,11 +1237,15 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
|
||||
stack_guard!();
|
||||
if depth > 12 {
|
||||
return;
|
||||
}
|
||||
// Subtrees the body walker has already been through.
|
||||
if walked.contains(&node.id()) {
|
||||
return;
|
||||
}
|
||||
// (functionTypes is empty for Java; lambda_expression halts the scan)
|
||||
if depth > 0 && matches!(node.kind(), "lambda_literal" | "lambda_expression") {
|
||||
return;
|
||||
@@ -1242,7 +1253,7 @@ impl<'t> Walker<'t> {
|
||||
self.maybe_capture_fn_refs(node);
|
||||
for i in 0..node.named_child_count() {
|
||||
if let Some(c) = node.named_child(i) {
|
||||
self.scan_fn_ref_subtree(c, depth + 1);
|
||||
self.scan_fn_ref_subtree(c, depth + 1, walked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,7 +733,10 @@ impl<'t> Walker<'t> {
|
||||
})
|
||||
}
|
||||
|
||||
fn try_visit_hook(&mut self, node: Node<'t>) -> bool {
|
||||
/// The Kotlin visitNode hook: `Some` when it consumed the node, holding the
|
||||
/// initializers it walked as such (`walkInitializer`), which the caller's
|
||||
/// fn-ref scan of the node skips.
|
||||
fn try_visit_hook(&mut self, node: Node<'t>) -> Option<Vec<usize>> {
|
||||
// An own-line accessor already walked by its owning property below. The
|
||||
// ownership test re-derives the property's kind rather than remembering
|
||||
// it: a destructured or local declaration mints no node, so its
|
||||
@@ -741,10 +744,10 @@ impl<'t> Walker<'t> {
|
||||
if matches!(node.kind(), "getter" | "setter") {
|
||||
return accessor_owner(node)
|
||||
.and_then(|owner| self.property_kind(owner))
|
||||
.is_some();
|
||||
.map(|_| Vec::new());
|
||||
}
|
||||
if node.kind() != "property_declaration" {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let var_decl = (0..node.named_child_count())
|
||||
.filter_map(|i| node.named_child(i))
|
||||
@@ -764,11 +767,11 @@ impl<'t> Walker<'t> {
|
||||
for init in property_initializers(node) {
|
||||
self.visit_function_body(init);
|
||||
}
|
||||
return true;
|
||||
return Some(Vec::new());
|
||||
};
|
||||
let name = self.text(name_node).to_string();
|
||||
if name.is_empty() {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let Some(kind) = self.property_kind(node) else {
|
||||
// A local — no node is minted, but the initializer is still code.
|
||||
@@ -778,7 +781,7 @@ impl<'t> Walker<'t> {
|
||||
for init in property_initializers(node) {
|
||||
self.visit_function_body(init);
|
||||
}
|
||||
return true;
|
||||
return Some(Vec::new());
|
||||
};
|
||||
// The `type`-field signature read is dead (zero fields) → signature
|
||||
// undefined; NO docstring/visibility/isStatic — the modifiers merge in
|
||||
@@ -790,26 +793,30 @@ impl<'t> Walker<'t> {
|
||||
// the idiomatic Android callback field) contributed NO call edge at all.
|
||||
// The property also OWNS any accessor written on its own line, which the
|
||||
// grammar makes a following SIBLING rather than a child; those bodies
|
||||
// used to attribute to the enclosing class.
|
||||
// used to attribute to the enclosing class. The initializers are
|
||||
// walked as such: the fn-ref scan skips them, or it would take each
|
||||
// function value again for the enclosing file or class.
|
||||
let mut walked = Vec::new();
|
||||
if let Some(row) = row {
|
||||
self.stack.push(Scope { row, kind, name: name.clone() });
|
||||
for init in property_initializers(node) {
|
||||
self.visit_function_body(init);
|
||||
walked.push(init.id());
|
||||
}
|
||||
for acc in following_accessors(node) {
|
||||
self.visit_function_body(acc);
|
||||
}
|
||||
self.stack.pop();
|
||||
}
|
||||
true
|
||||
Some(walked)
|
||||
}
|
||||
|
||||
// --- the dispatcher (visitNode, Kotlin-relevant branches) -----------------------
|
||||
|
||||
fn visit_node(&mut self, node: Node<'t>) {
|
||||
stack_guard!();
|
||||
if self.try_visit_hook(node) {
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
if let Some(walked) = self.try_visit_hook(node) {
|
||||
self.scan_fn_ref_subtree(node, 0, &walked);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -856,7 +863,7 @@ impl<'t> Walker<'t> {
|
||||
// Hook-declined destructuring: extractField/extractVariable both
|
||||
// find no matching children for kotlin — NOTHING minted, RHS
|
||||
// invisible; candidates-only scan.
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
self.scan_fn_ref_subtree(node, 0, &[]);
|
||||
skip_children = true;
|
||||
} else if kind == "import_header" {
|
||||
self.extract_import(node);
|
||||
@@ -1574,11 +1581,15 @@ impl<'t> Walker<'t> {
|
||||
});
|
||||
}
|
||||
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
|
||||
stack_guard!();
|
||||
if depth > 12 {
|
||||
return;
|
||||
}
|
||||
// Subtrees the body walker has already been through.
|
||||
if walked.contains(&node.id()) {
|
||||
return;
|
||||
}
|
||||
// Halts at functionTypes (function_declaration) + the fixed list —
|
||||
// lambda_literal halts (no captures inside `by lazy { }` under a
|
||||
// hook-consumed property).
|
||||
@@ -1594,7 +1605,7 @@ impl<'t> Walker<'t> {
|
||||
self.maybe_capture_fn_refs(node);
|
||||
for i in 0..node.named_child_count() {
|
||||
if let Some(c) = node.named_child(i) {
|
||||
self.scan_fn_ref_subtree(c, depth + 1);
|
||||
self.scan_fn_ref_subtree(c, depth + 1, walked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,8 +431,10 @@ impl<'t> Walker<'t> {
|
||||
self.extract_class(node);
|
||||
skip_children = true;
|
||||
} else if kind == "assignment" && !self.inside_class_like() {
|
||||
self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
// The right-hand side extract_variable walked under the assigned
|
||||
// name took its own candidates; the scan covers the rest.
|
||||
let walked = self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0, &walked);
|
||||
skip_children = true;
|
||||
} else if kind == "import_statement" || kind == "import_from_statement" {
|
||||
self.extract_import(node);
|
||||
@@ -564,7 +566,9 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
/// extractVariable's python branch: `left = right` at module scope.
|
||||
fn extract_variable(&mut self, node: Node<'t>) {
|
||||
/// Returns the right-hand side when it was walked under the assigned
|
||||
/// name, for the declaration scan to skip.
|
||||
fn extract_variable(&mut self, node: Node<'t>) -> Vec<usize> {
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let left = node.child_by_field_name("left").or_else(|| node.named_child(0));
|
||||
let right = node.child_by_field_name("right").or_else(|| node.named_child(1));
|
||||
@@ -589,16 +593,19 @@ impl<'t> Walker<'t> {
|
||||
// module-level `app = FastAPI()` / `handler = lambda: run()` dropped
|
||||
// every call on the right-hand side. A tuple target mints no symbol, so
|
||||
// its RHS is walked at the enclosing scope rather than lost.
|
||||
let mut walked = Vec::new();
|
||||
if let Some(right) = right {
|
||||
match assigned {
|
||||
Some((row, name)) => {
|
||||
self.stack.push(Scope { row, kind: "variable", name });
|
||||
self.visit_function_body(right);
|
||||
self.stack.pop();
|
||||
walked.push(right.id());
|
||||
}
|
||||
None => self.visit_function_body(right),
|
||||
}
|
||||
}
|
||||
walked
|
||||
}
|
||||
|
||||
fn extract_import(&mut self, node: Node<'t>) {
|
||||
@@ -955,11 +962,15 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
|
||||
stack_guard!();
|
||||
if depth > 12 {
|
||||
return;
|
||||
}
|
||||
// Subtrees the body walker has already been through.
|
||||
if walked.contains(&node.id()) {
|
||||
return;
|
||||
}
|
||||
// Halts at functionTypes ∪ the fixed arrow/lambda list — python's
|
||||
// `lambda` kind is NOT in that list (mirrored).
|
||||
if depth > 0
|
||||
@@ -974,7 +985,7 @@ impl<'t> Walker<'t> {
|
||||
self.maybe_capture_fn_refs(node);
|
||||
for i in 0..node.named_child_count() {
|
||||
if let Some(c) = node.named_child(i) {
|
||||
self.scan_fn_ref_subtree(c, depth + 1);
|
||||
self.scan_fn_ref_subtree(c, depth + 1, walked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,8 +500,10 @@ impl<'t> Walker<'t> {
|
||||
// Inside a class-like scope the gate fails and the else-ladder
|
||||
// falls through with children VISITED — a trait const's value
|
||||
// expression emits calls refs from the trait node.
|
||||
self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
// The initializer extract_variable walked under the declared
|
||||
// symbol took its own candidates; the scan covers the rest.
|
||||
let walked = self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0, &walked);
|
||||
skip_children = true;
|
||||
} else if kind == "use_declaration" {
|
||||
self.extract_import(node);
|
||||
@@ -721,9 +723,10 @@ impl<'t> Walker<'t> {
|
||||
|
||||
/// extractVariable's generic fallback: kind is ALWAYS `variable` (no
|
||||
/// isConst hook), every direct `identifier` child mints a node positioned
|
||||
/// at the CHILD, docstring shared, isExported present-false, no signature,
|
||||
/// and the initializer value is never body-walked.
|
||||
fn extract_variable(&mut self, node: Node<'t>) {
|
||||
/// at the CHILD, docstring shared, isExported present-false, no signature.
|
||||
/// The initializer is walked under the declared symbol (#693), and returned
|
||||
/// then for the declaration scan to skip.
|
||||
fn extract_variable(&mut self, node: Node<'t>) -> Vec<usize> {
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let name_field = node.child_by_field_name("name");
|
||||
// An impl's associated const is its type's (`Limits::MAX`), as its methods are.
|
||||
@@ -759,16 +762,19 @@ impl<'t> Walker<'t> {
|
||||
// `static REGISTRY: Lazy<T> = Lazy::new(|| build())` dropped every call
|
||||
// inside the initializer, so a handler table or a lazily-built
|
||||
// singleton linked to nothing.
|
||||
let mut walked = Vec::new();
|
||||
if let Some(value) = node.child_by_field_name("value") {
|
||||
match declared {
|
||||
Some((row, name)) => {
|
||||
self.stack.push(Scope { row, kind: "variable", name });
|
||||
self.visit_function_body(value);
|
||||
self.stack.pop();
|
||||
walked.push(value.id());
|
||||
}
|
||||
None => self.visit_function_body(value),
|
||||
}
|
||||
}
|
||||
walked
|
||||
}
|
||||
|
||||
/// extractImport via the rust hook: import node named by the ROOT module +
|
||||
@@ -1430,11 +1436,15 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
|
||||
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
|
||||
stack_guard!();
|
||||
if depth > 12 {
|
||||
return;
|
||||
}
|
||||
// Subtrees the body walker has already been through.
|
||||
if walked.contains(&node.id()) {
|
||||
return;
|
||||
}
|
||||
if depth > 0
|
||||
&& matches!(
|
||||
node.kind(),
|
||||
@@ -1447,7 +1457,7 @@ impl<'t> Walker<'t> {
|
||||
self.maybe_capture_fn_refs(node);
|
||||
for i in 0..node.named_child_count() {
|
||||
if let Some(c) = node.named_child(i) {
|
||||
self.scan_fn_ref_subtree(c, depth + 1);
|
||||
self.scan_fn_ref_subtree(c, depth + 1, walked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,9 +530,11 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the initializers it walked under a component wrapper, whose
|
||||
/// fn-ref candidates that walk already took (the caller's scan of the
|
||||
/// declaration skips them). Mirrors TreeSitterExtractor.extractVariable.
|
||||
/// Returns the initializers it walked under the symbol they declare — a
|
||||
/// constant's or a variable's (#693), a component wrapper's — whose fn-ref
|
||||
/// candidates that walk already took for the symbol (the caller's scan of
|
||||
/// the declaration skips them; it would take each again for the file).
|
||||
/// Mirrors TreeSitterExtractor.extractVariable.
|
||||
pub(super) fn extract_variable(&mut self, node: Node<'t>) -> Option<HashSet<usize>> {
|
||||
let mut walked: Option<HashSet<usize>> = None;
|
||||
let is_const = self.is_const_decl(node);
|
||||
@@ -619,6 +621,7 @@ impl<'t> Walker<'t> {
|
||||
// the ownedObject branch of TreeSitterExtractor.extractVariable.
|
||||
if let (Some(row), Some(obj)) = (var_row, self.owned_object_value(value)) {
|
||||
self.extract_owned_object_members(obj, row, kind, &name, true, false);
|
||||
walked.get_or_insert_with(HashSet::new).insert(obj.id());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -678,6 +681,7 @@ impl<'t> Walker<'t> {
|
||||
self.stack.push(Scope { row, kind, name: name.clone() });
|
||||
self.visit_function_body(v);
|
||||
self.stack.pop();
|
||||
walked.get_or_insert_with(HashSet::new).insert(v.id());
|
||||
}
|
||||
None => self.visit_function_body(v),
|
||||
}
|
||||
|
||||
@@ -700,12 +700,16 @@ impl<'t> Walker<'t> {
|
||||
{
|
||||
if classify_ts_class_member(node) == Member::Property {
|
||||
let prop = self.extract_property(node);
|
||||
// The initializer walked under the property took its own
|
||||
// candidates; the scan covers the rest of the field.
|
||||
let mut walked: Option<HashSet<usize>> = None;
|
||||
if let (Some((row, name)), Some(value)) = (prop, node.child_by_field_name("value")) {
|
||||
self.stack.push(Scope { row, kind: "property", name });
|
||||
self.visit_function_body(value);
|
||||
self.stack.pop();
|
||||
walked = Some(HashSet::from([value.id()]));
|
||||
}
|
||||
self.scan_fn_ref_subtree(node, 0, None);
|
||||
self.scan_fn_ref_subtree(node, 0, walked.as_ref());
|
||||
} else {
|
||||
self.extract_method(node);
|
||||
}
|
||||
@@ -719,8 +723,9 @@ impl<'t> Walker<'t> {
|
||||
} else if self.variant.is_ts() && kind == "type_alias_declaration" {
|
||||
skip_children = self.extract_type_alias(node);
|
||||
} else if is_variable_type(kind) && !self.inside_class_like() {
|
||||
// A component wrapper's initializer extract_variable walked under
|
||||
// the component captured its own candidates; the scan skips it.
|
||||
// An initializer extract_variable walked under the symbol it
|
||||
// declares (a constant's, a component wrapper's) captured its own
|
||||
// candidates; the scan skips it.
|
||||
let walked = self.extract_variable(node);
|
||||
self.scan_fn_ref_subtree(node, 0, walked.as_ref());
|
||||
skip_children = true;
|
||||
|
||||
@@ -293,12 +293,13 @@ export const kotlinExtractor: LanguageExtractor = {
|
||||
// grammar makes a following SIBLING rather than a child; those bodies used
|
||||
// to attribute to the enclosing class. Consumed here so the accessor
|
||||
// branch below can skip them without any cross-node state.
|
||||
const inits = created
|
||||
? [...kotlinPropertyInitializers(node), ...kotlinFollowingAccessors(node)]
|
||||
: [];
|
||||
if (created && inits.length > 0) {
|
||||
// The initializers are walked as such, so the dispatcher's
|
||||
// function-as-value scan of the property skips them: it would take each
|
||||
// function value again for the enclosing file or class.
|
||||
if (created) {
|
||||
ctx.pushScope(created.id);
|
||||
for (const init of inits) ctx.visitFunctionBody(init, created.id);
|
||||
for (const init of kotlinPropertyInitializers(node)) ctx.walkInitializer(init, created.id);
|
||||
for (const accessor of kotlinFollowingAccessors(node)) ctx.visitFunctionBody(accessor, created.id);
|
||||
ctx.popScope();
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -56,11 +56,12 @@ export interface ExtractorContext {
|
||||
visitFunctionBody(body: SyntaxNode, functionId: string): void;
|
||||
/**
|
||||
* Walk a declaration's initializer as code the scope on top of the stack
|
||||
* runs, the way a function body is walked. When the hook then reports the
|
||||
* node handled, the function-as-value scan the dispatcher runs over it skips
|
||||
* what was walked here: the walk captured those candidates already.
|
||||
* runs, the way a function body is walked (`functionId` as for
|
||||
* visitFunctionBody). When the hook then reports the node handled, the
|
||||
* function-as-value scan the dispatcher runs over it skips what was walked
|
||||
* here: the walk captured those candidates already.
|
||||
*/
|
||||
walkInitializer(node: SyntaxNode): void;
|
||||
walkInitializer(node: SyntaxNode, functionId?: string): void;
|
||||
/** Add an unresolved reference */
|
||||
addUnresolvedReference(ref: UnresolvedReference): void;
|
||||
/** Push a node ID onto the scope stack (for containment/qualified name building) */
|
||||
|
||||
@@ -1521,9 +1521,12 @@ export class TreeSitterExtractor {
|
||||
this.nodeStack.pop();
|
||||
}
|
||||
// A field initializer can also register callbacks
|
||||
// (`static handlers = { click: onClick }`) — scan it for
|
||||
// function-as-value candidates (capture-only, halts at functions).
|
||||
this.scanFnRefSubtree(node, 0);
|
||||
// (`static handlers = { click: onClick }`). The walk above took those
|
||||
// for the property, so the scan for function-as-value candidates
|
||||
// (capture-only, halts at functions) skips the initializer it walked
|
||||
// and covers the rest of the field: the class would keep a second
|
||||
// reference to each function from the same spot.
|
||||
this.scanFnRefSubtree(node, 0, propNode && valueNode ? new Set([valueNode.id]) : undefined);
|
||||
skipChildren = true;
|
||||
} else {
|
||||
this.extractMethod(node);
|
||||
@@ -1616,16 +1619,18 @@ export class TreeSitterExtractor {
|
||||
const definedType = declaredType && isClassLikeDefinition(declaredType) ? declaredType : null;
|
||||
if (definedType) this.visitNode(definedType);
|
||||
const walked = this.extractVariable(node);
|
||||
// extractVariable doesn't walk every initializer shape (object literals
|
||||
// are deliberately skipped; Python/Ruby don't walk at all), so scan the
|
||||
// declaration subtree for function-as-value candidates — `const routes =
|
||||
// { home: renderHome }`, `handlers = {"recv": target_cb}`. The scan halts
|
||||
// at nested function definitions (their bodies are walked — and
|
||||
// attributed — separately) and flush-time dedup absorbs any overlap with
|
||||
// initializers extractVariable DOES walk. A type walked above, and a
|
||||
// component wrapper's initializer extractVariable walked under the
|
||||
// component (which must not hand the component it wraps to the file),
|
||||
// captured their own.
|
||||
// extractVariable doesn't walk every initializer (a store's members are
|
||||
// extracted one by one, and only TS/JS, Python, Go and Rust walk one at
|
||||
// all), so scan the declaration subtree for function-as-value candidates
|
||||
// — `const routes = { home: renderHome }`, `handlers = {"recv":
|
||||
// target_cb}`. The scan halts at nested function definitions (their
|
||||
// bodies are walked — and attributed — separately). It skips what was
|
||||
// walked already: a type walked above, and every initializer
|
||||
// extractVariable walked under the symbol it declares — a constant's, a
|
||||
// component wrapper's (which must not hand the component it wraps to the
|
||||
// file). Those captured their own, and flush-time dedup keys on the
|
||||
// source, so the file would keep a second reference to each function
|
||||
// from the same spot.
|
||||
this.scanFnRefSubtree(node, 0, definedType ? new Set([definedType.id]) : walked);
|
||||
skipChildren = true; // extractVariable handles children
|
||||
}
|
||||
@@ -2016,9 +2021,9 @@ export class TreeSitterExtractor {
|
||||
createNode: (kind, name, node, extra) => self.createNode(kind, name, node, extra),
|
||||
visitNode: (node) => self.visitNode(node),
|
||||
visitFunctionBody: (body, functionId) => self.visitFunctionBody(body, functionId),
|
||||
walkInitializer: (node) => {
|
||||
walkInitializer: (node, functionId = '') => {
|
||||
(ctx.walked ??= new Set()).add(node.id);
|
||||
self.visitFunctionBody(node, '');
|
||||
self.visitFunctionBody(node, functionId);
|
||||
},
|
||||
addUnresolvedReference: (ref) => self.unresolvedReferences.push(ref),
|
||||
pushScope: (nodeId) => self.nodeStack.push(nodeId),
|
||||
@@ -2869,8 +2874,8 @@ export class TreeSitterExtractor {
|
||||
/**
|
||||
* Extract a class field declaration (e.g. Java field_declaration, C# field_declaration).
|
||||
* Extracts each declarator as a 'field' kind node inside the owning class.
|
||||
* Returns the C# declarators it walked, for the function-as-value scan to
|
||||
* skip.
|
||||
* Returns the initializers it walked (a C# declarator, a Java `value`), for
|
||||
* the function-as-value scan to skip.
|
||||
*/
|
||||
private extractField(node: SyntaxNode): Set<number> {
|
||||
const walked = new Set<number>();
|
||||
@@ -2988,11 +2993,11 @@ export class TreeSitterExtractor {
|
||||
decl.namedChild(decl.namedChildCount - 1),
|
||||
varDecl ? getChildByField(varDecl, 'type') : null,
|
||||
);
|
||||
// Its function-as-value candidates are the field's, captured
|
||||
// here once. (VB.NET captures none; Java's scan still takes
|
||||
// its initializers for the class as well.)
|
||||
walked.add(decl.id);
|
||||
}
|
||||
// Its function-as-value candidates are the field's, captured here
|
||||
// once (VB.NET captures none): the scan would take Java's again for
|
||||
// the class.
|
||||
walked.add(valueNode.id);
|
||||
this.visitFunctionBody(valueNode, fieldNode.id);
|
||||
this.nodeStack.pop();
|
||||
}
|
||||
@@ -3584,9 +3589,11 @@ export class TreeSitterExtractor {
|
||||
* Extracts top-level and module-level variable declarations.
|
||||
* Captures the variable name and first 100 chars of initializer in signature for searchability.
|
||||
*
|
||||
* Returns the initializers it walked under a component wrapper, whose
|
||||
* function-as-value candidates that walk already took (the caller's scan of
|
||||
* the declaration skips them).
|
||||
* Returns the initializers it walked under the symbol they declare — a
|
||||
* constant's or a variable's (#693), a component wrapper's — whose
|
||||
* function-as-value candidates that walk already took for the symbol. The
|
||||
* caller's scan of the declaration skips them: it would take each again for
|
||||
* the file, as a second reference to the same function from the same spot.
|
||||
*/
|
||||
private extractVariable(node: SyntaxNode): Set<number> | undefined {
|
||||
if (!this.extractor) return undefined;
|
||||
@@ -3686,6 +3693,7 @@ export class TreeSitterExtractor {
|
||||
const ownedObject = varNode ? this.ownedObjectValue(valueNode) : null;
|
||||
if (varNode && ownedObject) {
|
||||
this.extractOwnedObjectMembers(ownedObject, varNode.id, true, false);
|
||||
(walked ??= new Set()).add(ownedObject.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3772,6 +3780,7 @@ export class TreeSitterExtractor {
|
||||
if (varNode) this.nodeStack.push(varNode.id);
|
||||
this.visitFunctionBody(valueNode, '');
|
||||
if (varNode) this.nodeStack.pop();
|
||||
if (varNode) (walked ??= new Set()).add(valueNode.id);
|
||||
}
|
||||
|
||||
if (extractObjectMethods && objectOfFns) {
|
||||
@@ -3820,6 +3829,7 @@ export class TreeSitterExtractor {
|
||||
if (assigned) this.nodeStack.push(assigned.id);
|
||||
this.visitFunctionBody(right, '');
|
||||
if (assigned) this.nodeStack.pop();
|
||||
if (assigned) (walked ??= new Set()).add(right.id);
|
||||
}
|
||||
} else if (this.language === 'go') {
|
||||
// Go: var_declaration, short_var_declaration, const_declaration
|
||||
@@ -3860,6 +3870,7 @@ export class TreeSitterExtractor {
|
||||
if (varNode) this.nodeStack.push(varNode.id);
|
||||
this.visitFunctionBody(valueField, varNode?.id ?? '');
|
||||
if (varNode) this.nodeStack.pop();
|
||||
if (varNode) (walked ??= new Set()).add(valueField.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4016,6 +4027,7 @@ export class TreeSitterExtractor {
|
||||
if (declared) this.nodeStack.push(declared.id);
|
||||
this.visitFunctionBody(valueNode, '');
|
||||
if (declared) this.nodeStack.pop();
|
||||
if (declared) (walked ??= new Set()).add(valueNode.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user