Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- In Java, Kotlin and Scala, an interface method that is overloaded now links each overload to the implementation's overload with the same parameters, not to every implementation method of that name, so `codegraph_explore` and callers no longer follow `put(String)` into `put(List<String>, double)`. A generic interface's method still reaches every implementation of its parameter count. Thanks @hxj1225 for the report. (#2663)
- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
- In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600)
Expand Down
150 changes: 150 additions & 0 deletions __tests__/interface-impl-overloads.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* An interface overload is implemented by the overload with its parameters
* (#2663). The interface-impl synthesizer linked every overload of an
* interface method to every same-named overload of the implementation, so
* `putTairValue(String)` reached `putTairValue(List<String>, double)` and the
* other way round. In Java, Kotlin and Scala an override repeats the
* parameter list, so a base overload pairs with the implementation overloads
* of its parameter count, and of those with the one whose parameter types are
* written alike when there is one. A generic interface's `save(T)` still
* reaches every one-parameter `save` of its implementation.
*
* The generic case is written in Kotlin: a Java `implements Store<String>`
* gets no `implements` edge today, generic or not of this change.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-iface-overloads-'));
const files: Record<string, string> = {
// The issue's example.
'src/ExampleService.java': `import java.util.List;

interface ExampleService {
Object putTairValue(String tairKey);

Object putTairValue(List<String> tairKey, double count);
}
`,
'src/ExampleServiceImpl.java': `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;
}
}
`,
// Same parameter count, told apart by type, `final` and an annotation aside.
'src/Store.java': `interface Store {
void put(String key);
void put(int index);
}

class MapStore implements Store {
public void put(@Deprecated final String key) {}
public void put(int index) {}
}
`,
// A type parameter: no implementation's types are written alike, so every one of its count.
'src/Repository.kt': `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) {}
}
`,
'src/Cache.kt': `interface Cache {
fun put(key: String): Any?
fun put(keys: List<String>, count: Double = 1.0): Any?
}

class MemoryCache : Cache {
override fun put(key: String): Any? = null
override fun put(keys: List<String>, count: Double): Any? = null
}
`,
'src/Repo.scala': `trait Repo {
def find(id: Long): Any
def find(ids: List[Long], limit: Int): Any
}

class SqlRepo extends Repo {
def find(id: Long): Any = null
def find(ids: List[Long], limit: Int): Any = null
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});

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

/** `Owner::name:line -> Owner::name:line` of every interface-impl edge from methods of `owner`. */
const implEdges = (owner: string): string[] => {
const methods = cg.getNodesByKind('method').filter((n) => n.qualifiedName.startsWith(`${owner}::`));
return methods.flatMap((m) => cg.getOutgoingEdges(m.id)
.filter((e) => e.kind === 'calls' && e.metadata?.synthesizedBy === 'interface-impl')
.map((e) => {
const t = cg.getNode(e.target)!;
return `${m.qualifiedName}:${m.startLine} -> ${t.qualifiedName}:${t.startLine}`;
})).sort();
};

describe('an interface overload is implemented by the overload with its parameters (#2663)', () => {
it('Java: by parameter count', () => {
expect(implEdges('ExampleService')).toEqual([
'ExampleService::putTairValue:4 -> ExampleServiceImpl::putTairValue:4',
'ExampleService::putTairValue:6 -> ExampleServiceImpl::putTairValue:9',
]);
});

it('Java: by parameter types at the same count', () => {
expect(implEdges('Store')).toEqual([
'Store::put:2 -> MapStore::put:7',
'Store::put:3 -> MapStore::put:8',
]);
});

it('a type parameter: every implementation overload of its count, as before', () => {
expect(implEdges('Repository')).toEqual([
'Repository::save:2 -> UserRepository::save:7',
'Repository::save:2 -> UserRepository::save:8',
'Repository::save:3 -> UserRepository::save:9',
]);
});

it('Kotlin and Scala: by parameter count, a default value aside', () => {
expect(implEdges('Cache')).toEqual([
'Cache::put:2 -> MemoryCache::put:7',
'Cache::put:3 -> MemoryCache::put:8',
]);
expect(implEdges('Repo')).toEqual([
'Repo::find:2 -> SqlRepo::find:7',
'Repo::find:3 -> SqlRepo::find:8',
]);
});
});
82 changes: 79 additions & 3 deletions src/resolution/callback-synthesizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,80 @@ function goPromotions(
};
}

/**
* Languages where an override repeats the parameter list of what it
* overrides, so an interface overload is implemented by the same-named
* method with the same parameters: Java, Kotlin, C# and Scala. TypeScript is
* not one — an implementation may take fewer parameters than its interface
* declares.
*/
const PARAMETER_PAIRED_LANGS = new Set(['java', 'kotlin', 'csharp', 'scala']);

/**
* The parameter types a stored method signature lists, as written and with
* whitespace dropped — `(String tairKey)` and `Object (List<String> tairKey,
* double count)` (Java), `(keys: List<String>, count: Double = 1.0): Any?`
* (Kotlin), `(keys: List[String], count: Double): Any` (Scala). Null when the
* signature is missing or takes a variable number of arguments, which a
* parameter count cannot pair.
*/
function signatureParameterTypes(signature: string | undefined): string[] | null {
if (!signature) return null;
const open = signature.indexOf('(');
if (open < 0) return null;
const params: string[] = [];
let depth = 0;
let start = open + 1;
for (let i = open; i < signature.length; i++) {
const ch = signature[i]!;
if (ch === '(' || ch === '<' || ch === '[' || ch === '{') depth++;
else if (ch === ')' || ch === '>' || ch === ']' || ch === '}') {
depth--;
if (depth === 0) {
params.push(signature.slice(start, i));
break;
}
} else if (ch === ',' && depth === 1) {
params.push(signature.slice(start, i));
start = i + 1;
}
}
if (depth !== 0) return null;
const types: string[] = [];
for (const raw of params) {
const param = raw.replace(/@\w+(?:\.\w+)*(?:\([^()]*\))?/g, '').trim();
if (param === '') continue;
if (/\.\.\.|\bvararg\b|\bparams\b|\*\s*$/.test(param)) return null;
const colon = param.indexOf(':');
const type = colon >= 0
? param.slice(colon + 1).replace(/=.*$/, '')
: param.replace(/\bfinal\b/g, '').replace(/[A-Za-z_$][\w$]*\s*$/, '');
types.push(type.replace(/\s+/g, ''));
}
return types;
}

/**
* The implementation overloads an interface method `base` pairs with, among
* `impls` of its name (#2663): in Java, Kotlin, C# and Scala the ones with as
* many parameters as `base`, and of those the ones whose parameter types are
* written alike when there are any — a generic interface's `save(T entity)`
* is implemented as `save(User entity)`, so types that differ do not rule an
* overload out. Every one, as before, when a signature can't be read, takes
* varargs, or no overload has `base`'s parameter count.
*/
function pairedOverloads(base: Node, impls: Node[]): Node[] {
if (impls.length < 2 || !PARAMETER_PAIRED_LANGS.has(base.language)) return impls;
const want = signatureParameterTypes(base.signature);
if (!want) return impls;
const typed = impls.map((m) => ({ m, types: signatureParameterTypes(m.signature) }));
if (typed.some((t) => t.types === null)) return impls;
const sameArity = typed.filter((t) => t.types!.length === want.length);
if (sameArity.length === 0) return impls;
const exact = sameArity.filter((t) => t.types!.every((type, i) => type === want[i]));
return (exact.length > 0 ? exact : sameArity).map((t) => t.m);
}

async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
let scanned255 = 0;
const edges: Edge[] = [];
Expand Down Expand Up @@ -1644,8 +1718,10 @@ 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 impl overloads it pairs with
// (pairedOverloads) — by parameters where the language makes an override
// repeat them, else every same-name one (keying by name alone would drop
// all but one and miss the resolved overload). 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.
Expand All @@ -1665,7 +1741,7 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield
if (promotes) unmatched.push(bm);
continue;
}
for (const m of impls) {
for (const m of pairedOverloads(bm, impls)) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
if (bm.id === m.id) continue;
const key = `${bm.id}>${m.id}`;
Expand Down