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 @@ -323,6 +323,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Go, the methods a type gets from an embedded standard-library type, like `io.Closer`, `http.ResponseWriter`, `testing.TB`, `*os.File` or `sync.Mutex`, or from the built-in `error`, now count when working out which types implement an interface, each with its parameters and results. Before, they were left out: prometheus's `TBRun`, which is all of `testing.TB` plus `Run`, was listed as implemented by a type that only has a `Run`, an interface made only of standard-library ones, like `slog.Handler` plus `io.Closer`, had no implementations at all, and a struct embedding `sync.Mutex` didn't implement an interface of your own that asks for `Lock` and `Unlock`. An interface that embeds a type from another module, whose methods can't be seen, is now implemented only by a type that embeds the same one or by the only type with all its other methods, not by every type that shares a method name with it. Re-index Go projects after upgrading.
- 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 now counts as implementing an interface only when its methods take and return the interface's types, not just as many of them: caddy's TLS matchers, whose `Match` takes a `*tls.ClientHelloInfo`, no longer implement its HTTP `RequestMatcher`, whose `Match` takes an `*http.Request`. Before, each false match also sent a call through the interface to a method it never reaches, and could push a real implementer off the list. 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 JavaScript and TypeScript, a subclass's `static` method no longer counts as overriding an instance method of the same name in its base class, or the reverse, since calling one never runs the other. In webpack, `codegraph_explore`'s call path and impact went on from `Module`'s `deserialize(context)` into the `static deserialize(context)` of `NormalModule` and other modules. A static method still overrides its base's static method and an instance method its base's instance method, and JavaScript class members are now marked static, as TypeScript ones already were. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and @yang0228 for the pull request. (#2761)
Expand Down
277 changes: 277 additions & 0 deletions __tests__/go-implements-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/**
* Go wants an implementer's methods identical to the interface's, and the
* parameter and result counts alone let a type with the right number of the
* wrong types through:
*
* type RequestMatcher interface { Match(*http.Request) bool } // caddyhttp
* func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool // caddytls
*
* so caddy's TLS matchers "implemented" its HTTP one, and each false entry also
* took a place under the per-interface cap. The pass now compares the types the
* text can settle: each by its name, with the package left off, since the two
* declarations may sit in different packages and write `Context` and
* `caddy.Context`. A true alias, a type parameter and a type that holds a list
* of its own say nothing, and a gRPC server still crosses to its client
* interface at the server's counts.
*/
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 { Node } from '../src/types';

const FILES: Record<string, string> = {
'go.mod': 'module example.com/app\n\ngo 1.22\n',
// caddy's two matcher interfaces, and a matcher of each.
'httpx/matcher.go': `package httpx

import "net/http"

type RequestMatcher interface {
Match(*http.Request) bool
}

type MatchHost []string

func (m MatchHost) Match(r *http.Request) bool { return false }

type Encode struct{}

func (e *Encode) Match(rw *responseWriter) bool { return false }

type responseWriter struct{}
`,
'tlsx/matcher.go': `package tlsx

import "crypto/tls"

type ConnectionMatcher interface {
Match(*tls.ClientHelloInfo) bool
}

type MatchServerName []string

func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { return false }
`,
// A type its own package writes bare and another through its package.
'core/modules.go': `package core

type Context struct{}

type Provisioner interface {
Provision(Context) error
}

type Own struct{}

func (o *Own) Provision(ctx Context) error { return nil }
`,
'plugin/plugin.go': `package plugin

import (
"example.com/app/core"
mod "example.com/app/core"
)

type Plugin struct{}

func (p *Plugin) Provision(ctx core.Context) error { return nil }

type Renamed struct{}

func (r *Renamed) Provision(ctx mod.Context) error { return nil }
`,
// A grouped list on one side, none on the other; the results are named on one.
'lists/lists.go': `package lists

type Copier interface {
Copy(string, string) (int, error)
}

type Grouped struct{}

func (g *Grouped) Copy(from, to string) (n int, err error) { return 0, nil }

type Bytes struct{}

func (b *Bytes) Copy(from []byte, to string) (int, error) { return 0, nil }
`,
// interface{} and any, uint8 and byte are one type.
'spelled/spelled.go': `package spelled

type Sink interface {
Put(v any, b []byte)
}

type Old struct{}

func (o *Old) Put(v interface{}, b []uint8) {}
`,
// A true alias stands for another name; a defined type does not.
'aliased/aliased.go': `package aliased

type Handle = uint64

type Store interface {
Get(id uint64) string
}

type ByAlias struct{}

func (a *ByAlias) Get(id Handle) string { return "" }

type Other uint64

type ByDefined struct{}

func (d *ByDefined) Get(id Other) string { return "" }
`,
// A generic declaration's parameters stand for any type, its other types don't.
'generic/generic.go': `package generic

type Keyed interface {
Key() string
Value() int
}

type Pair[K comparable, V any] struct{}

func (p *Pair[K, V]) Key() K { var k K; return k }
func (p *Pair[K, V]) Value() V { var v V; return v }

type Tagged[T any] struct{}

func (t *Tagged[T]) Key() T { var k T; return k }
func (t *Tagged[T]) Value() int64 { return 0 }

type Getter[T any] interface {
Get() T
}

type Text struct{}

func (t *Text) Get() string { return "" }
`,
// What a generic base passes on is read with its parameters.
'promoted/promoted.go': `package promoted

type Base[T any] struct{}

func (b *Base[T]) Load() T { var v T; return v }

type Loader interface {
Load() string
}

type Holder struct {
Base[string]
}
`,
// A type that holds a list of its own isn't compared.
'funcs/funcs.go': `package funcs

import "context"

type Runner interface {
Run(fn func(context.Context) error)
}

type Job struct{}

func (j *Job) Run(fn func(ctx context.Context) error) {}
`,
// A gRPC server and its generated client interface, at the server's counts.
'pb/rpc_grpc.pb.go': `package pb

import (
"context"

"google.golang.org/grpc"
)

type RangeRequest struct{}
type RangeResponse struct{}

type KVClient interface {
Range(ctx context.Context, in *RangeRequest, opts ...grpc.CallOption) (*RangeResponse, error)
}
`,
'server/kv.go': `package server

import (
"context"

pb "example.com/app/pb"
)

type kvServer struct{}

func (s *kvServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) {
return nil, nil
}
`,
};

