From f165fff30b17c1243786f95f330ca71579dd2479 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sun, 11 Oct 2026 12:46:25 -0600 Subject: [PATCH 1/3] fix(resolve): link Go calls through a local that shadows a project package A Go local declared as `log := log.G(ctx)` took the name of the imported project package from its own line, so `log.G` and the later `log.Errorf` both stayed unresolved. A project package named like a standard-library one (`log`, `context`) was also taken for the standard library even with nothing shadowing it. On the right of a `:=` the name now means what it meant before the line, and an import that resolves inside the module is no longer dropped as a standard-library call. --- CHANGELOG.md | 1 + README.md | 2 +- __tests__/go-stdlib-named-locals.test.ts | 16 ++++++++++---- codegraph-kernel/src/resolve/bound.rs | 8 +++++-- codegraph-kernel/src/resolve/member_fn_ref.rs | 14 ++++++++++++- codegraph-kernel/src/resolve/prefilter.rs | 21 ++++++++++++++++++- 6 files changed, 53 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a0a69f6b..e4b18bd07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -502,6 +502,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - React Router routes kept in a table of their own are now indexed: the ASP.NET Core React template's `AppRoutes` array that `App.js` maps into ``, a `routes` array imported into `useRoutes(routes)` or `createBrowserRouter(routes)` or returned by a function as in `useRoutes(routes(isLoggedIn))`, route objects kept one per file and listed in `createBrowserRouter([MainRoutes, LoginRoutes])`, and routes written in place in `useRoutes([…])`. Before, route objects were read only from a file that itself creates a data router, so these apps had no routes, and their `navigate('/login')` calls and `` links led nowhere. Each route links to the page its `element` renders, past a guard like `` or ``, and through `lazy(() => import(…))` to the page it loads rather than a same-named page elsewhere in the repository. An `index: true` route is the page at its parent's address, and a parent route around others counts as their layout. A `{ path, element }` list that nothing hands to the router, such as a menu, still makes no routes. Re-index React projects after upgrading. - In Go, a call through a parameter or local variable named like a standard-library package, such as `context`, `trace`, `printer` or `token`, now links to the method it calls. Before, every call written `context.…` or `printer.…` was taken for a call into the standard library and linked to nothing: gin's `context.AbortWithError(…)` inside `func(context *Context)`, harbor's `context.IsAuthenticated()` after `context := NewSecurityContext(…)`, etcd's `printer.DBHashKV(…)` and `trace.Step(…)`. The variable's declaration decides the method: the type it is declared as, or the package of the project function its value comes from. A variable holding a value from outside your project, like `scanner := bufio.NewScanner(r)` or `url, err := url.Parse(raw)`, still links to nothing, and one whose type can't be told links only to a method no other type in the project has, never to one picked because it matches the variable's name. Re-index Go projects after upgrading. +- In Go, a local declared from a function of a project package that shares its name, like harbor's `log := log.G(ctx)`, now links `log.G` to that function and the later `log.Errorf` to the method of the logger it returns. Before, both linked to nothing: the new local hid the package from its own line, and a project package named like a standard-library one, such as `log` or `context`, was taken for the standard library. A direct call into such a project package, `log.G(ctx)` with no local, now links too. Re-index Go projects after upgrading. - React Router routes written as JSX now read `` the way route objects do: inside `}>`, `} />` is the page at `/`, and `Layout` counts as the layout of every route nested in it, as does a guard with no path of its own like `}>`. Before, the index route was never read, so `/` led to the layout itself and a `` holding only an index route led to nothing. A route's `element` written on lines of its own, the way Prettier formats a long one, or wrapped in `}>`, now links to the page it shows rather than to nothing or to the fallback, and a path written in braces, like `path={"agents"}`, is read like any other. Re-index React projects after upgrading. - A React Router route whose `lazy` loader picks its page from the module it loads now links to that page: `lazy: async () => { const { TestTab } = await import('./tabs'); return { Component: TestTab }; }`, `const { default: Component } = await import('./pages/Login')`, `import('./reports').then((m) => ({ Component: m.ReportList }))`, `({ Component: (await import('./reports')).ReportPage })`, React Router 7's `lazy: { Component: async () => (await import('./show')).Show }`, and a page the loader shows inside a guard, like `element: `. Before, such a route linked to the module's default export, else its `Component` export, which is a different page or none at all, as with luci-go's tabs loaded from a folder that forwards them. A loader written as a method, `async lazy() { … }`, is now read too, so its route is no longer missing, and a loader that hands React Router only a `loader` no longer makes the module's default export the route's page or the layout of the routes inside it. Re-index React projects after upgrading. diff --git a/README.md b/README.md index 5ca47f9dd..dcb26c6ff 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ C# property accessors and expression-bodied properties contribute calls and refe C# field and property initializers retain their calls and references under the member that owns them. Target-typed `new()` resolves relative declared types through enclosing namespaces and honors `global::` qualification without requiring a redundant `using`. VB.NET resolves typed receivers, enclosing and inherited members, and field or property reads through values and Shared types without choosing unrelated project declarations. Names beginning with keywords, such as `SharedCache`, `Dimension` and `NewItem`, retain their declarations. -Go declaration comments attach to the declared type, and defined types retain references to their named component types. Assertion calls follow the asserted project type and each declared return in a method chain. Local bindings shadow package imports. C/C++ types defined beside variables retain their members and comments; nested template arguments preserve receiver ownership, and leading `::` names require a visible global declaration. C++ declaration attributes and macros preserve source positions. Multiplication operands remain intact, and anonymous pointer declarations share their first declared type name. Macro-opened namespaces combine with ordinary nested scopes when resolving included owners. Renamed and default imports are retried when their module appears during sync. Named-only components are not default exports; explicit forwarded defaults retain their target. Whitespace before a type argument does not create a JSX rendering edge. React JSX respects component imports and local shadowing; lazy routes follow component exports through local barrels. Named flow paths prefer an equally long path containing more requested symbols. +Go declaration comments attach to the declared type, and defined types retain references to their named component types. Assertion calls follow the asserted project type and each declared return in a method chain. Local bindings shadow package imports, except on the right of the `:=` that declares them, and a project package named like a standard-library one is not taken for it. C/C++ types defined beside variables retain their members and comments; nested template arguments preserve receiver ownership, and leading `::` names require a visible global declaration. C++ declaration attributes and macros preserve source positions. Multiplication operands remain intact, and anonymous pointer declarations share their first declared type name. Macro-opened namespaces combine with ordinary nested scopes when resolving included owners. Renamed and default imports are retried when their module appears during sync. Named-only components are not default exports; explicit forwarded defaults retain their target. Whitespace before a type argument does not create a JSX rendering edge. React JSX respects component imports and local shadowing; lazy routes follow component exports through local barrels. Named flow paths prefer an equally long path containing more requested symbols. The native resolver includes upstream's Go import-name assumptions, type-position checks, alias method forwarding and embedded interface method sets. Chained Go calls follow declared return types at each fluent step; an unproven receiver stays unresolved instead of borrowing an unrelated method by name. C++ bases use enclosing-scope directives, parent-scope lookup and translation-unit visibility; included test-named headers remain eligible, while declared external library receivers avoid unrelated project methods. Incremental sync retries newly available Liquid targets, lazy route modules and navigation destinations. Navigation in an imported route-table file also survives removing and restoring its destination. Route-table nodes and their references commit together, so a failed reference write can be retried without leaving an incomplete route. React layout links exclude Vue and Angular routes. C++ receiver declaration scans exclude multiline raw-string contents. diff --git a/__tests__/go-stdlib-named-locals.test.ts b/__tests__/go-stdlib-named-locals.test.ts index 50967e33f..0365585d6 100644 --- a/__tests__/go-stdlib-named-locals.test.ts +++ b/__tests__/go-stdlib-named-locals.test.ts @@ -206,6 +206,10 @@ func Refresh(ctx context.Context) { log := log.G(ctx) log.Errorf("refresh quota: %v", ctx) } + +func Handle(ctx context.Context) { + _ = log.G(ctx) +} `, // kubernetes' post-start hooks: the hook context gets Done from the // standard library's context it embeds, not from a project type. @@ -427,11 +431,15 @@ describe('a Go parameter or local named like a standard-library package', () => expect(reachedAt('etcdmain/grpc_proxy.go', 'tls.ClientConfig()')).toEqual(['transport/tls.go:TLSInfo::ClientConfig']); }); - // Not ported to the native resolver: a local declared as `log := log.G(ctx)` - // shadows the imported `log` package from its own line, and `log.G` and the - // later `log.Errorf` both stay unresolved. `it.fails` turns red once they resolve. - it.fails('calls the Logger method of a local that takes the name of the project package that hands it out', () => { + // `log := log.G(ctx)`: the new local takes the name of the imported project + // package from its own line, but the right of `:=` still means the package, + // so `log.G` calls the package's function and the later `log.Errorf` calls the + // method of the `*Logger` it returns. + it('calls the Logger method of a local that takes the name of the project package that hands it out', () => { + expect(reachedAt('quota/util.go', 'log := log.G(')).toEqual(['lib/log/log.go:G']); expect(reachedAt('quota/util.go', 'log.Errorf(')).toEqual(['lib/log/log.go:Logger::Errorf']); + // Nothing shadows it in `Handle`, and the import is the project's, not the standard library's. + expect(reachedAt('quota/util.go', '_ = log.G(')).toEqual(['lib/log/log.go:G']); }); it('takes the type named like the variable among that package\'s', () => { diff --git a/codegraph-kernel/src/resolve/bound.rs b/codegraph-kernel/src/resolve/bound.rs index 29d6e5983..54bc4d963 100644 --- a/codegraph-kernel/src/resolve/bound.rs +++ b/codegraph-kernel/src/resolve/bound.rs @@ -1423,10 +1423,14 @@ Ok(self.dart_member_of(type_name, method, r, false)? let mut factory_site = site.clone(); factory_site.line = binding.line; factory_site.reference_name = name.clone(); - let factory_binding = innermost_binding( + // `log := log.G(ctx)`: on the right of `:=`, `log` is what it + // named before the new local. + let factory_root = name.split('.').next().unwrap_or(&name); + let factory_binding = innermost_binding_unless( &site_bindings, - name.split('.').next().unwrap_or(&name), + factory_root, Some(binding.line), + factory_root == receiver, ) .cloned(); let mut callee: Option> = None; diff --git a/codegraph-kernel/src/resolve/member_fn_ref.rs b/codegraph-kernel/src/resolve/member_fn_ref.rs index b951a27d2..64c796d18 100644 --- a/codegraph-kernel/src/resolve/member_fn_ref.rs +++ b/codegraph-kernel/src/resolve/member_fn_ref.rs @@ -208,7 +208,19 @@ impl KernelResolver { /// parameter, a local)? Files without binding rows keep the import. pub(super) fn is_shadowed_import(&mut self, name: &str, r: &ResolveRefIn) -> Res { let rows = self.bindings(&r.file_path)?; - Ok(innermost_binding(&rows, name, Some(r.line)).is_some_and(|b| b.kind != "import")) + let before = self.go_declares_on_line(name, r, &rows); + Ok(innermost_binding_unless(&rows, name, Some(r.line), before).is_some_and(|b| b.kind != "import")) + } + + /// Go: does `r` sit on the line that declares `name` with `:=`? The name + /// there, on the right of the declaration, is not yet the new local. + pub(super) fn go_declares_on_line(&mut self, name: &str, r: &ResolveRefIn, rows: &[KBinding]) -> bool { + let declared = r.language == "go" + && innermost_binding(rows, name, Some(r.line)).is_some_and(|b| b.kind == "local" && b.line == r.line); + declared + && self.read_file(&r.file_path) + .and_then(|ls| ls.get((r.line - 1).max(0) as usize).cloned()) + .is_some_and(|text| text.split_once(":=").is_some_and(|(names, _)| names.split(',').any(|n| n.trim() == name))) } /// The Python receiver arms: `self`/`cls`, `self.field`, a local or diff --git a/codegraph-kernel/src/resolve/prefilter.rs b/codegraph-kernel/src/resolve/prefilter.rs index 0fbdc154f..aa7dfa941 100644 --- a/codegraph-kernel/src/resolve/prefilter.rs +++ b/codegraph-kernel/src/resolve/prefilter.rs @@ -52,6 +52,7 @@ impl KernelResolver { if let Some(dot) = name.find('.') { if dot > 0 && GO_STDLIB_PACKAGES.contains(&name[..dot]) && !self.is_shadowed_import(&name[..dot], r).unwrap_or(false) + && self.go_imported_package_dir(&r.file_path, &name[..dot]).ok().flatten().is_none() && self.go_asserted_local(&name[..dot], r).ok().flatten().is_none() { return true; } @@ -591,12 +592,29 @@ pub(super) fn innermost_binding<'a>( rows: &'a [KBinding], name: &str, line: Option, +) -> Option<&'a KBinding> { + innermost_binding_unless(rows, name, line, false) +} + +/// `innermost_binding` that, when `before_declaration`, passes over a local +/// declared on `line` itself. A local's scope spans its whole block, so on +/// its own line the narrowest row is the new local, while the right of a Go +/// `:=` still sees what the name meant before: `log := log.G(ctx)` calls the +/// imported package's `G`. +pub(super) fn innermost_binding_unless<'a>( + rows: &'a [KBinding], + name: &str, + line: Option, + before_declaration: bool, ) -> Option<&'a KBinding> { let mut best: Option<&KBinding> = None; for r in rows { if r.name != name { continue; } + if before_declaration && r.kind == "local" && Some(r.line) == line { + continue; + } if let Some(l) = line { if l < r.scope_start || l > r.scope_end { continue; @@ -614,7 +632,8 @@ impl KernelResolver { /// not replace a value; a future local still supplies shadowing evidence. pub(super) fn receiver_binding(&mut self, name: &str, site: &ResolveRefIn) -> Res> { let rows = self.bindings(&site.file_path)?; - let Some(binding) = innermost_binding(&rows, name, Some(site.line)) else { return Ok(None) }; + let before = self.go_declares_on_line(name, site, &rows); + let Some(binding) = innermost_binding_unless(&rows, name, Some(site.line), before) else { return Ok(None) }; if site.language != "python" { return Ok(Some(binding.clone())); } From 24638ae2b2056b18cbd3da9ea3bcdf962216796c Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sun, 11 Oct 2026 12:58:38 -0600 Subject: [PATCH 2/3] fix(resolve): treat every name a Go := line declares as shadowed on its right A factory call whose package name is declared beside the receiver, as in `logger, log := log.G(ctx), 0`, looked the package up as the new local and linked nothing. The lookup now passes over a local declared on that line for any name on the left, not only the receiver. --- __tests__/go-stdlib-named-locals.test.ts | 7 +++++++ codegraph-kernel/src/resolve/bound.rs | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/__tests__/go-stdlib-named-locals.test.ts b/__tests__/go-stdlib-named-locals.test.ts index 0365585d6..2b649bc0d 100644 --- a/__tests__/go-stdlib-named-locals.test.ts +++ b/__tests__/go-stdlib-named-locals.test.ts @@ -210,6 +210,11 @@ func Refresh(ctx context.Context) { func Handle(ctx context.Context) { _ = log.G(ctx) } + +func Pair(ctx context.Context) { + logger, log := log.G(ctx), 0 + logger.Errorf("pair %d", log) +} `, // kubernetes' post-start hooks: the hook context gets Done from the // standard library's context it embeds, not from a project type. @@ -440,6 +445,8 @@ describe('a Go parameter or local named like a standard-library package', () => expect(reachedAt('quota/util.go', 'log.Errorf(')).toEqual(['lib/log/log.go:Logger::Errorf']); // Nothing shadows it in `Handle`, and the import is the project's, not the standard library's. expect(reachedAt('quota/util.go', '_ = log.G(')).toEqual(['lib/log/log.go:G']); + // A name the line declares beside the receiver is shadowed from its line too. + expect(reachedAt('quota/util.go', 'logger.Errorf(')).toEqual(['lib/log/log.go:Logger::Errorf']); }); it('takes the type named like the variable among that package\'s', () => { diff --git a/codegraph-kernel/src/resolve/bound.rs b/codegraph-kernel/src/resolve/bound.rs index 54bc4d963..e35ebafb2 100644 --- a/codegraph-kernel/src/resolve/bound.rs +++ b/codegraph-kernel/src/resolve/bound.rs @@ -1423,14 +1423,14 @@ Ok(self.dart_member_of(type_name, method, r, false)? let mut factory_site = site.clone(); factory_site.line = binding.line; factory_site.reference_name = name.clone(); - // `log := log.G(ctx)`: on the right of `:=`, `log` is what it - // named before the new local. + // `log := log.G(ctx)`, `logger, log := log.G(ctx), 0`: on the + // right of `:=`, a name the line declares is what it named before. let factory_root = name.split('.').next().unwrap_or(&name); let factory_binding = innermost_binding_unless( &site_bindings, factory_root, Some(binding.line), - factory_root == receiver, + names.iter().any(|n| n == factory_root), ) .cloned(); let mut callee: Option> = None; From 380d56691511c21fea708220116fddcf65b8598b Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sun, 11 Oct 2026 13:04:23 -0600 Subject: [PATCH 3/3] fix(resolve): limit the Go := shadow exception to the declaration's right side A call after a `;` on the declaring line, as in `log := log.G(ctx); log.Errorf(x)`, is a later statement and sees the new local, not the package. The check now reads the call's column and the statement's end, and the factory lookup passes the factory call's own column. --- __tests__/go-stdlib-named-locals.test.ts | 6 ++++++ codegraph-kernel/src/resolve/bound.rs | 2 ++ codegraph-kernel/src/resolve/member_fn_ref.rs | 19 +++++++++++++------ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/__tests__/go-stdlib-named-locals.test.ts b/__tests__/go-stdlib-named-locals.test.ts index 2b649bc0d..4dfd4466b 100644 --- a/__tests__/go-stdlib-named-locals.test.ts +++ b/__tests__/go-stdlib-named-locals.test.ts @@ -211,6 +211,10 @@ func Handle(ctx context.Context) { _ = log.G(ctx) } +func Later(ctx context.Context) { + log := log.G(ctx); log.Errorf("later") +} + func Pair(ctx context.Context) { logger, log := log.G(ctx), 0 logger.Errorf("pair %d", log) @@ -445,6 +449,8 @@ describe('a Go parameter or local named like a standard-library package', () => expect(reachedAt('quota/util.go', 'log.Errorf(')).toEqual(['lib/log/log.go:Logger::Errorf']); // Nothing shadows it in `Handle`, and the import is the project's, not the standard library's. expect(reachedAt('quota/util.go', '_ = log.G(')).toEqual(['lib/log/log.go:G']); + // After the `;` the line is a later statement: `log` is the local there. + expect(reachedAt('quota/util.go', 'log.Errorf("later")').sort()).toEqual(['lib/log/log.go:G', 'lib/log/log.go:Logger::Errorf']); // A name the line declares beside the receiver is shadowed from its line too. expect(reachedAt('quota/util.go', 'logger.Errorf(')).toEqual(['lib/log/log.go:Logger::Errorf']); }); diff --git a/codegraph-kernel/src/resolve/bound.rs b/codegraph-kernel/src/resolve/bound.rs index e35ebafb2..536ed641f 100644 --- a/codegraph-kernel/src/resolve/bound.rs +++ b/codegraph-kernel/src/resolve/bound.rs @@ -1422,6 +1422,8 @@ Ok(self.dart_member_of(type_name, method, r, false)? let name = caps[2].to_string(); let mut factory_site = site.clone(); factory_site.line = binding.line; + // The call sits on the declaration line, at the factory's name. + factory_site.column = declaration[..caps.get(2).map_or(0, |m| m.start())].encode_utf16().count() as i64; factory_site.reference_name = name.clone(); // `log := log.G(ctx)`, `logger, log := log.G(ctx), 0`: on the // right of `:=`, a name the line declares is what it named before. diff --git a/codegraph-kernel/src/resolve/member_fn_ref.rs b/codegraph-kernel/src/resolve/member_fn_ref.rs index 64c796d18..6e46997b2 100644 --- a/codegraph-kernel/src/resolve/member_fn_ref.rs +++ b/codegraph-kernel/src/resolve/member_fn_ref.rs @@ -212,15 +212,22 @@ impl KernelResolver { Ok(innermost_binding_unless(&rows, name, Some(r.line), before).is_some_and(|b| b.kind != "import")) } - /// Go: does `r` sit on the line that declares `name` with `:=`? The name - /// there, on the right of the declaration, is not yet the new local. + /// Go: does `r` sit on the right of a `:=` that declares `name`? The name + /// there is not yet the new local. The right side runs to the next `;`: + /// a call after it is a later statement and sees the local. pub(super) fn go_declares_on_line(&mut self, name: &str, r: &ResolveRefIn, rows: &[KBinding]) -> bool { let declared = r.language == "go" && innermost_binding(rows, name, Some(r.line)).is_some_and(|b| b.kind == "local" && b.line == r.line); - declared - && self.read_file(&r.file_path) - .and_then(|ls| ls.get((r.line - 1).max(0) as usize).cloned()) - .is_some_and(|text| text.split_once(":=").is_some_and(|(names, _)| names.split(',').any(|n| n.trim() == name))) + let Some(text) = declared + .then(|| self.read_file(&r.file_path).and_then(|ls| ls.get((r.line - 1).max(0) as usize).cloned())) + .flatten() + else { + return false; + }; + let Some(at) = text.find(":=") else { return false }; + let end = text[at..].find(';').map_or(text.len(), |i| at + i); + let col = super::names::js_unit_to_byte(&text, r.column.max(0) as usize); + text[..at].split(',').any(|n| n.trim() == name) && col > at && col < end } /// The Python receiver arms: `self`/`cls`, `self.field`, a local or