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.
- 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
- package/web/assets/{agent-CNGl256w.js → agent-DUsqJyKb.js} +3 -3
- package/web/assets/{architectureDiagram-UL44E2DR-DJTnN4-A.js → architectureDiagram-UL44E2DR-CH1dpzu7.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-D6tMtD_-.js → chunk-LCXTWHL2-CT3mhvQX.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-C3CY7gW4.js → chunk-RG4AUYOV-Cq8U5h8M.js} +1 -1
- package/web/assets/{classDiagram-KGZ6W3CR-COdiqi1G.js → classDiagram-KGZ6W3CR-EWTFMYba.js} +1 -1
- package/web/assets/{classDiagram-v2-72OJOZXJ-B7YiUGDv.js → classDiagram-v2-72OJOZXJ-ChoxqWRC.js} +1 -1
- package/web/assets/{diagram-3NCE3AQN-aSkD3QID.js → diagram-3NCE3AQN-8BFf7hQK.js} +1 -1
- package/web/assets/{diagram-GF46GFSD-DlsGDkUv.js → diagram-GF46GFSD-C8QiS9Ir.js} +1 -1
- package/web/assets/{diagram-QXG6HAR7-NPw8jZAE.js → diagram-QXG6HAR7-WS98nzev.js} +1 -1
- package/web/assets/{diagram-WEQXMOUZ-CsLi0zm5.js → diagram-WEQXMOUZ-BZATDqtC.js} +1 -1
- package/web/assets/{erDiagram-L5TCEMPS-cjritYTk.js → erDiagram-L5TCEMPS-CZ37_biE.js} +1 -1
- package/web/assets/{flowDiagram-H6V6AXG4-hAr62LB-.js → flowDiagram-H6V6AXG4-Dfb084rT.js} +1 -1
- package/web/assets/{index-Cj2GDX22.js → index-BWcdXaGJ.js} +87 -87
- package/web/assets/{infoDiagram-3YFTVSEB-_sF9KVQz.js → infoDiagram-3YFTVSEB-I_J4CdiH.js} +1 -1
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-BtwawoWC.js → ishikawaDiagram-BNXS4ZKH-dB0oTk1D.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-CljJPOuK.js → kanban-definition-75IXJCU3-DOIXYxOz.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-DsqPS_X-.js → mindmap-definition-2TDM6QVE-BxCigzq7.js} +1 -1
- package/web/assets/{pieDiagram-CU6KROY3-CTUDdOgg.js → pieDiagram-CU6KROY3-CRVFetTd.js} +1 -1
- package/web/assets/{requirementDiagram-JXO7QTGE-DM8hDKq-.js → requirementDiagram-JXO7QTGE-B-EabbGj.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-m6_R47U2.js → sequenceDiagram-VS2MUI6T-BuTgbKYt.js} +1 -1
- package/web/assets/{stateDiagram-7D4R322I-Cc1HvF6o.js → stateDiagram-7D4R322I-CS7W450D.js} +1 -1
- package/web/assets/{stateDiagram-v2-36443NZ5-Kw9j23FO.js → stateDiagram-v2-36443NZ5-CHqsr-h8.js} +1 -1
- package/web/assets/{timeline-definition-O6YCAMPW-BtENAtHS.js → timeline-definition-O6YCAMPW-sBajeUFi.js} +1 -1
- package/web/assets/{vennDiagram-MWXL3ELB-DYHpZsEN.js → vennDiagram-MWXL3ELB-Dg8mzKiR.js} +1 -1
- package/web/assets/{wardleyDiagram-CUQ6CDDI-BNPunZ-h.js → wardleyDiagram-CUQ6CDDI-9aiVlfYW.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BGBiR7xJ.js → xychartDiagram-N2JHSOCM-Nl7B2WLC.js} +1 -1
- package/web/index.html +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"
|