From 90c5dc6fc964e22b3ec1fb69b97406d948e3220f Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Fri, 23 Oct 2026 13:26:58 -0500 Subject: [PATCH] fix(extraction): a Go method carries the exportedness of its name Every Go method was indexed as unexported, whatever the case of its name, in both engines: extractMethod never set isExported, and the kernel's go.rs mirrored the omission. Plain functions carried the flag, so dead-code results listed exported methods like Close as dead. A method that is a top-level declaration (methodsAreTopLevel, only Go) now gets the extractor's isExported, as a function does, and go.rs sets is_exported from the same name-case rule in extract_method. Added on top of the contributor's change: findGoPackageMember, which resolves pkg.Name through a project import, matched any exported node of that name in the package's directory, and methods stayed out only because none was exported. With the flag set, a package-level call like harbor's log.Errorf(...) went to the method Logger::Errorf declared beside the function, and prometheus's embedded storage.Appender resolved to a method named Appender, which dropped its implements edges. A method is no member of its package, so it is now skipped there. Two comments in name-matcher.ts that said the flag is unset on every Go method now say why the name's case is still read. Validation: the contributor's test fails on main and passes here, and the new package-call test fails with the PR as first written and passes here and on main, in both engines. A/B against main on harbor 296f304, prometheus a2fda0e, gin 0f09c3a and cobra adbc881: nodes differ only in is_exported 0 -> 1 on uppercase Go methods (5,704, 3,880, 391 and 141). Edges are identical on prometheus and cobra; 22 retargets remain, all name-only guesses between an interface's method declaration and the struct method implementing it, which now win: gin's engine().GET and its siblings reach RouterGroup's methods, and harbor's jobservice routes reach DefaultHandler's. The PR as first written had retargeted 1,384 harbor calls and removed 60 prometheus implements and dispatch sites. The dead-code report lists 3 Go symbols on harbor instead of 167, and 0 on prometheus instead of 11. Kernel parity: 2,493 of 2,493 Go files byte-identical. The controls express and flask are byte-identical. Index CPU on harbor, median of 5 interleaved: 40.8 s on main, 39.8 s here. EXTRACTION_VERSION stays: main already moved it to 28 after 1.6.2, so 1.6.2 indexes get the re-index hint anyway. Original PR #1734 by @danusha2345. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/extraction.test.ts | 54 +++++++++++++++++++++++++++++++ codegraph-kernel/src/go.rs | 5 ++- src/extraction/tree-sitter.ts | 10 ++++++ src/resolution/import-resolver.ts | 4 ++- src/resolution/name-matcher.ts | 6 ++-- 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a1d3dfd..e5990dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -226,6 +226,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, a type defined from other types, like etcd's `type WatchChan <-chan WatchResponse` or gin's `type HandlerFunc func(*Context)` and `type HandlersChain []HandlerFunc`, now links to the types it is built from, so impact on `WatchResponse` or gin's `Context` reaches these types and the code that uses them. Before, such a type linked to nothing. One written through another package, like etcd's `type PutResponse pb.PutResponse`, links to that package's type, and a recursive one like `type stateFn func(*Lexer) stateFn` doesn't link to itself. Re-index Go projects after upgrading. - In Go, a struct now links to the types its fields are declared with, like etcd's `PutResponse` holding a `*ResponseHeader` or gin's `Context` holding a `HandlersChain`, so impact on `ResponseHeader` or `HandlersChain` reaches every struct that holds one and the code that uses those structs, and `codegraph_node` lists a struct's field types. Before, a struct linked only to the types it embeds. A field written through another package, like `*pb.ResponseHeader`, links to that package's type, one from outside your project, like `context.Context`, links to nothing, the types an embedded generic is filled in with, like the `Pod` in client-go's `listers.ResourceIndexer[*v1.Pod]`, link too, and a struct that holds itself, like a list node's `next *node`, doesn't link to itself. `codegraph_explore` still answers a question about a flow with the code that runs it, not with the structs that merely hold one of its types. Re-index Go projects after upgrading. - In Go, an unexported name used in another file of its package, like prometheus's `memSeries` in `tsdb/head_append.go`, now links to that package's own declaration when a package nested inside it, like `tsdb/agent`, declares the same name. Before, such a parameter, result, struct field, composite literal or call linked to nothing. A call or a function value that the surrounding function binds itself, like a test's `init := func(…)` followed by `init(t)`, or a local `volumeSpec` variable passed on, no longer links to the package's declaration of that name. A name bound in one `case` of a `switch` or `select`, or a parameter named inside a function-typed result like `func() func(run string)`, is not such a binding anywhere else, so a call there still links. Thanks @inth3shadows. (#2580) Re-index Go projects after upgrading. +- In Go, a method whose name starts with an uppercase letter, like `func (w *writer) Close()`, is now marked exported, as a function of that name already was. Before, every Go method was marked unexported, so dead-code results listed exported methods that code outside the project may call. Re-index Go projects after upgrading. Thanks @danusha2345. (#1734) - In Go, when a function uses a package function as a value, like `apply(run)`, and also has a local or parameter of the same name somewhere, like a `run := 2` inside an `if` or a `case`, a loop variable, or a closure's parameter, each use is now read on its own: a use the local covers links to nothing, and a use outside it links to the package function. Before, only the function's first use was looked at, so a local used first hid every later use of the package function. Re-index Go projects after upgrading. Thanks @inth3shadows for the report. (#2596) - In Go, a method an interface declares, like etcd's `KV.Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)`, now links to the types its parameters and results name, so impact on `PutResponse` or `OpOption` reaches the interface method and every caller that goes through the interface. Before, an interface's methods linked to no types at all. Re-index Go projects after upgrading. - In Go, a package imported under an explicit name, like `v3 "go.etcd.io/etcd/client/v3"`, now owns that name even when another import's path ends in it, like `"github.com/cheggaaa/pb/v3"` (package `pb`) in etcd's benchmark tool. Before, whichever of the two came first took every `v3.` name, so `v3.Client` and `v3.Op` could link to nothing. Re-index Go projects after upgrading. diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index ad71de8c..a481cb8a 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -13742,6 +13742,60 @@ describe('C/C++ kernel-port preParse blanks (R7a)', () => { } }); + it('Go: a method carries the exportedness of its name, like a function', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-go-method-exported-')); + try { + fs.writeFileSync( + path.join(dir, 'log.go'), + 'package log\n\ntype writer struct{}\n\nfunc (w *writer) Close() error { return nil }\n\nfunc (w *writer) flush() {}\n\nfunc Open() *writer { return &writer{} }\n\nfunc helper() {}\n' + ); + const cg = await CodeGraph.init(dir, { index: true }); + try { + const flag = (name: string) => + cg.getNodesByKind('method').concat(cg.getNodesByKind('function')).find((n) => n.name === name)!.isExported; + expect(flag('Close')).toBe(true); + expect(flag('flush')).toBe(false); + expect(flag('Open')).toBe(true); + expect(flag('helper')).toBe(false); + } finally { + cg.close(); + } + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('Go: a call through a package stays on its function, not an exported method of that name', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-go-package-member-')); + try { + fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/app\n\ngo 1.22\n'); + fs.mkdirSync(path.join(dir, 'applog')); + // The method comes first, so a lookup by name meets it before the function. + fs.writeFileSync( + path.join(dir, 'applog', 'log.go'), + 'package applog\n\ntype Logger struct{}\n\nfunc (l *Logger) Infof(format string) {}\n\nfunc Infof(format string) {}\n' + ); + fs.mkdirSync(path.join(dir, 'svc')); + fs.writeFileSync( + path.join(dir, 'svc', 'use.go'), + 'package svc\n\nimport "example.com/app/applog"\n\nfunc Use(l *applog.Logger) {\n\tapplog.Infof("x")\n\tl.Infof("y")\n}\n' + ); + const cg = await CodeGraph.init(dir, { index: true }); + try { + const use = cg.getNodesByKind('function').find((n) => n.name === 'Use')!; + const calls = cg.getOutgoingEdgesFrom([use.id], ['calls']).map((e) => { + const target = cg.getNode(e.target)!; + return `${e.line} ${target.kind} ${target.qualifiedName}`; + }).sort(); + expect(calls).toEqual(['6 function Infof', '7 method Logger::Infof']); + } finally { + cg.close(); + } + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + it('blankCStatementMacroCalls blanks indented iterator macros, keeps the block', async () => { const { blankCStatementMacroCalls } = await import('../src/extraction/languages/c-cpp'); const src = [ diff --git a/codegraph-kernel/src/go.rs b/codegraph-kernel/src/go.rs index a4557e13..eec327f9 100644 --- a/codegraph-kernel/src/go.rs +++ b/codegraph-kernel/src/go.rs @@ -524,7 +524,10 @@ impl<'t> Walker<'t> { signature: self.signature_of(node), return_type: self.return_type_of(node), qualified_name: receiver_type.as_ref().map(|r| format!("{r}::{name}")), - ..Extra::default() // extractMethod passes no isExported + // methodsAreTopLevel: a Go method is a top-level declaration, so + // extractMethod gives it the function's exportedness (name case). + is_exported: Some(self.is_exported(node)), + ..Extra::default() }; let Some(row) = self.create_node("method", &name, node, extra) else { return }; diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index fb47f50a..988a607e 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -2790,10 +2790,20 @@ export class TreeSitterExtractor { // on every ordinary method (breaks kernel↔wasm parity JSON equality). const isAbstract = this.extractor.isAbstract?.(node) ? true : undefined; const returnType = this.extractor.getReturnType?.(node, this.source); + // A method that is a top-level declaration (Go: `func (r *T) Name()`) has + // the same exportedness rule as a function — the name's case — and a + // consumer asking "can another package name this?" needs it on methods + // too. Class members keep the flag unset: their reachability is the + // class's, and the languages whose isExported walks the parent chain + // (JS/TS) would otherwise re-mark every member of an exported class. + const isExported = this.extractor.methodsAreTopLevel + ? this.extractor.isExported?.(node, this.source) + : undefined; const extraProps: Partial = { docstring, signature, visibility, + isExported, isAsync, isStatic, isAbstract, diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index e0f8f4df..58a571da 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -3935,7 +3935,9 @@ function findGoPackageMember( if (pkgDir == null) return null; for (const node of context.getNodesByName(memberName)) { if (node.language !== 'go') continue; - if (!node.isExported) continue; + // A method is no member of its package: `pkg.Infof` is the package's + // function, never a method `(*Logger).Infof` declared beside it. + if (!node.isExported || node.kind === 'method') continue; if (path.posix.dirname(node.filePath.replace(/\\/g, '/')) === pkgDir) { return { original: ref, diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index f35cecd3..cc168392 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -3370,7 +3370,7 @@ function isSfcPrivate(n: Node, context: ResolutionContext): boolean { * Whether Go lets `ref`'s file name the Go declaration `candidate`: an * exported name from anywhere, an unexported one only inside its own package, * which is a directory. By the name's first letter, not the extractor's flag: - * the flag is unset for every Go method, exported or not. + * an index built before Go methods carried the flag has it unset on every one. * * A package's external tests (`package a_test` beside `package a`) share its * directory but are a package of their own: a name one of them declares at @@ -3412,8 +3412,8 @@ function isGoNameVisible(candidate: Node, ref: UnresolvedRef, context: Resolutio * files its `part` directives name — so another file of the library names * it, and a file of another library never does (#2466). * - **Go**: an unexported (lowercase) identifier is package-local, and a - * package is a directory. Judged by the name's case: the extractor's - * `isExported` is unset for every Go method. + * package is a directory. Judged by the name's case, which an older index's + * Go methods don't carry in `isExported`. * - **Rust**: a non-`pub` item is visible to its module and that module's * descendants, never to a sibling module or another crate — `.count()` on * an iterator resolved onto a `fn count` in a different crate. A method in