gitnexus 1.6.6-rc.45 → 1.6.6-rc.47

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.
Files changed (42) hide show
  1. package/dist/core/ingestion/languages/kotlin/captures.d.ts +2 -2
  2. package/dist/core/ingestion/languages/kotlin/captures.js +347 -14
  3. package/dist/core/ingestion/languages/kotlin/companion-scopes.d.ts +7 -0
  4. package/dist/core/ingestion/languages/kotlin/companion-scopes.js +56 -0
  5. package/dist/core/ingestion/languages/kotlin/owners.d.ts +2 -1
  6. package/dist/core/ingestion/languages/kotlin/owners.js +60 -4
  7. package/dist/core/ingestion/languages/kotlin/query.js +33 -0
  8. package/dist/core/ingestion/languages/kotlin/scope-resolver.d.ts +31 -21
  9. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +47 -21
  10. package/dist/core/ingestion/languages/kotlin/simple-hooks.js +11 -0
  11. package/dist/core/ingestion/registry-primary-flag.js +1 -0
  12. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +37 -0
  13. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
  14. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +195 -7
  15. package/package.json +1 -1
  16. package/web/assets/{agent-CNGl256w.js → agent-DUsqJyKb.js} +3 -3
  17. package/web/assets/{architectureDiagram-UL44E2DR-DJTnN4-A.js → architectureDiagram-UL44E2DR-CH1dpzu7.js} +1 -1
  18. package/web/assets/{chunk-LCXTWHL2-D6tMtD_-.js → chunk-LCXTWHL2-CT3mhvQX.js} +1 -1
  19. package/web/assets/{chunk-RG4AUYOV-C3CY7gW4.js → chunk-RG4AUYOV-Cq8U5h8M.js} +1 -1
  20. package/web/assets/{classDiagram-KGZ6W3CR-COdiqi1G.js → classDiagram-KGZ6W3CR-EWTFMYba.js} +1 -1
  21. package/web/assets/{classDiagram-v2-72OJOZXJ-B7YiUGDv.js → classDiagram-v2-72OJOZXJ-ChoxqWRC.js} +1 -1
  22. package/web/assets/{diagram-3NCE3AQN-aSkD3QID.js → diagram-3NCE3AQN-8BFf7hQK.js} +1 -1
  23. package/web/assets/{diagram-GF46GFSD-DlsGDkUv.js → diagram-GF46GFSD-C8QiS9Ir.js} +1 -1
  24. package/web/assets/{diagram-QXG6HAR7-NPw8jZAE.js → diagram-QXG6HAR7-WS98nzev.js} +1 -1
  25. package/web/assets/{diagram-WEQXMOUZ-CsLi0zm5.js → diagram-WEQXMOUZ-BZATDqtC.js} +1 -1
  26. package/web/assets/{erDiagram-L5TCEMPS-cjritYTk.js → erDiagram-L5TCEMPS-CZ37_biE.js} +1 -1
  27. package/web/assets/{flowDiagram-H6V6AXG4-hAr62LB-.js → flowDiagram-H6V6AXG4-Dfb084rT.js} +1 -1
  28. package/web/assets/{index-Cj2GDX22.js → index-BWcdXaGJ.js} +87 -87
  29. package/web/assets/{infoDiagram-3YFTVSEB-_sF9KVQz.js → infoDiagram-3YFTVSEB-I_J4CdiH.js} +1 -1
  30. package/web/assets/{ishikawaDiagram-BNXS4ZKH-BtwawoWC.js → ishikawaDiagram-BNXS4ZKH-dB0oTk1D.js} +1 -1
  31. package/web/assets/{kanban-definition-75IXJCU3-CljJPOuK.js → kanban-definition-75IXJCU3-DOIXYxOz.js} +1 -1
  32. package/web/assets/{mindmap-definition-2TDM6QVE-DsqPS_X-.js → mindmap-definition-2TDM6QVE-BxCigzq7.js} +1 -1
  33. package/web/assets/{pieDiagram-CU6KROY3-CTUDdOgg.js → pieDiagram-CU6KROY3-CRVFetTd.js} +1 -1
  34. package/web/assets/{requirementDiagram-JXO7QTGE-DM8hDKq-.js → requirementDiagram-JXO7QTGE-B-EabbGj.js} +1 -1
  35. package/web/assets/{sequenceDiagram-VS2MUI6T-m6_R47U2.js → sequenceDiagram-VS2MUI6T-BuTgbKYt.js} +1 -1
  36. package/web/assets/{stateDiagram-7D4R322I-Cc1HvF6o.js → stateDiagram-7D4R322I-CS7W450D.js} +1 -1
  37. package/web/assets/{stateDiagram-v2-36443NZ5-Kw9j23FO.js → stateDiagram-v2-36443NZ5-CHqsr-h8.js} +1 -1
  38. package/web/assets/{timeline-definition-O6YCAMPW-BtENAtHS.js → timeline-definition-O6YCAMPW-sBajeUFi.js} +1 -1
  39. package/web/assets/{vennDiagram-MWXL3ELB-DYHpZsEN.js → vennDiagram-MWXL3ELB-Dg8mzKiR.js} +1 -1
  40. package/web/assets/{wardleyDiagram-CUQ6CDDI-BNPunZ-h.js → wardleyDiagram-CUQ6CDDI-9aiVlfYW.js} +1 -1
  41. package/web/assets/{xychartDiagram-N2JHSOCM-BGBiR7xJ.js → xychartDiagram-N2JHSOCM-Nl7B2WLC.js} +1 -1
  42. package/web/index.html +1 -1
@@ -1,2 +1,2 @@
1
- import type { CaptureMatch } from '../../../../_shared/index.js';
2
- export declare function emitKotlinScopeCaptures(sourceText: string, _filePath: string, cachedTree?: unknown): readonly CaptureMatch[];
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, _filePath, cachedTree) {
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
- const mname = member.namedChildren.find((c) => c.type === 'simple_identifier')?.text;
307
- const paramsIdx = member.namedChildren.findIndex((c) => c.type === 'function_value_parameters');
308
- const rtype = paramsIdx < 0
309
- ? undefined
310
- : member.namedChildren.slice(paramsIdx + 1).find((c) => isKotlinTypeNode(c))?.text;
311
- if (mname !== undefined && rtype !== undefined)
312
- mmap.set(mname, rtype);
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, where
412
- * `receiver` is a simple identifier whose type is in `localTypes` and
413
- * `method` is declared on that type in `classMembers.methods`. */
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 === undefined)
427
- return null;
428
- return classMembers.methods.get(normalizeKotlinType(recvType))?.get(methodName) ?? null;
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
- if (parent.ownedDefs.some((def) => isClassLike(def.type)))
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
- if (def.ownerId !== undefined)
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 || def.qualifiedName.includes('.'))
116
+ if (def.qualifiedName === undefined)
70
117
  return;
71
118
  if (owner.qualifiedName === undefined || owner.qualifiedName.length === 0)
72
119
  return;
73
- def.qualifiedName = `${owner.qualifiedName}.${def.qualifiedName}`;
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"