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:
Colby Mchenry
2026-10-23 00:35:03 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 3f58e34383
commit e32021f6fb
5 changed files with 840 additions and 9 deletions
+28 -6
View File
@@ -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<Edge[]> {
async function interfaceOverrideEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
let scanned255 = 0;
const edges: Edge[] = [];
const seen = new Set<string>();
@@ -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<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
// 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) },
+410
View File
@@ -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);
}
+16 -3
View File
@@ -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> 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`). */