circle-ir 4.14.2 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/sinks/golang.json +4 -4
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +11 -7
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/const-table-lookup.d.ts +29 -0
- package/dist/analysis/const-table-lookup.d.ts.map +1 -0
- package/dist/analysis/const-table-lookup.js +145 -0
- package/dist/analysis/const-table-lookup.js.map +1 -0
- package/dist/analysis/constant-caller-gate.d.ts +39 -0
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
- package/dist/analysis/constant-caller-gate.js +298 -0
- package/dist/analysis/constant-caller-gate.js.map +1 -0
- package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/evaluator.js +3 -1
- package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
- package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
- package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/patterns.js +33 -0
- package/dist/analysis/constant-propagation/patterns.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +396 -49
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/constant-propagation/types.d.ts +9 -0
- package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +149 -4
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/go-strings-replacer.d.ts +21 -0
- package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
- package/dist/analysis/go-strings-replacer.js +145 -0
- package/dist/analysis/go-strings-replacer.js.map +1 -0
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +148 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/java-array-element-keys.d.ts +14 -0
- package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
- package/dist/analysis/java-array-element-keys.js +36 -0
- package/dist/analysis/java-array-element-keys.js.map +1 -0
- package/dist/analysis/java-canonical-prefix.d.ts +42 -0
- package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
- package/dist/analysis/java-canonical-prefix.js +145 -0
- package/dist/analysis/java-canonical-prefix.js.map +1 -0
- package/dist/analysis/java-crlf-strip.d.ts +30 -0
- package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/java-crlf-strip.js +195 -0
- package/dist/analysis/java-crlf-strip.js.map +1 -0
- package/dist/analysis/js-crlf-strip.d.ts +26 -0
- package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/js-crlf-strip.js +244 -0
- package/dist/analysis/js-crlf-strip.js.map +1 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
- package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
- package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +43 -0
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +127 -12
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +20 -1
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +76 -2
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +2045 -156
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +214 -6
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +7 -1
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +509 -27
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js +2 -2
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +1606 -60
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.js +191 -4
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +30 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
- package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
- package/dist/analysis/project-callee-escape-gate.js +243 -0
- package/dist/analysis/project-callee-escape-gate.js.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.js +197 -0
- package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
- package/dist/analysis/rust-literal-guard.d.ts +33 -0
- package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard.js +422 -0
- package/dist/analysis/rust-literal-guard.js.map +1 -0
- package/dist/analysis/scalar-derivation.d.ts +33 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -0
- package/dist/analysis/scalar-derivation.js +368 -0
- package/dist/analysis/scalar-derivation.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +2 -2
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +695 -34
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +26 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +375 -24
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +16 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +19 -2
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +25539 -19200
- package/dist/core/circle-ir-core.cjs +3945 -2126
- package/dist/core/circle-ir-core.js +3945 -2126
- package/dist/core/extractors/calls.d.ts +12 -1
- package/dist/core/extractors/calls.d.ts.map +1 -1
- package/dist/core/extractors/calls.js +332 -22
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +336 -10
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/types.d.ts +5 -1
- package/dist/core/extractors/types.d.ts.map +1 -1
- package/dist/core/extractors/types.js +62 -0
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +6 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +2 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/graph/code-graph.d.ts +9 -0
- package/dist/graph/code-graph.d.ts.map +1 -1
- package/dist/graph/code-graph.js +11 -0
- package/dist/graph/code-graph.js.map +1 -1
- package/dist/navigation/navigation-index.d.ts +134 -14
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +1158 -104
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/types.d.ts +59 -2
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/resolution/cross-file.d.ts +16 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +110 -42
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/resolution/type-hierarchy.d.ts +3 -0
- package/dist/resolution/type-hierarchy.d.ts.map +1 -1
- package/dist/resolution/type-hierarchy.js +45 -1
- package/dist/resolution/type-hierarchy.js.map +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/wasm/web-tree-sitter.wasm +0 -0
- package/package.json +3 -3
|
@@ -28,9 +28,24 @@
|
|
|
28
28
|
* repository, of which 7,246 are correctly unanswered (the target is outside
|
|
29
29
|
* the tree) and 455 are these.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Two more, found by asking for the callers of overloaded and of overridden
|
|
32
|
+
* methods in a 6,900-file Java repository, where every such answer was wrong:
|
|
33
|
+
*
|
|
34
|
+
* - **Overloads.** A call binds to one overload, chosen the way the compiler
|
|
35
|
+
* chooses — by arity, then by argument types in the compiler's three
|
|
36
|
+
* phases, then the most specific — and only where the types read off the
|
|
37
|
+
* source make the choice certain. Where they do not, the site is
|
|
38
|
+
* `unresolved` with `overload-ambiguous` and the candidates. A symbol may
|
|
39
|
+
* carry a parameter list to ask about one overload; a query without one
|
|
40
|
+
* on an overloaded method gets its answers grouped per overload.
|
|
41
|
+
* - **Dispatch.** A call through a supertype's declaration can land on an
|
|
42
|
+
* override, and a call to an override is a caller of the operation it
|
|
43
|
+
* overrides. Both are answered `polymorphic`, with `via` naming what the
|
|
44
|
+
* site binds to, and the query's `scope` says what dispatch it cannot see.
|
|
45
|
+
*
|
|
46
|
+
* What it deliberately does **not** do: invent a receiver type, or an
|
|
47
|
+
* argument type. Where no type is known the answer is `inferred`, ambiguous,
|
|
48
|
+
* or absent, and `inferred` is a permanent floor — see `Tier`.
|
|
34
49
|
*/
|
|
35
50
|
const REFLECTIVE_TYPES = new Set([
|
|
36
51
|
'Class', 'Method', 'Field', 'Constructor', 'AccessibleObject',
|
|
@@ -43,8 +58,63 @@ const REFLECTIVE_METHODS = new Set([
|
|
|
43
58
|
]);
|
|
44
59
|
const LOMBOK_ACCESSOR = new Set(['Getter', 'Setter', 'Data', 'Value']);
|
|
45
60
|
const LOMBOK_BUILDER = new Set(['Builder', 'SuperBuilder']);
|
|
61
|
+
const PRIMITIVES = new Set(['byte', 'short', 'char', 'int', 'long', 'float', 'double', 'boolean']);
|
|
62
|
+
/** Primitive widening: what each primitive may be passed as, itself included. */
|
|
63
|
+
const WIDENS_TO = {
|
|
64
|
+
byte: ['byte', 'short', 'int', 'long', 'float', 'double'],
|
|
65
|
+
short: ['short', 'int', 'long', 'float', 'double'],
|
|
66
|
+
char: ['char', 'int', 'long', 'float', 'double'],
|
|
67
|
+
int: ['int', 'long', 'float', 'double'],
|
|
68
|
+
long: ['long', 'float', 'double'],
|
|
69
|
+
float: ['float', 'double'],
|
|
70
|
+
double: ['double'],
|
|
71
|
+
boolean: ['boolean'],
|
|
72
|
+
};
|
|
73
|
+
const BOXES = {
|
|
74
|
+
byte: 'Byte', short: 'Short', char: 'Character', int: 'Integer',
|
|
75
|
+
long: 'Long', float: 'Float', double: 'Double', boolean: 'Boolean',
|
|
76
|
+
};
|
|
77
|
+
const UNBOXES = Object.fromEntries(Object.entries(BOXES).map(([p, b]) => [b, p]));
|
|
78
|
+
/**
|
|
79
|
+
* JDK classes whose supertypes are known here, so a value of one can be ruled
|
|
80
|
+
* in or out of a parameter type without the JDK on hand. Every one is a class,
|
|
81
|
+
* not an interface; the `final` ones can only ever be themselves.
|
|
82
|
+
*/
|
|
83
|
+
const JDK_SUPERS = (() => {
|
|
84
|
+
const num = ['Number', 'Comparable', 'Serializable', 'Object'];
|
|
85
|
+
const temporal = ['Temporal', 'TemporalAccessor', 'TemporalAdjuster', 'Comparable', 'Serializable', 'Object'];
|
|
86
|
+
return {
|
|
87
|
+
String: { final: true, supers: ['CharSequence', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc', 'Object'] },
|
|
88
|
+
Long: { final: true, supers: num }, Integer: { final: true, supers: num },
|
|
89
|
+
Short: { final: true, supers: num }, Byte: { final: true, supers: num },
|
|
90
|
+
Double: { final: true, supers: num }, Float: { final: true, supers: num },
|
|
91
|
+
Boolean: { final: true, supers: ['Comparable', 'Serializable', 'Object'] },
|
|
92
|
+
Character: { final: true, supers: ['Comparable', 'Serializable', 'Object'] },
|
|
93
|
+
BigDecimal: { final: false, supers: num }, BigInteger: { final: false, supers: num },
|
|
94
|
+
LocalDate: { final: true, supers: ['ChronoLocalDate', ...temporal] },
|
|
95
|
+
LocalDateTime: { final: true, supers: ['ChronoLocalDateTime', ...temporal] },
|
|
96
|
+
LocalTime: { final: true, supers: temporal },
|
|
97
|
+
ZonedDateTime: { final: true, supers: ['ChronoZonedDateTime', ...temporal] },
|
|
98
|
+
OffsetDateTime: { final: true, supers: temporal },
|
|
99
|
+
Instant: { final: true, supers: temporal },
|
|
100
|
+
Duration: { final: true, supers: ['TemporalAmount', 'Comparable', 'Serializable', 'Object'] },
|
|
101
|
+
Period: { final: true, supers: ['ChronoPeriod', 'TemporalAmount', 'Serializable', 'Object'] },
|
|
102
|
+
UUID: { final: true, supers: ['Comparable', 'Serializable', 'Object'] },
|
|
103
|
+
Locale: { final: true, supers: ['Cloneable', 'Serializable', 'Object'] },
|
|
104
|
+
Class: { final: true, supers: ['Type', 'GenericDeclaration', 'AnnotatedElement', 'Serializable', 'Object'] },
|
|
105
|
+
Date: { final: false, supers: ['Cloneable', 'Comparable', 'Serializable', 'Object'] },
|
|
106
|
+
StringBuilder: { final: true, supers: ['CharSequence', 'Appendable', 'Comparable', 'Serializable', 'Object'] },
|
|
107
|
+
Object: { final: false, supers: [] },
|
|
108
|
+
};
|
|
109
|
+
})();
|
|
110
|
+
/** JDK interfaces with no supertype of their own. */
|
|
111
|
+
const LEAF_INTERFACES = new Set(['Serializable', 'Cloneable', 'Comparable', 'AutoCloseable', 'Runnable', 'Iterable', 'CharSequence']);
|
|
112
|
+
const JAVA_LANG = new Set([
|
|
113
|
+
'String', 'Long', 'Integer', 'Short', 'Byte', 'Double', 'Float', 'Boolean', 'Character',
|
|
114
|
+
'Object', 'Number', 'Class', 'StringBuilder', 'CharSequence', 'Comparable', 'Iterable', 'Enum',
|
|
115
|
+
]);
|
|
46
116
|
/** Bumped when the shape of a cached per-file record changes. */
|
|
47
|
-
const CACHE_VERSION = 'nav-
|
|
117
|
+
const CACHE_VERSION = 'nav-3';
|
|
48
118
|
const fileCache = new Map();
|
|
49
119
|
export function buildNavigationIndex(files, options = {}) {
|
|
50
120
|
return new NavigationIndex(files, options);
|
|
@@ -62,6 +132,10 @@ export class NavigationIndex {
|
|
|
62
132
|
parseMs;
|
|
63
133
|
/** Direct subtypes, by supertype FQN. */
|
|
64
134
|
subtypes = new Map();
|
|
135
|
+
/** A call's binding, once made. The index is immutable, so it never goes stale. */
|
|
136
|
+
bindings = new Map();
|
|
137
|
+
/** Calls by `line:column`, per file, built on first use. */
|
|
138
|
+
callsAt = new Map();
|
|
65
139
|
constructor(input, options) {
|
|
66
140
|
const t0 = now();
|
|
67
141
|
// A file in a language this index cannot resolve is **not searched**, and
|
|
@@ -132,6 +206,10 @@ export class NavigationIndex {
|
|
|
132
206
|
* reported 147 callers of one method where the right answer was 0 `exact`,
|
|
133
207
|
* 69 unverified and 78 contradicted by the resolver's own receiver type. Here
|
|
134
208
|
* the 78 do not appear as answers at all, and the 69 arrive labelled.
|
|
209
|
+
*
|
|
210
|
+
* An overloaded method is answered per overload: with a parameter list in
|
|
211
|
+
* the symbol, for that overload alone; without one, for every overload,
|
|
212
|
+
* grouped and labelled, with a `note` saying so.
|
|
135
213
|
*/
|
|
136
214
|
resolveCallers(query, opts = {}) {
|
|
137
215
|
const t0 = now();
|
|
@@ -141,26 +219,107 @@ export class NavigationIndex {
|
|
|
141
219
|
if (symbol === undefined) {
|
|
142
220
|
return this.finish('callers', query, answers, unresolved, t0, opts);
|
|
143
221
|
}
|
|
144
|
-
const
|
|
222
|
+
const q = parseSymbol(symbol);
|
|
223
|
+
const target = this.typesByFqn.get(q.owner);
|
|
224
|
+
const declared = target ? this.declaredOverloads(target, q.name) : [];
|
|
225
|
+
const wanted = q.params ? declared.filter(o => this.signatureMatches(o, q.params, target)) : declared;
|
|
226
|
+
const overloaded = declared.length > 1;
|
|
227
|
+
const extra = {};
|
|
228
|
+
if (q.params && target && declared.length > 0 && wanted.length === 0) {
|
|
229
|
+
extra.note =
|
|
230
|
+
`no overload of ${q.base} has the parameter list (${q.params.join(',')}); ` +
|
|
231
|
+
`declared: ${declared.map(o => `${q.base}${signatureOf(o)}`).join(', ')}`;
|
|
232
|
+
return this.finish('callers', query, answers, unresolved, t0, opts, extra);
|
|
233
|
+
}
|
|
234
|
+
const wantedSet = new Set(wanted);
|
|
235
|
+
const dispatchOpen = target !== undefined && q.name !== '<init>' && wanted.some(o => this.dispatchIsOpen(target, o));
|
|
145
236
|
for (const rec of this.files.values()) {
|
|
146
237
|
for (const call of rec.calls) {
|
|
147
|
-
if (!this.nameCouldMatch(call,
|
|
238
|
+
if (!this.nameCouldMatch(call, q.name, q.owner))
|
|
148
239
|
continue;
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
answers.push({ ...bound, methodName: call.method_name, site: this.siteOf(call, rec) });
|
|
152
|
-
}
|
|
153
|
-
else if (!bound) {
|
|
240
|
+
const b = this.bindCall(call, rec);
|
|
241
|
+
if (!b) {
|
|
154
242
|
unresolved.push({
|
|
155
243
|
site: this.siteOf(call, rec),
|
|
156
244
|
reason: this.reasonFor(call, rec),
|
|
157
245
|
methodName: call.method_name,
|
|
158
246
|
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (!target)
|
|
250
|
+
continue;
|
|
251
|
+
if (b.kind === 'ambiguous') {
|
|
252
|
+
// Ambiguous among overloads none of which is the target is not this
|
|
253
|
+
// query's business; among overloads that include it, it is the
|
|
254
|
+
// honest half of the answer.
|
|
255
|
+
const relevant = b.candidates.some(c => (c.owner === target && wantedSet.has(c.overload)) || this.dispatchRelation(c.owner, c.overload, b.receiver, target, wanted) !== undefined);
|
|
256
|
+
if (relevant) {
|
|
257
|
+
unresolved.push({
|
|
258
|
+
site: this.siteOf(call, rec),
|
|
259
|
+
reason: 'overload-ambiguous',
|
|
260
|
+
methodName: call.method_name,
|
|
261
|
+
candidates: b.candidates.map(c => `${memberName(c.owner, c.overload)}${signatureOf(c.overload)}`),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (b.owner === target && b.name === q.name) {
|
|
267
|
+
if (!b.overload) {
|
|
268
|
+
// A constructor call that fits none of the declared constructors,
|
|
269
|
+
// or a type that declares none: the type is certain, the overload
|
|
270
|
+
// is not, so a query about one overload cannot count it.
|
|
271
|
+
if (q.params && declared.length > 0) {
|
|
272
|
+
unresolved.push({
|
|
273
|
+
site: this.siteOf(call, rec),
|
|
274
|
+
reason: 'overload-ambiguous',
|
|
275
|
+
methodName: call.method_name,
|
|
276
|
+
candidates: declared.map(o => `${q.base}${signatureOf(o)}`),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
answers.push({ ...b.result, methodName: call.method_name, site: this.siteOf(call, rec) });
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (!wantedSet.has(b.overload))
|
|
285
|
+
continue; // another overload: answered elsewhere
|
|
286
|
+
answers.push({
|
|
287
|
+
...b.result,
|
|
288
|
+
...(overloaded ? { signature: signatureOf(b.overload) } : {}),
|
|
289
|
+
methodName: call.method_name,
|
|
290
|
+
site: this.siteOf(call, rec),
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
159
293
|
}
|
|
160
|
-
|
|
294
|
+
if (!dispatchOpen || !b.overload)
|
|
295
|
+
continue;
|
|
296
|
+
const rel = this.dispatchRelation(b.owner, b.overload, b.receiver, target, wanted);
|
|
297
|
+
if (!rel)
|
|
298
|
+
continue;
|
|
299
|
+
const via = `${memberName(b.owner, b.overload)}`;
|
|
300
|
+
answers.push({
|
|
301
|
+
target: q.base,
|
|
302
|
+
tier: 'polymorphic',
|
|
303
|
+
...(b.result.candidates ? { candidates: b.result.candidates } : {}),
|
|
304
|
+
...(overloaded ? { signature: signatureOf(rel.overload) } : {}),
|
|
305
|
+
via: { member: via, relation: rel.relation },
|
|
306
|
+
evidence: rel.relation === 'supertype'
|
|
307
|
+
? `the site calls ${via}, declared on supertype ${b.owner.fqn}; ${q.base} overrides it, so the call lands on it when the receiver is a ${target.fqn}`
|
|
308
|
+
: `the site calls ${via}, which overrides ${q.base}; it reaches ${q.base}'s body only through super, and is a caller of the operation ${q.base} declares`,
|
|
309
|
+
methodName: call.method_name,
|
|
310
|
+
site: this.siteOf(call, rec),
|
|
311
|
+
});
|
|
161
312
|
}
|
|
162
313
|
}
|
|
163
|
-
|
|
314
|
+
if (overloaded && !q.params) {
|
|
315
|
+
extra.overloads = declared.map(o => ({
|
|
316
|
+
signature: signatureOf(o),
|
|
317
|
+
answers: answers.filter(a => a.signature === signatureOf(o)).length,
|
|
318
|
+
}));
|
|
319
|
+
extra.note = `${declared.length} overloads; pass a signature to narrow, e.g. ${q.base}${signatureOf(declared[0])}`;
|
|
320
|
+
}
|
|
321
|
+
const scope = dispatchOpen ? this.dispatchScope(q.base) : undefined;
|
|
322
|
+
return this.finish('callers', query, answers, unresolved, t0, opts, extra, scope);
|
|
164
323
|
}
|
|
165
324
|
/** Every call made from inside `symbol`'s body, bound the same way. */
|
|
166
325
|
resolveCallees(query, opts = {}) {
|
|
@@ -171,9 +330,11 @@ export class NavigationIndex {
|
|
|
171
330
|
if (symbol === undefined) {
|
|
172
331
|
return this.finish('callees', query, answers, unresolved, t0, opts);
|
|
173
332
|
}
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
333
|
+
const q = parseSymbol(symbol);
|
|
334
|
+
const owner = this.typesByFqn.get(q.owner);
|
|
335
|
+
const declared = owner ? this.declaredOverloads(owner, q.name) : [];
|
|
336
|
+
const bodies = q.params ? declared.filter(o => this.signatureMatches(o, q.params, owner)) : declared;
|
|
337
|
+
if (!owner || bodies.length === 0) {
|
|
177
338
|
return this.finish('callees', query, answers, unresolved, t0, opts);
|
|
178
339
|
}
|
|
179
340
|
const rec = this.files.get(owner.file);
|
|
@@ -181,11 +342,25 @@ export class NavigationIndex {
|
|
|
181
342
|
return this.finish('callees', query, answers, unresolved, t0, opts);
|
|
182
343
|
for (const call of rec.calls) {
|
|
183
344
|
const line = call.location.line;
|
|
184
|
-
if (line
|
|
345
|
+
if (!bodies.some(o => line >= o.startLine && line <= o.endLine))
|
|
185
346
|
continue;
|
|
186
|
-
const
|
|
187
|
-
if (bound)
|
|
188
|
-
answers.push({
|
|
347
|
+
const b = this.bindCall(call, rec);
|
|
348
|
+
if (b?.kind === 'bound') {
|
|
349
|
+
answers.push({
|
|
350
|
+
...b.result,
|
|
351
|
+
...(b.overload && this.declaredOverloads(b.owner, b.name).length > 1 ? { signature: signatureOf(b.overload) } : {}),
|
|
352
|
+
methodName: call.method_name,
|
|
353
|
+
site: this.siteOf(call, rec),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
else if (b?.kind === 'ambiguous') {
|
|
357
|
+
unresolved.push({
|
|
358
|
+
site: this.siteOf(call, rec),
|
|
359
|
+
reason: 'overload-ambiguous',
|
|
360
|
+
methodName: call.method_name,
|
|
361
|
+
candidates: b.candidates.map(c => `${memberName(c.owner, c.overload)}${signatureOf(c.overload)}`),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
189
364
|
else {
|
|
190
365
|
unresolved.push({
|
|
191
366
|
site: this.siteOf(call, rec),
|
|
@@ -206,42 +381,59 @@ export class NavigationIndex {
|
|
|
206
381
|
*
|
|
207
382
|
* For `callers` the symbol is the call's own target — "who else calls what
|
|
208
383
|
* this line calls". For `callees` it is the method whose body holds the
|
|
209
|
-
* line — "what does the method I am looking at call".
|
|
210
|
-
*
|
|
211
|
-
*
|
|
384
|
+
* line — "what does the method I am looking at call". Either carries a
|
|
385
|
+
* parameter list when the name is overloaded, so the follow-up query asks
|
|
386
|
+
* about that overload alone. Returns nothing when the site names no call,
|
|
387
|
+
* when the call cannot be bound to one target, or when the name is written
|
|
388
|
+
* twice on one line, which the key cannot separate.
|
|
212
389
|
*/
|
|
213
390
|
symbolAt(file, line, methodName, kind) {
|
|
214
391
|
const rec = this.files.get(file);
|
|
215
392
|
if (!rec)
|
|
216
393
|
return undefined;
|
|
217
|
-
if (kind === 'callees')
|
|
218
|
-
|
|
219
|
-
if (!t)
|
|
220
|
-
return undefined;
|
|
221
|
-
for (const [name, m] of t.methods) {
|
|
222
|
-
if (line >= m.startLine && line <= m.endLine)
|
|
223
|
-
return `${t.fqn}.${name}`;
|
|
224
|
-
}
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
394
|
+
if (kind === 'callees')
|
|
395
|
+
return this.methodAt(rec, line);
|
|
227
396
|
const at = rec.calls.filter((c) => c.location.line === line && c.method_name === methodName);
|
|
228
397
|
// The same name twice on one line is beyond this key, and picking one of
|
|
229
398
|
// them would be a guess presented as an answer.
|
|
230
399
|
if (at.length !== 1)
|
|
231
400
|
return undefined;
|
|
232
|
-
return this.
|
|
401
|
+
return this.symbolOfCall(at[0], rec);
|
|
233
402
|
}
|
|
234
403
|
// ------------------------------------------------------------ the binding
|
|
404
|
+
/** `bindCall`, reduced to what callers outside the query paths need. */
|
|
405
|
+
symbolOfCall(call, rec) {
|
|
406
|
+
const b = this.bindCall(call, rec);
|
|
407
|
+
if (b?.kind !== 'bound')
|
|
408
|
+
return undefined;
|
|
409
|
+
if (b.overload && this.declaredOverloads(b.owner, b.name).length > 1) {
|
|
410
|
+
return `${b.result.target}${signatureOf(b.overload)}`;
|
|
411
|
+
}
|
|
412
|
+
return b.result.target;
|
|
413
|
+
}
|
|
235
414
|
/**
|
|
236
|
-
* Bind one call site to a target, or to nothing.
|
|
415
|
+
* Bind one call site to a target, to several candidates, or to nothing.
|
|
237
416
|
*
|
|
238
417
|
* The order matters and is the whole contract: a receiver type is sought
|
|
239
418
|
* first, and only when none can be had does a name-only bind happen, which
|
|
240
|
-
* is labelled `inferred` and never anything better.
|
|
419
|
+
* is labelled `inferred` and never anything better. Within the type the
|
|
420
|
+
* receiver names, the overload is chosen by `selectOverload`, and a choice
|
|
421
|
+
* it cannot make is `ambiguous`, not a pick.
|
|
241
422
|
*/
|
|
242
|
-
|
|
423
|
+
bindCall(call, rec, depth = 0) {
|
|
424
|
+
const memo = this.bindings.get(call);
|
|
425
|
+
if (memo !== undefined)
|
|
426
|
+
return memo ?? undefined;
|
|
427
|
+
const b = this.bindUncached(call, rec, depth);
|
|
428
|
+
// A bind made too deep inside another call's argument typing may have
|
|
429
|
+
// been cut short, so only a top-level result is remembered.
|
|
430
|
+
if (depth === 0)
|
|
431
|
+
this.bindings.set(call, b ?? null);
|
|
432
|
+
return b;
|
|
433
|
+
}
|
|
434
|
+
bindUncached(call, rec, depth) {
|
|
243
435
|
// A constructor arrives as a call named after the type, with no receiver.
|
|
244
|
-
const ctor = this.
|
|
436
|
+
const ctor = this.bindConstructor(call, rec, depth);
|
|
245
437
|
if (ctor)
|
|
246
438
|
return ctor;
|
|
247
439
|
const recv = this.receiverTypeOf(call, rec);
|
|
@@ -253,19 +445,25 @@ export class NavigationIndex {
|
|
|
253
445
|
if (recv?.kind === 'foreign')
|
|
254
446
|
return undefined;
|
|
255
447
|
if (recv?.kind === 'project') {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
448
|
+
// The receiver's type is known; a method it does not have (at this
|
|
449
|
+
// arity) would be the contradiction the audit found 78 of, so no.
|
|
450
|
+
return this.bindInType(recv.fqn, call, rec, depth, recv.evidence, recv.fqn);
|
|
451
|
+
}
|
|
452
|
+
// `super.m()` names the superclass's member outright.
|
|
453
|
+
if (call.receiver === 'super') {
|
|
454
|
+
const encl = this.enclosingTypeOf(call, rec);
|
|
455
|
+
const sup = encl?.extendsName ? this.resolveTypeName(encl.extendsName, rec) : undefined;
|
|
456
|
+
if (sup && this.typesByFqn.has(sup)) {
|
|
457
|
+
return this.bindInType(sup, call, rec, depth, `super call inside ${encl.fqn}`, sup);
|
|
458
|
+
}
|
|
261
459
|
return undefined;
|
|
262
460
|
}
|
|
263
461
|
// A statically imported member names its owner outright.
|
|
264
462
|
const owner = this.staticOwnerFor(call, rec);
|
|
265
463
|
if (owner) {
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
return
|
|
464
|
+
const b = this.bindInType(owner.fqn, call, rec, depth, owner.evidence, undefined);
|
|
465
|
+
if (b)
|
|
466
|
+
return b;
|
|
269
467
|
}
|
|
270
468
|
// No receiver at all: an unqualified call is `this` or the enclosing type's
|
|
271
469
|
// own static method. The enclosing type is known exactly, so this is not a
|
|
@@ -274,11 +472,8 @@ export class NavigationIndex {
|
|
|
274
472
|
// lives in a subclass. Every path to a target now goes through `tierFor`.
|
|
275
473
|
if (call.receiver === null && !call.receiver_type) {
|
|
276
474
|
const encl = this.enclosingTypeOf(call, rec);
|
|
277
|
-
if (encl) {
|
|
278
|
-
|
|
279
|
-
if (hit) {
|
|
280
|
-
return this.tierFor(hit.owner, call.method_name, `unqualified call inside ${encl.fqn}`);
|
|
281
|
-
}
|
|
475
|
+
if (encl && this.lookupThroughHierarchy(encl.fqn, call.method_name)) {
|
|
476
|
+
return this.bindInType(encl.fqn, call, rec, depth, `unqualified call inside ${encl.fqn}`, encl.fqn);
|
|
282
477
|
}
|
|
283
478
|
}
|
|
284
479
|
// Nothing typed the receiver. A name-only bind is the floor, and only when
|
|
@@ -286,14 +481,49 @@ export class NavigationIndex {
|
|
|
286
481
|
// not evidence of anything.
|
|
287
482
|
const byName = this.uniqueByName(call.method_name);
|
|
288
483
|
if (byName) {
|
|
484
|
+
const evidence = `bound by method name alone — no receiver type was available; ${call.method_name} is declared exactly once in the searched tree, on ${byName.fqn}`;
|
|
485
|
+
const cands = this.visibleOverloads(byName.fqn, call.method_name);
|
|
486
|
+
const sel = this.selectOverload(cands, call, rec, depth);
|
|
487
|
+
if (sel.kind === 'ambiguous') {
|
|
488
|
+
return { kind: 'ambiguous', owner: byName, name: call.method_name, candidates: sel.candidates, evidence };
|
|
489
|
+
}
|
|
490
|
+
if (sel.kind === 'none')
|
|
491
|
+
return undefined;
|
|
289
492
|
return {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
493
|
+
kind: 'bound',
|
|
494
|
+
owner: sel.pick.owner,
|
|
495
|
+
name: call.method_name,
|
|
496
|
+
overload: sel.pick.overload,
|
|
497
|
+
result: { target: `${sel.pick.owner.fqn}.${call.method_name}`, tier: 'inferred', evidence },
|
|
293
498
|
};
|
|
294
499
|
}
|
|
295
500
|
return undefined;
|
|
296
501
|
}
|
|
502
|
+
/** Bind `call` to a member of `fqn` or of a supertype of it, choosing the overload. */
|
|
503
|
+
bindInType(fqn, call, rec, depth, evidence, receiver) {
|
|
504
|
+
const cands = this.visibleOverloads(fqn, call.method_name);
|
|
505
|
+
if (cands.length === 0)
|
|
506
|
+
return undefined;
|
|
507
|
+
const sel = this.selectOverload(cands, call, rec, depth);
|
|
508
|
+
if (sel.kind === 'none')
|
|
509
|
+
return undefined;
|
|
510
|
+
if (sel.kind === 'ambiguous') {
|
|
511
|
+
const owner = sel.candidates[0].owner;
|
|
512
|
+
return { kind: 'ambiguous', owner, name: call.method_name, candidates: sel.candidates, receiver, evidence };
|
|
513
|
+
}
|
|
514
|
+
const { owner, overload } = sel.pick;
|
|
515
|
+
const overloaded = this.declaredOverloads(owner, call.method_name).length > 1 || cands.length > 1;
|
|
516
|
+
return {
|
|
517
|
+
kind: 'bound',
|
|
518
|
+
owner,
|
|
519
|
+
name: call.method_name,
|
|
520
|
+
overload,
|
|
521
|
+
receiver,
|
|
522
|
+
result: overloaded
|
|
523
|
+
? this.tierFor(owner, call.method_name, overload, `${evidence}; ${sel.how}`, true)
|
|
524
|
+
: this.tierFor(owner, call.method_name, overload, evidence),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
297
527
|
/**
|
|
298
528
|
* The tier for a target, once the declaring type is known.
|
|
299
529
|
*
|
|
@@ -306,9 +536,9 @@ export class NavigationIndex {
|
|
|
306
536
|
* member named. The target stays the declaring member, which is what a
|
|
307
537
|
* static resolver answers; only the tier changes.
|
|
308
538
|
*/
|
|
309
|
-
tierFor(owner, method, evidence) {
|
|
310
|
-
const impls = this.implementorsWith(owner.fqn,
|
|
311
|
-
const bodyless = this.isBodyless(owner,
|
|
539
|
+
tierFor(owner, method, overload, evidence, overloaded = false) {
|
|
540
|
+
const impls = this.implementorsWith(owner.fqn, overload);
|
|
541
|
+
const bodyless = this.isBodyless(owner, overload);
|
|
312
542
|
if (impls.length > 1 || bodyless) {
|
|
313
543
|
return {
|
|
314
544
|
target: `${owner.fqn}.${method}`,
|
|
@@ -322,11 +552,13 @@ export class NavigationIndex {
|
|
|
322
552
|
return {
|
|
323
553
|
target: `${owner.fqn}.${method}`,
|
|
324
554
|
tier: 'exact',
|
|
325
|
-
evidence:
|
|
555
|
+
evidence: overloaded
|
|
556
|
+
? `${evidence}; it is declared on ${owner.fqn}, and it has a body`
|
|
557
|
+
: `${evidence}; ${method} is the unique method of that name on ${owner.fqn}, and it has a body`,
|
|
326
558
|
};
|
|
327
559
|
}
|
|
328
|
-
/** `new Widget()` → `app.Widget.<init
|
|
329
|
-
|
|
560
|
+
/** `new Widget()` → `app.Widget.<init>`, with the constructor chosen like any overload. */
|
|
561
|
+
bindConstructor(call, rec, depth) {
|
|
330
562
|
const looksCtor = call.is_constructor === true ||
|
|
331
563
|
(call.receiver === null &&
|
|
332
564
|
!!call.receiver_type &&
|
|
@@ -337,12 +569,650 @@ export class NavigationIndex {
|
|
|
337
569
|
const t = this.resolveTypeRecord(call.receiver_type ?? call.method_name, rec);
|
|
338
570
|
if (!t)
|
|
339
571
|
return undefined;
|
|
340
|
-
|
|
572
|
+
const result = {
|
|
341
573
|
target: `${t.fqn}.<init>`,
|
|
342
574
|
tier: 'exact',
|
|
343
575
|
evidence: `object creation of ${t.fqn}, declared at ${t.file}:${t.startLine}`,
|
|
344
576
|
};
|
|
577
|
+
const ctors = this.declaredOverloads(t, '<init>').map(overload => ({ owner: t, overload }));
|
|
578
|
+
// A type with no declared constructor has the implicit one; a call that
|
|
579
|
+
// fits none of the declared ones (a record's canonical constructor, say)
|
|
580
|
+
// still certainly creates this type. Either way the type is the answer
|
|
581
|
+
// and the overload is not known.
|
|
582
|
+
if (ctors.length === 0)
|
|
583
|
+
return { kind: 'bound', owner: t, name: '<init>', result };
|
|
584
|
+
const sel = this.selectOverload(ctors, call, rec, depth);
|
|
585
|
+
if (sel.kind === 'ambiguous') {
|
|
586
|
+
return { kind: 'ambiguous', owner: t, name: '<init>', candidates: sel.candidates, evidence: result.evidence ?? '' };
|
|
587
|
+
}
|
|
588
|
+
if (sel.kind === 'none')
|
|
589
|
+
return { kind: 'bound', owner: t, name: '<init>', result };
|
|
590
|
+
return {
|
|
591
|
+
kind: 'bound',
|
|
592
|
+
owner: t,
|
|
593
|
+
name: '<init>',
|
|
594
|
+
overload: sel.pick.overload,
|
|
595
|
+
result: ctors.length > 1 ? { ...result, evidence: `${result.evidence}; ${sel.how}` } : result,
|
|
596
|
+
};
|
|
345
597
|
}
|
|
598
|
+
// ---------------------------------------------------- overload selection
|
|
599
|
+
/**
|
|
600
|
+
* The overloads of `name` a receiver of type `fqn` can see: its own, then
|
|
601
|
+
* its supertypes', nearest first, leaving out any a nearer type overrides.
|
|
602
|
+
*/
|
|
603
|
+
visibleOverloads(fqn, name) {
|
|
604
|
+
const out = [];
|
|
605
|
+
const seen = new Set();
|
|
606
|
+
const queue = [fqn];
|
|
607
|
+
while (queue.length > 0) {
|
|
608
|
+
const f = queue.shift();
|
|
609
|
+
if (seen.has(f))
|
|
610
|
+
continue;
|
|
611
|
+
seen.add(f);
|
|
612
|
+
const t = this.typesByFqn.get(f);
|
|
613
|
+
if (!t)
|
|
614
|
+
continue;
|
|
615
|
+
for (const o of t.overloads.get(name) ?? []) {
|
|
616
|
+
if (o.isConstructor)
|
|
617
|
+
continue;
|
|
618
|
+
if (out.some(c => this.sameErasure(c, { owner: t, overload: o })))
|
|
619
|
+
continue;
|
|
620
|
+
out.push({ owner: t, overload: o });
|
|
621
|
+
}
|
|
622
|
+
const rec = this.files.get(t.file);
|
|
623
|
+
for (const sup of t.superNames) {
|
|
624
|
+
const supFqn = rec ? this.resolveTypeName(sup, rec) : undefined;
|
|
625
|
+
if (supFqn)
|
|
626
|
+
queue.push(supFqn);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
/** The declarations of `name` on `t` itself. `<init>` is its constructors. */
|
|
632
|
+
declaredOverloads(t, name) {
|
|
633
|
+
if (name === '<init>')
|
|
634
|
+
return (t.overloads.get(t.simpleName) ?? []).filter(o => o.isConstructor);
|
|
635
|
+
return (t.overloads.get(name) ?? []).filter(o => !o.isConstructor);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Choose the overload a call invokes, the way the compiler does — or say
|
|
639
|
+
* that the types read off the source do not decide it.
|
|
640
|
+
*
|
|
641
|
+
* Arity first. Then the compiler's three phases: without boxing or
|
|
642
|
+
* varargs, with boxing, with varargs. In the first phase that has any
|
|
643
|
+
* applicable overload, the most specific one wins. Every argument type here
|
|
644
|
+
* is a lower bound on what is known — an argument that cannot be typed fits
|
|
645
|
+
* anything — so the rule is applied only where it is sound: an overload is
|
|
646
|
+
* picked when it is **certainly** applicable and more specific than every
|
|
647
|
+
* overload that **might** be. Anything less is `ambiguous`.
|
|
648
|
+
*/
|
|
649
|
+
selectOverload(cands, call, rec, depth) {
|
|
650
|
+
const n = call.arguments.length;
|
|
651
|
+
const byArity = cands.filter(c => fitsArity(c.overload, n));
|
|
652
|
+
if (byArity.length === 0)
|
|
653
|
+
return { kind: 'none' };
|
|
654
|
+
const args = call.arguments.map(a => this.argumentType(a, call, rec, depth));
|
|
655
|
+
if (byArity.length === 1) {
|
|
656
|
+
// The only declaration in the tree at this arity, but the argument
|
|
657
|
+
// types certainly rule it out: what is called is an overload this
|
|
658
|
+
// index cannot see, inherited from a library type. Not ours.
|
|
659
|
+
if ([1, 2, 3].every(p => this.applicable(byArity[0], args, p) === 'no'))
|
|
660
|
+
return { kind: 'none' };
|
|
661
|
+
return {
|
|
662
|
+
kind: 'one',
|
|
663
|
+
pick: byArity[0],
|
|
664
|
+
how: `overload ${signatureOf(byArity[0].overload)} is the only one taking ${n} argument(s)`,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const fit = new Map();
|
|
668
|
+
for (const c of byArity)
|
|
669
|
+
fit.set(c, [1, 2, 3].map(p => this.applicable(c, args, p)));
|
|
670
|
+
const possible = byArity.filter(c => fit.get(c).some(f => f !== 'no'));
|
|
671
|
+
if (possible.length === 1) {
|
|
672
|
+
return {
|
|
673
|
+
kind: 'one',
|
|
674
|
+
pick: possible[0],
|
|
675
|
+
how: `overload ${signatureOf(possible[0].overload)} is the only one the argument types admit`,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
// The argument types rule out every overload: something read off the
|
|
679
|
+
// source is wrong, so nothing is chosen.
|
|
680
|
+
if (possible.length === 0)
|
|
681
|
+
return { kind: 'ambiguous', candidates: byArity };
|
|
682
|
+
for (const phase of [1, 2, 3]) {
|
|
683
|
+
const P = possible.filter(c => fit.get(c)[phase - 1] !== 'no');
|
|
684
|
+
if (P.length === 0)
|
|
685
|
+
continue;
|
|
686
|
+
const C = P.filter(c => fit.get(c)[phase - 1] === 'yes');
|
|
687
|
+
if (C.length === 0)
|
|
688
|
+
return { kind: 'ambiguous', candidates: possible };
|
|
689
|
+
const best = C.find(m => P.every(o => o === m || this.moreSpecific(m, o, phase)));
|
|
690
|
+
if (best) {
|
|
691
|
+
return {
|
|
692
|
+
kind: 'one',
|
|
693
|
+
pick: best,
|
|
694
|
+
how: `overload ${signatureOf(best.overload)} is the most specific one the argument types admit`,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
return { kind: 'ambiguous', candidates: P };
|
|
698
|
+
}
|
|
699
|
+
return { kind: 'ambiguous', candidates: possible };
|
|
700
|
+
}
|
|
701
|
+
/** Whether `c` applies to arguments of these types in compiler phase 1, 2 or 3. */
|
|
702
|
+
applicable(c, args, phase) {
|
|
703
|
+
const o = c.overload;
|
|
704
|
+
const declRec = this.files.get(c.owner.file);
|
|
705
|
+
const params = o.params.map(p => this.typeRef(p, declRec));
|
|
706
|
+
let worst = 'yes';
|
|
707
|
+
const meet = (f) => {
|
|
708
|
+
if (f === 'no')
|
|
709
|
+
worst = 'no';
|
|
710
|
+
else if (f === 'maybe' && worst === 'yes')
|
|
711
|
+
worst = 'maybe';
|
|
712
|
+
};
|
|
713
|
+
if (phase < 3) {
|
|
714
|
+
if (args.length !== params.length)
|
|
715
|
+
return 'no';
|
|
716
|
+
args.forEach((a, i) => meet(this.fits(a, params[i], phase)));
|
|
717
|
+
return worst;
|
|
718
|
+
}
|
|
719
|
+
if (!o.varargs || args.length < params.length - 1)
|
|
720
|
+
return 'no';
|
|
721
|
+
const last = params[params.length - 1];
|
|
722
|
+
const elem = { ...last, dims: Math.max(0, last.dims - 1) };
|
|
723
|
+
args.forEach((a, i) => meet(this.fits(a, i < params.length - 1 ? params[i] : elem, 2)));
|
|
724
|
+
return worst;
|
|
725
|
+
}
|
|
726
|
+
/** `m` is more specific than `o`: each of its parameter types fits the other's. */
|
|
727
|
+
moreSpecific(m, o, phase) {
|
|
728
|
+
if (phase === 3)
|
|
729
|
+
return false;
|
|
730
|
+
const mp = m.overload.params.map(p => this.typeRef(p, this.files.get(m.owner.file)));
|
|
731
|
+
const op = o.overload.params.map(p => this.typeRef(p, this.files.get(o.owner.file)));
|
|
732
|
+
if (mp.length !== op.length)
|
|
733
|
+
return false;
|
|
734
|
+
return mp.every((t, i) => this.fits(t, op[i], 1) === 'yes');
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Whether a value of type `a` can be passed as `p` — certainly, possibly,
|
|
738
|
+
* or certainly not. `a === null` is an argument that could not be typed.
|
|
739
|
+
*/
|
|
740
|
+
fits(a, p, phase) {
|
|
741
|
+
if (!a)
|
|
742
|
+
return 'maybe';
|
|
743
|
+
if (p.typeVar)
|
|
744
|
+
return a.prim && a.dims === 0 && phase === 1 ? 'no' : 'maybe';
|
|
745
|
+
if (a.simple === 'null' && a.dims === 0)
|
|
746
|
+
return p.prim && p.dims === 0 ? 'no' : 'yes';
|
|
747
|
+
if (a.typeVar)
|
|
748
|
+
return 'maybe';
|
|
749
|
+
if (a.dims > 0 || p.dims > 0) {
|
|
750
|
+
if (a.dims === p.dims) {
|
|
751
|
+
const ae = { ...a, dims: 0 };
|
|
752
|
+
const pe = { ...p, dims: 0 };
|
|
753
|
+
if (ae.prim || pe.prim)
|
|
754
|
+
return ae.prim && pe.prim && ae.simple === pe.simple ? 'yes' : 'no';
|
|
755
|
+
return this.fits(ae, pe, 1);
|
|
756
|
+
}
|
|
757
|
+
if (a.dims > 0 && p.dims === 0) {
|
|
758
|
+
if (['Object', 'Cloneable', 'Serializable'].includes(p.simple))
|
|
759
|
+
return 'yes';
|
|
760
|
+
return p.prim || this.isProjectRef(p) || p.simple in JDK_SUPERS ? 'no' : 'maybe';
|
|
761
|
+
}
|
|
762
|
+
if (a.dims > p.dims && p.simple === 'Object' && !p.prim)
|
|
763
|
+
return 'yes';
|
|
764
|
+
return 'no';
|
|
765
|
+
}
|
|
766
|
+
if (a.prim && p.prim)
|
|
767
|
+
return WIDENS_TO[a.simple]?.includes(p.simple) ? 'yes' : 'no';
|
|
768
|
+
if (a.prim) {
|
|
769
|
+
if (phase === 1)
|
|
770
|
+
return 'no';
|
|
771
|
+
const boxed = BOXES[a.simple];
|
|
772
|
+
return boxed ? this.refFits({ simple: boxed, fqn: `java.lang.${boxed}`, dims: 0, prim: false, typeVar: false }, p) : 'no';
|
|
773
|
+
}
|
|
774
|
+
if (p.prim) {
|
|
775
|
+
if (phase === 1)
|
|
776
|
+
return 'no';
|
|
777
|
+
const unboxed = UNBOXES[a.simple];
|
|
778
|
+
if (unboxed && (!a.fqn || a.fqn === `java.lang.${a.simple}`))
|
|
779
|
+
return WIDENS_TO[unboxed].includes(p.simple) ? 'yes' : 'no';
|
|
780
|
+
return this.isProjectRef(a) || a.simple in JDK_SUPERS ? 'no' : 'maybe';
|
|
781
|
+
}
|
|
782
|
+
return this.refFits(a, p);
|
|
783
|
+
}
|
|
784
|
+
/** Reference to reference: subtyping, as far as the tree and the JDK table know it. */
|
|
785
|
+
refFits(a, p) {
|
|
786
|
+
if (a.fqn && p.fqn && a.fqn === p.fqn)
|
|
787
|
+
return 'yes';
|
|
788
|
+
if (p.simple === 'Object' && (!p.fqn || p.fqn === 'java.lang.Object'))
|
|
789
|
+
return 'yes';
|
|
790
|
+
const aProject = this.isProjectRef(a);
|
|
791
|
+
const pProject = this.isProjectRef(p);
|
|
792
|
+
if (aProject) {
|
|
793
|
+
const sup = this.supertypesOf(a.fqn);
|
|
794
|
+
if (pProject) {
|
|
795
|
+
if (sup.project.has(p.fqn))
|
|
796
|
+
return 'yes';
|
|
797
|
+
// A library type never extends one of ours, so only a supertype name
|
|
798
|
+
// this index could not resolve keeps the door open.
|
|
799
|
+
return sup.unresolved.length > 0 ? 'maybe' : 'no';
|
|
800
|
+
}
|
|
801
|
+
if (sup.foreign.includes(p.simple))
|
|
802
|
+
return 'yes';
|
|
803
|
+
// Every supertype is in the tree or a JDK type whose own supertypes are
|
|
804
|
+
// known, and none of them is `p`: it cannot be one.
|
|
805
|
+
const closed = sup.unresolved.length === 0 && sup.foreign.every(f => LEAF_INTERFACES.has(f) || (JDK_SUPERS[f] && !JDK_SUPERS[f].supers.includes(p.simple)));
|
|
806
|
+
if (closed && this.isKnownForeignRef(p))
|
|
807
|
+
return 'no';
|
|
808
|
+
const jdk = JDK_SUPERS[p.simple];
|
|
809
|
+
if (jdk) {
|
|
810
|
+
// A JDK class (not interface) is reachable only through a superclass
|
|
811
|
+
// chain that leaves the tree.
|
|
812
|
+
return sup.foreignClassChain ? 'maybe' : 'no';
|
|
813
|
+
}
|
|
814
|
+
return 'maybe';
|
|
815
|
+
}
|
|
816
|
+
if (pProject) {
|
|
817
|
+
// A library type cannot be a subtype of a type in this tree. Only an
|
|
818
|
+
// argument whose type could not be placed at all might still be ours.
|
|
819
|
+
return a.fqn || a.simple in JDK_SUPERS ? 'no' : 'maybe';
|
|
820
|
+
}
|
|
821
|
+
if (a.simple === p.simple)
|
|
822
|
+
return a.fqn && p.fqn && a.fqn !== p.fqn ? 'no' : 'yes';
|
|
823
|
+
const ja = JDK_SUPERS[a.simple];
|
|
824
|
+
if (ja) {
|
|
825
|
+
if (ja.supers.includes(p.simple))
|
|
826
|
+
return 'yes';
|
|
827
|
+
if (p.simple in JDK_SUPERS)
|
|
828
|
+
return 'no';
|
|
829
|
+
return ja.final ? 'no' : 'maybe';
|
|
830
|
+
}
|
|
831
|
+
const jp = JDK_SUPERS[p.simple];
|
|
832
|
+
if (jp?.final)
|
|
833
|
+
return 'no';
|
|
834
|
+
return 'maybe';
|
|
835
|
+
}
|
|
836
|
+
/** Is `t` certainly a type outside this tree? */
|
|
837
|
+
isKnownForeignRef(t) {
|
|
838
|
+
if (t.fqn)
|
|
839
|
+
return !this.isProjectFqn(t.fqn) && !this.typesByFqn.has(t.fqn);
|
|
840
|
+
return JAVA_LANG.has(t.simple) || t.simple in JDK_SUPERS;
|
|
841
|
+
}
|
|
842
|
+
/** Is `t` a type declared in this tree? */
|
|
843
|
+
isProjectRef(t) {
|
|
844
|
+
return !!t.fqn && this.typesByFqn.has(t.fqn);
|
|
845
|
+
}
|
|
846
|
+
/** Every supertype of a project type, split into what the tree holds and what it does not. */
|
|
847
|
+
supertypesOf(fqn) {
|
|
848
|
+
const project = new Set();
|
|
849
|
+
const foreign = [];
|
|
850
|
+
const unresolved = [];
|
|
851
|
+
let foreignClassChain = false;
|
|
852
|
+
const walk = (f, viaClass) => {
|
|
853
|
+
if (project.has(f))
|
|
854
|
+
return;
|
|
855
|
+
project.add(f);
|
|
856
|
+
const t = this.typesByFqn.get(f);
|
|
857
|
+
const rec = t ? this.files.get(t.file) : undefined;
|
|
858
|
+
if (!t || !rec)
|
|
859
|
+
return;
|
|
860
|
+
for (const sup of t.superNames) {
|
|
861
|
+
const resolved = this.resolveTypeName(sup, rec);
|
|
862
|
+
const isClassEdge = viaClass && sup === t.extendsName;
|
|
863
|
+
if (resolved && this.typesByFqn.has(resolved)) {
|
|
864
|
+
walk(resolved, isClassEdge);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const simple = eraseType(sup).split('.').pop() ?? sup;
|
|
868
|
+
if (this.isKnownForeign(sup, rec))
|
|
869
|
+
foreign.push(simple);
|
|
870
|
+
else
|
|
871
|
+
unresolved.push(simple);
|
|
872
|
+
if (isClassEdge)
|
|
873
|
+
foreignClassChain = true;
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
walk(fqn, true);
|
|
877
|
+
return { project, foreign, unresolved, foreignClassChain };
|
|
878
|
+
}
|
|
879
|
+
/** A type name that certainly names something outside the tree. */
|
|
880
|
+
isKnownForeign(name, rec) {
|
|
881
|
+
const bare = eraseType(name);
|
|
882
|
+
const simple = bare.split('.').pop() ?? bare;
|
|
883
|
+
if (bare.includes('.'))
|
|
884
|
+
return !this.isProjectFqn(bare);
|
|
885
|
+
const imported = rec.imports.get(simple);
|
|
886
|
+
if (imported)
|
|
887
|
+
return !this.isProjectFqn(imported);
|
|
888
|
+
return JAVA_LANG.has(simple) || simple in JDK_SUPERS;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* The static type of one argument, or `null` when it cannot be known. The
|
|
892
|
+
* extractor types what the syntax and the declarations in scope say; an
|
|
893
|
+
* argument that is itself a call is typed here by binding that call and
|
|
894
|
+
* reading the chosen overload's return type.
|
|
895
|
+
*/
|
|
896
|
+
argumentType(arg, call, rec, depth) {
|
|
897
|
+
if (arg.type)
|
|
898
|
+
return this.typeRef(arg.type, rec);
|
|
899
|
+
if (!arg.location || depth >= 4)
|
|
900
|
+
return null;
|
|
901
|
+
const name = outerCallName(arg.expression);
|
|
902
|
+
if (!name)
|
|
903
|
+
return null;
|
|
904
|
+
const inner = this.callAt(rec, arg.location.line, arg.location.column, name);
|
|
905
|
+
if (!inner || inner === call)
|
|
906
|
+
return null;
|
|
907
|
+
const b = this.bindCall(inner, rec, depth + 1);
|
|
908
|
+
let returned;
|
|
909
|
+
let receiver;
|
|
910
|
+
if (b?.kind === 'bound' && b.overload?.returnType) {
|
|
911
|
+
returned = { text: b.overload.returnType, owner: b.owner };
|
|
912
|
+
receiver = b.receiver;
|
|
913
|
+
}
|
|
914
|
+
else if (!b) {
|
|
915
|
+
// An accessor that annotation processing writes: what it returns is the
|
|
916
|
+
// field it reads.
|
|
917
|
+
const recv = this.receiverTypeOf(inner, rec);
|
|
918
|
+
receiver = recv?.kind === 'project' ? recv.fqn : inner.receiver === null ? this.enclosingTypeOf(inner, rec)?.fqn : undefined;
|
|
919
|
+
if (receiver && inner.arguments.length === 0)
|
|
920
|
+
returned = this.generatedGetterType(receiver, inner.method_name);
|
|
921
|
+
}
|
|
922
|
+
if (!returned)
|
|
923
|
+
return null;
|
|
924
|
+
const sub = this.substitute(returned.text, returned.owner, receiver);
|
|
925
|
+
if (!sub)
|
|
926
|
+
return null;
|
|
927
|
+
const t = this.typeRef(sub.text, sub.rec);
|
|
928
|
+
return t.typeVar ? null : t;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* The type a generated `getX()` / `isX()` returns on `fqn`: the written
|
|
932
|
+
* type of field `x`, with the type that declares the field.
|
|
933
|
+
*/
|
|
934
|
+
generatedGetterType(fqn, method) {
|
|
935
|
+
const m = /^(get|is)([A-Z]\w*)$/.exec(method);
|
|
936
|
+
const t = this.typesByFqn.get(fqn);
|
|
937
|
+
if (!m || !t || !this.wouldBeGenerated(t, method))
|
|
938
|
+
return undefined;
|
|
939
|
+
const field = m[2].charAt(0).toLowerCase() + m[2].slice(1);
|
|
940
|
+
const seen = new Set();
|
|
941
|
+
const queue = [fqn];
|
|
942
|
+
while (queue.length > 0) {
|
|
943
|
+
const f = queue.shift();
|
|
944
|
+
if (seen.has(f))
|
|
945
|
+
continue;
|
|
946
|
+
seen.add(f);
|
|
947
|
+
const ft = this.typesByFqn.get(f);
|
|
948
|
+
const frec = ft ? this.files.get(ft.file) : undefined;
|
|
949
|
+
if (!ft || !frec)
|
|
950
|
+
continue;
|
|
951
|
+
const type = ft.fieldTypes.get(field);
|
|
952
|
+
if (type)
|
|
953
|
+
return { text: type, owner: ft };
|
|
954
|
+
for (const sup of ft.superNames) {
|
|
955
|
+
const s = this.resolveTypeName(sup, frec);
|
|
956
|
+
if (s)
|
|
957
|
+
queue.push(s);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* A member's written type as seen from a receiver of type `from`: a type
|
|
964
|
+
* parameter of the declaring type is replaced by the argument the
|
|
965
|
+
* superclass chain from `from` up to it supplies — `Loan extends
|
|
966
|
+
* Base<Long>` makes `Base<T>.getId()`'s `T` a `Long`. Returns the text and
|
|
967
|
+
* the file it must be resolved in, or nothing when a type parameter is
|
|
968
|
+
* left unbound.
|
|
969
|
+
*/
|
|
970
|
+
substitute(written, owner, from) {
|
|
971
|
+
const ownRec = this.files.get(owner.file);
|
|
972
|
+
const bare = eraseType(written).replace(/(\[\])+$/, '');
|
|
973
|
+
if (!owner.typeParams.includes(bare))
|
|
974
|
+
return { text: written, rec: ownRec };
|
|
975
|
+
if (!from || from === owner.fqn)
|
|
976
|
+
return undefined;
|
|
977
|
+
const queue = [{ fqn: from, env: new Map() }];
|
|
978
|
+
const seen = new Set();
|
|
979
|
+
while (queue.length > 0) {
|
|
980
|
+
const { fqn, env } = queue.shift();
|
|
981
|
+
if (fqn === owner.fqn) {
|
|
982
|
+
const bound = env.get(bare);
|
|
983
|
+
if (!bound)
|
|
984
|
+
return undefined;
|
|
985
|
+
const dims = written.slice(written.lastIndexOf(bare) + bare.length).match(/\[\]/g)?.length ?? 0;
|
|
986
|
+
return { text: `${bound.text}${'[]'.repeat(dims)}`, rec: bound.rec };
|
|
987
|
+
}
|
|
988
|
+
if (seen.has(fqn))
|
|
989
|
+
continue;
|
|
990
|
+
seen.add(fqn);
|
|
991
|
+
const t = this.typesByFqn.get(fqn);
|
|
992
|
+
const rec = t ? this.files.get(t.file) : undefined;
|
|
993
|
+
if (!t || !rec)
|
|
994
|
+
continue;
|
|
995
|
+
for (const sup of t.superNames) {
|
|
996
|
+
const supFqn = this.resolveTypeName(sup, rec);
|
|
997
|
+
const st = supFqn ? this.typesByFqn.get(supFqn) : undefined;
|
|
998
|
+
if (!st)
|
|
999
|
+
continue;
|
|
1000
|
+
const open = sup.indexOf('<');
|
|
1001
|
+
const args = open === -1 ? [] : splitTopLevel(sup.slice(open + 1, sup.lastIndexOf('>'))).map(a => a.trim());
|
|
1002
|
+
const next = new Map();
|
|
1003
|
+
st.typeParams.forEach((p, i) => {
|
|
1004
|
+
const a = args[i];
|
|
1005
|
+
if (!a)
|
|
1006
|
+
return;
|
|
1007
|
+
next.set(p, env.get(a) ?? { text: a, rec });
|
|
1008
|
+
});
|
|
1009
|
+
queue.push({ fqn: st.fqn, env: next });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
/** The outermost call named `name` that starts at `line:column` in `rec`. */
|
|
1015
|
+
callAt(rec, line, column, name) {
|
|
1016
|
+
let byPos = this.callsAt.get(rec.path);
|
|
1017
|
+
if (!byPos) {
|
|
1018
|
+
byPos = new Map();
|
|
1019
|
+
for (const c of rec.calls)
|
|
1020
|
+
push(byPos, `${c.location.line}:${c.location.column}`, c);
|
|
1021
|
+
this.callsAt.set(rec.path, byPos);
|
|
1022
|
+
}
|
|
1023
|
+
const here = (byPos.get(`${line}:${column}`) ?? []).filter(c => c.method_name === name);
|
|
1024
|
+
// `a.x().x()` puts two calls named `x` at one position; the outer one has
|
|
1025
|
+
// the longer receiver.
|
|
1026
|
+
return here.sort((x, y) => (y.receiver?.length ?? 0) - (x.receiver?.length ?? 0))[0];
|
|
1027
|
+
}
|
|
1028
|
+
/** A written type, resolved from one file's point of view. */
|
|
1029
|
+
typeRef(written, rec) {
|
|
1030
|
+
let t = written.replace(/@[\w.]+(\([^)]*\))?\s*/g, '').replace(/\bfinal\s+/g, '').trim();
|
|
1031
|
+
let dims = 0;
|
|
1032
|
+
if (t.endsWith('...')) {
|
|
1033
|
+
dims++;
|
|
1034
|
+
t = t.slice(0, -3).trim();
|
|
1035
|
+
}
|
|
1036
|
+
t = eraseType(t);
|
|
1037
|
+
while (t.endsWith('[]')) {
|
|
1038
|
+
dims++;
|
|
1039
|
+
t = t.slice(0, -2).trim();
|
|
1040
|
+
}
|
|
1041
|
+
const simple = t.split('.').pop() ?? t;
|
|
1042
|
+
if (PRIMITIVES.has(t))
|
|
1043
|
+
return { simple: t, fqn: t, dims, prim: true, typeVar: false };
|
|
1044
|
+
if (t === 'null')
|
|
1045
|
+
return { simple: 'null', fqn: null, dims: 0, prim: false, typeVar: false };
|
|
1046
|
+
let fqn = null;
|
|
1047
|
+
if (t.includes('.')) {
|
|
1048
|
+
fqn = (rec ? this.resolveTypeName(t, rec) : undefined) ?? t;
|
|
1049
|
+
}
|
|
1050
|
+
else if (rec) {
|
|
1051
|
+
// An import names the type outright, ours or not. Failing that, a JDK
|
|
1052
|
+
// name is the JDK's unless this package declares its own — the tree-wide
|
|
1053
|
+
// unique-simple-name fallback must not turn `Date` into a project type.
|
|
1054
|
+
const imported = rec.imports.get(t);
|
|
1055
|
+
if (imported)
|
|
1056
|
+
fqn = imported;
|
|
1057
|
+
else if ((JAVA_LANG.has(t) || t in JDK_SUPERS) && !this.typesByFqn.has(rec.pkg ? `${rec.pkg}.${t}` : t))
|
|
1058
|
+
fqn = null;
|
|
1059
|
+
else
|
|
1060
|
+
fqn = this.resolveTypeName(t, rec) ?? null;
|
|
1061
|
+
}
|
|
1062
|
+
if (!fqn && JAVA_LANG.has(t))
|
|
1063
|
+
fqn = `java.lang.${t}`;
|
|
1064
|
+
const typeVar = !fqn && /^[A-Z][A-Z0-9]?$/.test(t);
|
|
1065
|
+
return { simple, fqn, dims, prim: false, typeVar };
|
|
1066
|
+
}
|
|
1067
|
+
/** Whether overload `o` of `owner` is what a query's parameter list names. */
|
|
1068
|
+
signatureMatches(o, params, owner) {
|
|
1069
|
+
if (o.params.length !== params.length)
|
|
1070
|
+
return false;
|
|
1071
|
+
const rec = this.files.get(owner.file);
|
|
1072
|
+
return o.params.every((p, i) => {
|
|
1073
|
+
const want = this.typeRef(params[i], undefined);
|
|
1074
|
+
const have = this.typeRef(p, rec);
|
|
1075
|
+
if (want.dims !== have.dims)
|
|
1076
|
+
return false;
|
|
1077
|
+
const wantBare = eraseType(params[i].replace(/\.\.\.$/, '').replace(/(\[\])+$/, ''));
|
|
1078
|
+
if (wantBare.includes('.')) {
|
|
1079
|
+
if (have.fqn && !have.prim)
|
|
1080
|
+
return have.fqn === wantBare;
|
|
1081
|
+
return have.simple === want.simple;
|
|
1082
|
+
}
|
|
1083
|
+
return have.simple === want.simple;
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
/** Same name and same erased parameter types: one overrides the other. */
|
|
1087
|
+
sameErasure(a, b) {
|
|
1088
|
+
const pa = a.overload.params;
|
|
1089
|
+
const pb = b.overload.params;
|
|
1090
|
+
if (a.overload.name !== b.overload.name || pa.length !== pb.length)
|
|
1091
|
+
return false;
|
|
1092
|
+
const ra = this.files.get(a.owner.file);
|
|
1093
|
+
const rb = this.files.get(b.owner.file);
|
|
1094
|
+
return pa.every((p, i) => {
|
|
1095
|
+
const x = this.typeRef(p, ra);
|
|
1096
|
+
const y = this.typeRef(pb[i], rb);
|
|
1097
|
+
if (x.typeVar || y.typeVar)
|
|
1098
|
+
return x.dims === y.dims;
|
|
1099
|
+
return x.simple === y.simple && x.dims === y.dims;
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
// --------------------------------------------------------------- dispatch
|
|
1103
|
+
/**
|
|
1104
|
+
* How a site bound to `owner.overload` reaches `target`'s overload, if it
|
|
1105
|
+
* does: through a supertype's declaration the target overrides, or by
|
|
1106
|
+
* calling an override of the target.
|
|
1107
|
+
*/
|
|
1108
|
+
dispatchRelation(owner, overload, receiver, target, wanted) {
|
|
1109
|
+
if (owner === target || overload.isConstructor || overload.modifiers.includes('static'))
|
|
1110
|
+
return undefined;
|
|
1111
|
+
for (const w of wanted) {
|
|
1112
|
+
if (w.modifiers.includes('static') || w.modifiers.includes('private'))
|
|
1113
|
+
continue;
|
|
1114
|
+
if (!this.sameErasure({ owner, overload }, { owner: target, overload: w }))
|
|
1115
|
+
continue;
|
|
1116
|
+
if (this.isSubtype(target.fqn, owner.fqn)) {
|
|
1117
|
+
// The receiver's static type must admit the target's type, or the
|
|
1118
|
+
// object can never be one: a sibling's call does not land here.
|
|
1119
|
+
if (!receiver || this.isSubtype(target.fqn, receiver))
|
|
1120
|
+
return { relation: 'supertype', overload: w };
|
|
1121
|
+
}
|
|
1122
|
+
else if (this.isSubtype(owner.fqn, target.fqn)) {
|
|
1123
|
+
return { relation: 'override', overload: w };
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return undefined;
|
|
1127
|
+
}
|
|
1128
|
+
/** Whether calls to `o` on `t` can dispatch anywhere but `t`'s own body. */
|
|
1129
|
+
dispatchIsOpen(t, o) {
|
|
1130
|
+
if (o.isConstructor || o.modifiers.includes('static') || o.modifiers.includes('private'))
|
|
1131
|
+
return false;
|
|
1132
|
+
if (this.isBodyless(t, o))
|
|
1133
|
+
return true;
|
|
1134
|
+
const self = { owner: t, overload: o };
|
|
1135
|
+
// Not `visibleOverloads`: it leaves out exactly the declarations an
|
|
1136
|
+
// override hides, which are the ones that make dispatch open.
|
|
1137
|
+
const declaresAbove = (f, seen) => {
|
|
1138
|
+
if (seen.has(f))
|
|
1139
|
+
return false;
|
|
1140
|
+
seen.add(f);
|
|
1141
|
+
const st = this.typesByFqn.get(f);
|
|
1142
|
+
const rec = st ? this.files.get(st.file) : undefined;
|
|
1143
|
+
if (!st || !rec)
|
|
1144
|
+
return false;
|
|
1145
|
+
if (st !== t && (st.overloads.get(o.name) ?? []).some(x => this.sameErasure({ owner: st, overload: x }, self)))
|
|
1146
|
+
return true;
|
|
1147
|
+
return st.superNames.some(sup => {
|
|
1148
|
+
const s = this.resolveTypeName(sup, rec);
|
|
1149
|
+
return s !== undefined && declaresAbove(s, seen);
|
|
1150
|
+
});
|
|
1151
|
+
};
|
|
1152
|
+
if (declaresAbove(t.fqn, new Set()))
|
|
1153
|
+
return true;
|
|
1154
|
+
const seen = new Set();
|
|
1155
|
+
const below = (f) => {
|
|
1156
|
+
for (const sub of this.subtypes.get(f) ?? []) {
|
|
1157
|
+
if (seen.has(sub))
|
|
1158
|
+
continue;
|
|
1159
|
+
seen.add(sub);
|
|
1160
|
+
const st = this.typesByFqn.get(sub);
|
|
1161
|
+
if (st && (st.overloads.get(o.name) ?? []).some(x => this.sameErasure({ owner: st, overload: x }, self)))
|
|
1162
|
+
return true;
|
|
1163
|
+
if (below(sub))
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
return false;
|
|
1167
|
+
};
|
|
1168
|
+
return below(t.fqn);
|
|
1169
|
+
}
|
|
1170
|
+
/** `a` is `b` or a subtype of it, by the edges the tree holds. */
|
|
1171
|
+
isSubtype(a, b) {
|
|
1172
|
+
if (a === b)
|
|
1173
|
+
return true;
|
|
1174
|
+
const seen = new Set();
|
|
1175
|
+
const walk = (f) => {
|
|
1176
|
+
if (seen.has(f))
|
|
1177
|
+
return false;
|
|
1178
|
+
seen.add(f);
|
|
1179
|
+
const t = this.typesByFqn.get(f);
|
|
1180
|
+
const rec = t ? this.files.get(t.file) : undefined;
|
|
1181
|
+
if (!t || !rec)
|
|
1182
|
+
return false;
|
|
1183
|
+
for (const sup of t.superNames) {
|
|
1184
|
+
const s = this.resolveTypeName(sup, rec);
|
|
1185
|
+
if (!s)
|
|
1186
|
+
continue;
|
|
1187
|
+
if (s === b || walk(s))
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
return false;
|
|
1191
|
+
};
|
|
1192
|
+
return walk(a);
|
|
1193
|
+
}
|
|
1194
|
+
/** The query's scope, with what dispatch it cannot see said out loud. */
|
|
1195
|
+
dispatchScope(symbol) {
|
|
1196
|
+
return {
|
|
1197
|
+
searched: this.scope.searched,
|
|
1198
|
+
excluded: [
|
|
1199
|
+
...this.scope.excluded,
|
|
1200
|
+
{
|
|
1201
|
+
pattern: 'subtypes outside the searched tree',
|
|
1202
|
+
reason: `dispatch: an override of ${symbol}, or a call through a supertype, in a library or module not indexed here is not seen`,
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
pattern: 'reflection',
|
|
1206
|
+
reason: 'dynamic: Method.invoke, proxies and method handles are not followed',
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
pattern: 'framework dispatch',
|
|
1210
|
+
reason: 'a framework that invokes a method by annotation, configuration or naming convention leaves no call site to answer',
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
// ---------------------------------------------------------- receiver type
|
|
346
1216
|
/**
|
|
347
1217
|
* The receiver's static type, where it can be had without inventing one.
|
|
348
1218
|
*
|
|
@@ -392,7 +1262,8 @@ export class NavigationIndex {
|
|
|
392
1262
|
* `Builder.of().step("a")` is typed by resolving `Builder`, then taking
|
|
393
1263
|
* `of`'s declared return type, then `step`'s. A step whose return type is
|
|
394
1264
|
* unknown ends the walk and the whole expression is untyped — a half-walked
|
|
395
|
-
* chain is not a type.
|
|
1265
|
+
* chain is not a type. So does a step whose overloads of that arity
|
|
1266
|
+
* disagree on what they return.
|
|
396
1267
|
*/
|
|
397
1268
|
typeOfExpression(expr, rec, depth) {
|
|
398
1269
|
if (!expr || depth > 8)
|
|
@@ -425,27 +1296,37 @@ export class NavigationIndex {
|
|
|
425
1296
|
rec.staticWildcards.find(w => this.typesByFqn.get(w)?.methods.has(root.name)) ??
|
|
426
1297
|
rec.types.find(t => t.methods.has(root.name))?.fqn;
|
|
427
1298
|
const owner = ownerFqn ? this.typesByFqn.get(ownerFqn) : undefined;
|
|
428
|
-
const
|
|
429
|
-
if (!owner || !
|
|
1299
|
+
const returnType = owner ? this.returnTypeAt(owner.fqn, root.name, root.argc) : undefined;
|
|
1300
|
+
if (!owner || !returnType)
|
|
430
1301
|
return undefined;
|
|
431
|
-
current = this.resolveTypeRecord(
|
|
1302
|
+
current = this.resolveTypeRecord(returnType, rec);
|
|
432
1303
|
if (!current)
|
|
433
1304
|
return undefined;
|
|
434
|
-
evidence = `chain rooted at ${owner.fqn}.${root.name}, which returns ${
|
|
1305
|
+
evidence = `chain rooted at ${owner.fqn}.${root.name}, which returns ${returnType}`;
|
|
435
1306
|
}
|
|
436
1307
|
for (const step of chain.steps) {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
if (!m || !m.returnType)
|
|
1308
|
+
const returnType = this.returnTypeAt(current.fqn, step.name, step.argc);
|
|
1309
|
+
if (!returnType)
|
|
440
1310
|
return undefined;
|
|
441
|
-
const next = this.resolveTypeRecord(
|
|
1311
|
+
const next = this.resolveTypeRecord(returnType, rec);
|
|
442
1312
|
if (!next)
|
|
443
1313
|
return undefined;
|
|
444
|
-
evidence += ` → ${step.name}() returns ${
|
|
1314
|
+
evidence += ` → ${step.name}() returns ${returnType}`;
|
|
445
1315
|
current = next;
|
|
446
1316
|
}
|
|
447
1317
|
return { kind: 'project', fqn: current.fqn, evidence };
|
|
448
1318
|
}
|
|
1319
|
+
/**
|
|
1320
|
+
* The return type of `name` called with `argc` arguments on `fqn`, when
|
|
1321
|
+
* every overload of that arity agrees on it.
|
|
1322
|
+
*/
|
|
1323
|
+
returnTypeAt(fqn, name, argc) {
|
|
1324
|
+
const cands = this.visibleOverloads(fqn, name).filter(c => fitsArity(c.overload, argc));
|
|
1325
|
+
const types = new Set(cands.map(c => c.overload.returnType));
|
|
1326
|
+
if (types.size !== 1)
|
|
1327
|
+
return undefined;
|
|
1328
|
+
return [...types][0] ?? undefined;
|
|
1329
|
+
}
|
|
449
1330
|
/** The owner named by a static import of this call's method. */
|
|
450
1331
|
staticOwnerFor(call, rec) {
|
|
451
1332
|
if (call.receiver !== null)
|
|
@@ -475,6 +1356,21 @@ export class NavigationIndex {
|
|
|
475
1356
|
}
|
|
476
1357
|
return best ?? rec.types[0];
|
|
477
1358
|
}
|
|
1359
|
+
/** The method whose body holds `line`, with a parameter list when its name is overloaded. */
|
|
1360
|
+
methodAt(rec, line) {
|
|
1361
|
+
const t = this.enclosingTypeAt(rec, line);
|
|
1362
|
+
if (!t)
|
|
1363
|
+
return undefined;
|
|
1364
|
+
for (const [name, list] of t.overloads) {
|
|
1365
|
+
for (const o of list) {
|
|
1366
|
+
if (line < o.startLine || line > o.endLine)
|
|
1367
|
+
continue;
|
|
1368
|
+
const member = o.isConstructor ? `${t.fqn}.<init>` : `${t.fqn}.${name}`;
|
|
1369
|
+
return list.length > 1 ? `${member}${signatureOf(o)}` : member;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return undefined;
|
|
1373
|
+
}
|
|
478
1374
|
/** Find `method` on `fqn` or on a supertype of it, inside the tree. */
|
|
479
1375
|
lookupThroughHierarchy(fqn, method, seen = new Set()) {
|
|
480
1376
|
if (seen.has(fqn))
|
|
@@ -510,24 +1406,21 @@ export class NavigationIndex {
|
|
|
510
1406
|
* That is the conservative direction: it costs an answer its `exact` label
|
|
511
1407
|
* and never grants one.
|
|
512
1408
|
*/
|
|
513
|
-
isBodyless(owner,
|
|
514
|
-
|
|
515
|
-
if (!m)
|
|
516
|
-
return false;
|
|
517
|
-
if (m.modifiers.includes('abstract'))
|
|
1409
|
+
isBodyless(owner, o) {
|
|
1410
|
+
if (o.modifiers.includes('abstract'))
|
|
518
1411
|
return true;
|
|
519
1412
|
if (owner.kind !== 'interface')
|
|
520
1413
|
return false;
|
|
521
|
-
if (
|
|
1414
|
+
if (o.modifiers.includes('static') || o.modifiers.includes('default'))
|
|
522
1415
|
return false;
|
|
523
|
-
const line = this.files.get(owner.file)?.lines?.[
|
|
1416
|
+
const line = this.files.get(owner.file)?.lines?.[o.startLine - 1];
|
|
524
1417
|
if (line === undefined)
|
|
525
1418
|
return true;
|
|
526
1419
|
return !/\b(default|static)\b/.test(line);
|
|
527
1420
|
}
|
|
528
1421
|
/**
|
|
529
|
-
* Types in the tree that declare a **runnable** `
|
|
530
|
-
* below it.
|
|
1422
|
+
* Types in the tree that declare a **runnable** override of `o` and are
|
|
1423
|
+
* `fqn` or below it.
|
|
531
1424
|
*
|
|
532
1425
|
* "Runnable" is the whole of it: `candidates` answers "which body could
|
|
533
1426
|
* run", so an abstract declaration does not belong there even when it is
|
|
@@ -535,15 +1428,19 @@ export class NavigationIndex {
|
|
|
535
1428
|
* with nothing overriding `step` has no candidates, and saying so is the
|
|
536
1429
|
* honest answer — listing `Base.step` would name a body that does not exist.
|
|
537
1430
|
*/
|
|
538
|
-
implementorsWith(fqn,
|
|
1431
|
+
implementorsWith(fqn, o) {
|
|
539
1432
|
const out = [];
|
|
1433
|
+
const start = this.typesByFqn.get(fqn);
|
|
1434
|
+
const self = start ? { owner: start, overload: o } : undefined;
|
|
540
1435
|
const walk = (f, seen) => {
|
|
541
1436
|
if (seen.has(f))
|
|
542
1437
|
return;
|
|
543
1438
|
seen.add(f);
|
|
544
1439
|
const t = this.typesByFqn.get(f);
|
|
545
|
-
if (t && t.
|
|
546
|
-
|
|
1440
|
+
if (t && t.kind !== 'interface' && self) {
|
|
1441
|
+
const own = (t.overloads.get(o.name) ?? []).find(x => !x.isConstructor && this.sameErasure({ owner: t, overload: x }, self));
|
|
1442
|
+
if (own && !this.isBodyless(t, own))
|
|
1443
|
+
out.push(f);
|
|
547
1444
|
}
|
|
548
1445
|
for (const sub of this.subtypes.get(f) ?? [])
|
|
549
1446
|
walk(sub, seen);
|
|
@@ -702,13 +1599,11 @@ export class NavigationIndex {
|
|
|
702
1599
|
}
|
|
703
1600
|
// -------------------------------------------------------------- plumbing
|
|
704
1601
|
/** A site whose method name could be the one asked for. */
|
|
705
|
-
nameCouldMatch(call, wantedName,
|
|
1602
|
+
nameCouldMatch(call, wantedName, owner) {
|
|
706
1603
|
if (wantedName === '<init>') {
|
|
707
|
-
const
|
|
708
|
-
const t = this.typesByFqn.get(typeFqn);
|
|
1604
|
+
const t = this.typesByFqn.get(owner);
|
|
709
1605
|
return !!t && (call.is_constructor === true || call.method_name === t.simpleName);
|
|
710
1606
|
}
|
|
711
|
-
void rec;
|
|
712
1607
|
return call.method_name === wantedName;
|
|
713
1608
|
}
|
|
714
1609
|
querySymbol(query, kind) {
|
|
@@ -729,17 +1624,9 @@ export class NavigationIndex {
|
|
|
729
1624
|
if (kind === 'callers') {
|
|
730
1625
|
if (!call)
|
|
731
1626
|
return undefined;
|
|
732
|
-
return this.
|
|
733
|
-
}
|
|
734
|
-
const t = this.enclosingTypeAt(rec, query.site.line);
|
|
735
|
-
if (!t)
|
|
736
|
-
return undefined;
|
|
737
|
-
for (const [name, m] of t.methods) {
|
|
738
|
-
if (query.site.line >= m.startLine && query.site.line <= m.endLine) {
|
|
739
|
-
return `${t.fqn}.${name}`;
|
|
740
|
-
}
|
|
1627
|
+
return this.symbolOfCall(call, rec);
|
|
741
1628
|
}
|
|
742
|
-
return
|
|
1629
|
+
return this.methodAt(rec, query.site.line);
|
|
743
1630
|
}
|
|
744
1631
|
siteOf(call, rec) {
|
|
745
1632
|
const site = {
|
|
@@ -753,11 +1640,16 @@ export class NavigationIndex {
|
|
|
753
1640
|
site.text = text.trim();
|
|
754
1641
|
return site;
|
|
755
1642
|
}
|
|
756
|
-
finish(kind, query, answers, unresolved, t0, opts) {
|
|
1643
|
+
finish(kind, query, answers, unresolved, t0, opts, extra = {}, scope = this.scope) {
|
|
757
1644
|
let kept = answers;
|
|
758
1645
|
if (opts.tiers)
|
|
759
1646
|
kept = kept.filter(a => opts.tiers.includes(a.tier));
|
|
760
|
-
|
|
1647
|
+
// Answers to an overloaded method arrive grouped per overload; the order
|
|
1648
|
+
// of the groups is the order of `overloads`, i.e. of declaration.
|
|
1649
|
+
const group = new Map((extra.overloads ?? []).map((o, i) => [o.signature, i]));
|
|
1650
|
+
kept = [...kept].sort((a, b) => (group.get(a.signature ?? '') ?? 0) - (group.get(b.signature ?? '') ?? 0) ||
|
|
1651
|
+
a.site.file.localeCompare(b.site.file) ||
|
|
1652
|
+
a.site.line - b.site.line);
|
|
761
1653
|
const total = kept.length;
|
|
762
1654
|
let truncated;
|
|
763
1655
|
if (opts.limit !== undefined && total > opts.limit) {
|
|
@@ -768,7 +1660,7 @@ export class NavigationIndex {
|
|
|
768
1660
|
query: { kind, ...(query.symbol ? { symbol: query.symbol } : { site: query.site }) },
|
|
769
1661
|
answers: kept,
|
|
770
1662
|
unresolved: [...unresolved].sort((a, b) => a.site.file.localeCompare(b.site.file) || a.site.line - b.site.line),
|
|
771
|
-
scope
|
|
1663
|
+
scope,
|
|
772
1664
|
timing: {
|
|
773
1665
|
parseMs: round(this.parseMs),
|
|
774
1666
|
indexMs: round(this.indexMs),
|
|
@@ -777,6 +1669,10 @@ export class NavigationIndex {
|
|
|
777
1669
|
};
|
|
778
1670
|
if (truncated)
|
|
779
1671
|
out.truncated = truncated;
|
|
1672
|
+
if (extra.overloads)
|
|
1673
|
+
out.overloads = extra.overloads;
|
|
1674
|
+
if (extra.note)
|
|
1675
|
+
out.note = extra.note;
|
|
780
1676
|
return out;
|
|
781
1677
|
}
|
|
782
1678
|
}
|
|
@@ -808,6 +1704,7 @@ function buildFileRecord(f) {
|
|
|
808
1704
|
const tpkg = t.package ?? pkg;
|
|
809
1705
|
const path = [tpkg, t.enclosing_type, t.name].filter(Boolean).join('.');
|
|
810
1706
|
const methods = new Map();
|
|
1707
|
+
const overloads = new Map();
|
|
811
1708
|
for (const m of t.methods ?? []) {
|
|
812
1709
|
// A later overload does not replace an earlier one's range; the widest
|
|
813
1710
|
// range wins, so a callee query over the method's body sees all of it.
|
|
@@ -818,6 +1715,20 @@ function buildFileRecord(f) {
|
|
|
818
1715
|
endLine: prev ? Math.max(prev.endLine, m.end_line) : m.end_line,
|
|
819
1716
|
modifiers: [...new Set([...(prev?.modifiers ?? []), ...(m.modifiers ?? [])])],
|
|
820
1717
|
});
|
|
1718
|
+
const params = (m.parameters ?? []).map(p => p.type ?? '?');
|
|
1719
|
+
// The extraction reports a constructor's return type as `null` and a
|
|
1720
|
+
// method's as at least `void`; a constructor is named after its type.
|
|
1721
|
+
const isConstructor = m.return_type === null && m.name === t.name;
|
|
1722
|
+
push(overloads, m.name, {
|
|
1723
|
+
name: m.name,
|
|
1724
|
+
params,
|
|
1725
|
+
varargs: params.length > 0 && params[params.length - 1].endsWith('...'),
|
|
1726
|
+
returnType: m.return_type ?? null,
|
|
1727
|
+
isConstructor,
|
|
1728
|
+
startLine: m.start_line,
|
|
1729
|
+
endLine: m.end_line,
|
|
1730
|
+
modifiers: m.modifiers ?? [],
|
|
1731
|
+
});
|
|
821
1732
|
}
|
|
822
1733
|
types.push({
|
|
823
1734
|
fqn: path,
|
|
@@ -826,11 +1737,15 @@ function buildFileRecord(f) {
|
|
|
826
1737
|
isRecord: t.is_record === true,
|
|
827
1738
|
file: f.path,
|
|
828
1739
|
superNames: [t.extends, ...(t.implements ?? [])].filter((x) => !!x),
|
|
1740
|
+
extendsName: t.extends ?? null,
|
|
829
1741
|
annotations: (t.annotations ?? []).map(stripAnnotation),
|
|
830
1742
|
fieldNames: (t.fields ?? []).map(x => x.name),
|
|
1743
|
+
fieldTypes: new Map((t.fields ?? []).filter(x => x.type).map(x => [x.name, x.type])),
|
|
1744
|
+
typeParams: t.type_parameters ?? [],
|
|
831
1745
|
getterFields: lombokFields(f.source, 'Getter'),
|
|
832
1746
|
setterFields: lombokFields(f.source, 'Setter'),
|
|
833
1747
|
methods,
|
|
1748
|
+
overloads,
|
|
834
1749
|
startLine: t.start_line,
|
|
835
1750
|
endLine: t.end_line,
|
|
836
1751
|
});
|
|
@@ -858,6 +1773,140 @@ function lombokFields(source, kind) {
|
|
|
858
1773
|
function stripAnnotation(a) {
|
|
859
1774
|
return a.replace(/^@/, '').replace(/\(.*$/, '').split('.').pop() ?? a;
|
|
860
1775
|
}
|
|
1776
|
+
/** `pkg.Type.method(A,B)` → its parts. The parameter list may hold generics with commas. */
|
|
1777
|
+
function parseSymbol(symbol) {
|
|
1778
|
+
const open = symbol.indexOf('(');
|
|
1779
|
+
const base = (open === -1 ? symbol : symbol.slice(0, open)).trim();
|
|
1780
|
+
const out = { base, owner: stripLast(base), name: lastSegment(base) };
|
|
1781
|
+
if (open !== -1) {
|
|
1782
|
+
const close = symbol.lastIndexOf(')');
|
|
1783
|
+
out.params = splitTopLevel(symbol.slice(open + 1, close === -1 ? undefined : close))
|
|
1784
|
+
.map(p => p.trim())
|
|
1785
|
+
.filter(Boolean);
|
|
1786
|
+
}
|
|
1787
|
+
return out;
|
|
1788
|
+
}
|
|
1789
|
+
/** Split on commas outside `<...>`. */
|
|
1790
|
+
function splitTopLevel(text) {
|
|
1791
|
+
const out = [];
|
|
1792
|
+
let depth = 0;
|
|
1793
|
+
let buf = '';
|
|
1794
|
+
for (const ch of text) {
|
|
1795
|
+
if (ch === '<')
|
|
1796
|
+
depth++;
|
|
1797
|
+
if (ch === '>')
|
|
1798
|
+
depth--;
|
|
1799
|
+
if (ch === ',' && depth === 0) {
|
|
1800
|
+
out.push(buf);
|
|
1801
|
+
buf = '';
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
buf += ch;
|
|
1805
|
+
}
|
|
1806
|
+
out.push(buf);
|
|
1807
|
+
return out;
|
|
1808
|
+
}
|
|
1809
|
+
/** `(Long,String)` — simple names, generics erased, `String...` for varargs. */
|
|
1810
|
+
function signatureOf(o) {
|
|
1811
|
+
return `(${o.params.map(p => {
|
|
1812
|
+
const varargs = p.endsWith('...');
|
|
1813
|
+
let t = eraseType(p.replace(/@[\w.]+(\([^)]*\))?\s*/g, '').replace(/\bfinal\s+/g, '').replace(/\.\.\.$/, '').trim());
|
|
1814
|
+
let dims = '';
|
|
1815
|
+
while (t.endsWith('[]')) {
|
|
1816
|
+
dims += '[]';
|
|
1817
|
+
t = t.slice(0, -2).trim();
|
|
1818
|
+
}
|
|
1819
|
+
return `${t.split('.').pop() ?? t}${dims}${varargs ? '...' : ''}`;
|
|
1820
|
+
}).join(',')})`;
|
|
1821
|
+
}
|
|
1822
|
+
/** `pkg.Type.method`, or `pkg.Type.<init>` for a constructor. */
|
|
1823
|
+
function memberName(owner, o) {
|
|
1824
|
+
return o.isConstructor ? `${owner.fqn}.<init>` : `${owner.fqn}.${o.name}`;
|
|
1825
|
+
}
|
|
1826
|
+
/** Remove every `<...>` from a written type. */
|
|
1827
|
+
function eraseType(t) {
|
|
1828
|
+
let out = '';
|
|
1829
|
+
let depth = 0;
|
|
1830
|
+
for (const ch of t) {
|
|
1831
|
+
if (ch === '<') {
|
|
1832
|
+
depth++;
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
if (ch === '>') {
|
|
1836
|
+
depth--;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (depth === 0)
|
|
1840
|
+
out += ch;
|
|
1841
|
+
}
|
|
1842
|
+
return out.trim();
|
|
1843
|
+
}
|
|
1844
|
+
/** Whether a call with `n` arguments can reach overload `o` at all. */
|
|
1845
|
+
function fitsArity(o, n) {
|
|
1846
|
+
if (o.params.length === n)
|
|
1847
|
+
return true;
|
|
1848
|
+
return o.varargs && n >= o.params.length - 1;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* The name of the outermost call an expression is, or nothing when it is not
|
|
1852
|
+
* a call: `a.b(x).c(y)` → `c`, `make(x)` → `make`, `a.<T>m()` → `m`.
|
|
1853
|
+
*/
|
|
1854
|
+
function outerCallName(expr) {
|
|
1855
|
+
const text = expr.trim();
|
|
1856
|
+
if (!text.endsWith(')'))
|
|
1857
|
+
return undefined;
|
|
1858
|
+
let depth = 0;
|
|
1859
|
+
let open = -1;
|
|
1860
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
1861
|
+
const ch = text[i];
|
|
1862
|
+
if (ch === ')')
|
|
1863
|
+
depth++;
|
|
1864
|
+
else if (ch === '(') {
|
|
1865
|
+
depth--;
|
|
1866
|
+
if (depth === 0) {
|
|
1867
|
+
open = i;
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (open <= 0)
|
|
1873
|
+
return undefined;
|
|
1874
|
+
const m = /([A-Za-z_$][\w$]*)\s*$/.exec(text.slice(0, open));
|
|
1875
|
+
if (!m)
|
|
1876
|
+
return undefined;
|
|
1877
|
+
const before = text.slice(0, open - m[0].length).trimEnd();
|
|
1878
|
+
// `new Foo(…)` is a constructor, typed by the extractor, not here.
|
|
1879
|
+
if (/\bnew$/.test(before))
|
|
1880
|
+
return undefined;
|
|
1881
|
+
return m[1];
|
|
1882
|
+
}
|
|
1883
|
+
/** Top-level arguments in an argument list's text, counted. */
|
|
1884
|
+
function countArgs(argText) {
|
|
1885
|
+
if (argText.trim() === '')
|
|
1886
|
+
return 0;
|
|
1887
|
+
let depth = 0;
|
|
1888
|
+
let quote = null;
|
|
1889
|
+
let n = 1;
|
|
1890
|
+
for (let i = 0; i < argText.length; i++) {
|
|
1891
|
+
const ch = argText[i];
|
|
1892
|
+
if (quote) {
|
|
1893
|
+
if (ch === quote && argText[i - 1] !== '\\')
|
|
1894
|
+
quote = null;
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (ch === '"' || ch === "'") {
|
|
1898
|
+
quote = ch;
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
if (ch === '(' || ch === '[' || ch === '{' || ch === '<')
|
|
1902
|
+
depth++;
|
|
1903
|
+
else if (ch === ')' || ch === ']' || ch === '}' || ch === '>')
|
|
1904
|
+
depth--;
|
|
1905
|
+
else if (ch === ',' && depth === 0)
|
|
1906
|
+
n++;
|
|
1907
|
+
}
|
|
1908
|
+
return n;
|
|
1909
|
+
}
|
|
861
1910
|
/**
|
|
862
1911
|
* Split a receiver expression into a root and a list of `.name(...)` steps.
|
|
863
1912
|
*
|
|
@@ -899,14 +1948,19 @@ function splitCallChain(text) {
|
|
|
899
1948
|
parts.push(buf);
|
|
900
1949
|
if (parts.length === 0)
|
|
901
1950
|
return null;
|
|
1951
|
+
const argcOf = (p) => {
|
|
1952
|
+
const open = p.indexOf('(');
|
|
1953
|
+
const close = p.lastIndexOf(')');
|
|
1954
|
+
return open === -1 || close < open ? 0 : countArgs(p.slice(open + 1, close));
|
|
1955
|
+
};
|
|
902
1956
|
const head = parts[0].trim();
|
|
903
1957
|
let root;
|
|
904
1958
|
if (head === 'this')
|
|
905
|
-
root = { kind: 'this', name: 'this' };
|
|
1959
|
+
root = { kind: 'this', name: 'this', argc: 0 };
|
|
906
1960
|
else if (/^[A-Z]\w*$/.test(head))
|
|
907
|
-
root = { kind: 'type', name: head };
|
|
1961
|
+
root = { kind: 'type', name: head, argc: 0 };
|
|
908
1962
|
else if (/^[a-zA-Z_]\w*\s*\(/.test(head))
|
|
909
|
-
root = { kind: 'call', name: head.slice(0, head.indexOf('(')).trim() };
|
|
1963
|
+
root = { kind: 'call', name: head.slice(0, head.indexOf('(')).trim(), argc: argcOf(head) };
|
|
910
1964
|
else
|
|
911
1965
|
return null; // a plain identifier, an index, an expression — not typed here
|
|
912
1966
|
const steps = [];
|
|
@@ -915,7 +1969,7 @@ function splitCallChain(text) {
|
|
|
915
1969
|
const m = /^([a-zA-Z_]\w*)\s*\(/.exec(p);
|
|
916
1970
|
if (!m)
|
|
917
1971
|
return null; // a field access mid-chain is not walked
|
|
918
|
-
steps.push({ name: m[1], line: 0 });
|
|
1972
|
+
steps.push({ name: m[1], line: 0, argc: argcOf(p) });
|
|
919
1973
|
}
|
|
920
1974
|
return { root, steps };
|
|
921
1975
|
}
|