diff --git a/CHANGELOG.md b/CHANGELOG.md index a9c7d4a00..4eb1c2ad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -383,6 +383,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, calls through a method receiver or parameter of an unexported type, the usual shape of gRPC and HTTP handlers (`s.service.AddItem()` inside `func (s *server) Create()`), now resolve, and always within that type's own package: another package's `server` with a same-named method no longer takes the call, and a method the type gets from a struct or interface it embeds is found too. Re-index Go projects after upgrading. Thanks @GoDiao for the report and the fix. (#2323) +- In Go, a call through a variable that holds the result of a type assertion, like `st.Fetch(key)` after `st := v.(storage.Store)` or `rc.Read(p)` after `rc, ok := v.(ReadCloser)`, now links to that method of the asserted type, as a call made through the assertion itself does, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, such a call linked to whichever method shared the name or to nothing, and a variable named like a standard-library package, like grpc-go's `parser, ok := builder.(balancer.ConfigParser)`, was taken for that package. Re-index Go projects after upgrading. - In Go, calls into a module whose `go.mod` is not at the project root now resolve, whether it is a `server/` backend next to a `web/` frontend or one of several modules side by side as in etcd, so calls like `store.New()` and `s.db.CreateItem()` find their targets instead of being treated as calls into a third-party package. A name written through a package, like a `job.OPCommand` result type or a field of type `artifact.Manager`, now links to that package's symbol rather than a same-named one elsewhere, which also corrects links in projects with a single `go.mod`. Re-index Go projects after upgrading. Thanks @GoDiao for the report and @danusha2345 for the fix. (#2322) - JavaScript and TypeScript named-object resolution reuses scoped file facts and releases them when the resolver closes. diff --git a/README.md b/README.md index bd9de166e..cfed71685 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates. ## About this fork -This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`09511055`](https://github.com/colbymchenry/codegraph/commit/09511055) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. +This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`16bc05a2`](https://github.com/colbymchenry/codegraph/commit/16bc05a2) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below). @@ -80,7 +80,7 @@ Session search uses `.codegraph/sessions-v2.db`. The first session search import ### What the fork adds -Compared with upstream `main` at `09511055` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit. +Compared with upstream `main` at `16bc05a2` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit. | Feature | Upstream | Fork | What it does | |---|:-:|:-:|---| diff --git a/__tests__/go-asserted-locals.test.ts b/__tests__/go-asserted-locals.test.ts new file mode 100644 index 000000000..cae77a705 --- /dev/null +++ b/__tests__/go-asserted-locals.test.ts @@ -0,0 +1,381 @@ +/** + * A Go local bound from a type assertion has the asserted type: + * + * if f, ok := w.(http.Flusher); ok { + * f.Flush() // http.Flusher's, nothing in the project + * } + * wr := v.(*Wrapper) + * wr.Flush() // what *Wrapper has, promoted from *storage.Base + * + * Receiver inference read `v := T{}`, `var v T` and parameters, never + * `v := x.(T)` or `v, ok := x.(T)`, so such a call went to whichever project + * method name matching picked. The asserted type is now found where Go finds + * it, as for a call made through the assertion itself (`x.(T).M()`): in the + * file's own package or one it dot-imports for a bare name, in the imported + * project package for `pkg.T`, an alias being the type it names. A type from + * outside the project, a predeclared one or an alias of such a type links + * nothing. A type literal, or a type declared inside a function, says nothing + * the index can follow: the call goes to name matching, as before. The + * binding is the one in scope at the call: an assertion in an `if` header + * does not reach a later variable of that name, nor does a nearer + * declaration in a block that has ended hide it. A local named like a + * standard-library package (`parser`) is the local. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; + +/** + * The module the tests index. Namesakes sit where name matching looked + * first: an `alpha` package that sorts ahead of `storage`, and a same-file + * `sink` with each method name. + */ +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + 'alpha/alpha.go': `package alpha + +type Store interface { + Fetch(key string) string +} + +type sink struct{} + +func (s *sink) Fetch(key string) string { return "" } + +func (s *sink) Flush() error { return nil } + +func (s *sink) Read(p []byte) (int, error) { return 0, nil } + +func (s *sink) Error() string { return "" } + +func (s *sink) Len() int { return 0 } + +func (s *sink) Start() {} + +type Ctx struct{} + +func (c *Ctx) Done() <-chan struct{} { return nil } + +type BufFlusher struct{} + +func (b *BufFlusher) Flush() error { return nil } +`, + 'storage/storage.go': `package storage + +type Store interface { + Fetch(key string) string +} + +type Base struct{} + +func (b *Base) Flush() error { return nil } +`, + 'store/store.go': `package store + +import ( + "context" + + "example.com/app/storage" +) + +type Reader interface { + Read(p []byte) (int, error) +} + +type ReadCloser interface { + Reader + Close() error +} + +type Job struct{} + +func (j *Job) Start() {} + +type Wrapper struct { + *storage.Base + job *Job +} + +type List[T any] struct{} + +func (l *List[T]) Len() int { return 0 } + +type Ctx = context.Context + +type Keeper = storage.Store +`, + 'store/use.go': `package store + +import ( + "net/http" + + "example.com/app/storage" +) + +type sink struct{} + +func (s *sink) Flush() error { return nil } + +func (s *sink) Fetch(key string) string { return "" } + +func (s *sink) Read(p []byte) (int, error) { return 0, nil } + +func (s *sink) Error() string { return "" } + +func (s *sink) Drain() {} + +func register(fn any) {} + +func lookup(key string) any { return nil } + +func Flusher(w http.ResponseWriter) { + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } +} + +func Promoted(v any) { + wr := v.(*Wrapper) + wr.Flush() +} + +func Embedded(v any, p []byte) { + rc, ok := v.(ReadCloser) + if ok { + rc.Read(p) + } +} + +func Qualified(v any) string { + var st, _ = v.(storage.Store) + return st.Fetch("k") +} + +func Aliased(v any) string { + k := v.(Keeper) + return k.Fetch("k") +} + +func Generic(v any) int { + l := v.(*List[int]) + return l.Len() +} + +func Quoted() string { + s, _ := lookup(")").(storage.Store) + return s.Fetch("k") +} + +func Outside(v any) { + c := v.(Ctx) + c.Done() + if err, ok := v.(error); ok { + err.Error() + } +} + +func Unseen(v any) { + d, _ := v.(interface{ Drain() }) + d.Drain() + type drainer interface{ Drain() } + if dr, ok := v.(drainer); ok { + dr.Drain() + } +} + +func Named(v any) string { + if parser, ok := v.(storage.Store); ok { + return parser.Fetch("k") + } + return "" +} + +func Chain(v any) { + wr := v.(*Wrapper) + wr.job.Start() +} + +func Values(v any, w http.ResponseWriter) { + st := v.(storage.Store) + register(st.Fetch) + f := w.(http.Flusher) + register(f.Flush) +} + +func Assigned(v any) string { + var st storage.Store + st = v.(*sink) + return st.Fetch("k") +} + +func Scoped(w http.ResponseWriter, sinks []*sink) { + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + for _, f := range sinks { + f.Drain() + } +} + +func Shadowed(v any) string { + s, ok := v.(storage.Store) + if !ok { + s := &sink{} + return s.Fetch("fallback") + } + return s.Fetch("k") +} + +func Commented(s *sink) { + // s, ok := v.(http.Flusher) + s.Drain() +} + +func Switched(v any) { + switch x := v.(type) { + case *sink: + x.Drain() + } +} +`, + 'dot/dot.go': `package dot + +import . "example.com/app/storage" + +func Get(v any) string { + s := v.(Store) + return s.Fetch("k") +} +`, +}; + +describe.each(['default', 'wasm'])('Go locals bound from a type assertion have the asserted type (%s)', (backend) => { + let root = ''; + let cg: CodeGraph | undefined; + let kernel: string | undefined; + + beforeAll(async () => { + kernel = process.env.CODEGRAPH_KERNEL; + if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0'; + else delete process.env.CODEGRAPH_KERNEL; + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-asserted-')); + for (const [rel, content] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + // One file with a Windows checkout's line endings. + fs.writeFileSync(path.join(root, rel), rel === 'store/use.go' ? content.replace(/\n/g, '\r\n') : content); + } + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL; + else process.env.CODEGRAPH_KERNEL = kernel; + }); + + /** + * ` -> ::` for every edge + * of `kind` the function `name` in `file` has to a function or method. + */ + function links(file: string, name: string, kind: 'calls' | 'references' = 'calls'): string[] { + const fn = cg!.getNodesInFile(file).find((n) => n.qualifiedName === name && n.kind === 'function'); + expect(fn, `${name} in ${file}`).toBeDefined(); + const lines = FILES[file]!.split('\n'); + return cg! + .getOutgoingEdges(fn!.id) + .filter((e) => e.kind === kind) + .map((e) => ({ e, target: cg!.getNode(e.target)! })) + .filter(({ target }) => target.kind === 'method' || target.kind === 'function') + .map(({ e, target }) => `${lines[e.line! - 1]!.trim()} -> ${target.filePath}::${target.qualifiedName}`) + .sort(); + } + + it('links nothing through an asserted type from outside the project', () => { + // Name matching took alpha's BufFlusher::Flush, whose owner shares the + // receiver's name. + expect(links('store/use.go', 'Flusher')).toEqual([]); + }); + + it("links the asserted project type's method: its own, an embedded type's, or one through a qualifier", () => { + // *Wrapper's Flush is promoted from *storage.Base; ReadCloser's Read from Reader. + expect(links('store/use.go', 'Promoted')).toEqual(['wr.Flush() -> storage/storage.go::Base::Flush']); + expect(links('store/use.go', 'Embedded')).toEqual(['rc.Read(p) -> store/store.go::Reader::Read']); + // `var v, _ = x.(T)` declares as `v, _ := x.(T)` does. + expect(links('store/use.go', 'Qualified')).toEqual(['return st.Fetch("k") -> storage/storage.go::Store::Fetch']); + expect(links('store/use.go', 'Generic')).toEqual(['return l.Len() -> store/store.go::List::Len']); + // A string argument holding a parenthesis is not the end of the call. + expect(links('store/use.go', 'Quoted')).toEqual([ + 'return s.Fetch("k") -> storage/storage.go::Store::Fetch', + 's, _ := lookup(")").(storage.Store) -> store/use.go::lookup', + ]); + }); + + it('follows an alias and a dot import to the type', () => { + expect(links('store/use.go', 'Aliased')).toEqual(['return k.Fetch("k") -> storage/storage.go::Store::Fetch']); + expect(links('dot/dot.go', 'Get')).toEqual(['return s.Fetch("k") -> storage/storage.go::Store::Fetch']); + }); + + it('links nothing through an alias of an outside type or a predeclared type', () => { + expect(links('store/use.go', 'Outside')).toEqual([]); + }); + + it('leaves a call through a type literal or a type declared in a function to the other strategies', () => { + // Upstream name-matches these to sink::Drain, the project's one Drain. The + // fork does not name-guess a callee for a receiver whose declared type the + // project does not declare, so the call stays unlinked; what matters is + // that the assertion does not send it to a wrong type. + expect(links('store/use.go', 'Unseen')).toEqual([]); + }); + + it('takes a local named like a standard-library package for the local', () => { + expect(links('store/use.go', 'Named')).toEqual(['return parser.Fetch("k") -> storage/storage.go::Store::Fetch']); + }); + + it("reaches a field's method through an asserted local", () => { + expect(links('store/use.go', 'Chain')).toEqual(['wr.job.Start() -> store/store.go::Job::Start']); + }); + + it("links a method value taken from an asserted local as the call would", () => { + expect(links('store/use.go', 'Values', 'references')).toEqual([ + 'register(st.Fetch) -> storage/storage.go::Store::Fetch', + ]); + }); + + it("leaves an assertion assigned to a declared variable that variable's type", () => { + // `st = v.(*sink)` declares nothing: st is still a storage.Store. + expect(links('store/use.go', 'Assigned')).toEqual(['return st.Fetch("k") -> storage/storage.go::Store::Fetch']); + }); + + it('reads the binding in scope at the call', () => { + // The `if` header's f ends with its block: the loop's f is a *sink. + expect(links('store/use.go', 'Scoped')).toEqual(['f.Drain() -> store/use.go::sink::Drain']); + // The block's s := &sink{} ends with the block; the asserted s is back. + expect(links('store/use.go', 'Shadowed')).toEqual([ + 'return s.Fetch("fallback") -> store/use.go::sink::Fetch', + 'return s.Fetch("k") -> storage/storage.go::Store::Fetch', + ]); + // A comment declares nothing. + expect(links('store/use.go', 'Commented')).toEqual(['s.Drain() -> store/use.go::sink::Drain']); + // A type switch's variable has its clause's type, which is not read: the + // call resolves as it did before. Upstream name-matches it to sink::Drain; + // the fork leaves a receiver of an unread type unlinked. + expect(links('store/use.go', 'Switched')).toEqual([]); + }); + + it('never links a namesake in another package or a same-file type', () => { + const decoys = [ + ...cg!.getNodesInFile('alpha/alpha.go'), + ...cg!.getNodesInFile('store/use.go').filter((n) => ['sink::Flush', 'sink::Read', 'sink::Error'].includes(n.qualifiedName)), + ].filter((n) => n.kind === 'method'); + expect(decoys.length).toBe(12); + for (const decoy of decoys) { + expect( + cg!.getIncomingEdges(decoy.id).filter((e) => (e.kind === 'calls' || e.kind === 'references') && e.provenance !== 'heuristic'), + decoy.qualifiedName, + ).toEqual([]); + } + }); +}); diff --git a/codegraph-kernel/src/resolve/fields.rs b/codegraph-kernel/src/resolve/fields.rs index 4a0b723bf..9816c3592 100644 --- a/codegraph-kernel/src/resolve/fields.rs +++ b/codegraph-kernel/src/resolve/fields.rs @@ -16,10 +16,25 @@ impl KernelResolver { return Ok(None); } let (base, field) = (segs[0], segs[1]); - let Some(base_type) = self.infer_local_receiver_type(base, r, true)? else { - return Ok(None); + // A local bound from a type assertion has the asserted type. + let asserted = match self.go_asserted_local_type(base, r) { + Some(written) => match self.go_written_type(&written, r)? { + Some(super::go_asserted::GoAsserted::Project { name, dir }) => Some((name, dir)), + Some(super::go_asserted::GoAsserted::Outside) => return Ok(None), + None => None, + }, + None => None, + }; + let (base_type, base_dir) = match asserted { + Some(found) => found, + None => { + let Some(base_type) = self.infer_local_receiver_type(base, r, true)? else { + return Ok(None); + }; + let Some(found) = self.go_type_package(&base_type, &r.file_path)? else { return Ok(None); }; + found + } }; - let Some((base_type, base_dir)) = self.go_type_package(&base_type, &r.file_path)? else { return Ok(None); }; // `\bFIELD\s+\*?\[?\]?TYPE` static FIELD_TYPE: LazyLock = LazyLock::new(|| Affix::new("", r"\s+\*?\[?\]?([A-Za-z_][A-Za-z0-9_.]*)", true, false, false)); diff --git a/codegraph-kernel/src/resolve/go_asserted.rs b/codegraph-kernel/src/resolve/go_asserted.rs new file mode 100644 index 000000000..fe681c04e --- /dev/null +++ b/codegraph-kernel/src/resolve/go_asserted.rs @@ -0,0 +1,215 @@ +//! A Go local bound from a type assertion has the asserted type: +//! +//! ```go +//! if f, ok := w.(http.Flusher); ok { f.Flush() } // http.Flusher's, nothing in the project +//! wr := v.(*Wrapper) // wr.Flush() is what *Wrapper has +//! ``` +//! +//! The local's declaration is read from the syntax tree, so the binding is the +//! one in scope at the call: an `if` header's variable does not reach a later +//! variable of that name, a block's shadowing declaration ends with the block, +//! and a comment declares nothing. A parameter, a range or type-switch +//! variable, or a `:=` whose value is not just the assertion declares the +//! name some other way and is left to the other receiver inference. + +use super::*; +use tree_sitter::Node as TsNode; + +/// What a Go type written at a call site names. +pub(super) enum GoAsserted { + /// A type the project declares, with the directory of its package. + Project { name: String, dir: String }, + /// A type from outside the project or a predeclared one: no method the + /// project declares. + Outside, +} + +/// How a name in scope is declared. +enum Declared { + Asserted(String), + Other, +} + +fn text_of<'a>(node: TsNode, text: &'a str) -> &'a str { + &text[node.start_byte()..node.end_byte()] +} + +/// The value of `names = values` when it is one type assertion, for the name +/// at `index` of `names` (the asserted value goes to the first name). +fn declared_by(names: &[TsNode], values: Option, typed: bool, name: &str, text: &str) -> Option { + let index = names.iter().position(|n| text_of(*n, text) == name)?; + let asserted = (|| { + if typed || index != 0 { return None; } + let values = values?; + let mut cursor = values.walk(); + let items: Vec<_> = values.named_children(&mut cursor).collect(); + let [only] = items.as_slice() else { return None; }; + if only.kind() != "type_assertion_expression" { return None; } + only.child_by_field_name("type").map(|t| text_of(t, text).to_string()) + })(); + Some(asserted.map_or(Declared::Other, Declared::Asserted)) +} + +fn identifiers<'t>(list: TsNode<'t>) -> Vec> { + if list.kind() == "identifier" { return vec![list]; } + let mut cursor = list.walk(); + list.named_children(&mut cursor).filter(|n| n.kind() == "identifier").collect() +} + +/// `name`'s declaration among one statement, if the statement declares it. +fn statement_declares(stmt: TsNode, name: &str, text: &str) -> Option { + match stmt.kind() { + "short_var_declaration" => { + let left = identifiers(stmt.child_by_field_name("left")?); + declared_by(&left, stmt.child_by_field_name("right"), false, name, text) + } + "var_declaration" | "const_declaration" | "var_spec_list" => { + let mut cursor = stmt.walk(); + let mut found = None; + for spec in stmt.named_children(&mut cursor) { + let hit = if spec.kind() == "var_spec_list" { statement_declares(spec, name, text) } else if matches!(spec.kind(), "var_spec" | "const_spec") { + let mut c = spec.walk(); + let names: Vec<_> = spec.children_by_field_name("name", &mut c).collect(); + let typed = spec.child_by_field_name("type").is_some() || spec.kind() == "const_spec"; + declared_by(&names, spec.child_by_field_name("value"), typed, name, text) + } else { None }; + if hit.is_some() { found = hit; } + } + found + } + _ => None, + } +} + +fn params_declare(list: Option, name: &str, text: &str) -> bool { + let Some(list) = list else { return false; }; + let mut cursor = list.walk(); + for param in list.named_children(&mut cursor) { + let mut c = param.walk(); + if param.children_by_field_name("name", &mut c).any(|n| text_of(n, text) == name) { return true; } + } + false +} + +/// The declaration of `name` that `scope` itself holds before `site`, if any. +fn scope_declares(scope: TsNode, name: &str, site: usize, text: &str) -> Option { + match scope.kind() { + "block" | "expression_case" | "default_case" | "type_case" | "communication_case" => { + let mut cursor = scope.walk(); + let mut found = None; + for stmt in scope.named_children(&mut cursor).filter(|s| s.end_byte() <= site) { + if let Some(hit) = statement_declares(stmt, name, text) { found = Some(hit); } + } + found + } + "if_statement" | "expression_switch_statement" | "type_switch_statement" | "for_statement" => { + let mut found = None; + if let Some(init) = scope.child_by_field_name("initializer").filter(|i| i.end_byte() <= site) { + found = statement_declares(init, name, text); + } + if scope.kind() == "type_switch_statement" { + if let Some(alias) = scope.child_by_field_name("alias").filter(|a| a.end_byte() <= site) { + if identifiers(alias).iter().any(|n| text_of(*n, text) == name) { found = Some(Declared::Other); } + } + } + if scope.kind() == "for_statement" { + let mut cursor = scope.walk(); + for clause in scope.named_children(&mut cursor).filter(|c| c.end_byte() <= site) { + match clause.kind() { + "range_clause" => { + if let Some(left) = clause.child_by_field_name("left") { + if identifiers(left).iter().any(|n| text_of(*n, text) == name) { found = Some(Declared::Other); } + } + } + "for_clause" => { + if let Some(hit) = clause.child_by_field_name("initializer").and_then(|i| statement_declares(i, name, text)) { found = Some(hit); } + } + _ => {} + } + } + } + found + } + "function_declaration" | "method_declaration" | "func_literal" => { + let declared = params_declare(scope.child_by_field_name("parameters"), name, text) + || params_declare(scope.child_by_field_name("receiver"), name, text) + || params_declare(scope.child_by_field_name("result").filter(|r| r.kind() == "parameter_list"), name, text); + declared.then_some(Declared::Other) + } + _ => None, + } +} + +impl KernelResolver { + /// The type a Go local was asserted to, as written, when the declaration + /// of `name` in scope at `r` binds the value of a type assertion. + pub(super) fn go_asserted_local_type(&mut self, name: &str, r: &ResolveRefIn) -> Option { + if r.language != "go" || name.is_empty() || !name.bytes().all(|b| b == b'_' || b.is_ascii_alphanumeric()) { return None; } + let file = self.read_file(&r.file_path)?; + let text = file.text(); + // Only a file that asserts a type can bind from one. + if !text.contains(".(") { return None; } + let tree = self.parsed_tree(&file, r)?; + let site = super::iteration::descendant_for_position(tree.root_node(), text, + ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1)); + let at = site.start_byte(); + let mut scope = Some(site); + while let Some(node) = scope { + if let Some(declared) = scope_declares(node, name, at, text) { + return match declared { Declared::Asserted(ty) => Some(ty), Declared::Other => None }; + } + if matches!(node.kind(), "function_declaration" | "method_declaration") { return None; } + scope = node.parent(); + } + None + } + + /// The project type a Go type written at `r` names, found where Go finds + /// it: in the file's own package or a package it dot-imports for a bare + /// name, in the imported package for a qualified one. `None` for a type + /// the index cannot tell (a type literal, a type declared inside a + /// function, a type parameter): the call goes to the other strategies. + pub(super) fn go_written_type(&mut self, written: &str, r: &ResolveRefIn) -> Res> { + let Some(hit) = re!(r"^\*?\s*(?:([A-Za-z_]\w*)\s*\.\s*)?([A-Za-z_]\w*)\s*(?:\[[\s\S]*\])?$").captures(written.trim()) else { return Ok(None); }; + let qualifier = hit.get(1).map(|m| m.as_str().to_string()); + let name = hit[2].to_string(); + let declared = self.nodes_by_name(&name)?.iter() + .filter(|n| n.language == "go" && matches!(n.kind.as_str(), "struct" | "interface" | "type_alias") && !n.qualified_name.contains("::")) + .map(|n| pos_dirname(&n.file_path).to_string()).collect::>(); + let dir = if let Some(qualifier) = qualifier { + match self.go_imported_package_dir(&r.file_path, &qualifier)? { + None => return Ok(Some(GoAsserted::Outside)), + Some(dir) if declared.contains(&dir) => dir, + Some(_) => return Ok(None), + } + } else { + let mut dirs = vec![pos_dirname(&r.file_path).to_string()]; + for import in self.import_mappings(&r.file_path)?.iter().filter(|i| i.local_name == ".") { + if let Some(dir) = self.go_package_dir(&import.source, &r.file_path) { dirs.push(dir); } + } + match dirs.into_iter().find(|d| declared.contains(d)) { + Some(dir) => dir, + None => return Ok(GO_BUILTIN_FIELD_TYPES.contains(name.as_str()).then_some(GoAsserted::Outside)), + } + }; + Ok(Some(GoAsserted::Project { name, dir })) + } + + /// The type of the Go local `name` at `r` when it is bound from a type + /// assertion whose type is settled. `None`: not such a local. + pub(super) fn go_asserted_local(&mut self, name: &str, r: &ResolveRefIn) -> Res> { + let Some(written) = self.go_asserted_local_type(name, r) else { return Ok(None); }; + self.go_written_type(&written, r) + } + + /// The verdict for `receiver.method` when the receiver is an asserted + /// local: the method the asserted type has, or nothing. `None`: not an + /// asserted local; the other strategies decide. + pub(super) fn go_asserted_member(&mut self, receiver: &str, method: &str, r: &ResolveRefIn) -> Res>> { + Ok(match self.go_asserted_local(receiver, r)? { + None => None, + Some(GoAsserted::Outside) => Some(None), + Some(GoAsserted::Project { name, dir }) => Some(self.go_method_in_package(&name, method, &dir, r)?), + }) + } +} diff --git a/codegraph-kernel/src/resolve/member_fn_ref.rs b/codegraph-kernel/src/resolve/member_fn_ref.rs index 9ed8018e9..77b6c1022 100644 --- a/codegraph-kernel/src/resolve/member_fn_ref.rs +++ b/codegraph-kernel/src/resolve/member_fn_ref.rs @@ -48,6 +48,8 @@ impl KernelResolver { if r.language == "go" { let typed = if receiver.contains('.') { Some(self.match_go_field_chain_call(receiver, member, r)?) + } else if let Some(verdict) = self.go_asserted_member(receiver, member, r)? { + Some(verdict) } else if let Some(raw) = self.infer_local_receiver_type(receiver, r, true)? { Some(self.go_method_on_declared_type(&raw, &r.file_path, member, r)?) } else { diff --git a/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index 4f0d6b0b3..01c927c8e 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -672,6 +672,9 @@ impl KernelResolver { } if inferable { + if r.language == "go" { + if let Some(verdict) = self.go_asserted_member(&object_or_class, &method_name, r)? { return Ok(verdict); } + } // A PHP `instanceof` branch narrows the receiver inside its body. if let Some(t) = probe!(r, "mc:guarded", self.infer_guarded_receiver(&object_or_class, r)?) { return self.match_bound_type_member(&t, &method_name, r); @@ -835,6 +838,9 @@ if inferred.is_none() && r.language == "dart" { if let Some(hit) = self.enum_constant_call(&object_or_class, &method_name, r)? { return Ok(Some(hit)); } } if inferable { + if r.language == "go" { + if let Some(verdict) = self.go_asserted_member(&object_or_class, &method_name, r)? { return Ok(verdict); } + } // No binding anchor under requireReceiverEvidence=false — the // inferrers run at the ref's own site with qualified names // normalized (preserveQualifiedName=false). diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index c996d38d1..fbb2f1b91 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -554,6 +554,7 @@ mod vbnet; mod receivers; mod bound; mod fields; +mod go_asserted; mod method_call; mod pipeline; mod file_refs; diff --git a/codegraph-kernel/src/resolve/prefilter.rs b/codegraph-kernel/src/resolve/prefilter.rs index a34f44dc9..0fbdc154f 100644 --- a/codegraph-kernel/src/resolve/prefilter.rs +++ b/codegraph-kernel/src/resolve/prefilter.rs @@ -51,7 +51,8 @@ impl KernelResolver { if r.language == "go" && !is_binding_receiver_call(r) { 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.is_shadowed_import(&name[..dot], r).unwrap_or(false) + && self.go_asserted_local(&name[..dot], r).ok().flatten().is_none() { return true; } }