describe('go-implements compares what the signatures say of the types', () => {
let dir: string;
let cg: CodeGraph;
const implementers = (iface: string): string[] => {
const node = cg.getNodesByKind('interface').find((n: Node) => n.name === iface);
expect(node, iface).toBeDefined();
return cg.getIncomingEdges(node!.id).filter((e) => e.kind === 'implements')
.map((e) => cg.getNode(e.source)!.name).sort();
};

beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-impl-types-'));
for (const [rel, text] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
fs.writeFileSync(path.join(dir, rel), text);
}
cg = await CodeGraph.init(dir, { index: true });
}, 60_000);

afterAll(() => {
cg?.close();
if (dir) fs.rmSync(dir, { recursive: true, force: true });
});

it('keeps a matcher of another parameter type out', () => {
expect(implementers('RequestMatcher')).toEqual(['MatchHost']);
expect(implementers('ConnectionMatcher')).toEqual(['MatchServerName']);
});

it('reads a type by its name, however its package is written', () => {
expect(implementers('Provisioner')).toEqual(['Own', 'Plugin', 'Renamed']);
});

it('reads a grouped list, a named result and an unnamed list alike', () => {
expect(implementers('Copier')).toEqual(['Grouped']);
});

it('takes `interface{}` for `any` and `uint8` for `byte`', () => {
expect(implementers('Sink')).toEqual(['Old']);
});

it('lets a true alias stand for its type, and no defined type', () => {
expect(implementers('Store')).toEqual(['ByAlias']);
});

it('reads a declaration\'s type parameters as any type, and the rest as written', () => {
expect(implementers('Keyed')).toEqual(['Pair']);
expect(implementers('Getter')).toEqual(['Text']);
});

it('reads what a generic base promotes with its parameters', () => {
expect(implementers('Loader')).toEqual(['Base', 'Holder']);
});

it('leaves a type with a list of its own uncompared', () => {
expect(implementers('Runner')).toEqual(['Job']);
});

it('still crosses from a gRPC client interface to the server of its service', () => {
expect(implementers('KVClient')).toEqual(['kvServer']);
});
});
16 changes: 8 additions & 8 deletions __tests__/go-promoted-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,14 @@ describe('a call through a Go interface reaches the method embedding promotes',
it('links the interface method to the embedded type that declares it', () => {
const seriesSet = one('SeriesSet', 'storage/interface.go');
const chunkSet = one('ChunkSeriesSet', 'storage/interface.go');
// blockSeriesSet's embedding, the first of the two.
const embedding = at('tsdb/querier.go', '\tblockBaseSeriesSet');
// Both structs satisfy both interfaces by name, and both get Next from
// the same base: one edge, through the first of them.
for (const iface of [seriesSet, chunkSet]) {
expect(dispatch(cg, methodOf(cg, iface, 'Next'))).toEqual([`blockBaseSeriesSet::Next <- blockSeriesSet @${embedding}`]);
expect(dispatch(cg, methodOf(cg, iface, 'Err'))).toEqual([`blockBaseSeriesSet::Err <- blockSeriesSet @${embedding}`]);
expect(dispatch(cg, methodOf(cg, iface, 'At'))).toEqual(['blockChunkSeriesSet::At', 'blockSeriesSet::At']);
const lines = FILES['tsdb/querier.go']!.split('\n');
const embedded = lines.flatMap((text, i) => (text === '\tblockBaseSeriesSet' ? [`tsdb/querier.go:${i + 1}`] : []));
// Each struct's `At` returns its own interface's type, and each gets Next
// from the same base, through its own embedding.
for (const [iface, type, embedding] of [[seriesSet, 'blockSeriesSet', embedded[0]], [chunkSet, 'blockChunkSeriesSet', embedded[1]]] as const) {
expect(dispatch(cg, methodOf(cg, iface, 'Next'))).toEqual([`blockBaseSeriesSet::Next <- ${type} @${embedding}`]);
expect(dispatch(cg, methodOf(cg, iface, 'Err'))).toEqual([`blockBaseSeriesSet::Err <- ${type} @${embedding}`]);
expect(dispatch(cg, methodOf(cg, iface, 'At'))).toEqual([`${type}::At`]);
}
});

Expand Down
Loading