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