mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
fix(java,kotlin,csharp,scala): an overload links only the overrides with its parameters (#2820)
The override pass (interface-impl) grouped a class's methods by name and
linked every overload of an interface or base-class method to every
same-named method of the class. Issue #2663's ExampleService got four edges
for two overloads: putTairValue(String) also reached
putTairValue(List<String>, double), and the other way round, so flows,
callers and impact walked into overloads a call never runs. Java, Kotlin,
C# and Scala make an override repeat the parameter list of what it
overrides, and the stored Java and Kotlin signatures already told the
overloads apart; nothing compared them.
A new override-parameters module picks, of a class's same-named methods,
the ones that can override a base overload. The pass asks it only when a
name is overloaded on either side, so a name declared once on each side
links as before. A pair with another parameter count never links. Of those
with the base's count, the ones whose parameter types are written alike
win: qualifiers, annotations, final, default values and C#'s BCL names for
its keywords aside, varargs as arrays. When none is, the ones whose types
can't be told apart are kept: a type parameter of the base method or of a
type around it matches any type in its place (save(T) implemented as
save(String), List<T> as List<String>), a raw or nullable spelling of the
same type may be it, and a method written in another language than the
base is compared by count alone. An override's own type parameter matches
no concrete type of the base, as these languages rule. Java, Kotlin and
Scala lists come from the stored signatures; C# stores none, so its list
is read from the declaration past attributes, comments and string
literals, as the resolver's call-site overload choice already reads one.
Extraction is unchanged, so both engines give the same input.
Validation:
- New override-overload-pairing test (12 cases, Java, Kotlin, C#, Scala):
11 fail on main, the single-method case passes on both; all pass with
the fix on the kernel and on the wasm path.
- A/B on main df1cd1e1, override edges main -> fix; nodes and every other
edge identical in each: hadoop-common-project (hadoop 2c01c36f)
4651 -> 3762 (-1045 +156), guava 5765 -> 4360 (-1427 +22), mybatis-3
1126 -> 898 (-278 +50), Exposed 999 -> 884 (-115), okhttp 709 -> 665
(-44), Newtonsoft.Json 527 -> 404 (-125 +2), serilog 98 -> 91 (-7),
twitter/util 838 -> 699 (-139). Controls gin (Go) and nest (TypeScript):
graph dumps byte-identical.
- Triage: of the 3180 removed edges, 1761 pair methods with different
parameter counts and 1419 methods whose concrete parameter types differ;
every one of the 230 added edges is a same-parameter override the
per-class cap of 40 had cut off. An independent re-read finds no
same-typed override missing next to any removed pair, and each group of
base methods that lost every link into a class was checked against the
source (the class overrides other overloads only, or the method is final
or static).
- Cost: the pass alone, median of 9 alternating runs, +3 to +96 ms per
index (guava 715 -> 811 ms, hadoop-common 501 -> 555 ms); whole index,
median of 3 interleaved: Newtonsoft.Json 12.3 s -> 11.9 s, okhttp
12.3 s -> 12.9 s, within noise.
Fixes #2663
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3f58e34383
commit
e32021f6fb
@@ -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 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 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 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<String>, 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 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 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.
|
- 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.
|
||||||
|
|||||||
@@ -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<String>, 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<string, string> = {
|
||||||
|
// 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<String> 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<String> 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<String> lines);
|
||||||
|
void write(String... parts);
|
||||||
|
void write(int[] data, int length);
|
||||||
|
void write(List<Integer> codes, String sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CodecImpl implements Codec {
|
||||||
|
public void write(List<String> 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> T map(Class<T> type);
|
||||||
|
Object map(String name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReflectMapper implements Mapper {
|
||||||
|
public <U> U map(Class<U> 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 extends Appendable> 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 extends Appendable> A appendTo(A appendable, java.util.Iterator<?> parts) { return appendable; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'src/main/kotlin/com/example/Repos.kt': `package com.example
|
||||||
|
|
||||||
|
interface Repository<T> {
|
||||||
|
fun save(entity: T)
|
||||||
|
fun save(entities: List<T>, flush: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserRepository : Repository<String> {
|
||||||
|
override fun save(entity: String) {}
|
||||||
|
fun save(entities: Map<String, Int>) {}
|
||||||
|
override fun save(entities: List<String>, flush: Boolean) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cache {
|
||||||
|
fun put(key: String): Any?
|
||||||
|
fun put(keys: List<String>, 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<String>, 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<string> keys, double count);
|
||||||
|
object Put(int index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Store : IStore
|
||||||
|
{
|
||||||
|
public object Put(string key) { return null; }
|
||||||
|
public object Put(List<string> 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<string> 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<String> keys, Double count = 1.0) => null;
|
||||||
|
// Put(string) and Put(List<string>, double) above.
|
||||||
|
public object Put(
|
||||||
|
Int32 index) => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRepo<T>
|
||||||
|
{
|
||||||
|
void Save(T entity);
|
||||||
|
void Save(IList<T> entities, bool flush);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserRepo : IRepo<string>
|
||||||
|
{
|
||||||
|
public void Save(string entity) { }
|
||||||
|
public void Save(IList<string> 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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -43,6 +43,7 @@ import { enclosingFn, makeLineAt } from './synth-utils';
|
|||||||
import { resolveImportPath } from './import-resolver';
|
import { resolveImportPath } from './import-resolver';
|
||||||
import { cppSpecializationArguments, crossesCodeBoundary, goRefQualification, isManualMockHiddenFrom, isManualMockPath, jsCodeBindsName } from './name-matcher';
|
import { cppSpecializationArguments, crossesCodeBoundary, goRefQualification, isManualMockHiddenFrom, isManualMockPath, jsCodeBindsName } from './name-matcher';
|
||||||
import { goStdlibMethods } from './go-stdlib-methods';
|
import { goStdlibMethods } from './go-stdlib-methods';
|
||||||
|
import { overrideFilter } from './override-parameters';
|
||||||
import { declaredComponent, exportedComponent, lazyValueModules, wrappedComponentName } from './frameworks/react';
|
import { declaredComponent, exportedComponent, lazyValueModules, wrappedComponentName } from './frameworks/react';
|
||||||
import { jsxCode } from './jsx-code';
|
import { jsxCode } from './jsx-code';
|
||||||
import { isTestPath } from '../search/query-utils';
|
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
|
* interface method. Bridge it like cpp-override: for each class that
|
||||||
* `implements` an interface (or `extends` an abstract base), link each
|
* `implements` an interface (or `extends` an abstract base), link each
|
||||||
* base/interface method → the class's same-name method (the override) so
|
* 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.
|
* (reachability-correct); capped per class, gated to JVM languages.
|
||||||
*/
|
*/
|
||||||
// Languages whose static `implements`/`extends` edges should bridge an
|
// Languages whose static `implements`/`extends` edges should bridge an
|
||||||
@@ -1573,7 +1576,7 @@ function goPromotions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
|
async function interfaceOverrideEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
||||||
let scanned255 = 0;
|
let scanned255 = 0;
|
||||||
const edges: Edge[] = [];
|
const edges: Edge[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -1610,6 +1613,21 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield
|
|||||||
protocolMemo.set(base.id, methods);
|
protocolMemo.set(base.id, methods);
|
||||||
return 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<string, Map<string, number>>();
|
||||||
|
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
|
// A Go struct also satisfies an interface with the methods its embedded
|
||||||
// types promote into it (goImplementsEdges counts them), and a call through
|
// types promote into it (goImplementsEdges counts them), and a call through
|
||||||
// the interface then runs the embedded type's method. Those links are made
|
// 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';
|
const promotes = goStruct && sup.provenance === 'heuristic' && base.kind === 'interface';
|
||||||
// Group impl methods by name to handle OVERLOADS: an interface `list()` and
|
// 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
|
// `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
|
// each base overload links the same-name impl methods that can override
|
||||||
// alone would drop all but one and miss the resolved overload). A Go
|
// 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
|
// interface's unexported method is matched in its own package only
|
||||||
// (goMethodKey): a namesake the type declares elsewhere is another
|
// (goMethodKey): a namesake the type declares elsewhere is another
|
||||||
// method, and the one the interface asks for is promoted.
|
// 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);
|
if (promotes) unmatched.push(bm);
|
||||||
continue;
|
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 (added >= MAX_CALLBACKS_PER_CHANNEL) break;
|
||||||
if (bm.id === m.id) continue;
|
if (bm.id === m.id) continue;
|
||||||
// A JS/TS static method overrides only a static one, and an
|
// A JS/TS static method overrides only a static one, and an
|
||||||
@@ -5113,7 +5135,7 @@ export const SYNTH_PASSES: SynthPassDef[] = [
|
|||||||
{
|
{
|
||||||
name: 'ifaceEdges',
|
name: 'ifaceEdges',
|
||||||
gate: (has) => has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY),
|
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: '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) },
|
{ name: 'goGrpcEdges', gate: (has) => has('go'), run: (q, _c, y) => goGrpcStubImplEdges(q, y) },
|
||||||
|
|||||||
@@ -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<String>, 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<string> = 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<Record<string, string>> = {
|
||||||
|
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<String>`), 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<string, readonly string[] | null>();
|
||||||
|
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> 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<string, ReadonlySet<string>>();
|
||||||
|
const scopeOf = (m: Node): ReadonlySet<string> => {
|
||||||
|
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<string>, 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<String>` is `List<T>`, and `String` is `T`. A type too long to match
|
||||||
|
* cheaply might be, and so fits.
|
||||||
|
*/
|
||||||
|
function fitsWithTypeParameters(pattern: string, typeParameters: ReadonlySet<string>, 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(`(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`).test(pattern));
|
||||||
|
}
|
||||||
|
let named = false;
|
||||||
|
const source = pattern
|
||||||
|
.split(/([A-Za-z_$][\w$]*)/)
|
||||||
|
.map((part, i) => {
|
||||||
|
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<String>?` 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<String> 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<String>): 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<T>(…)` — 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(`(?<![\\w@])@?${escapeRegExp(method.name)}\\s*(?:<[^<>()]*>)?\\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<String> 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<java.lang.String>` is `List<String>`), 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$])|(?<![\w$]) /g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Java, Kotlin and Scala annotations taken out of a parameter: `@Named("id")`, `@param:JsonProperty("x")`. */
|
||||||
|
function stripAnnotations(text: string): string {
|
||||||
|
let out = '';
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const name = text[i] === '@' ? /^@\s*[A-Za-z_$][\w$]*(?:\s*:\s*[A-Za-z_$][\w$]*)?(?:\s*\.\s*[A-Za-z_$][\w$]*)*/.exec(text.slice(i)) : null;
|
||||||
|
if (!name) {
|
||||||
|
out += text[i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let next = i + name[0].length;
|
||||||
|
let open = next;
|
||||||
|
while (open < text.length && /\s/.test(text[open]!)) open++;
|
||||||
|
if (text[open] === '(') {
|
||||||
|
const close = closingBracket(text, open);
|
||||||
|
next = close < 0 ? text.length : close + 1;
|
||||||
|
}
|
||||||
|
out += ' ';
|
||||||
|
i = next - 1;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The parameters of a list, at its top-level commas. */
|
||||||
|
function splitParameters(list: string): string[] {
|
||||||
|
const params: string[] = [];
|
||||||
|
let start = 0;
|
||||||
|
for (;;) {
|
||||||
|
const comma = topLevelIndex(list.slice(start), (i, text) => 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);
|
||||||
|
}
|
||||||
@@ -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;
|
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
|
return context
|
||||||
.getNodesInFile(ref.filePath)
|
.getNodesInFile(at.filePath)
|
||||||
.filter((n) => DECLARATION_KINDS.has(n.kind) && n.startLine <= ref.line && n.endLine >= ref.line);
|
.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> T map(Class<T> type)`), its
|
||||||
|
* class's (`interface Repository<T>`), an enclosing class's.
|
||||||
|
*/
|
||||||
|
export function typeParametersInScope(node: Node, context: ResolutionContext): ReadonlySet<string> {
|
||||||
|
const names = new Set<string>();
|
||||||
|
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`). */
|
/** The type parameters a declaration declares, each with the default a C++ head gives it (`typename Parser = TcParser`). */
|
||||||
|
|||||||
Reference in New Issue
Block a user