diff --git a/CHANGELOG.md b/CHANGELOG.md index 08226636c..bdc659ef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, a call through an interface now reaches a method that the implementing struct gets from a type it embeds: prometheus's `targetScraper` gets `Report` from the `*Target` it embeds, so a call through the `scraper` interface now reaches `Target.Report`, and etcd's `Hasher.MemberID` reaches `EtcdServer.MemberID` through `hasherAdapter`. Before, the call reached only structs that declare the method themselves, often just a test double, so `codegraph_explore`'s call path stopped at the interface. The method is picked the way Go picks it, from the nearest embedded type, and none is linked when two embedded types at the same depth both have it or it comes from an embedded interface. Re-index Go projects after upgrading. - In Go, a type defined over a slice, map, function or basic type now counts as implementing the interfaces its methods satisfy, just as a struct does: prometheus's `type staticDiscoverer []*targetgroup.Group` is a `Discoverer`, gin's `type formSource map[string][]string` a `setter`, etcd's `type Int64Comparable int64` a `Comparable`, and an adapter like `type HandlerFunc func(…)` a `Handler`. Before, only structs were considered, so these types were missing from an interface's implementations in `codegraph_explore`, impact and the type hierarchy, and a call through the interface, like `Discoverer.Run` or `setter.TrySet`, stopped there instead of reaching their methods. Re-index Go projects after upgrading. - In Go, a type alias like `type Event = mvccpb.Event`, alone or inside a `type ( … )` group, is now indexed and links to the type it names, so impact on that type reaches the code that uses the alias. Before, aliases were missing: a use written through the alias's package, like `clientv3.Event` or prometheus's `storage.AOptions{…}`, linked to nothing, and one inside the package, like `&PrometheusVersion{}` in prometheus's `web` package, linked to whichever type of that name the project had, there a test helper's struct. A method called on a value of an alias's type now reaches the aliased type's method rather than a namesake's, and a generic alias like `type Items[T any] = List[T]` links to `List`, so `xs.Len()` on one reaches `List.Len` instead of nothing. Re-index Go projects after upgrading. +- In Go, a struct that embeds another is no longer treated as overriding the methods it redeclares. gin's `Engine` embeds `RouterGroup` and has a `Use` of its own, but calling `Use` on a `RouterGroup` only ever runs `RouterGroup.Use`; before, `codegraph_explore`'s call path and impact went on from it into `Engine.Use` and the code only the engine runs. The same held for every struct or defined type embedded in another that redeclares one of its methods, like etcd's `Mutex` inside `lockerMutex`. A call through an interface still reaches each type that implements it, the embedding struct included. Re-index Go projects after upgrading. - In Angular templates, a property binding, an interpolation, a structural directive or a control-flow block that calls one of the component's own members, like `[name]="icon()"`, `{{ label() }}`, `*ngIf="isOpen()"` or `@if (loading()) {`, now links the component to that member, and so does reading a getter, like `[disabled]="!canSave"`, or handing a method to a child component, like `[displayWith]="displayFn"` or `trackBy: trackById`. Before, only event bindings such as `(click)="save()"` were read, so a signal, a `computed` value, a getter or a method used only from a template had no callers and looked unused. A pipe, a template variable, a call on another object such as `form.reset()`, and a plain field read like `[value]="title"` still link nothing. Re-index Angular projects after upgrading. - 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 or type written through an import whose path ends in a version, or in something other than the package's name, is now known to go through that import: `yaml.Unmarshal(…)` after `import "go.yaml.in/yaml/v3"` or `"gopkg.in/yaml.v3"`, `sqlite3.Error` after `import "github.com/mattn/go-sqlite3"`, `klog.V(2)` after `import "k8s.io/klog/v2"`. Before, only the last part of the path named such an import (`v3`, `yaml.v3`, `go-sqlite3`), so the name was matched on its own and could link to any project function, method or type that shared it: kubernetes' `klog.V(…)` calls were linked to a logging wrapper's `V` method, and etcd's `semver.Version` parameters to an unrelated `Version` function. A name from another module now links to nothing, and one through a package of your own project, like `kit.New()` after `import "example.com/kit/v2"`, links to that package's symbol. A comment in an import block is also no longer taken for the name of the import after it. Re-index Go projects after upgrading. Thanks @danusha2345 for the comment fix. (#2374) diff --git a/__tests__/go-embedding-no-override.test.ts b/__tests__/go-embedding-no-override.test.ts new file mode 100644 index 000000000..d6d116634 --- /dev/null +++ b/__tests__/go-embedding-no-override.test.ts @@ -0,0 +1,250 @@ +/** + * Go has no inheritance. gin's Engine embeds RouterGroup and declares a Use + * of its own: + * + * type Engine struct { + * RouterGroup + * … + * } + * + * func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { + * engine.RouterGroup.Use(middleware...) + * … + * } + * + * The embedding is an `extends` edge (#2397), and the interface-dispatch + * bridge read every `extends` edge as an override, so it linked + * RouterGroup.Use → Engine.Use. But a call on a *RouterGroup only ever runs + * RouterGroup's own Use; nothing dispatches it to the embedder's. A flow or + * impact walk through RouterGroup.Use then went on into Engine.Use. Only an + * interface dispatches: a call through IRoutes still reaches both, through + * the implements edges go-implements adds, and so does a call through an + * interface a struct embeds. Java and TypeScript subclasses keep their + * overrides. + */ +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'; +import type { Edge, Node } from '../src/types'; + +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + 'gin/gin.go': `package gin + +type HandlerFunc func() + +type HandlersChain []HandlerFunc + +func (c HandlersChain) Last() HandlerFunc { return nil } + +type IRoutes interface { + Use(...HandlerFunc) IRoutes +} + +type RouterGroup struct{ Handlers HandlersChain } + +func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { + group.Handlers = append(group.Handlers, middleware...) + return group +} + +type Engine struct { + RouterGroup + trees []string +} + +func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { + engine.RouterGroup.Use(middleware...) + engine.rebuild404Handlers() + return engine +} + +func (engine *Engine) rebuild404Handlers() {} + +// A Last of its own over the defined type it embeds. +type routeInfo struct { + HandlersChain + path string +} + +func (r routeInfo) Last() HandlerFunc { return nil } +`, + // etcd's client/v3/concurrency: embedded through a pointer. + 'concurrency/mutex.go': `package concurrency + +type Locker interface { + Lock() + Unlock() +} + +type Mutex struct{ key string } + +func (m *Mutex) Lock() {} +func (m *Mutex) Unlock() {} + +type lockerMutex struct{ *Mutex } + +func (lm *lockerMutex) Lock() { lm.Mutex.Lock() } +func (lm *lockerMutex) Unlock() { lm.Mutex.Unlock() } +`, + // etcd's generated gRPC stub, and a test server that overrides one RPC. + 'api/auth.go': `package api + +type AuthServer interface { + Authenticate(name string) error + UserAdd(name string) error +} + +type UnimplementedAuthServer struct{} + +func (UnimplementedAuthServer) Authenticate(name string) error { return nil } +func (UnimplementedAuthServer) UserAdd(name string) error { return nil } +`, + 'client/mock.go': `package client + +import "example.com/app/api" + +type mockAuthServer struct { + api.UnimplementedAuthServer +} + +func (mockAuthServer) Authenticate(name string) error { return nil } +`, + // A struct that embeds the interface it wraps does dispatch: a call + // through Appender on a limitAppender runs limitAppender's Append. + 'storage/appender.go': `package storage + +type Appender interface { + Append(v float64) error + Commit() error +} + +type limitAppender struct { + Appender + limit int +} + +func (a *limitAppender) Append(v float64) error { return a.Appender.Append(v) } +`, + 'java/src/main/java/app/Base.java': 'package app;\n\npublic class Base {\n public void run() {}\n}\n', + 'java/src/main/java/app/Derived.java': + 'package app;\n\npublic class Derived extends Base {\n @Override\n public void run() {}\n}\n', + 'web/shapes.ts': + 'export class Shape {\n area(): number { return 0; }\n}\n\nexport class Square extends Shape {\n area(): number { return 1; }\n}\n', +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-embedding-override-')); + for (const [rel, content] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); +}, 120_000); + +afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +const meta = (e: Edge): Record => (e.metadata ?? {}) as Record; + +/** The one non-import node with this name in this file. */ +function one(name: string, file: string): Node { + const found = cg.getNodesByName(name).filter((n) => n.filePath === file && n.kind !== 'import'); + expect(found, `${name} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** The method `name` that the type `owner` in `file` contains. */ +function method(owner: string, name: string, file: string): Node { + const found = cg + .getOutgoingEdgesFrom([one(owner, file).id], ['contains']) + .map((e) => cg.getNode(e.target)) + .filter((n): n is Node => n?.kind === 'method' && n.name === name); + expect(found, `${owner}.${name} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** Where the interface-dispatch bridge sends a call to this method. */ +function dispatchesTo(m: Node): string[] { + return cg + .getOutgoingEdgesFrom([m.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort(); +} + +/** The type that contains a method. */ +function ownerOf(id: string): Node | undefined { + return cg + .getIncomingEdgesTo([id], ['contains']) + .map((e) => cg.getNode(e.source)) + .find((n): n is Node => !!n && n.kind !== 'file'); +} + +describe('a Go embedding is not an override', () => { + it('embeds structs and a defined type through declared supertype edges', () => { + // The shapes the bridge used to read as inheritance. + const types = ['Engine', 'routeInfo', 'lockerMutex', 'mockAuthServer'].map((name) => + cg.getNodesByName(name).find((n) => n.language === 'go' && n.kind === 'struct')! + ); + const embeds = cg + .getOutgoingEdgesFrom(types.map((n) => n.id), ['extends']) + .filter((e) => e.provenance !== 'heuristic') + .map((e) => `${cg.getNode(e.source)!.name} -> ${cg.getNode(e.target)!.kind} ${cg.getNode(e.target)!.name}`) + .sort(); + expect(embeds).toEqual([ + 'Engine -> struct RouterGroup', + 'lockerMutex -> struct Mutex', + 'mockAuthServer -> struct UnimplementedAuthServer', + 'routeInfo -> type_alias HandlersChain', + ]); + }); + + it('links no method of an embedded struct or defined type to the embedder\'s method of that name', () => { + expect(dispatchesTo(method('RouterGroup', 'Use', 'gin/gin.go'))).toEqual([]); + expect(dispatchesTo(method('HandlersChain', 'Last', 'gin/gin.go'))).toEqual([]); + expect(dispatchesTo(method('Mutex', 'Lock', 'concurrency/mutex.go'))).toEqual([]); + expect(dispatchesTo(method('UnimplementedAuthServer', 'Authenticate', 'api/auth.go'))).toEqual([]); + // Nor anywhere else: every Go dispatch edge starts at an interface method. + const goMethods = cg.getNodesByKind('method').filter((n) => n.language === 'go'); + const fromConcrete = cg + .getOutgoingEdgesFrom(goMethods.map((n) => n.id), ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl' && ownerOf(e.source)?.kind !== 'interface') + .map((e) => `${cg.getNode(e.source)!.qualifiedName} -> ${cg.getNode(e.target)!.qualifiedName}`); + expect(fromConcrete).toEqual([]); + }); + + it('ends a walk through the embedded type\'s method at that method', () => { + const groupUse = method('RouterGroup', 'Use', 'gin/gin.go'); + const engineUse = method('Engine', 'Use', 'gin/gin.go'); + expect(cg.getCallees(groupUse.id).map((c) => c.node.qualifiedName)).not.toContain('Engine::Use'); + expect(cg.getCallers(engineUse.id).map((c) => c.node.qualifiedName)).not.toContain('RouterGroup::Use'); + expect([...cg.getImpactRadius(engineUse.id).nodes.values()].map((n) => n.qualifiedName)).not.toContain( + 'RouterGroup::Use' + ); + }); + + it('still dispatches a call through an interface to each type that implements it', () => { + expect(dispatchesTo(method('IRoutes', 'Use', 'gin/gin.go'))).toEqual(['Engine::Use', 'RouterGroup::Use']); + expect(dispatchesTo(method('Locker', 'Lock', 'concurrency/mutex.go'))).toEqual([ + 'Mutex::Lock', + 'lockerMutex::Lock', + ]); + // limitAppender embeds Appender: a declared edge, not a synthesized one. + expect(dispatchesTo(method('Appender', 'Append', 'storage/appender.go'))).toEqual(['limitAppender::Append']); + }); + + it('keeps the overrides of languages with inheritance', () => { + const run = method('Base', 'run', 'java/src/main/java/app/Base.java'); + expect(dispatchesTo(run).map((qn) => qn.replace(/^.*?(Derived)/, '$1'))).toEqual(['Derived::run']); + const area = method('Shape', 'area', 'web/shapes.ts'); + expect(dispatchesTo(area).map((qn) => qn.replace(/^.*?(Square)/, '$1'))).toEqual(['Square::area']); + }); +}); diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index 87229132a..a8ca74ccf 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -1316,6 +1316,12 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield for (const sup of sups) { const base = queries.getNodeById(sup.target); if (!base || !IFACE_OVERRIDE_LANGS.has(base.language) || base.id === cls.id) continue; + // Go has no inheritance: a Go type's supertype edge to a struct or a + // defined type is an embedding (`type Engine struct { RouterGroup }`), + // and a call on the embedded type runs its own method, never the + // embedder's of the same name. Only a call through an interface + // dispatches. + if (cls.language === 'go' && base.kind !== 'interface') continue; const promotes = goStruct && sup.provenance === 'heuristic' && base.kind === 'interface'; // Group impl methods by name to handle OVERLOADS: an interface `list()` and // `list(params)` are distinct nodes and a call may resolve to either, so