Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
5 changes: 4 additions & 1 deletion codegraph-kernel/src/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down
10 changes: 10 additions & 0 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node> = {
docstring,
signature,
visibility,
isExported,
isAsync,
isStatic,
isAbstract,
Expand Down
4 changes: 3 additions & 1 deletion src/resolution/import-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down