diff --git a/CHANGELOG.md b/CHANGELOG.md index e478835a..15f9099a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,6 +199,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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) +- In Java, Kotlin, C# and Scala, each overload of an interface or base-class method now links only to the overrides with the same parameters. Before, every overload linked to every method of that name in the implementing class, so `ExampleService.putTairValue(String)` also led to `putTairValue(List, double)`, and `codegraph_explore`'s call path, callers and impact went into overloads that call never runs. Where a parameter is a type parameter, like `save(T entity)` implemented as `save(String entity)`, or the class is written in another language than its interface, the number of parameters decides; a method declared once on each side links as before. Re-index Java, Kotlin, C# and Scala projects after upgrading. Thanks @hxj1225 for the report and @danusha2345 for the pull request. (#2663) - In JavaScript and TypeScript, a `super.run()` call now links to the method of the class's parent, or of the nearest ancestor that has it, like three.js's `super.copy(source)` reaching `Object3D.copy`, so `codegraph_explore`'s call path and impact follow an override into the method it builds on. Before, such a call inside a method of the same name was dropped, and elsewhere it went to whatever method had the name, often the class's own or a sibling class's in the same file. A call from a static method reaches the parent's static method, and a call to a parent from outside your project, like `HTMLElement`, `Set` or `Promise`, links nothing. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Puma7 for the report and the pull request. (#2497) - In Go, a call through a struct field that holds a function, like prometheus's `d.refreshf(ctx)`, now reaches the functions put in that field, so `codegraph_explore` follows service discovery from `Discovery.Run` into each provider's `refresh` and names the line where each provider wires its method in, like `RefreshF: d.refresh`. The field can be set in a struct literal or by an assignment, copied from another struct's field (`refreshf: opts.RefreshF`), or given a method of an interface value, and two unrelated structs with a field of the same name, like `fn` or `handler`, stay apart. Before, such a flow stopped at the field. Re-index Go projects after upgrading. - 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. diff --git a/__tests__/override-overload-pairing.test.ts b/__tests__/override-overload-pairing.test.ts new file mode 100644 index 00000000..168c9078 --- /dev/null +++ b/__tests__/override-overload-pairing.test.ts @@ -0,0 +1,385 @@ +/** + * An overload is overridden by the overload with its parameters (#2663). The + * override edges (`synthesizedBy: interface-impl`) linked every overload of an + * interface or base-class method to every same-named method of the class, so + * `ExampleService.putTairValue(String)` reached + * `ExampleServiceImpl.putTairValue(List, double)` and the other way + * round. Java, Kotlin, C# and Scala make an override repeat the parameter list + * of what it overrides, so an overload now pairs with the methods of its + * parameter count, and of those with the ones whose parameter types are its + * own; where a type can't be compared (a type parameter, a type written in + * another language), the count alone decides. A name declared once on each + * side links as before. + */ +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'; + +const FILES: Record = { + // The issue's example. + 'src/main/java/com/example/ExampleService.java': `package com.example; + +import java.util.List; + +interface ExampleService { + Object putTairValue(String tairKey); + + Object putTairValue(List tairKey, double count); +} +`, + 'src/main/java/com/example/ExampleServiceImpl.java': `package com.example; + +import java.util.List; + +class ExampleServiceImpl implements ExampleService { + @Override + public Object putTairValue(String tairKey) { + return null; + } + + @Override + public Object putTairValue(List tairKey, double count) { + return null; + } +} +`, + 'src/main/java/com/example/Shapes.java': `package com.example; + +import java.util.List; + +// One parameter each, told apart by type; \`final\` and annotations aside. +interface Slots { + void put(String key); + void put(int index); +} + +class ArraySlots implements Slots { + public void put(@Deprecated final String key) {} + public void put(int index) {} +} + +// A subclass overriding one overload of its base. +class Base { + void draw(int x) {} + void draw(String label) {} +} + +class Sub extends Base { + @Override + void draw(int x) {} +} + +// An implementation with an overload of its own. +interface Logger { + void log(String msg); +} + +class FileLogger implements Logger { + public void log(String msg) {} + public void log(String msg, int level) {} +} + +// Written another way but the same types: qualified, varargs as an array, +// a C-style array, a raw type. +interface Codec { + void write(java.util.List lines); + void write(String... parts); + void write(int[] data, int length); + void write(List codes, String sep); +} + +class CodecImpl implements Codec { + public void write(List lines) {} + public void write(String[] parts) {} + public void write(int data[], int length) {} + public void write(List codes, String sep) {} +} + +// Generic methods: a type parameter can be any type. +interface Mapper { + T map(Class type); + Object map(String name); +} + +class ReflectMapper implements Mapper { + public U map(Class type) { return null; } + public Object map(String name) { return null; } +} + +// One method on each side: linked as before, whatever its parameters. +interface Single { + void only(int x); +} + +class SingleImpl implements Single { + public void only(long x) {} +} + +// A Java interface a Kotlin class implements: the count decides. +interface Sink { + void accept(int x); + void accept(String s, int n); +} + +// A method's own type parameter stands in for no concrete type of its base: +// the generic appendTo overrides only the generic one. +class Appender { + A appendTo(A appendable, java.util.Iterator parts) { return appendable; } + StringBuilder appendTo(StringBuilder builder, java.util.Iterator parts) { return builder; } +} + +class SkippingAppender extends Appender { + @Override + A appendTo(A appendable, java.util.Iterator parts) { return appendable; } +} +`, + 'src/main/kotlin/com/example/Repos.kt': `package com.example + +interface Repository { + fun save(entity: T) + fun save(entities: List, flush: Boolean) +} + +class UserRepository : Repository { + override fun save(entity: String) {} + fun save(entities: Map) {} + override fun save(entities: List, flush: Boolean) {} +} + +interface Cache { + fun put(key: String): Any? + fun put(keys: List, count: Double = 1.0): Any? + fun put(key: String, value: Int): Any? +} + +class MemoryCache : Cache { + override fun put(key: String): Any? = null + override fun put(keys: List, count: Double): Any? = null + override fun put(key: kotlin.String, value: Int): Any? = null +} + +open class Shape { + open fun draw(x: Int) {} + open fun draw(label: String) {} +} + +class Circle : Shape() { + override fun draw(x: Int) {} +} + +class KotlinSink : Sink { + override fun accept(x: Int) {} + override fun accept(s: String, n: Int) {} +} +`, + 'src/Store.cs': `using System; +using System.Collections.Generic; + +namespace Example +{ + public interface IStore + { + object Put(string key); + object Put(List keys, double count); + object Put(int index); + } + + public class Store : IStore + { + public object Put(string key) { return null; } + public object Put(List keys, double count) { return null; } + public object Put(int index) { return null; } + } + + public class ExplicitStore : IStore + { + object IStore.Put(string key) => null; + object IStore.Put(List keys, double count) => null; + object IStore.Put(int index) => null; + } + + // BCL names for the keywords, a namespace, a default value, an + // attribute and a comment that mention the method. + public class SpelledStore : IStore + { + [Obsolete("Put(int) is the one to use")] + public object Put(String key) => null; + public object Put(System.Collections.Generic.List keys, Double count = 1.0) => null; + // Put(string) and Put(List, double) above. + public object Put( + Int32 index) => null; + } + + public interface IRepo + { + void Save(T entity); + void Save(IList entities, bool flush); + } + + public class UserRepo : IRepo + { + public void Save(string entity) { } + public void Save(IList entities, bool flush) { } + } + + public abstract class Widget + { + public virtual void Draw(int x) { } + public virtual void Draw(string label) { } + public virtual void Move(ref int x) { } + public virtual void Move(int x) { } + } + + public class Button : Widget + { + public override void Draw(int x) { } + public override void Move(ref int x) { } + } +} +`, + 'src/Repo.scala': `package example + +trait Repo { + def find(id: Long): Any + def find(ids: List[Long], limit: Int = 10): Any +} + +class SqlRepo extends Repo { + def find(id: Long): Any = null + def find(ids: List[Long], limit: Int): Any = null +} +`, +}; + +let root = ''; +let graph: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-override-overloads-')); + 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); + } + graph = await CodeGraph.init(root, { index: true }); +}, 120_000); + +afterAll(() => { + graph?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** `Owner::name:line -> Owner::name:line` for every override edge out of `owner`'s methods. */ +function overrides(owner: string): string[] { + const label = (id: string): string => { + const n = graph.getNode(id)!; + return `${n.qualifiedName.split('::').slice(-2).join('::')}:${n.startLine}`; + }; + return graph + .getNodesByKind('method') + .filter((m) => m.qualifiedName.split('::').slice(-2)[0] === owner) + .flatMap((m) => graph.getOutgoingEdges(m.id) + .filter((e) => e.kind === 'calls' && e.metadata?.synthesizedBy === 'interface-impl') + .map((e) => `${label(e.source)} -> ${label(e.target)}`)) + .sort(); +} + +describe('an overload is overridden by the overload with its parameters (#2663)', () => { + it("Java: the issue's interface and implementation", () => { + expect(overrides('ExampleService')).toEqual([ + 'ExampleService::putTairValue:6 -> ExampleServiceImpl::putTairValue:6', + 'ExampleService::putTairValue:8 -> ExampleServiceImpl::putTairValue:11', + ]); + }); + + it('Java: by parameter types at the same count, past `final` and annotations', () => { + expect(overrides('Slots')).toEqual([ + 'Slots::put:7 -> ArraySlots::put:12', + 'Slots::put:8 -> ArraySlots::put:13', + ]); + }); + + it('Java: a subclass overrides only the overload it repeats', () => { + expect(overrides('Base')).toEqual(['Base::draw:18 -> Sub::draw:23']); + }); + + it("Java: an implementation's own overload is no override", () => { + expect(overrides('Logger')).toEqual(['Logger::log:29 -> FileLogger::log:33']); + }); + + it('Java: the same types written another way', () => { + expect(overrides('Codec')).toEqual([ + 'Codec::write:40 -> CodecImpl::write:47', + 'Codec::write:41 -> CodecImpl::write:48', + 'Codec::write:42 -> CodecImpl::write:49', + 'Codec::write:43 -> CodecImpl::write:50', + ]); + }); + + it('Java: a type parameter matches any type in its place', () => { + expect(overrides('Mapper')).toEqual([ + 'Mapper::map:55 -> ReflectMapper::map:60', + 'Mapper::map:56 -> ReflectMapper::map:61', + ]); + }); + + it("Java: an override's own type parameter stands in for no concrete type", () => { + expect(overrides('Appender')).toEqual(['Appender::appendTo:82 -> SkippingAppender::appendTo:87']); + }); + + it('a name declared once on each side links as before', () => { + expect(overrides('Single')).toEqual(['Single::only:66 -> SingleImpl::only:70']); + }); + + it('Kotlin: by count where a type parameter stands in, by type elsewhere', () => { + expect(overrides('Repository')).toEqual([ + 'Repository::save:4 -> UserRepository::save:10', + 'Repository::save:4 -> UserRepository::save:9', + 'Repository::save:5 -> UserRepository::save:11', + ]); + expect(overrides('Cache')).toEqual([ + 'Cache::put:15 -> MemoryCache::put:21', + 'Cache::put:16 -> MemoryCache::put:22', + 'Cache::put:17 -> MemoryCache::put:23', + ]); + expect(overrides('Shape')).toEqual(['Shape::draw:27 -> Circle::draw:32']); + }); + + it('Kotlin implementing a Java interface: by parameter count', () => { + expect(overrides('Sink')).toEqual([ + 'Sink::accept:75 -> KotlinSink::accept:36', + 'Sink::accept:76 -> KotlinSink::accept:37', + ]); + }); + + it('C#: by parameter types, read from the declarations', () => { + expect(overrides('IStore')).toEqual([ + 'IStore::Put:10 -> ExplicitStore::Put:24', + 'IStore::Put:10 -> SpelledStore::Put:35', + 'IStore::Put:10 -> Store::Put:17', + 'IStore::Put:8 -> ExplicitStore::Put:22', + 'IStore::Put:8 -> SpelledStore::Put:31', + 'IStore::Put:8 -> Store::Put:15', + 'IStore::Put:9 -> ExplicitStore::Put:23', + 'IStore::Put:9 -> SpelledStore::Put:33', + 'IStore::Put:9 -> Store::Put:16', + ]); + expect(overrides('IRepo')).toEqual([ + 'IRepo::Save:41 -> UserRepo::Save:47', + 'IRepo::Save:42 -> UserRepo::Save:48', + ]); + // `Move(ref int)` and `Move(int)` are two overloads. + expect(overrides('Widget')).toEqual([ + 'Widget::Draw:53 -> Button::Draw:61', + 'Widget::Move:55 -> Button::Move:62', + ]); + }); + + it('Scala: by parameter types, past a default value', () => { + expect(overrides('Repo')).toEqual([ + 'Repo::find:4 -> SqlRepo::find:9', + 'Repo::find:5 -> SqlRepo::find:10', + ]); + }); +}); diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index 4727e6d0..51a56a91 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -43,6 +43,7 @@ import { enclosingFn, makeLineAt } from './synth-utils'; import { resolveImportPath } from './import-resolver'; import { cppSpecializationArguments, crossesCodeBoundary, goRefQualification, isManualMockHiddenFrom, isManualMockPath, jsCodeBindsName } from './name-matcher'; import { goStdlibMethods } from './go-stdlib-methods'; +import { overrideFilter } from './override-parameters'; import { declaredComponent, exportedComponent, lazyValueModules, wrappedComponentName } from './frameworks/react'; import { jsxCode } from './jsx-code'; import { isTestPath } from '../search/query-utils'; @@ -808,7 +809,9 @@ async function cppOverrideEdges(queries: QueryBuilder, ctx: ResolutionContext, o * interface method. Bridge it like cpp-override: for each class that * `implements` an interface (or `extends` an abstract base), link each * base/interface method → the class's same-name method (the override) so - * trace/callees reach the implementation. Over-approximation accepted + * trace/callees reach the implementation. Of an overloaded name, an overload + * links the methods with its parameters where the language makes an override + * repeat them (./override-parameters, #2663). Over-approximation accepted * (reachability-correct); capped per class, gated to JVM languages. */ // Languages whose static `implements`/`extends` edges should bridge an @@ -1573,7 +1576,7 @@ function goPromotions( }; } -async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise { +async function interfaceOverrideEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise { let scanned255 = 0; const edges: Edge[] = []; const seen = new Set(); @@ -1610,6 +1613,21 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield protocolMemo.set(base.id, methods); return methods; }; + // Of a class's same-named methods, the ones that can override one of a + // base's overloads (#2663): Java, Kotlin, C# and Scala make an override + // repeat the parameters of what it overrides. Asked only for a name that + // is overloaded on either side. + const overridesOf = overrideFilter(ctx); + const nameCounts = new Map>(); + const declaredNamed = (base: Node, name: string): number => { + let counts = nameCounts.get(base.id); + if (!counts) { + counts = new Map(); + for (const m of baseMethodsOf(base)) counts.set(m.name, (counts.get(m.name) ?? 0) + 1); + nameCounts.set(base.id, counts); + } + return counts.get(name) ?? 0; + }; // A Go struct also satisfies an interface with the methods its embedded // types promote into it (goImplementsEdges counts them), and a call through // the interface then runs the embedded type's method. Those links are made @@ -1654,8 +1672,11 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield 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 - // link every base overload → every same-name impl overload (keying by name - // alone would drop all but one and miss the resolved overload). A Go + // each base overload links the same-name impl methods that can override + // it — by parameters where the language makes an override repeat them + // (overrideFilter), else every one (keying by name alone would drop all + // but one and miss the resolved overload). A name declared once on each + // side pairs as it is. A Go // interface's unexported method is matched in its own package only // (goMethodKey): a namesake the type declares elsewhere is another // method, and the one the interface asks for is promoted. @@ -1675,7 +1696,8 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield if (promotes) unmatched.push(bm); continue; } - for (const m of impls) { + const overloaded = impls.length > 1 || declaredNamed(base, bm.name) > 1; + for (const m of overloaded ? overridesOf(bm, impls) : impls) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; if (bm.id === m.id) continue; // A JS/TS static method overrides only a static one, and an @@ -5113,7 +5135,7 @@ export const SYNTH_PASSES: SynthPassDef[] = [ { name: 'ifaceEdges', gate: (has) => has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY), - run: (q, _c, y) => interfaceOverrideEdges(q, y), + run: (q, c, y) => interfaceOverrideEdges(q, c, y), }, { name: 'kotlinExpectActual', gate: (has) => has('kotlin'), run: (q, _c, y) => kotlinExpectActualEdges(q, y) }, { name: 'goGrpcEdges', gate: (has) => has('go'), run: (q, _c, y) => goGrpcStubImplEdges(q, y) }, diff --git a/src/resolution/override-parameters.ts b/src/resolution/override-parameters.ts new file mode 100644 index 00000000..617db393 --- /dev/null +++ b/src/resolution/override-parameters.ts @@ -0,0 +1,410 @@ +/** + * Which of a class's same-named methods overrides an overload of a type it + * implements or extends (#2663). Java, Kotlin, C# and Scala make an override + * repeat the parameter list of the method it overrides, so of + * `ExampleServiceImpl`'s two `putTairValue`s, `putTairValue(String)` is the + * one a call to `ExampleService.putTairValue(String)` runs, never + * `putTairValue(List, double)`. The override pass + * (interfaceOverrideEdges) asks this module once a name is overloaded on + * either side; a name declared once on each side pairs as it always has. + * + * Parameter lists come from the stored signatures (`Object (String tairKey)` + * in Java, `(tairKey: String): Any?` in Kotlin, `(id: Long): Any` in Scala). + * C# methods store none, so theirs is read from the declaration, as the + * resolver's own overload choice reads it (name-matcher's overloadSetOf). + */ +import type { Node } from '../types'; +import type { ResolutionContext } from './types'; +import { typeParametersInScope } from './type-parameters'; +import { escapeRegExp } from '../utils'; + +/** The languages whose overrides repeat the parameter list of the method they override. */ +export const PARAMETER_OVERRIDE_LANGUAGES: ReadonlySet = new Set(['java', 'kotlin', 'csharp', 'scala']); + +/** How far a C# declaration is read for its parameter list: attributes, then a parameter a line. */ +const CSHARP_HEAD_LINES = 40; + +/** C#'s keyword for each BCL type it aliases: `String key` and `string key` are one parameter type. */ +const CSHARP_KEYWORD_TYPES: Readonly> = { + String: 'string', Object: 'object', Boolean: 'bool', Byte: 'byte', SByte: 'sbyte', Char: 'char', + Decimal: 'decimal', Double: 'double', Single: 'float', Int16: 'short', UInt16: 'ushort', Int32: 'int', + UInt32: 'uint', Int64: 'long', UInt64: 'ulong', IntPtr: 'nint', UIntPtr: 'nuint', +}; + +/** How two parameter types compare: alike, possibly alike, or told apart. */ +type Fit = 'same' | 'maybe' | 'differs'; + +/** + * A filter for one run of the override pass: `(base, methods)` keeps the + * methods that can override `base`, from same-named methods of a class + * implementing or extending its type, in their order: + * + * - those with as many parameters as `base`, never one with another count; + * - of those, the ones whose parameter types are `base`'s, when there are any; + * - else the ones whose types can't be told apart from `base`'s: a type + * parameter of `base`, its class or a class around it stands for any type + * in its place (`save(T entity)` implemented as `save(String entity)`), a + * type with its type arguments dropped or nullable may be the same type (a + * raw `List` for `List`), and the types of a method in another + * language (a Kotlin class implementing a Java interface) aren't compared + * at all. + * + * Every method, as before, when `base`'s parameters can't be read or its + * language isn't one of {@link PARAMETER_OVERRIDE_LANGUAGES}; a method whose + * own parameters can't be read is kept with those that might match. + */ +export function overrideFilter(context: ResolutionContext): (base: Node, methods: readonly Node[]) => Node[] { + const types = new Map(); + const typesOf = (m: Node): readonly string[] | null => { + let found = types.get(m.id); + if (found === undefined) { + found = parameterTypes(m, context); + types.set(m.id, found); + } + return found; + }; + // Asked of a base method only: an override's own type parameter facing a + // concrete type of the base is no override in these languages (` A + // appendTo(A, …)` does not override `appendTo(StringBuilder, …)`), and the + // overrides of one base sit in many files that would each be read. + const scopes = new Map>(); + const scopeOf = (m: Node): ReadonlySet => { + let found = scopes.get(m.id); + if (!found) { + found = typeParametersInScope(m, context); + scopes.set(m.id, found); + } + return found; + }; + return (base, methods) => { + const want = PARAMETER_OVERRIDE_LANGUAGES.has(base.language) ? typesOf(base) : null; + if (!want) return [...methods]; + const same: Node[] = []; + const maybe: Node[] = []; + for (const m of methods) { + const have = PARAMETER_OVERRIDE_LANGUAGES.has(m.language) ? typesOf(m) : null; + if (!have) { + maybe.push(m); + continue; + } + if (have.length !== want.length) continue; + if (m.language !== base.language) { + maybe.push(m); + continue; + } + let fit: Fit = 'same'; + for (let i = 0; i < want.length && fit !== 'differs'; i++) { + const f = compareTypes(want[i]!, () => scopeOf(base), have[i]!, base.language); + if (f !== 'same') fit = f; + } + if (fit === 'same') same.push(m); + else if (fit === 'maybe') maybe.push(m); + } + return same.length > 0 ? same : maybe; + }; +} + +/** + * How parameter type `a` of a base method compares with `b`, the override's + * type in the same place. `aScope` gives the type parameters the base can + * name, asked for only when nothing cheaper decides: reading them reads the + * base's source. + */ +function compareTypes(a: string, aScope: () => ReadonlySet, b: string, language: string): Fit { + if (a === b) return 'same'; + if (looseType(a, language) === looseType(b, language)) return 'maybe'; + return fitsWithTypeParameters(a, aScope(), b) ? 'maybe' : 'differs'; +} + +/** Past this length a type is not matched against a pattern: each type parameter in it is a `.+`. */ +const MAX_PATTERN_TYPE_LENGTH = 200; + +/** + * Whether `type` is `pattern` with each type parameter it names filled in: + * `List` is `List`, and `String` is `T`. A type too long to match + * cheaply might be, and so fits. + */ +function fitsWithTypeParameters(pattern: string, typeParameters: ReadonlySet, type: string): boolean { + if (typeParameters.size === 0) return false; + if (pattern.length > MAX_PATTERN_TYPE_LENGTH || type.length > MAX_PATTERN_TYPE_LENGTH) { + return [...typeParameters].some((name) => new RegExp(`(? { + if (i % 2 === 1 && typeParameters.has(part)) { + named = true; + return '.+'; + } + return escapeRegExp(part); + }) + .join(''); + return named && new RegExp(`^${source}$`).test(type); +} + +/** A type without its type arguments and nullable marks: `List?` is `List`. */ +function looseType(type: string, language: string): string { + const args = language === 'scala' ? /\[[^[\]]*\]/g : /<[^<>]*>/g; + let t = type; + for (let previous = ''; previous !== t; ) { + previous = t; + t = t.replace(args, ''); + } + return t.replace(/\?/g, ''); +} + +/** The parameter types `method` declares, each normalized (normalizeType); null when they can't be read. */ +function parameterTypes(method: Node, context: ResolutionContext): readonly string[] | null { + const list = parameterList(method, context); + if (list === null) return null; + const types: string[] = []; + for (const param of splitParameters(list)) { + const type = parameterType(param, method.language); + if (type === RECEIVER) continue; + if (type === null) return null; + types.push(normalizeType(type, method.language)); + } + return types; +} + +/** The text of `method`'s parameter list, without its parentheses; null when it can't be found. */ +function parameterList(method: Node, context: ResolutionContext): string | null { + if (method.language === 'csharp') return csharpParameterList(method, context); + const sig = method.signature?.trim(); + if (!sig) return null; + if (method.language === 'java') { + // The return type, then the list: `Object (List tairKey, double count)`. + const groups = parenGroups(sig); + const last = groups[groups.length - 1]; + return last && last[1] === sig.length - 1 ? sig.slice(last[0] + 1, last[1]) : null; + } + // Kotlin and Scala: the list, then the return type: `(keys: List): Any?`. + // A Scala `def size: Int` takes no parameter list at all. + if (method.language === 'scala' && sig.startsWith(':')) return ''; + const first = parenGroups(sig)[0]; + return first && first[0] === 0 ? sig.slice(1, first[1]) : null; +} + +/** + * The parameter list of a C# method, read from its declaration: past its + * attribute sections (`[Obsolete("Put(int) instead")]`), the `( … )` after + * its name — and type parameters, `Save(…)` — with comments and string + * literals blanked. An explicit interface implementation, `object + * IStore.Put(string key)`, is named `Put` like any other. + */ +function csharpParameterList(method: Node, context: ResolutionContext): string | null { + const lines = context.getFileLines?.(method.filePath) ?? context.readFile(method.filePath)?.split(/\r?\n/) ?? null; + if (!lines || method.startLine < 1 || method.startLine > lines.length) return null; + const end = Math.min(lines.length, Math.max(method.endLine, method.startLine), method.startLine - 1 + CSHARP_HEAD_LINES); + const first = lines[method.startLine - 1]!; + const head = blankCsharpTrivia( + [first.slice(Math.min(method.startColumn, first.length)), ...lines.slice(method.startLine, end)].join('\n') + ); + let at = 0; + for (;;) { + while (at < head.length && /\s/.test(head[at]!)) at++; + if (head[at] !== '[') break; + const close = closingBracket(head, at); + if (close < 0) return null; + at = close + 1; + } + const name = new RegExp(`(?()]*>)?\\s*\\(`, 'g'); + name.lastIndex = at; + const found = name.exec(head); + if (!found) return null; + const open = found.index + found[0].length - 1; + const close = closingBracket(head, open); + return close < 0 ? null : head.slice(open + 1, close); +} + +/** C# source with its comments and the contents of its string and character literals blanked, newlines kept. */ +function blankCsharpTrivia(text: string): string { + let out = ''; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (c === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) { + const close = text[i + 1] === '/' ? text.indexOf('\n', i) : text.indexOf('*/', i + 2); + const stop = close < 0 ? text.length : text[i + 1] === '/' ? close : close + 2; + out += text.slice(i, stop).replace(/[^\n]/g, ' '); + i = stop - 1; + continue; + } + if (c === '"' || c === "'") { + // `@"…"` and `$@"…"` escape a quote by doubling it; the rest by a backslash. + const verbatim = c === '"' && (text[i - 1] === '@' || (text[i - 2] === '@' && text[i - 1] === '$')); + let j = i + 1; + while (j < text.length) { + if (verbatim ? text[j] === '"' && text[j + 1] === '"' : text[j] === '\\') { + j += 2; + continue; + } + if (text[j] === c || (!verbatim && text[j] === '\n')) break; + j++; + } + out += c + text.slice(i + 1, j).replace(/[^\n]/g, ' ') + (text[j] ?? ''); + i = j; + continue; + } + out += c; + } + return out; +} + +/** A parameter that is no parameter: Java's receiver, `void m(Foo this, int x)`. */ +const RECEIVER = Symbol('receiver'); + +/** The type text a parameter declares, as written; null when it can't be read. */ +function parameterType(param: string, language: string): string | null | typeof RECEIVER { + if (language === 'csharp') return csharpParameterType(param); + const bare = stripAnnotations(param).trim(); + if (language === 'java') { + // `final List lines`, `String... parts`, a C-style `int data[]`. + const m = /^([\s\S]*?)([A-Za-z_$][\w$]*)((?:\s*\[\s*\])*)$/.exec(bare.replace(/\bfinal\b/g, ' ').trim()); + if (!m || !m[1]!.trim()) return null; + if (m[2] === 'this') return RECEIVER; + return m[1]! + m[3]!.replace(/\s+/g, ''); + } + // Kotlin and Scala: `vararg keys: String`, `count: Double = 1.0`, `f: => Unit`. + const colon = topLevelIndex(bare, (i) => bare[i] === ':'); + if (colon < 0) return null; + let type = bare.slice(colon + 1); + const assign = topLevelIndex(type, (i) => type[i] === '=' && type[i + 1] !== '>'); + if (assign >= 0) type = type.slice(0, assign); + if (!type.trim()) return null; + return /\bvararg\b/.test(bare.slice(0, colon)) ? `vararg ${type}` : type; +} + +/** + * A C# parameter's type: past its attributes and the modifiers that are no + * part of it (`this`, `params`, `scoped`), keeping `ref`, `out` and `in`, + * which are, and before its name and default value. + */ +function csharpParameterType(param: string): string | null { + let p = param.trim(); + while (p.startsWith('[')) { + const close = closingBracket(p, 0); + if (close < 0) return null; + p = p.slice(close + 1).trim(); + } + const assign = topLevelIndex(p, (i) => p[i] === '=' && !/[=!<>]/.test(p[i - 1] ?? '') && !/[=>]/.test(p[i + 1] ?? '')); + if (assign >= 0) p = p.slice(0, assign).trim(); + const m = /^([\s\S]*?\S)\s+@?[A-Za-z_]\w*$/.exec(p); + if (!m) return null; + let type = m[1]!.trim(); + const kept: string[] = []; + for (let mod = /^(this|params|scoped|ref|out|in|readonly)\s+/.exec(type); mod; mod = /^(this|params|scoped|ref|out|in|readonly)\s+/.exec(type)) { + if (mod[1] !== 'this' && mod[1] !== 'params' && mod[1] !== 'scoped') kept.push(mod[1]!); + type = type.slice(mod[0].length); + } + return type ? [...kept, type].join(' ') : null; +} + +/** + * A parameter type as the languages compare it: qualifiers dropped + * (`java.util.List` is `List`), Java's varargs an + * array, C#'s BCL names its keywords (`String` is `string`), and whitespace + * kept only between two words (`out T`, `vararg String`). + */ +function normalizeType(type: string, language: string): string { + let t = type.replace(/\s+/g, ' ').trim(); + if (language === 'java') t = t.replace(/\.\.\./g, '[]'); + t = t.replace(/\b[A-Za-z_]\w*\s*::\s*/g, ''); + t = t.replace(/\b(?:[A-Za-z_$][\w$]*\s*\.\s*)+(?=[A-Za-z_$])/g, ''); + if (language === 'csharp') t = t.replace(/\b[A-Z][A-Za-z0-9]*\b/g, (w) => CSHARP_KEYWORD_TYPES[w] ?? w); + return t.replace(/ (?![\w$])|(? text[i] === ','); + const param = (comma < 0 ? list.slice(start) : list.slice(start, start + comma)).trim(); + if (param) params.push(param); + if (comma < 0) return params; + start += comma + 1; + } +} + +/** + * The first index in `text` outside brackets and string literals where `at` + * holds, or -1. A `<` opens type arguments when written right after a name + * (`List<`), and the `>` of `->` and `=>` closes none. + */ +function topLevelIndex(text: string, at: (i: number, text: string) => boolean): number { + let depth = 0; + let angles = 0; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (c === '"' || c === "'") { + i = closingQuote(text, i); + continue; + } + if (depth === 0 && angles === 0 && at(i, text)) return i; + if (c === '(' || c === '[' || c === '{') depth++; + else if (c === ')' || c === ']' || c === '}') depth = Math.max(0, depth - 1); + else if (c === '<' && /[\w$?\]>]/.test(text[i - 1] ?? '') && text[i + 1] !== '<' && text[i + 1] !== '=') angles++; + else if (c === '>' && angles > 0 && text[i - 1] !== '-' && text[i - 1] !== '=') angles--; + } + return -1; +} + +/** The top-level `( … )` groups of `text`, as [open, close] index pairs; string literals skipped. */ +function parenGroups(text: string): Array<[number, number]> { + const groups: Array<[number, number]> = []; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (c === '"' || c === "'") i = closingQuote(text, i); + else if (c === '(') { + const close = closingBracket(text, i); + if (close < 0) return groups; + groups.push([i, close]); + i = close; + } + } + return groups; +} + +/** The index of the bracket closing the one at `open` — `(`, `[` or `{` — or -1; string literals skipped. */ +function closingBracket(text: string, open: number): number { + let depth = 0; + for (let i = open; i < text.length; i++) { + const c = text[i]!; + if (c === '"' || c === "'") i = closingQuote(text, i); + else if (c === '(' || c === '[' || c === '{') depth++; + else if ((c === ')' || c === ']' || c === '}') && --depth === 0) return i; + } + return -1; +} + +/** The index of the quote closing the string or character literal opening at `open`, or the text's end. */ +function closingQuote(text: string, open: number): number { + const quote = text[open]!; + let j = open + 1; + while (j < text.length && text[j] !== quote) j += text[j] === '\\' ? 2 : 1; + return Math.min(j, text.length - 1); +} diff --git a/src/resolution/type-parameters.ts b/src/resolution/type-parameters.ts index 42a2a63e..91f1d8ba 100644 --- a/src/resolution/type-parameters.ts +++ b/src/resolution/type-parameters.ts @@ -149,10 +149,23 @@ function scalaMemberInScope(result: ResolvedRef, ref: UnresolvedRef, context: Re return /^[A-Za-z_]\w*$/.test(owner.name) && /^[A-Za-z_]\w*$/.test(target.name) ? imports.test(text) : true; } -function enclosingDeclarations(ref: UnresolvedRef, context: ResolutionContext): Node[] { +function enclosingDeclarations(at: { filePath: string; line: number }, context: ResolutionContext): Node[] { return context - .getNodesInFile(ref.filePath) - .filter((n) => DECLARATION_KINDS.has(n.kind) && n.startLine <= ref.line && n.endLine >= ref.line); + .getNodesInFile(at.filePath) + .filter((n) => DECLARATION_KINDS.has(n.kind) && n.startLine <= at.line && n.endLine >= at.line); +} + +/** + * The type parameters in scope at a declaration: its own and those of the + * declarations around it — a method's (` T map(Class type)`), its + * class's (`interface Repository`), an enclosing class's. + */ +export function typeParametersInScope(node: Node, context: ResolutionContext): ReadonlySet { + const names = new Set(); + for (const declaration of enclosingDeclarations({ filePath: node.filePath, line: node.startLine }, context)) { + for (const name of declaredTypeParameters(declaration, context).keys()) names.add(name); + } + return names; } /** The type parameters a declaration declares, each with the default a C++ head gives it (`typename Parser = TcParser`). */