gitnexus 1.6.6-rc.44 → 1.6.6-rc.46
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/dist/core/ingestion/languages/kotlin/captures.d.ts +2 -2
- package/dist/core/ingestion/languages/kotlin/captures.js +347 -14
- package/dist/core/ingestion/languages/kotlin/companion-scopes.d.ts +7 -0
- package/dist/core/ingestion/languages/kotlin/companion-scopes.js +56 -0
- package/dist/core/ingestion/languages/kotlin/owners.d.ts +2 -1
- package/dist/core/ingestion/languages/kotlin/owners.js +60 -4
- package/dist/core/ingestion/languages/kotlin/query.js +33 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.d.ts +31 -21
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +47 -21
- package/dist/core/ingestion/languages/kotlin/simple-hooks.js +11 -0
- package/dist/core/ingestion/registry-primary-flag.js +1 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +37 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +195 -7
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare function emitKotlinScopeCaptures(sourceText: string,
|
|
1
|
+
import { type CaptureMatch } from '../../../../_shared/index.js';
|
|
2
|
+
export declare function emitKotlinScopeCaptures(sourceText: string, filePath: string, cachedTree?: unknown): readonly CaptureMatch[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { makeScopeId } from '../../../../_shared/index.js';
|
|
1
2
|
import { findNodeAtRange, nodeToCapture, syntheticCapture, } from '../../utils/ast-helpers.js';
|
|
2
3
|
import { getTreeSitterBufferSize } from '../../constants.js';
|
|
3
4
|
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
@@ -7,8 +8,9 @@ import { recordKotlinCacheHit, recordKotlinCacheMiss } from './cache-stats.js';
|
|
|
7
8
|
import { normalizeKotlinType } from './interpret.js';
|
|
8
9
|
import { synthesizeKotlinReceiverBinding } from './receiver-binding.js';
|
|
9
10
|
import { getKotlinParser, getKotlinScopeQuery } from './query.js';
|
|
11
|
+
import { markCompanionScope } from './companion-scopes.js';
|
|
10
12
|
const FUNCTION_DECL_TAGS = ['@declaration.function'];
|
|
11
|
-
export function emitKotlinScopeCaptures(sourceText,
|
|
13
|
+
export function emitKotlinScopeCaptures(sourceText, filePath, cachedTree) {
|
|
12
14
|
let tree = cachedTree;
|
|
13
15
|
if (tree === undefined) {
|
|
14
16
|
tree = parseSourceSafe(getKotlinParser(), sourceText, undefined, {
|
|
@@ -24,6 +26,7 @@ export function emitKotlinScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
24
26
|
out.push(...synthesizeKotlinLocalAssignmentBindings(tree.rootNode, returnTypes));
|
|
25
27
|
out.push(...synthesizeKotlinLoopBindings(tree.rootNode, returnTypes));
|
|
26
28
|
out.push(...synthesizeKotlinSmartCastBindings(tree.rootNode));
|
|
29
|
+
out.push(...synthesizeKotlinLambdaBindings(tree.rootNode, returnTypes));
|
|
27
30
|
for (const match of getKotlinScopeQuery().matches(tree.rootNode)) {
|
|
28
31
|
const grouped = {};
|
|
29
32
|
for (const capture of match.captures) {
|
|
@@ -32,6 +35,26 @@ export function emitKotlinScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
32
35
|
}
|
|
33
36
|
if (Object.keys(grouped).length === 0)
|
|
34
37
|
continue;
|
|
38
|
+
// Companion-object marker (#1756 / U4). The `@scope.companion`
|
|
39
|
+
// capture is a side-channel marker — it shares its range with the
|
|
40
|
+
// existing `(companion_object) @scope.class` rule, so the Class
|
|
41
|
+
// scope already exists in the scope tree. Record the scope id into
|
|
42
|
+
// the per-file companion-scope set so `populateCompanionMembersOn
|
|
43
|
+
// EnclosingClass` (owners.ts) can identify companion scopes
|
|
44
|
+
// unambiguously, regardless of whether they are anonymous, named,
|
|
45
|
+
// or contain nested classes. The match itself is consumed here and
|
|
46
|
+
// NOT pushed to the output — the scope-extractor would reject the
|
|
47
|
+
// `companion` kind suffix anyway, but suppressing the emit keeps
|
|
48
|
+
// downstream pipelines from re-processing the same range twice.
|
|
49
|
+
if (grouped['@scope.companion'] !== undefined) {
|
|
50
|
+
const scopeId = makeScopeId({
|
|
51
|
+
filePath,
|
|
52
|
+
range: grouped['@scope.companion'].range,
|
|
53
|
+
kind: 'Class',
|
|
54
|
+
});
|
|
55
|
+
markCompanionScope(filePath, scopeId);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
35
58
|
if (grouped['@import.statement'] !== undefined) {
|
|
36
59
|
const importNode = findNodeAtRange(tree.rootNode, grouped['@import.statement'].range, 'import_header');
|
|
37
60
|
if (importNode !== null) {
|
|
@@ -234,6 +257,286 @@ function buildNarrowedTypeBindingCapture(subject, bodyAnchor, typeNode) {
|
|
|
234
257
|
'@type-binding.narrowed': syntheticCapture('@type-binding.narrowed', bodyAnchor, '1'),
|
|
235
258
|
};
|
|
236
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Synthesize lambda-body type-bindings — issue #1757.
|
|
262
|
+
*
|
|
263
|
+
* For each `lambda_literal` we emit one or more `@type-binding.annotation`
|
|
264
|
+
* captures anchored INSIDE the lambda body (the lambda's `statements` child
|
|
265
|
+
* — or the `lambda_literal` itself when no statements child exists). The
|
|
266
|
+
* `@scope.block` query rule (see query.ts) makes each `lambda_literal` a
|
|
267
|
+
* Block scope, and the `@type-binding.lambda-scoped` marker forces the
|
|
268
|
+
* scope-extractor to keep the binding at the innermost (lambda body) scope
|
|
269
|
+
* via `kotlinBindingScopeFor`. This guarantees:
|
|
270
|
+
* - explicit parameter names (`{ user -> ... }`) bind only inside the
|
|
271
|
+
* body, NOT in the enclosing function scope;
|
|
272
|
+
* - implicit `it` is visible only inside the lambda body and shadows
|
|
273
|
+
* any same-named outer binding (`val it = "outer"; users.forEach
|
|
274
|
+
* { it.save() }` — inner `it` is the lambda parameter);
|
|
275
|
+
* - nested lambdas shadow deterministically (innermost lambda's `it`
|
|
276
|
+
* wins; outer lambda's parameters are still visible by their own
|
|
277
|
+
* names through the parent scope chain).
|
|
278
|
+
*
|
|
279
|
+
* Receiver-type inference is best-effort: the lambda's call-expression
|
|
280
|
+
* parent is inspected; if the receiver has a known local-variable type
|
|
281
|
+
* and the call's member is a well-known stdlib idiom (`forEach`/`map`/
|
|
282
|
+
* `filter` → element type of the collection; `let`/`apply`/`also`/`run`/
|
|
283
|
+
* `takeIf`/`takeUnless`/`use` → receiver type itself), the inferred type
|
|
284
|
+
* is attached. When inference fails (chained receivers, unknown member,
|
|
285
|
+
* non-stdlib idiom), we still emit the binding with a sentinel/erased
|
|
286
|
+
* type so the binding's scope semantics (no leak; no `it` cross-fire) are
|
|
287
|
+
* enforced — call-resolution from the body still falls through to free-
|
|
288
|
+
* call fallback, which is the correct behavior when the type is unknown.
|
|
289
|
+
*
|
|
290
|
+
* Standard-library coverage: `forEach`, `map`, `filter`, `flatMap`,
|
|
291
|
+
* `mapNotNull`, `filterNotNull`, `onEach`, `find`, `firstOrNull`,
|
|
292
|
+
* `lastOrNull`, `any`, `all`, `none`, `count`, `forEachIndexed`,
|
|
293
|
+
* `let`, `apply`, `also`, `run`, `takeIf`, `takeUnless`, `use`, `with`.
|
|
294
|
+
*
|
|
295
|
+
* Lambda-receiver typing for non-stdlib higher-order functions is a
|
|
296
|
+
* follow-up; the binding-existence guarantee above is the minimum
|
|
297
|
+
* acceptance criterion per the U9 plan.
|
|
298
|
+
*/
|
|
299
|
+
function synthesizeKotlinLambdaBindings(rootNode, returnTypes) {
|
|
300
|
+
const out = [];
|
|
301
|
+
const classMembers = collectKotlinClassMembers(rootNode);
|
|
302
|
+
for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
|
|
303
|
+
const localTypes = collectKotlinLocalTypeTexts(fnNode, returnTypes);
|
|
304
|
+
for (const lambdaNode of descendantsOfType(fnNode, 'lambda_literal')) {
|
|
305
|
+
const anchor = lambdaBodyAnchor(lambdaNode);
|
|
306
|
+
if (anchor === null)
|
|
307
|
+
continue;
|
|
308
|
+
const inferredType = inferKotlinLambdaReceiverType(lambdaNode, localTypes, returnTypes, classMembers);
|
|
309
|
+
const params = explicitLambdaParameters(lambdaNode);
|
|
310
|
+
if (params.length === 0) {
|
|
311
|
+
// No explicit `(x ->)` parameter list — implicit `it` is in
|
|
312
|
+
// scope inside the body. Synthesize the `it` type-binding so
|
|
313
|
+
// calls like `it.save()` resolve through the typeBinding chain.
|
|
314
|
+
const typeNode = inferredType?.typeNode ?? lambdaNode;
|
|
315
|
+
const typeText = inferredType?.typeText ?? '';
|
|
316
|
+
out.push(buildLambdaTypeBindingCapture(anchor, 'it', typeNode, typeText));
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Explicit parameters: `{ user -> ... }`, `{ (a, b) -> ... }`,
|
|
320
|
+
// `{ key, value -> ... }`. Emit one binding per parameter.
|
|
321
|
+
// For multi-arg lambdas (destructuring, `forEachIndexed { i, x
|
|
322
|
+
// -> ... }`), the per-arg type inference is finer than what we
|
|
323
|
+
// currently support — we bind the FIRST parameter to the
|
|
324
|
+
// inferred receiver type (matches single-arg idioms) and bind
|
|
325
|
+
// additional parameters with an empty/erased type, which still
|
|
326
|
+
// gates leakage but won't drive call resolution for those names.
|
|
327
|
+
for (let i = 0; i < params.length; i++) {
|
|
328
|
+
const paramName = params[i].text;
|
|
329
|
+
const typeNode = i === 0 ? (inferredType?.typeNode ?? params[i]) : params[i];
|
|
330
|
+
const typeText = i === 0 ? (inferredType?.typeText ?? '') : '';
|
|
331
|
+
out.push(buildLambdaTypeBindingCapture(anchor, paramName, typeNode, typeText));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
/** Anchor node used for synthesized lambda-body type-bindings.
|
|
339
|
+
* Prefers the `statements` child of `lambda_literal` (always strictly
|
|
340
|
+
* inside the lambda body, so the scope-extractor's `rangesEqual` auto-
|
|
341
|
+
* hoist check fails — the binding stays in the Block scope). Falls
|
|
342
|
+
* back to the lambda_literal itself when no statements child exists
|
|
343
|
+
* (e.g. empty lambda); the `@type-binding.lambda-scoped` marker in
|
|
344
|
+
* `kotlinBindingScopeFor` then forces no-hoist explicitly. */
|
|
345
|
+
function lambdaBodyAnchor(lambdaNode) {
|
|
346
|
+
const statements = lambdaNode.namedChildren.find((c) => c.type === 'statements');
|
|
347
|
+
return statements ?? lambdaNode;
|
|
348
|
+
}
|
|
349
|
+
/** Extract explicit lambda parameter `simple_identifier` nodes from a
|
|
350
|
+
* `lambda_literal`. Returns an empty array when no `lambda_parameters`
|
|
351
|
+
* is present (implicit `it` form). */
|
|
352
|
+
function explicitLambdaParameters(lambdaNode) {
|
|
353
|
+
const params = lambdaNode.namedChildren.find((c) => c.type === 'lambda_parameters');
|
|
354
|
+
if (params === undefined)
|
|
355
|
+
return [];
|
|
356
|
+
const out = [];
|
|
357
|
+
for (const child of params.namedChildren) {
|
|
358
|
+
if (child.type !== 'variable_declaration')
|
|
359
|
+
continue;
|
|
360
|
+
const ident = child.namedChildren.find((c) => c.type === 'simple_identifier');
|
|
361
|
+
if (ident !== undefined)
|
|
362
|
+
out.push(ident);
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
function buildLambdaTypeBindingCapture(anchor, name, typeNode, typeText) {
|
|
367
|
+
return {
|
|
368
|
+
'@type-binding.annotation': nodeToCapture('@type-binding.annotation', anchor),
|
|
369
|
+
'@type-binding.name': syntheticCapture('@type-binding.name', anchor, name),
|
|
370
|
+
'@type-binding.type': syntheticCapture('@type-binding.type', typeNode, typeText === '' ? '' : normalizeKotlinType(typeText)),
|
|
371
|
+
// Marker consumed by `kotlinBindingScopeFor` (simple-hooks.ts) to
|
|
372
|
+
// pin this binding inside the lambda Block scope — without it the
|
|
373
|
+
// scope-extractor would auto-hoist the binding to the enclosing
|
|
374
|
+
// function scope and `it` (or the lambda parameter name) would
|
|
375
|
+
// leak past the closing brace.
|
|
376
|
+
'@type-binding.lambda-scoped': syntheticCapture('@type-binding.lambda-scoped', anchor, '1'),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/** Stdlib higher-order functions whose lambda parameter receives the
|
|
380
|
+
* ELEMENT type of the receiver collection (Map / Iterable element). */
|
|
381
|
+
const KOTLIN_ELEMENT_TYPE_LAMBDAS = new Set([
|
|
382
|
+
'forEach',
|
|
383
|
+
'forEachIndexed',
|
|
384
|
+
'map',
|
|
385
|
+
'mapNotNull',
|
|
386
|
+
'mapIndexed',
|
|
387
|
+
'filter',
|
|
388
|
+
'filterNot',
|
|
389
|
+
'filterNotNull',
|
|
390
|
+
'filterIsInstance',
|
|
391
|
+
'flatMap',
|
|
392
|
+
'flatten',
|
|
393
|
+
'onEach',
|
|
394
|
+
'find',
|
|
395
|
+
'findLast',
|
|
396
|
+
'firstOrNull',
|
|
397
|
+
'lastOrNull',
|
|
398
|
+
'singleOrNull',
|
|
399
|
+
'any',
|
|
400
|
+
'all',
|
|
401
|
+
'none',
|
|
402
|
+
'count',
|
|
403
|
+
'partition',
|
|
404
|
+
'sortedBy',
|
|
405
|
+
'sortedByDescending',
|
|
406
|
+
'groupBy',
|
|
407
|
+
'associate',
|
|
408
|
+
'associateBy',
|
|
409
|
+
'associateWith',
|
|
410
|
+
'minByOrNull',
|
|
411
|
+
'maxByOrNull',
|
|
412
|
+
'sumOf',
|
|
413
|
+
'distinctBy',
|
|
414
|
+
]);
|
|
415
|
+
/** Stdlib scope functions whose lambda receives the RECEIVER itself as
|
|
416
|
+
* `it` (or as `this` for `apply`/`run`/`with`). For the binding-
|
|
417
|
+
* existence guarantee we treat both forms the same way — `it` binds
|
|
418
|
+
* to the receiver type; `apply`/`run`/`with` callers see free calls
|
|
419
|
+
* inside the body which fall through to free-call resolution against
|
|
420
|
+
* the enclosing scope (no `this`-aware dispatch yet — follow-up). */
|
|
421
|
+
const KOTLIN_SCOPE_FUNCTION_LAMBDAS = new Set(['let', 'also', 'takeIf', 'takeUnless', 'use']);
|
|
422
|
+
/** `apply`, `run`, `with` expose the receiver as `this` rather than
|
|
423
|
+
* `it`. We still synthesize an `it` binding because the lambda may
|
|
424
|
+
* reference the receiver elsewhere — but the more common usage
|
|
425
|
+
* (`user.apply { save() }`) goes through free-call resolution on the
|
|
426
|
+
* body, not through `it`. Including these here keeps the binding
|
|
427
|
+
* scope correct without claiming we resolve `this`-form correctly. */
|
|
428
|
+
const KOTLIN_THIS_RECEIVER_LAMBDAS = new Set(['apply', 'run', 'with']);
|
|
429
|
+
/** Walk up from `lambdaNode` to the enclosing `call_expression` and
|
|
430
|
+
* infer the lambda parameter's type from the call's receiver and
|
|
431
|
+
* member name. Returns null when the inference path is not yet
|
|
432
|
+
* supported (chained receivers, unknown member, non-stdlib idiom).
|
|
433
|
+
*
|
|
434
|
+
* Best-effort: a null return is harmless — `synthesizeKotlinLambda
|
|
435
|
+
* Bindings` still emits the binding with an empty type so the scope
|
|
436
|
+
* semantics (no leak, no cross-fire) are enforced; only the call-
|
|
437
|
+
* resolution path from `it.method()` may fall through to free-call
|
|
438
|
+
* fallback when the type isn't known. */
|
|
439
|
+
function inferKotlinLambdaReceiverType(lambdaNode, localTypes, returnTypes, classMembers) {
|
|
440
|
+
const callExpr = findEnclosingCallExpression(lambdaNode);
|
|
441
|
+
if (callExpr === null)
|
|
442
|
+
return null;
|
|
443
|
+
const callee = callExpr.namedChildren.find((c) => c.type === 'navigation_expression' || c.type === 'simple_identifier');
|
|
444
|
+
if (callee === undefined)
|
|
445
|
+
return null;
|
|
446
|
+
if (callee.type === 'simple_identifier') {
|
|
447
|
+
// `with(receiver) { ... }` — argument is the receiver. Not yet
|
|
448
|
+
// wired through; defer to follow-up.
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
// navigation_expression: <receiver>.<member>
|
|
452
|
+
const receiver = callee.namedChild(0);
|
|
453
|
+
const memberName = callee.namedChildren
|
|
454
|
+
.find((c) => c.type === 'navigation_suffix')
|
|
455
|
+
?.namedChildren.find((c) => c.type === 'simple_identifier')?.text;
|
|
456
|
+
if (receiver === null || memberName === undefined)
|
|
457
|
+
return null;
|
|
458
|
+
const receiverType = inferKotlinLambdaReceiverExpressionType(receiver, localTypes, returnTypes, classMembers);
|
|
459
|
+
if (receiverType === null)
|
|
460
|
+
return null;
|
|
461
|
+
if (KOTLIN_ELEMENT_TYPE_LAMBDAS.has(memberName)) {
|
|
462
|
+
const element = kotlinContainerElementType(receiverType, 'values');
|
|
463
|
+
if (element === null || element === '')
|
|
464
|
+
return null;
|
|
465
|
+
return { typeText: element, typeNode: lambdaNode };
|
|
466
|
+
}
|
|
467
|
+
if (KOTLIN_SCOPE_FUNCTION_LAMBDAS.has(memberName) ||
|
|
468
|
+
KOTLIN_THIS_RECEIVER_LAMBDAS.has(memberName)) {
|
|
469
|
+
// Strip nullable suffix for `?.let { ... }` semantics — inside the
|
|
470
|
+
// body, the receiver is non-null per Kotlin smart-cast.
|
|
471
|
+
const stripped = normalizeKotlinType(receiverType);
|
|
472
|
+
return { typeText: stripped, typeNode: lambdaNode };
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
/** Infer the static type of the expression that produced the lambda's
|
|
477
|
+
* enclosing call. Supports: `simple_identifier` (lookup in
|
|
478
|
+
* `localTypes`), `indexing_expression` on a Map-typed receiver, and
|
|
479
|
+
* `call_expression` whose callee return type is in `returnTypes`. */
|
|
480
|
+
function inferKotlinLambdaReceiverExpressionType(receiver, localTypes, returnTypes, classMembers) {
|
|
481
|
+
if (receiver.type === 'simple_identifier') {
|
|
482
|
+
return localTypes.get(receiver.text) ?? null;
|
|
483
|
+
}
|
|
484
|
+
if (receiver.type === 'indexing_expression') {
|
|
485
|
+
// `posts[user]` — the underlying receiver's container type tells
|
|
486
|
+
// us the element/value type.
|
|
487
|
+
const base = receiver.namedChild(0);
|
|
488
|
+
if (base === null)
|
|
489
|
+
return null;
|
|
490
|
+
const baseType = base.type === 'simple_identifier' ? localTypes.get(base.text) : null;
|
|
491
|
+
if (baseType === undefined || baseType === null)
|
|
492
|
+
return null;
|
|
493
|
+
// Indexing a Map returns the value type; indexing a List returns
|
|
494
|
+
// the element type. `kotlinContainerElementType` already encodes
|
|
495
|
+
// both via the 'values' tag.
|
|
496
|
+
return kotlinContainerElementType(baseType, 'values');
|
|
497
|
+
}
|
|
498
|
+
if (receiver.type === 'navigation_expression') {
|
|
499
|
+
// `users.map { ... }` chain — receiver is itself a navigation/
|
|
500
|
+
// call. Tier-2 chain inference: try the navigation field/method.
|
|
501
|
+
const navField = inferKotlinNavigationFieldType(receiver, localTypes, classMembers);
|
|
502
|
+
if (navField !== null)
|
|
503
|
+
return navField;
|
|
504
|
+
const callee = receiver.namedChildren
|
|
505
|
+
.find((c) => c.type === 'navigation_suffix')
|
|
506
|
+
?.namedChildren.find((c) => c.type === 'simple_identifier');
|
|
507
|
+
if (callee !== undefined) {
|
|
508
|
+
return inferKotlinNavigationCallReturnType(receiver, localTypes, classMembers);
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
if (receiver.type === 'call_expression') {
|
|
513
|
+
const callee = receiver.namedChildren.find((c) => c.type === 'simple_identifier');
|
|
514
|
+
if (callee === undefined)
|
|
515
|
+
return null;
|
|
516
|
+
return returnTypes.get(callee.text) ?? null;
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
/** Walk up from `lambdaNode` (lambda_literal) to the enclosing call:
|
|
521
|
+
* `lambda_literal → annotated_lambda → call_suffix → call_expression`
|
|
522
|
+
* for trailing lambdas, or `lambda_literal → value_argument →
|
|
523
|
+
* value_arguments → call_suffix → call_expression` for paren form.
|
|
524
|
+
* Returns null if the lambda is not inside a call. */
|
|
525
|
+
function findEnclosingCallExpression(lambdaNode) {
|
|
526
|
+
let current = lambdaNode.parent;
|
|
527
|
+
while (current !== null) {
|
|
528
|
+
if (current.type === 'call_expression')
|
|
529
|
+
return current;
|
|
530
|
+
// Don't cross out of the immediate call boundary — if we hit a
|
|
531
|
+
// function_body or function_declaration ancestor, the lambda is
|
|
532
|
+
// not call-bound.
|
|
533
|
+
if (current.type === 'function_body' || current.type === 'function_declaration') {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
current = current.parent;
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
237
540
|
function synthesizeKotlinLocalAssignmentBindings(rootNode, returnTypes) {
|
|
238
541
|
const out = [];
|
|
239
542
|
const classMembers = collectKotlinClassMembers(rootNode);
|
|
@@ -303,13 +606,23 @@ function collectKotlinClassMembers(rootNode) {
|
|
|
303
606
|
fmap.set(fname, ftype);
|
|
304
607
|
}
|
|
305
608
|
else if (member.type === 'function_declaration') {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
609
|
+
collectKotlinFunctionReturn(member, mmap);
|
|
610
|
+
}
|
|
611
|
+
else if (member.type === 'companion_object') {
|
|
612
|
+
// Companion-object methods (`companion object { fun create() … }`)
|
|
613
|
+
// are addressable via the outer class name (`Logger.create()`).
|
|
614
|
+
// Register them on the outer class so chain-binding for
|
|
615
|
+
// `val x = Logger.create(...)` picks up the return type (#1756).
|
|
616
|
+
// The receiver-side filtering needed to prevent
|
|
617
|
+
// `instance.companionMethod()` crossover is handled elsewhere.
|
|
618
|
+
const compBody = member.namedChildren.find((c) => c.type === 'class_body');
|
|
619
|
+
if (compBody !== undefined) {
|
|
620
|
+
for (const compMember of compBody.namedChildren) {
|
|
621
|
+
if (compMember.type !== 'function_declaration')
|
|
622
|
+
continue;
|
|
623
|
+
collectKotlinFunctionReturn(compMember, mmap);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
313
626
|
}
|
|
314
627
|
}
|
|
315
628
|
}
|
|
@@ -318,6 +631,15 @@ function collectKotlinClassMembers(rootNode) {
|
|
|
318
631
|
}
|
|
319
632
|
return { fields, methods };
|
|
320
633
|
}
|
|
634
|
+
function collectKotlinFunctionReturn(fnNode, target) {
|
|
635
|
+
const mname = fnNode.namedChildren.find((c) => c.type === 'simple_identifier')?.text;
|
|
636
|
+
const paramsIdx = fnNode.namedChildren.findIndex((c) => c.type === 'function_value_parameters');
|
|
637
|
+
const rtype = paramsIdx < 0
|
|
638
|
+
? undefined
|
|
639
|
+
: fnNode.namedChildren.slice(paramsIdx + 1).find((c) => isKotlinTypeNode(c))?.text;
|
|
640
|
+
if (mname !== undefined && rtype !== undefined)
|
|
641
|
+
target.set(mname, rtype);
|
|
642
|
+
}
|
|
321
643
|
function collectKotlinLocalTypeTexts(fnNode, returnTypes) {
|
|
322
644
|
const out = new Map();
|
|
323
645
|
for (const node of descendants(fnNode)) {
|
|
@@ -408,9 +730,19 @@ function inferKotlinNavigationFieldType(nav, localTypes, classMembers) {
|
|
|
408
730
|
return null;
|
|
409
731
|
return classMembers.fields.get(normalizeKotlinType(recvType))?.get(member) ?? null;
|
|
410
732
|
}
|
|
411
|
-
/** Resolve `receiver.method()` → method's declared return type
|
|
412
|
-
* `receiver` is a simple identifier
|
|
413
|
-
*
|
|
733
|
+
/** Resolve `receiver.method()` → method's declared return type. The
|
|
734
|
+
* `receiver` is a simple identifier; we try two interpretations in
|
|
735
|
+
* order:
|
|
736
|
+
*
|
|
737
|
+
* 1. `receiver` is a local variable whose type is in `localTypes` —
|
|
738
|
+
* look up `method` on that type's class members.
|
|
739
|
+
* 2. `receiver` is itself a class name (e.g. `Logger.create("app")`,
|
|
740
|
+
* a companion-object call via the class) — look up `method` on
|
|
741
|
+
* `classMembers.methods.get(receiver.text)` directly.
|
|
742
|
+
*
|
|
743
|
+
* Tier 2 supports `val logger = Logger.create(...)` patterns where the
|
|
744
|
+
* RHS is a companion-object factory: the loop variable's type is the
|
|
745
|
+
* factory's return type (#1756). */
|
|
414
746
|
function inferKotlinNavigationCallReturnType(navCallee, localTypes, classMembers) {
|
|
415
747
|
if (classMembers === undefined)
|
|
416
748
|
return null;
|
|
@@ -423,9 +755,10 @@ function inferKotlinNavigationCallReturnType(navCallee, localTypes, classMembers
|
|
|
423
755
|
if (methodName === undefined)
|
|
424
756
|
return null;
|
|
425
757
|
const recvType = localTypes.get(receiver.text);
|
|
426
|
-
if (recvType
|
|
427
|
-
return null;
|
|
428
|
-
|
|
758
|
+
if (recvType !== undefined) {
|
|
759
|
+
return classMembers.methods.get(normalizeKotlinType(recvType))?.get(methodName) ?? null;
|
|
760
|
+
}
|
|
761
|
+
return classMembers.methods.get(receiver.text)?.get(methodName) ?? null;
|
|
429
762
|
}
|
|
430
763
|
function inferKotlinIterableElementType(iterable, localTypes, returnTypes) {
|
|
431
764
|
if (iterable.type === 'simple_identifier') {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
/** Record a scope id as a companion-object scope for the given file. */
|
|
3
|
+
export declare function markCompanionScope(filePath: string, scopeId: ScopeId): void;
|
|
4
|
+
/** Check whether `scopeId` belongs to a companion-object scope in `filePath`. */
|
|
5
|
+
export declare function isCompanionScope(filePath: string, scopeId: ScopeId): boolean;
|
|
6
|
+
/** Clear all tracked companion scopes (for testing). */
|
|
7
|
+
export declare function clearCompanionScopes(): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file set of `ScopeId`s that came from a `companion_object` AST node
|
|
3
|
+
* (issue #1756 / U4 remediation).
|
|
4
|
+
*
|
|
5
|
+
* Populated during `emitKotlinScopeCaptures` from the `@scope.companion`
|
|
6
|
+
* marker capture (see `query.ts`) and consumed by
|
|
7
|
+
* `populateCompanionMembersOnEnclosingClass` in `owners.ts` to decide
|
|
8
|
+
* whether to promote a class scope's methods onto its enclosing class.
|
|
9
|
+
*
|
|
10
|
+
* The previous `ownedDefs.some(isClassLike)` heuristic in `owners.ts`
|
|
11
|
+
* silently misclassified two shapes as "regular classes":
|
|
12
|
+
* - named companions (`companion object Helper { ... }`) — the `Helper`
|
|
13
|
+
* `type_identifier` registered as a class-like def on the companion
|
|
14
|
+
* scope, hiding the companion-ness from the heuristic; AND
|
|
15
|
+
* - companions containing nested classes (`companion object { class
|
|
16
|
+
* Token; fun create() }`) — the nested class def lived on the
|
|
17
|
+
* companion scope, again hiding it from the heuristic.
|
|
18
|
+
*
|
|
19
|
+
* The marker capture lifts that distinction up to the parser layer
|
|
20
|
+
* where it is unambiguous (any `companion_object` AST node is a
|
|
21
|
+
* companion, regardless of what it contains).
|
|
22
|
+
*
|
|
23
|
+
* Parallels the C-language pattern in `c/static-linkage.ts`: per-file
|
|
24
|
+
* `Map<filePath, Set<key>>` side-channel for language-specific def /
|
|
25
|
+
* scope metadata that does not belong on the shared `Scope` /
|
|
26
|
+
* `SymbolDefinition` types.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: module-level state. `clearCompanionScopes()` is called once per
|
|
29
|
+
* workspace pass from `kotlinScopeResolver.loadResolutionConfig`, which
|
|
30
|
+
* the scope-resolution orchestrator awaits before extracting any
|
|
31
|
+
* `ParsedFile`s for this language (see `pipeline/phase.ts` and the
|
|
32
|
+
* mirror pattern in `c/scope-resolver.ts` — `clearStaticNames()`). This
|
|
33
|
+
* keeps server-mode and multi-repo-in-one-process callers safe from
|
|
34
|
+
* unbounded memory growth and from stale companion-scope ids from a
|
|
35
|
+
* previous workspace's files. Tests that exercise the captures /
|
|
36
|
+
* owners modules directly may still need to call `clearCompanionScopes`
|
|
37
|
+
* themselves (see `test/unit/kotlin-static-marker.test.ts`).
|
|
38
|
+
*/
|
|
39
|
+
const companionScopesByFile = new Map();
|
|
40
|
+
/** Record a scope id as a companion-object scope for the given file. */
|
|
41
|
+
export function markCompanionScope(filePath, scopeId) {
|
|
42
|
+
let scopes = companionScopesByFile.get(filePath);
|
|
43
|
+
if (scopes === undefined) {
|
|
44
|
+
scopes = new Set();
|
|
45
|
+
companionScopesByFile.set(filePath, scopes);
|
|
46
|
+
}
|
|
47
|
+
scopes.add(scopeId);
|
|
48
|
+
}
|
|
49
|
+
/** Check whether `scopeId` belongs to a companion-object scope in `filePath`. */
|
|
50
|
+
export function isCompanionScope(filePath, scopeId) {
|
|
51
|
+
return companionScopesByFile.get(filePath)?.has(scopeId) ?? false;
|
|
52
|
+
}
|
|
53
|
+
/** Clear all tracked companion scopes (for testing). */
|
|
54
|
+
export function clearCompanionScopes() {
|
|
55
|
+
companionScopesByFile.clear();
|
|
56
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
1
|
+
import type { ParsedFile, SymbolDefinition } from '../../../../_shared/index.js';
|
|
2
|
+
export declare function isKotlinStaticOnly(def: SymbolDefinition): boolean;
|
|
2
3
|
export declare function populateKotlinOwners(parsed: ParsedFile): void;
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { isClassLike, populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
|
2
|
+
import { isCompanionScope } from './companion-scopes.js';
|
|
3
|
+
/** Module-level identity-based marker for companion-promoted Kotlin
|
|
4
|
+
* method defs (the "this member can only be dispatched through the
|
|
5
|
+
* class name" set). Parallels the C language `static-linkage.ts`
|
|
6
|
+
* side-channel pattern but uses a WeakSet because the mark is
|
|
7
|
+
* per-def (no per-name keying needed). Eliminates the cast-and-
|
|
8
|
+
* mutate pattern the previous marker implementation required,
|
|
9
|
+
* removes any serialization-survival risk surface, and keeps the
|
|
10
|
+
* Kotlin-specific metadata off the shared `SymbolDefinition` type. */
|
|
11
|
+
const KOTLIN_STATIC_DEFS = new WeakSet();
|
|
12
|
+
export function isKotlinStaticOnly(def) {
|
|
13
|
+
return KOTLIN_STATIC_DEFS.has(def);
|
|
14
|
+
}
|
|
2
15
|
export function populateKotlinOwners(parsed) {
|
|
3
16
|
populateClassOwnedMembers(parsed);
|
|
4
17
|
populateCompanionMembersOnEnclosingClass(parsed);
|
|
@@ -37,15 +50,49 @@ function populateCompanionMembersOnEnclosingClass(parsed) {
|
|
|
37
50
|
const parent = scopesById.get(scope.parent);
|
|
38
51
|
if (parent === undefined || parent.kind !== 'Class')
|
|
39
52
|
continue;
|
|
40
|
-
|
|
53
|
+
// Identify companion-object scopes via the `@scope.companion` marker
|
|
54
|
+
// capture (see captures.ts / companion-scopes.ts) rather than via
|
|
55
|
+
// the old `parent.ownedDefs.some(isClassLike)` heuristic. The
|
|
56
|
+
// heuristic silently bypassed two real shapes (#1756 / U4):
|
|
57
|
+
// - named companions (`companion object Helper { ... }`) — `Helper`
|
|
58
|
+
// registered as a class-like def on the companion scope; AND
|
|
59
|
+
// - companions containing nested classes (`companion object {
|
|
60
|
+
// class Token; fun create() }`) — the nested class lived on
|
|
61
|
+
// the companion scope.
|
|
62
|
+
// Both bypasses left companion methods unpromoted and unmarked,
|
|
63
|
+
// breaking class-name dispatch (`Outer.create()`) and crossover
|
|
64
|
+
// suppression (`outer.create()`) for those shapes. The marker
|
|
65
|
+
// capture lifts the distinction to the parser layer where any
|
|
66
|
+
// `companion_object` AST node is a companion, full stop.
|
|
67
|
+
if (!isCompanionScope(parsed.filePath, parent.id))
|
|
41
68
|
continue;
|
|
42
69
|
const enclosing = findEnclosingClassWithDef(parent.parent, scopesById);
|
|
43
70
|
if (enclosing === undefined)
|
|
44
71
|
continue;
|
|
45
72
|
for (const def of scope.ownedDefs) {
|
|
46
|
-
|
|
73
|
+
// Class-like defs nested inside the companion's methods (rare —
|
|
74
|
+
// would be a local class declared inside a fun-body) are not
|
|
75
|
+
// companion members and must not be promoted. The companion's
|
|
76
|
+
// direct nested classes live in their OWN scope's ownedDefs
|
|
77
|
+
// (NOT the function-scope ownedDefs we iterate here), so this
|
|
78
|
+
// guard is defense-in-depth.
|
|
79
|
+
if (isClassLike(def.type))
|
|
47
80
|
continue;
|
|
81
|
+
// OVERRIDE rather than skip-when-set: for named companions,
|
|
82
|
+
// `populateClassOwnedMembers` already set `ownerId = Helper`
|
|
83
|
+
// (the named-companion class-like def). That is the WRONG
|
|
84
|
+
// owner — companion methods are dispatched through the
|
|
85
|
+
// enclosing outer class, not through the companion's own
|
|
86
|
+
// type name. Overwriting restores the intended ownership.
|
|
48
87
|
def.ownerId = enclosing.nodeId;
|
|
88
|
+
// Mark as static-only so `ScopeResolver.isStaticOnly` (see
|
|
89
|
+
// `isKotlinStaticOnly`) can filter these out of instance-receiver
|
|
90
|
+
// dispatch (#1756). Promoting the companion method onto the
|
|
91
|
+
// outer class lets `Foo.companionMethod()` resolve via Case 2;
|
|
92
|
+
// without this marker, `fooInstance.companionMethod()` would
|
|
93
|
+
// ALSO resolve to it via Case 4, which is incorrect (and a
|
|
94
|
+
// compile error in real Kotlin).
|
|
95
|
+
KOTLIN_STATIC_DEFS.add(def);
|
|
49
96
|
qualify(def, enclosing);
|
|
50
97
|
}
|
|
51
98
|
}
|
|
@@ -66,9 +113,18 @@ function findEnclosingClassWithDef(start, scopesById) {
|
|
|
66
113
|
return undefined;
|
|
67
114
|
}
|
|
68
115
|
function qualify(def, owner) {
|
|
69
|
-
if (def.qualifiedName === undefined
|
|
116
|
+
if (def.qualifiedName === undefined)
|
|
70
117
|
return;
|
|
71
118
|
if (owner.qualifiedName === undefined || owner.qualifiedName.length === 0)
|
|
72
119
|
return;
|
|
73
|
-
|
|
120
|
+
// For named companions, `populateClassOwnedMembers` qualified the
|
|
121
|
+
// def as `Helper.create`. Strip the companion-class prefix and
|
|
122
|
+
// re-qualify with the outer class so the graph-bridge lookup keys
|
|
123
|
+
// resolve to `Outer.create` rather than the spurious `Helper.create`.
|
|
124
|
+
// For unqualified defs (the anonymous-companion path), the simple
|
|
125
|
+
// name is unchanged — `populateClassOwnedMembers` found no class-like
|
|
126
|
+
// def in the anonymous companion scope, so the prior pass left the
|
|
127
|
+
// simple name in place.
|
|
128
|
+
const simple = def.qualifiedName.split('.').pop() ?? def.qualifiedName;
|
|
129
|
+
def.qualifiedName = `${owner.qualifiedName}.${simple}`;
|
|
74
130
|
}
|
|
@@ -8,6 +8,20 @@ const KOTLIN_SCOPE_QUERY = `
|
|
|
8
8
|
(companion_object) @scope.class
|
|
9
9
|
(function_declaration) @scope.function
|
|
10
10
|
|
|
11
|
+
;; Companion-object marker (issue #1756 / U4). Side-channel capture that
|
|
12
|
+
;; lets populateCompanionMembersOnEnclosingClass distinguish a companion
|
|
13
|
+
;; Class scope from a regular Class scope without inspecting ownedDefs.
|
|
14
|
+
;; Anonymous companions AND companions containing nested classes both
|
|
15
|
+
;; look like regular classes through the old ownedDefs-based heuristic;
|
|
16
|
+
;; the marker lifts the distinction up to the parser layer where it is
|
|
17
|
+
;; unambiguous (any companion_object AST node is a companion, full
|
|
18
|
+
;; stop). Consumed by markCompanionScope / isCompanionScope in
|
|
19
|
+
;; captures.ts / companion-scopes.ts. The scope-extractor ignores the
|
|
20
|
+
;; "companion" suffix (no ScopeKind mapping), so this rule contributes
|
|
21
|
+
;; no Scope record of its own — the existing (companion_object)
|
|
22
|
+
;; @scope.class rule still creates the Class scope.
|
|
23
|
+
(companion_object) @scope.companion
|
|
24
|
+
|
|
11
25
|
;; Smart-cast narrowing scopes (RFC #909 Ring 3, issue #1758).
|
|
12
26
|
;; Each is-test arm body and each if-then body becomes its own Block
|
|
13
27
|
;; scope so synthesized narrowed type-bindings (see captures.ts
|
|
@@ -21,6 +35,25 @@ const KOTLIN_SCOPE_QUERY = `
|
|
|
21
35
|
(check_expression)
|
|
22
36
|
(control_structure_body) @scope.block)
|
|
23
37
|
|
|
38
|
+
;; Lambda body scope (issue #1757). Each lambda_literal becomes its
|
|
39
|
+
;; own Block scope so synthesized lambda-parameter and implicit-'it'
|
|
40
|
+
;; type-bindings (see captures.ts synthesizeKotlinLambdaBindings) stay
|
|
41
|
+
;; inside the lambda — they must not leak to the enclosing function
|
|
42
|
+
;; scope and must shadow same-named outer bindings (val it = "outer";
|
|
43
|
+
;; users.forEach { it.save() } — inner 'it' is the lambda's, not the
|
|
44
|
+
;; outer String).
|
|
45
|
+
;;
|
|
46
|
+
;; Lambdas appear inside call_suffix for trailing-lambda syntax
|
|
47
|
+
;; (list.forEach { it.foo() }) and inside value_arguments for
|
|
48
|
+
;; explicit-paren syntax (list.forEach({ x -> x.foo() })); both AST
|
|
49
|
+
;; positions produce the same lambda_literal subtree, so a single
|
|
50
|
+
;; capture suffices.
|
|
51
|
+
;;
|
|
52
|
+
;; Uses @scope.block (not @scope.function) to match the smart-cast
|
|
53
|
+
;; precedent (#1758) — keeps narrowed/lambda bindings scope-local
|
|
54
|
+
;; without the auto-hoist semantics of Function scopes.
|
|
55
|
+
(lambda_literal) @scope.block
|
|
56
|
+
|
|
24
57
|
;; Declarations — types
|
|
25
58
|
(class_declaration
|
|
26
59
|
"interface"
|
|
@@ -2,28 +2,38 @@ import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolv
|
|
|
2
2
|
/**
|
|
3
3
|
* Kotlin scope resolver for RFC #909 Ring 3.
|
|
4
4
|
*
|
|
5
|
-
* Kotlin is
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
5
|
+
* **Migration status:** Kotlin is in `MIGRATED_LANGUAGES`. Default
|
|
6
|
+
* production resolution flows through the scope-resolution pipeline;
|
|
7
|
+
* the legacy DAG is consulted only when the per-language env var
|
|
8
|
+
* (`REGISTRY_PRIMARY_KOTLIN=0`) explicitly forces the legacy parity
|
|
9
|
+
* run for CI comparison.
|
|
9
10
|
*
|
|
10
|
-
* **Forced-mode parity (`REGISTRY_PRIMARY_KOTLIN=1`):**
|
|
11
|
-
* after the migration sub-issues #1758–#1763
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
11
|
+
* **Forced-mode parity (`REGISTRY_PRIMARY_KOTLIN=1`):** 208/208
|
|
12
|
+
* fixtures pass after the migration sub-issues #1758–#1763, the
|
|
13
|
+
* companion/instance dispatch fix #1756, and the lambda scopes
|
|
14
|
+
* fix #1757. Covers core import, receiver, companion, default-param,
|
|
15
|
+
* vararg, constructor, local assignment-chain, collection-iteration,
|
|
16
|
+
* smart casts (`when (x) { is T -> … }` and `if (x is T)` — #1758),
|
|
17
|
+
* cross-file iterable return propagation (#1759), single-level
|
|
18
|
+
* method-chain fixpoint receiver types (#1760), parameter-type-narrowed
|
|
19
|
+
* overload target-id selection (#1761), virtual dispatch via constructor
|
|
20
|
+
* RHS (`val x: Animal = Dog()` — #1762), interface default-method
|
|
21
|
+
* dispatch via implements-split MRO (#1763), companion-object vs
|
|
22
|
+
* instance member dispatch (#1756) via the `isStaticOnly` hook
|
|
23
|
+
* (including named companions and MRO-shadow / chain-typebinding /
|
|
24
|
+
* value-receiver crossover cases), and lambda-body Block scopes
|
|
25
|
+
* with scoped type-bindings for explicit parameters and implicit
|
|
26
|
+
* `it` (#1757) via `synthesizeKotlinLambdaBindings` plus the
|
|
27
|
+
* `(lambda_literal) @scope.block` query rule.
|
|
20
28
|
*
|
|
21
|
-
* **
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* **Legacy parity skip list:** `LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.kotlin`
|
|
30
|
+
* in `test/integration/resolvers/helpers.ts` records scope-resolver-only
|
|
31
|
+
* correctness wins that the legacy DAG cannot replicate. As of #1756 /
|
|
32
|
+
* #1757 there are 8 entries covering: the bare companion-vs-instance
|
|
33
|
+
* crossover, three MRO-shadow / standalone-chain cases, the chained-
|
|
34
|
+
* forEach lambda-scope case, the named-companion crossover, and the
|
|
35
|
+
* Case-0 / Case-3b / Case-5 companion crossovers under
|
|
36
|
+
* `kotlin-companion-other-cases`. Each entry is documented inline with
|
|
37
|
+
* its issue ref and rationale.
|
|
28
38
|
*/
|
|
29
39
|
export declare const kotlinScopeResolver: ScopeResolver;
|
|
@@ -4,37 +4,62 @@ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
|
|
|
4
4
|
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
|
|
5
5
|
import { kotlinProvider } from '../kotlin.js';
|
|
6
6
|
import { kotlinArityCompatibility, kotlinMergeBindings, populateKotlinOwners, resolveKotlinImportTarget, } from './index.js';
|
|
7
|
+
import { clearCompanionScopes } from './companion-scopes.js';
|
|
8
|
+
import { isKotlinStaticOnly } from './owners.js';
|
|
7
9
|
/**
|
|
8
10
|
* Kotlin scope resolver for RFC #909 Ring 3.
|
|
9
11
|
*
|
|
10
|
-
* Kotlin is
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
12
|
+
* **Migration status:** Kotlin is in `MIGRATED_LANGUAGES`. Default
|
|
13
|
+
* production resolution flows through the scope-resolution pipeline;
|
|
14
|
+
* the legacy DAG is consulted only when the per-language env var
|
|
15
|
+
* (`REGISTRY_PRIMARY_KOTLIN=0`) explicitly forces the legacy parity
|
|
16
|
+
* run for CI comparison.
|
|
14
17
|
*
|
|
15
|
-
* **Forced-mode parity (`REGISTRY_PRIMARY_KOTLIN=1`):**
|
|
16
|
-
* after the migration sub-issues #1758–#1763
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
18
|
+
* **Forced-mode parity (`REGISTRY_PRIMARY_KOTLIN=1`):** 208/208
|
|
19
|
+
* fixtures pass after the migration sub-issues #1758–#1763, the
|
|
20
|
+
* companion/instance dispatch fix #1756, and the lambda scopes
|
|
21
|
+
* fix #1757. Covers core import, receiver, companion, default-param,
|
|
22
|
+
* vararg, constructor, local assignment-chain, collection-iteration,
|
|
23
|
+
* smart casts (`when (x) { is T -> … }` and `if (x is T)` — #1758),
|
|
24
|
+
* cross-file iterable return propagation (#1759), single-level
|
|
25
|
+
* method-chain fixpoint receiver types (#1760), parameter-type-narrowed
|
|
26
|
+
* overload target-id selection (#1761), virtual dispatch via constructor
|
|
27
|
+
* RHS (`val x: Animal = Dog()` — #1762), interface default-method
|
|
28
|
+
* dispatch via implements-split MRO (#1763), companion-object vs
|
|
29
|
+
* instance member dispatch (#1756) via the `isStaticOnly` hook
|
|
30
|
+
* (including named companions and MRO-shadow / chain-typebinding /
|
|
31
|
+
* value-receiver crossover cases), and lambda-body Block scopes
|
|
32
|
+
* with scoped type-bindings for explicit parameters and implicit
|
|
33
|
+
* `it` (#1757) via `synthesizeKotlinLambdaBindings` plus the
|
|
34
|
+
* `(lambda_literal) @scope.block` query rule.
|
|
25
35
|
*
|
|
26
|
-
* **
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
36
|
+
* **Legacy parity skip list:** `LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.kotlin`
|
|
37
|
+
* in `test/integration/resolvers/helpers.ts` records scope-resolver-only
|
|
38
|
+
* correctness wins that the legacy DAG cannot replicate. As of #1756 /
|
|
39
|
+
* #1757 there are 8 entries covering: the bare companion-vs-instance
|
|
40
|
+
* crossover, three MRO-shadow / standalone-chain cases, the chained-
|
|
41
|
+
* forEach lambda-scope case, the named-companion crossover, and the
|
|
42
|
+
* Case-0 / Case-3b / Case-5 companion crossovers under
|
|
43
|
+
* `kotlin-companion-other-cases`. Each entry is documented inline with
|
|
44
|
+
* its issue ref and rationale.
|
|
33
45
|
*/
|
|
34
46
|
export const kotlinScopeResolver = {
|
|
35
47
|
language: SupportedLanguages.Kotlin,
|
|
36
48
|
languageProvider: kotlinProvider,
|
|
37
49
|
importEdgeReason: 'kotlin-scope: import',
|
|
50
|
+
loadResolutionConfig: () => {
|
|
51
|
+
// Drop the module-level `companionScopesByFile` table from any
|
|
52
|
+
// prior workspace pass before this run populates it via
|
|
53
|
+
// `emitKotlinScopeCaptures`. Mirrors the C resolver's
|
|
54
|
+
// `clearStaticNames()` call in `loadResolutionConfig` — the
|
|
55
|
+
// orchestrator awaits this hook exactly once per workspace pass
|
|
56
|
+
// (see `pipeline/phase.ts`), making it the right lifecycle seam
|
|
57
|
+
// for clearing per-language side-channel state. Returns
|
|
58
|
+
// `undefined` because Kotlin has no external resolution config
|
|
59
|
+
// to load.
|
|
60
|
+
clearCompanionScopes();
|
|
61
|
+
return undefined;
|
|
62
|
+
},
|
|
38
63
|
resolveImportTarget: (targetRaw, fromFile, allFilePaths) => {
|
|
39
64
|
const ws = { fromFile, allFilePaths };
|
|
40
65
|
return resolveKotlinImportTarget({ kind: 'named', localName: '_', importedName: '_', targetRaw }, ws);
|
|
@@ -44,6 +69,7 @@ export const kotlinScopeResolver = {
|
|
|
44
69
|
buildMro: (graph, parsedFiles, nodeLookup) => buildKotlinMro(graph, parsedFiles, nodeLookup),
|
|
45
70
|
populateOwners: (parsed) => populateKotlinOwners(parsed),
|
|
46
71
|
isSuperReceiver: (text) => text.trim() === 'super',
|
|
72
|
+
isStaticOnly: isKotlinStaticOnly,
|
|
47
73
|
fieldFallbackOnMethodLookup: false,
|
|
48
74
|
propagatesReturnTypesAcrossImports: true,
|
|
49
75
|
collapseMemberCallsByCallerTarget: false,
|
|
@@ -6,6 +6,17 @@ export function kotlinBindingScopeFor(decl, innermost, tree) {
|
|
|
6
6
|
// and erase the arm-local narrowing.
|
|
7
7
|
if (decl['@type-binding.narrowed'] !== undefined)
|
|
8
8
|
return innermost.id;
|
|
9
|
+
// Lambda-scoped bindings (issue #1757) — explicit lambda parameters
|
|
10
|
+
// and implicit `it` must stay inside the lambda body Block scope.
|
|
11
|
+
// Without this gating, the binding hoists to the enclosing function
|
|
12
|
+
// scope and:
|
|
13
|
+
// - `it` leaks past the closing brace of the lambda, shadowing the
|
|
14
|
+
// parameter-scope `it` (or outer `val it = "outer"`) for everything
|
|
15
|
+
// that follows in the function body.
|
|
16
|
+
// - Nested lambda parameters override each other across siblings.
|
|
17
|
+
// Same mechanism as the smart-cast precedent above.
|
|
18
|
+
if (decl['@type-binding.lambda-scoped'] !== undefined)
|
|
19
|
+
return innermost.id;
|
|
9
20
|
if (decl['@type-binding.return'] === undefined)
|
|
10
21
|
return null;
|
|
11
22
|
let current = innermost;
|
|
@@ -510,6 +510,43 @@ export interface ScopeResolver {
|
|
|
510
510
|
* Languages without file-local linkage semantics leave this undefined.
|
|
511
511
|
*/
|
|
512
512
|
readonly isFileLocalDef?: (def: SymbolDefinition) => boolean;
|
|
513
|
+
/**
|
|
514
|
+
* Optional predicate to identify members for which dispatch through
|
|
515
|
+
* an instance receiver is **invalid at the language level** — i.e.
|
|
516
|
+
* calling `instance.member()` would be a compile error or a
|
|
517
|
+
* type-system violation, even if a member of that name exists on
|
|
518
|
+
* the receiver's class. When provided, the receiver-bound calls
|
|
519
|
+
* pass filters out such members at every instance-receiver dispatch
|
|
520
|
+
* case (Case 0 compound receiver, Case 3b chain-typebinding, Case 4
|
|
521
|
+
* simple typeBinding, Case 5 value-receiver bridge) so the resolver
|
|
522
|
+
* does not emit a misleading `CALLS` edge for a call site the
|
|
523
|
+
* language itself would reject.
|
|
524
|
+
*
|
|
525
|
+
* **Reserved for the "instance receiver is invalid" semantic only.**
|
|
526
|
+
* Hooks for languages where static / class-level members are still
|
|
527
|
+
* legally callable through an instance (Python `@staticmethod`,
|
|
528
|
+
* JavaScript `static` methods accessed via the prototype chain in
|
|
529
|
+
* some lookup paths) should return `false` for those members — the
|
|
530
|
+
* filter would silently suppress legitimate edges otherwise. The
|
|
531
|
+
* canonical fit today is Kotlin companion-object methods, where
|
|
532
|
+
* `instance.companionMethod()` is a compile error.
|
|
533
|
+
*
|
|
534
|
+
* Case 2 (class-name receiver) is intentionally unaffected: a call
|
|
535
|
+
* through the class name (`Foo.staticMethod()`) is a legitimate
|
|
536
|
+
* dispatch.
|
|
537
|
+
*
|
|
538
|
+
* Case 0.5 (implicit `this` receiver) currently fires only for
|
|
539
|
+
* languages with `resolveThisViaEnclosingClass === true` (C++ at
|
|
540
|
+
* time of writing), none of which expose static-only semantics. A
|
|
541
|
+
* future language that enables BOTH `resolveThisViaEnclosingClass`
|
|
542
|
+
* AND `isStaticOnly` must wire the filter into Case 0.5's chain
|
|
543
|
+
* walk too — see the inline note in `receiver-bound-calls.ts`.
|
|
544
|
+
*
|
|
545
|
+
* Languages without static-only semantics leave this undefined and
|
|
546
|
+
* the legacy unfiltered behavior applies (every owned member of the
|
|
547
|
+
* receiver class is a dispatch candidate).
|
|
548
|
+
*/
|
|
549
|
+
readonly isStaticOnly?: (def: SymbolDefinition) => boolean;
|
|
513
550
|
/**
|
|
514
551
|
* Optional predicate to gate free-call fallback emission by caller-side
|
|
515
552
|
* visibility. When provided, `pickUniqueGlobalCallable` rejects candidates
|
|
@@ -46,7 +46,7 @@ import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
|
|
46
46
|
/** Subset of `ScopeResolver` consumed by this pass. Accepting the
|
|
47
47
|
* subset rather than the full provider keeps tests and partial
|
|
48
48
|
* refactors lighter — callers only need to populate what we read. */
|
|
49
|
-
type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn' | 'constraintCompatibility'>;
|
|
49
|
+
type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn' | 'constraintCompatibility' | 'isStaticOnly'>;
|
|
50
50
|
export declare function emitReceiverBoundCalls(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, handledSites: Set<string>, provider: ReceiverBoundProviderSubset, index: WorkspaceResolutionIndex, model: SemanticModel): number;
|
|
51
51
|
/**
|
|
52
52
|
* Sentinel returned by `pickOverload` when narrowing leaves >1 candidate
|
|
@@ -219,10 +219,29 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
219
219
|
if (currentClass !== undefined) {
|
|
220
220
|
const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)];
|
|
221
221
|
let memberDef;
|
|
222
|
+
// Static-only filter (#1756 / U3): same shape as Case 4's
|
|
223
|
+
// chain walk (skip-and-walk-on) but without overload
|
|
224
|
+
// narrowing — Case 0 uses `findOwnedMember` directly. When
|
|
225
|
+
// an owner's resolved candidate is static-only (Kotlin
|
|
226
|
+
// companion-promoted), continue to the next ancestor in
|
|
227
|
+
// the MRO chain so a legitimate instance member can bind.
|
|
228
|
+
// If the entire chain is static-only, no edge is emitted —
|
|
229
|
+
// unlike Case 4, Case 0 does NOT mark the site handled in
|
|
230
|
+
// that situation because compound receivers (`a.b.c()`)
|
|
231
|
+
// are not pre-emitted by `emitReferencesViaLookup` (the
|
|
232
|
+
// reference index has no compound-receiver entry for
|
|
233
|
+
// shapes like `Logger.create("a")`), so there's no wrong
|
|
234
|
+
// target to suppress.
|
|
222
235
|
for (const ownerId of chain) {
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
236
|
+
const candidate = findOwnedMember(ownerId, memberName, model);
|
|
237
|
+
if (candidate === undefined)
|
|
238
|
+
continue;
|
|
239
|
+
if (provider.isStaticOnly?.(candidate) === true) {
|
|
240
|
+
// Skip static-only candidate; walk to next ancestor.
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
memberDef = candidate;
|
|
244
|
+
break;
|
|
226
245
|
}
|
|
227
246
|
if (memberDef !== undefined) {
|
|
228
247
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
|
|
@@ -241,6 +260,17 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
241
260
|
// C++ `this->member()` (and same-shape receivers in other OO
|
|
242
261
|
// languages) should resolve against the enclosing class + MRO
|
|
243
262
|
// even when there is no explicit `this` typeBinding in scope.
|
|
263
|
+
//
|
|
264
|
+
// **Static-only filter dependency (#1756 / U3):** this case does
|
|
265
|
+
// NOT currently consult `provider.isStaticOnly`. Today it fires
|
|
266
|
+
// only for C++ (the sole `resolveThisViaEnclosingClass === true`
|
|
267
|
+
// language), which has no static-only semantics. Kotlin — the
|
|
268
|
+
// current `isStaticOnly` consumer — leaves `resolveThisVia
|
|
269
|
+
// EnclosingClass` unset, so Case 0.5 is dead code for Kotlin
|
|
270
|
+
// crossover suppression and U3 leaves it untouched. If any
|
|
271
|
+
// future language enables BOTH `resolveThisViaEnclosingClass`
|
|
272
|
+
// AND `isStaticOnly`, the chain-walk below MUST adopt the
|
|
273
|
+
// skip-and-walk-on filter pattern used by Cases 0, 3b, and 4.
|
|
244
274
|
if (provider.resolveThisViaEnclosingClass === true && receiverName === 'this') {
|
|
245
275
|
const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
|
|
246
276
|
if (enclosingClass !== undefined) {
|
|
@@ -431,10 +461,23 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
431
461
|
if (ownerDef !== undefined) {
|
|
432
462
|
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
|
433
463
|
let memberDef;
|
|
464
|
+
// Static-only filter (#1756 / U3): mirrors Case 0's chain
|
|
465
|
+
// walk — `findOwnedMember` without overload narrowing. When
|
|
466
|
+
// a static-only candidate is found at an ancestor, walk on
|
|
467
|
+
// so a legitimate instance member can bind. If the entire
|
|
468
|
+
// chain is static-only, no edge is emitted (Case 3b is fed
|
|
469
|
+
// by chain-typebinding receivers, not pre-emitted by
|
|
470
|
+
// `emitReferencesViaLookup` for compound shapes, so no
|
|
471
|
+
// handled-site marker is needed for chain-only-static).
|
|
434
472
|
for (const ownerId of chain) {
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
|
|
473
|
+
const candidate = findOwnedMember(ownerId, memberName, model);
|
|
474
|
+
if (candidate === undefined)
|
|
475
|
+
continue;
|
|
476
|
+
if (provider.isStaticOnly?.(candidate) === true) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
memberDef = candidate;
|
|
480
|
+
break;
|
|
438
481
|
}
|
|
439
482
|
if (memberDef !== undefined) {
|
|
440
483
|
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
|
|
@@ -475,16 +518,53 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
475
518
|
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
|
476
519
|
let memberDef;
|
|
477
520
|
let ambiguous = false;
|
|
521
|
+
// Track whether the chain walk filtered out any static-only
|
|
522
|
+
// candidates. When it did and the chain ended with no
|
|
523
|
+
// legitimate instance member, we mark the site as handled so
|
|
524
|
+
// `emitReferencesViaLookup` doesn't re-emit a wrong target
|
|
525
|
+
// from the pre-resolved reference index (which has no
|
|
526
|
+
// static-only awareness).
|
|
527
|
+
let allFilteredStaticOnly = false;
|
|
528
|
+
// Static-only filter (#1756 / U2): the filter must run INSIDE
|
|
529
|
+
// the chain walk and BEFORE arity narrowing.
|
|
530
|
+
//
|
|
531
|
+
// INSIDE: when a derived owner's only candidates are static-
|
|
532
|
+
// only (Kotlin companion-promoted), `pickFirstNonStaticOnly`
|
|
533
|
+
// returns `undefined` and the loop `continue`s to the next
|
|
534
|
+
// ancestor in the MRO chain — giving a legitimate ancestor
|
|
535
|
+
// instance method a chance to bind. The earlier after-chain
|
|
536
|
+
// filter aborted the entire site instead, producing a false
|
|
537
|
+
// negative whenever the most-derived owner shadowed an
|
|
538
|
+
// ancestor's instance method with a static-only companion
|
|
539
|
+
// member.
|
|
540
|
+
//
|
|
541
|
+
// BEFORE narrowing: filtering survivors of `lookupAllByOwner`
|
|
542
|
+
// (rather than survivors of `narrowOverloadCandidates`) means
|
|
543
|
+
// a same-arity static + instance pair on one owner doesn't
|
|
544
|
+
// collapse to `OVERLOAD_AMBIGUOUS`. Kotlin compile-resolves
|
|
545
|
+
// such a pair unambiguously to the instance method because
|
|
546
|
+
// companion members are not legal instance-dispatch
|
|
547
|
+
// candidates.
|
|
478
548
|
for (const ownerId of chain) {
|
|
479
|
-
const picked =
|
|
549
|
+
const picked = pickFirstNonStaticOnly(ownerId, memberName, site, model, provider);
|
|
480
550
|
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
481
551
|
ambiguous = true;
|
|
482
552
|
break;
|
|
483
553
|
}
|
|
554
|
+
if (picked === STATIC_ONLY_FILTERED) {
|
|
555
|
+
// At least one static-only candidate was filtered out at
|
|
556
|
+
// this owner; remember so we can mark handled if the
|
|
557
|
+
// chain ends with no legitimate match.
|
|
558
|
+
allFilteredStaticOnly = true;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
484
561
|
if (picked !== undefined) {
|
|
485
562
|
memberDef = picked;
|
|
486
563
|
break;
|
|
487
564
|
}
|
|
565
|
+
// `picked === undefined` means this owner had no member of
|
|
566
|
+
// this name at all. Walk on to the next ancestor in the
|
|
567
|
+
// MRO chain.
|
|
488
568
|
}
|
|
489
569
|
if (ambiguous) {
|
|
490
570
|
// Suppress and mark handled so `emitReferencesViaLookup`
|
|
@@ -493,6 +573,15 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
493
573
|
handledSites.add(siteKey);
|
|
494
574
|
continue;
|
|
495
575
|
}
|
|
576
|
+
if (memberDef === undefined && allFilteredStaticOnly) {
|
|
577
|
+
// The chain ended with no candidates because every viable
|
|
578
|
+
// owner had only static-only members. Mark handled so
|
|
579
|
+
// `emitReferencesViaLookup` doesn't re-emit a wrong target
|
|
580
|
+
// from the pre-resolved reference index. Parallels the old
|
|
581
|
+
// after-chain `isStaticOnly` suppression block.
|
|
582
|
+
handledSites.add(siteKey);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
496
585
|
if (memberDef !== undefined) {
|
|
497
586
|
// For read/write ACCESSES, mirror the legacy DAG's reason
|
|
498
587
|
// convention so consumers asserting `reason === 'write'`
|
|
@@ -549,6 +638,19 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
549
638
|
continue;
|
|
550
639
|
}
|
|
551
640
|
if (picked !== undefined) {
|
|
641
|
+
// Static-only filter (#1756 / U3): unlike Case 4 there's no
|
|
642
|
+
// MRO chain to walk here — Case 5 dispatches on a single
|
|
643
|
+
// owner via `pickOverload`. When the picked candidate is
|
|
644
|
+
// static-only (Kotlin companion-promoted), suppress the
|
|
645
|
+
// edge entirely and mark the site handled so
|
|
646
|
+
// `emitReferencesViaLookup` doesn't re-emit a wrong target
|
|
647
|
+
// from the pre-resolved reference index. Matches the after-
|
|
648
|
+
// chain handled-marker semantic used by Case 4's
|
|
649
|
+
// all-filtered fall-through.
|
|
650
|
+
if (provider.isStaticOnly?.(picked) === true) {
|
|
651
|
+
handledSites.add(siteKey);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
552
654
|
const reason = site.kind === 'write' || site.kind === 'read'
|
|
553
655
|
? site.kind
|
|
554
656
|
: picked.filePath !== parsed.filePath
|
|
@@ -610,3 +712,89 @@ function pickOverload(ownerId, memberName, site, model, provider) {
|
|
|
610
712
|
* collapses distinct types in arity-metadata).
|
|
611
713
|
*/
|
|
612
714
|
export const OVERLOAD_AMBIGUOUS = Symbol('overload-ambiguous');
|
|
715
|
+
/**
|
|
716
|
+
* Sentinel returned by `pickFirstNonStaticOnly` when the only candidates
|
|
717
|
+
* at the queried owner were filtered out by `provider.isStaticOnly`. Lets
|
|
718
|
+
* the Case 4 chain walk distinguish "owner had no member of this name"
|
|
719
|
+
* (return `undefined`, continue silently) from "owner had only static-
|
|
720
|
+
* only members" (return this sentinel, continue and remember so the
|
|
721
|
+
* post-chain handled-marker logic can suppress wrong-target re-emission
|
|
722
|
+
* from `emitReferencesViaLookup`). See #1756 / remediation plan U2.
|
|
723
|
+
*/
|
|
724
|
+
const STATIC_ONLY_FILTERED = Symbol('static-only-filtered');
|
|
725
|
+
/**
|
|
726
|
+
* Receiver-bound member lookup that filters static-only candidates BEFORE
|
|
727
|
+
* arity narrowing. Wraps the raw `lookupAllByOwner` → `narrowOverloadCandidates`
|
|
728
|
+
* pipeline so:
|
|
729
|
+
*
|
|
730
|
+
* 1. Candidates flagged by `provider.isStaticOnly` (Kotlin companion-
|
|
731
|
+
* promoted methods today) never enter the narrowing stage. A same-
|
|
732
|
+
* name same-arity static + instance pair on one owner therefore does
|
|
733
|
+
* NOT collapse to `OVERLOAD_AMBIGUOUS` — the instance member wins
|
|
734
|
+
* unambiguously, matching Kotlin's compile-time resolution.
|
|
735
|
+
* 2. The chain walk in `emitReceiverBoundCalls` Case 4 can fall through
|
|
736
|
+
* to ancestors when only static-only candidates exist at the
|
|
737
|
+
* most-derived owner (returns `STATIC_ONLY_FILTERED`), rather than
|
|
738
|
+
* aborting the site as the previous after-chain filter did.
|
|
739
|
+
*
|
|
740
|
+
* Returns:
|
|
741
|
+
* - `undefined` — no member with this name on this owner; chain walk
|
|
742
|
+
* continues silently.
|
|
743
|
+
* - `STATIC_ONLY_FILTERED` — at least one candidate existed but every
|
|
744
|
+
* one was static-only; chain walk continues and remembers so the
|
|
745
|
+
* post-chain handled-marker can fire if no ancestor binds.
|
|
746
|
+
* - `OVERLOAD_AMBIGUOUS` — narrowing on the surviving non-static
|
|
747
|
+
* candidates left >1 ambiguous match; chain walk aborts and the
|
|
748
|
+
* site is marked handled (existing sentinel handling preserved).
|
|
749
|
+
* - `SymbolDefinition` — single survivor (the chosen target).
|
|
750
|
+
*
|
|
751
|
+
* See remediation plan `docs/plans/2026-05-22-002-fix-lang-kotlin-1782-
|
|
752
|
+
* remediation-plan.md` § U2 for the full rationale.
|
|
753
|
+
*/
|
|
754
|
+
function pickFirstNonStaticOnly(ownerId, memberName, site, model, provider) {
|
|
755
|
+
const rawOverloads = model.methods.lookupAllByOwner(ownerId, memberName);
|
|
756
|
+
if (rawOverloads.length === 0) {
|
|
757
|
+
// Non-callable member (field / property / variable) — ACCESSES
|
|
758
|
+
// write/read sites target these too. Static-only filtering doesn't
|
|
759
|
+
// apply to fields, so delegate straight to `lookupFieldByOwner`.
|
|
760
|
+
return model.fields.lookupFieldByOwner(ownerId, memberName);
|
|
761
|
+
}
|
|
762
|
+
const isStaticOnly = provider.isStaticOnly;
|
|
763
|
+
let overloads = rawOverloads;
|
|
764
|
+
let filteredAny = false;
|
|
765
|
+
if (isStaticOnly !== undefined) {
|
|
766
|
+
const survivors = [];
|
|
767
|
+
for (const candidate of rawOverloads) {
|
|
768
|
+
if (isStaticOnly(candidate) === true) {
|
|
769
|
+
filteredAny = true;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
survivors.push(candidate);
|
|
773
|
+
}
|
|
774
|
+
overloads = survivors;
|
|
775
|
+
}
|
|
776
|
+
if (overloads.length === 0) {
|
|
777
|
+
// Every candidate was static-only; the caller (Case 4 chain walk)
|
|
778
|
+
// should walk on to the next owner AND remember that filtering
|
|
779
|
+
// happened so it can mark the site handled if the whole chain
|
|
780
|
+
// ends with no legitimate match.
|
|
781
|
+
return filteredAny ? STATIC_ONLY_FILTERED : undefined;
|
|
782
|
+
}
|
|
783
|
+
if (overloads.length === 1)
|
|
784
|
+
return overloads[0];
|
|
785
|
+
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
|
786
|
+
argumentTypeClasses: site.argumentTypeClasses,
|
|
787
|
+
conversionRankFn: provider.conversionRankFn,
|
|
788
|
+
constraintCompatibility: provider.constraintCompatibility,
|
|
789
|
+
});
|
|
790
|
+
// Same ambiguity handling as `pickOverload`: when normalization
|
|
791
|
+
// collapses the surviving overloads into a single bucket (e.g., C++
|
|
792
|
+
// `f(int)`/`f(long)` normalized to `['int']`), suppress rather than
|
|
793
|
+
// arbitrarily picking. When narrowing leaves >1 distinct candidate
|
|
794
|
+
// with no tie-breaker, suppress for the same reason.
|
|
795
|
+
if (isOverloadAmbiguousAfterNormalization(candidates, site.arity))
|
|
796
|
+
return OVERLOAD_AMBIGUOUS;
|
|
797
|
+
if (candidates.length > 1)
|
|
798
|
+
return OVERLOAD_AMBIGUOUS;
|
|
799
|
+
return candidates[0] ?? overloads[0];
|
|
800
|
+
}
|
package/package.json
CHANGED