umple-lsp-server 0.4.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +35 -0
  3. package/completions.scm +9 -3
  4. package/definitions.scm +5 -0
  5. package/highlights.scm +487 -0
  6. package/out/codeActions.d.ts +31 -0
  7. package/out/codeActions.js +361 -0
  8. package/out/codeActions.js.map +1 -0
  9. package/out/completionAnalysis.d.ts +9 -1
  10. package/out/completionAnalysis.js +1211 -64
  11. package/out/completionAnalysis.js.map +1 -1
  12. package/out/completionBuilder.d.ts +1 -1
  13. package/out/completionBuilder.js +463 -319
  14. package/out/completionBuilder.js.map +1 -1
  15. package/out/completionTriggers.d.ts +20 -0
  16. package/out/completionTriggers.js +69 -0
  17. package/out/completionTriggers.js.map +1 -0
  18. package/out/diagnosticSources.d.ts +3 -0
  19. package/out/diagnosticSources.js +11 -0
  20. package/out/diagnosticSources.js.map +1 -0
  21. package/out/diagramNavigation.js +3 -3
  22. package/out/diagramNavigation.js.map +1 -1
  23. package/out/documentSymbolBuilder.js +2 -37
  24. package/out/documentSymbolBuilder.js.map +1 -1
  25. package/out/formatter.d.ts +13 -1
  26. package/out/formatter.js +303 -10
  27. package/out/formatter.js.map +1 -1
  28. package/out/hoverBuilder.js +90 -23
  29. package/out/hoverBuilder.js.map +1 -1
  30. package/out/inlayHints.d.ts +21 -0
  31. package/out/inlayHints.js +98 -0
  32. package/out/inlayHints.js.map +1 -0
  33. package/out/referenceSearch.d.ts +1 -1
  34. package/out/referenceSearch.js +134 -7
  35. package/out/referenceSearch.js.map +1 -1
  36. package/out/resolver.js +82 -3
  37. package/out/resolver.js.map +1 -1
  38. package/out/semanticTokens.d.ts +32 -0
  39. package/out/semanticTokens.js +228 -0
  40. package/out/semanticTokens.js.map +1 -0
  41. package/out/server.js +216 -36
  42. package/out/server.js.map +1 -1
  43. package/out/snippets.d.ts +39 -0
  44. package/out/snippets.js +328 -0
  45. package/out/snippets.js.map +1 -0
  46. package/out/symbolIndex.d.ts +50 -0
  47. package/out/symbolIndex.js +170 -7
  48. package/out/symbolIndex.js.map +1 -1
  49. package/out/symbolPresentation.d.ts +3 -0
  50. package/out/symbolPresentation.js +45 -0
  51. package/out/symbolPresentation.js.map +1 -0
  52. package/out/symbolTypes.d.ts +1 -0
  53. package/out/tokenAnalysis.js +77 -4
  54. package/out/tokenAnalysis.js.map +1 -1
  55. package/out/tokenTypes.d.ts +8 -1
  56. package/out/tokenTypes.js +2 -0
  57. package/out/tokenTypes.js.map +1 -1
  58. package/out/treeUtils.js +17 -4
  59. package/out/treeUtils.js.map +1 -1
  60. package/out/workspaceSymbolBuilder.d.ts +3 -0
  61. package/out/workspaceSymbolBuilder.js +117 -0
  62. package/out/workspaceSymbolBuilder.js.map +1 -0
  63. package/package.json +5 -2
  64. package/references.scm +31 -3
  65. package/tree-sitter-umple.wasm +0 -0
  66. package/out/bin.d.ts +0 -2
  67. package/out/bin.js +0 -5
  68. package/out/bin.js.map +0 -1
  69. package/out/log.d.ts +0 -7
  70. package/out/log.js +0 -22
  71. package/out/log.js.map +0 -1
  72. package/out/tsconfig.tsbuildinfo +0 -1
@@ -51,6 +51,61 @@ const STRUCTURAL_TOKENS = new Set([
51
51
  function isOperatorToken(name) {
52
52
  return /^[<>-]/.test(name) && name.length > 1;
53
53
  }
54
+ // ── Typed-prefix detection helpers (topic 048 phase 1) ──────────────────────
55
+ /**
56
+ * The standard gate for typed-prefix detection: letter-leading identifier
57
+ * nodes. Shared by every typed-prefix classification block. Letter-leading
58
+ * ensures we don't misclassify digit-leading tokens (multiplicity bits, etc.)
59
+ * that the parser occasionally recovers as `identifier`.
60
+ */
61
+ function isLetterLeadingIdentifier(node) {
62
+ return node.type === "identifier" && /^[A-Za-z_]/.test(node.text);
63
+ }
64
+ /**
65
+ * Walk up from `start` looking for a `type_name` node. On hit, return true
66
+ * iff type_name's parent rule is in `parentTypes` AND the child field name
67
+ * equals `fieldName`. Bails at any node in `boundaryTypes`. Used by the
68
+ * declaration-type and return-type typed-prefix detections, whose shapes are
69
+ * `identifier < qualified_name < type_name < <parent-rule>` with a specific
70
+ * grammar-level field binding the type_name into its parent.
71
+ */
72
+ function isInsideTypeNameFieldSlot(start, parentTypes, fieldName, boundaryTypes) {
73
+ let n = start.parent;
74
+ while (n) {
75
+ if (n.type === "type_name") {
76
+ const p = n.parent;
77
+ if (p && parentTypes.has(p.type)) {
78
+ for (let i = 0; i < p.childCount; i++) {
79
+ if (p.child(i)?.id === n.id) {
80
+ return p.fieldNameForChild(i) === fieldName;
81
+ }
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ if (boundaryTypes.has(n.type))
87
+ return false;
88
+ n = n.parent;
89
+ }
90
+ return false;
91
+ }
92
+ const TYPE_SLOT_BOUNDARIES = new Set([
93
+ "class_definition",
94
+ "trait_definition",
95
+ "interface_definition",
96
+ "association_class_definition",
97
+ "source_file",
98
+ ]);
99
+ const DECL_TYPE_PARENTS = new Set([
100
+ "attribute_declaration",
101
+ "const_declaration",
102
+ ]);
103
+ const RETURN_TYPE_PARENTS = new Set([
104
+ "method_declaration",
105
+ "abstract_method_declaration",
106
+ "method_signature",
107
+ "trait_method_signature",
108
+ ]);
54
109
  // ── Main analysis function ──────────────────────────────────────────────────
55
110
  /**
56
111
  * Analyze the completion context at a given position in a parsed tree.
@@ -79,6 +134,32 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
79
134
  if (nodeAtCursor && isInsideComment(nodeAtCursor)) {
80
135
  return { ...empty, isComment: true };
81
136
  }
137
+ // Pre-compute prevLeaf — needed by both the suppression ladder
138
+ // (`isInsideMethodParamListStart` distinguishes param-type slots from
139
+ // suppress positions using prevLeaf) and the rest of the analyzer.
140
+ const earlyPrevLeaf = findPreviousLeaf(tree, content, line, column);
141
+ const cursorOffset = offsetAt(content, line, column);
142
+ if (earlyPrevLeaf?.type === "->" && earlyPrevLeaf.endIndex === cursorOffset) {
143
+ return empty;
144
+ }
145
+ // --- Topic 050: contexts where any popup would be wrong ────────────────
146
+ // Each guard returns `empty` (symbolKinds=null, keywords=[], operators=[])
147
+ // so completionBuilder shows nothing. Order matters: java_annotation
148
+ // first because it can wrap content that resembles other contexts; the
149
+ // structural ones (param-list, attribute initializer) before the textual
150
+ // identifier-fragment guard.
151
+ if (nodeAtCursor && isInsideJavaAnnotation(nodeAtCursor))
152
+ return empty;
153
+ if (nodeAtCursor && isInsideMethodParamListStart(nodeAtCursor, earlyPrevLeaf))
154
+ return empty;
155
+ if (nodeAtCursor && isInsideAttributeInitializer(nodeAtCursor))
156
+ return empty;
157
+ if (isInsideMalformedDashIdentifier(content, line, column, nodeAtCursor))
158
+ return empty;
159
+ if (nodeAtCursor && isBareCompleteMultiplicityAtEnd(nodeAtCursor, content, line, column))
160
+ return empty;
161
+ if (nodeAtCursor && isInsideBrokenMethodNameSlot(nodeAtCursor))
162
+ return empty;
82
163
  // --- Extract prefix from the token at cursor ---
83
164
  let prefix = "";
84
165
  if (column > 0 &&
@@ -96,7 +177,7 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
96
177
  return { ...empty, isDefinitionName: true };
97
178
  }
98
179
  // --- LookaheadIterator for keywords ---
99
- const prevLeaf = findPreviousLeaf(tree, content, line, column);
180
+ const prevLeaf = earlyPrevLeaf;
100
181
  const stateId = prevLeaf
101
182
  ? prevLeaf.nextParseState
102
183
  : (nodeAtCursor?.parseState ?? 0);
@@ -147,7 +228,21 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
147
228
  }
148
229
  // --- Trace completion fallback for zero-identifier case ("trace |") ---
149
230
  // Trace entity fallbacks for zero-identifier recovery
150
- const TRACE_PREFIX_KEYWORDS = new Set(["trace", "set", "get", "in", "out", "entry", "exit", "cardinality", "add", "remove"]);
231
+ const TRACE_PREFIX_KEYWORDS = new Set([
232
+ "trace",
233
+ "set",
234
+ "get",
235
+ "onlyGet",
236
+ "onlySet",
237
+ "in",
238
+ "out",
239
+ "entry",
240
+ "exit",
241
+ "cardinality",
242
+ "add",
243
+ "remove",
244
+ "transition",
245
+ ]);
151
246
  if (prevLeaf && TRACE_PREFIX_KEYWORDS.has(prevLeaf.type)) {
152
247
  // Check if inside a trace_statement or ERROR under class body
153
248
  let inTrace = prevLeaf.parent?.type === "trace_statement";
@@ -199,24 +294,373 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
199
294
  if (prevLeaf?.type === "isA" && prevLeaf.parent?.type === "ERROR") {
200
295
  const errorParent = prevLeaf.parent.parent;
201
296
  if (errorParent && CLASS_LIKE_TYPES.has(errorParent.type)) {
202
- symbolKinds = ["class", "interface", "trait"];
297
+ // Topic 052 item 1 — route blank `isA |` through the same scalar
298
+ // scope used by `isA P|` (typed prefix) and `isA T,|` (comma
299
+ // continuation). The array form fell through to the fallback path
300
+ // and emitted built-ins + `void`, none of which are valid `isA`
301
+ // parents. The scalar takes the symbol-only builder branch that
302
+ // returns class / interface / trait symbols only.
303
+ symbolKinds = "isa_typed_prefix";
304
+ }
305
+ }
306
+ // --- Typed-prefix on isa_declaration type identifier (topic 047 item 1) ---
307
+ // Once the user types a prefix inside the inheritance type list, the existing
308
+ // (isa_declaration) @scope.class_interface_trait scope query still matches,
309
+ // but the array form lets LookaheadIterator append class-body / top-level
310
+ // starters (ERROR, namespace, Java, generate, ...). Force the scalar
311
+ // `isa_typed_prefix` so completionBuilder takes the symbol-only early-return
312
+ // branch — mirrors topic 043's association_typed_prefix pattern.
313
+ //
314
+ // Walk shape is specialized (direct-ancestor match + trait_sm_binding hard-
315
+ // stop + ERROR-recovery fallback), so only the identifier gate is shared
316
+ // with items 2 / 3 via isLetterLeadingIdentifier.
317
+ if (nodeAtCursor && isLetterLeadingIdentifier(nodeAtCursor)) {
318
+ // Primary: identifier is inside an isa_declaration's isa_type_list.
319
+ // Hard-stop at trait_sm_binding so `isA T<sm as S|` is never misclassified.
320
+ let n = nodeAtCursor.parent;
321
+ let insideIsaDecl = false;
322
+ while (n) {
323
+ if (n.type === "trait_sm_binding")
324
+ break;
325
+ if (n.type === "isa_declaration") {
326
+ insideIsaDecl = true;
327
+ break;
328
+ }
329
+ if (TYPE_SLOT_BOUNDARIES.has(n.type))
330
+ break;
331
+ n = n.parent;
332
+ }
333
+ // Fallback: ERROR recovery where isa_declaration didn't form at all
334
+ // (e.g. `class S { isA P` with no closing brace). findPreviousLeaf backs
335
+ // up over the current identifier, so prevLeaf for `isA P|` is `isA` and
336
+ // for `isA Person, P|` is `,`. For trait-SM angle-bracket positions
337
+ // (`isA T<sm|`, `isA T<sm as S|`) prevLeaf is `<` / `as`, so the gate
338
+ // excludes them even when they share an ERROR region with `isA`.
339
+ if (!insideIsaDecl &&
340
+ (prevLeaf?.type === "isA" || prevLeaf?.type === ",")) {
341
+ let err = nodeAtCursor.parent;
342
+ while (err && err.type !== "ERROR")
343
+ err = err.parent;
344
+ if (err) {
345
+ // For the comma case, additionally confirm the enclosing ERROR has
346
+ // `isA` as an earlier child — rules out stray commas in unrelated
347
+ // broken constructs.
348
+ for (let i = 0; i < err.childCount; i++) {
349
+ const c = err.child(i);
350
+ if (!c)
351
+ continue;
352
+ if (c.startIndex >= nodeAtCursor.startIndex)
353
+ break;
354
+ if (c.type === "isA") {
355
+ insideIsaDecl = true;
356
+ break;
357
+ }
358
+ }
359
+ }
203
360
  }
361
+ if (insideIsaDecl)
362
+ symbolKinds = "isa_typed_prefix";
363
+ }
364
+ // --- Typed-prefix on declaration-type identifier (topic 047 item 2) ---
365
+ // Same class of bug as isa_typed_prefix: when the user types a prefix
366
+ // inside an attribute_declaration or const_declaration's type_name, the
367
+ // scope query has no dedicated capture so completion falls through to
368
+ // generic class_body (54+ LookaheadIterator keywords, zero type symbols).
369
+ // Force scalar `decl_type_typed_prefix` to take a symbol-only builder
370
+ // branch that offers built-in types + class / interface / trait / enum.
371
+ //
372
+ // ERROR-recovery where type_name sits under ERROR (e.g. class body without
373
+ // closing brace) is deliberately NOT matched — too ambiguous with isA /
374
+ // modifier prefixes.
375
+ if (nodeAtCursor &&
376
+ isLetterLeadingIdentifier(nodeAtCursor) &&
377
+ isInsideTypeNameFieldSlot(nodeAtCursor, DECL_TYPE_PARENTS, "type", TYPE_SLOT_BOUNDARIES)) {
378
+ symbolKinds = "decl_type_typed_prefix";
379
+ }
380
+ // --- Typed-prefix on method return-type identifier (topic 047 item 3) ---
381
+ // Same pattern as decl_type_typed_prefix, but for method return-type slots
382
+ // across method_declaration, abstract_method_declaration, method_signature,
383
+ // and trait_method_signature. All four rules use a `return_type` field.
384
+ // Parameter types sit under `param`, not a method rule, so the field/slot
385
+ // check excludes them cleanly without extra logic. Void IS valid here
386
+ // (unlike decl_type_typed_prefix, where it is reserved for this item).
387
+ if (nodeAtCursor &&
388
+ isLetterLeadingIdentifier(nodeAtCursor) &&
389
+ isInsideTypeNameFieldSlot(nodeAtCursor, RETURN_TYPE_PARENTS, "return_type", TYPE_SLOT_BOUNDARIES)) {
390
+ symbolKinds = "return_type_typed_prefix";
391
+ }
392
+ // Topic 052 item 4 — parameter-type completion. Three positive shapes
393
+ // covered by isInsideMethodParamTypeSlot (blank `(`, single-id param,
394
+ // blank continuation after `,`). The matching topic-050 suppressors
395
+ // already step aside via the same predicate, so this assignment is the
396
+ // sole authority for those positions.
397
+ if (nodeAtCursor && isInsideMethodParamTypeSlot(nodeAtCursor, prevLeaf)) {
398
+ symbolKinds = "param_type_typed_prefix";
204
399
  }
205
400
  if ((prevLeaf?.type === "before" || prevLeaf?.type === "after") &&
206
401
  prevLeaf.parent?.type === "ERROR") {
207
402
  const errorParent = prevLeaf.parent.parent;
208
403
  if (errorParent && CLASS_LIKE_TYPES.has(errorParent.type)) {
209
- symbolKinds = ["method"];
404
+ // Topic 052 item 2 — route blank `before |` / `after |` and the
405
+ // typed-prefix `before p|` cases through a method-symbol-only scalar
406
+ // scope. The array form fell through to the fallback path and
407
+ // emitted ~177 LookaheadIterator keywords (ERROR, namespace, Java,
408
+ // ...) ahead of the actual method symbols.
409
+ symbolKinds = "code_injection_method";
210
410
  }
211
411
  }
412
+ // Topic 052 item 3 — `filter { include ... }` target completion.
413
+ //
414
+ // Two recovery shapes:
415
+ // 1. Blank `include |`: parses as `filter_definition < ERROR(include)`
416
+ // and the scope query falls through to `filter_body` (the keyword-
417
+ // starters list). Detection: prevLeaf is the `include` keyword
418
+ // whose parent is ERROR whose parent is filter_definition.
419
+ // 2. Typed `include S|`: parses cleanly as
420
+ // `filter_definition < filter_statement < filter_value <
421
+ // (include, filter_pattern "S", ;)`. The existing
422
+ // `(filter_value) @scope.class` capture in completions.scm produces
423
+ // `["class"]`, which leaks built-ins via the fallback path. Detection:
424
+ // walk up from nodeAtCursor; if ancestor is `filter_value` and its
425
+ // first child is the `include` keyword, override the scope.
426
+ //
427
+ // Both routes set `filter_include_target` — class-symbol-only, no
428
+ // built-ins, no `void`. Negative scopes stay untouched: blank filter
429
+ // body keeps `filter_body`, `includeFilter |` keeps `filter_body`,
430
+ // `namespace |` keeps `null`.
431
+ if (prevLeaf?.type === "include" &&
432
+ prevLeaf.parent?.type === "ERROR" &&
433
+ prevLeaf.parent.parent?.type === "filter_definition") {
434
+ symbolKinds = "filter_include_target";
435
+ }
436
+ else if (nodeAtCursor) {
437
+ let n = nodeAtCursor;
438
+ while (n) {
439
+ if (n.type === "filter_value") {
440
+ // First non-extra child decides — the `include` literal opens
441
+ // the filter_value variant we want.
442
+ for (let i = 0; i < n.childCount; i++) {
443
+ const c = n.child(i);
444
+ if (!c || c.isExtra)
445
+ continue;
446
+ if (c.type === "include")
447
+ symbolKinds = "filter_include_target";
448
+ break;
449
+ }
450
+ break;
451
+ }
452
+ if (n.type === "filter_definition" || n.type === "source_file")
453
+ break;
454
+ n = n.parent;
455
+ }
456
+ }
457
+ // Topic 055 — `as |` blank slot inside an ERROR, recovered for two distinct
458
+ // syntaxes:
459
+ //
460
+ // 1. `class C { sm name as |` → referenced_statemachine target
461
+ // 2. `class C { isA T<sm as |` → trait_sm_binding target
462
+ //
463
+ // Disambiguation: the trait binding ERROR carries a `<` token sibling
464
+ // because the parser consumed `isA T<` but couldn't close it. The
465
+ // referenced_statemachine ERROR doesn't. Both routes are scalar so the
466
+ // builder never falls through to the raw-lookahead path that historically
467
+ // emitted ~175 keyword junk items.
212
468
  if (prevLeaf?.type === "as" && prevLeaf.parent?.type === "ERROR") {
213
- const errorParent = prevLeaf.parent.parent;
469
+ const errorNode = prevLeaf.parent;
470
+ const errorParent = errorNode.parent;
214
471
  if (errorParent &&
215
472
  (CLASS_LIKE_TYPES.has(errorParent.type) || errorParent.type === "attribute_declaration")) {
216
- symbolKinds = ["statemachine"];
473
+ let isTraitBinding = false;
474
+ for (let i = 0; i < errorNode.childCount; i++) {
475
+ const c = errorNode.child(i);
476
+ if (c && c.type === "<") {
477
+ isTraitBinding = true;
478
+ break;
479
+ }
480
+ }
481
+ symbolKinds = isTraitBinding
482
+ ? "trait_sm_binding_target"
483
+ : "referenced_sm_target";
484
+ }
485
+ }
486
+ // Topic 055 — `... as Sm.|` dotted-state continuation inside an ERROR.
487
+ // Cursor lands on the `.` token; tree-sitter typically wraps the bare dot
488
+ // in its own ERROR node, separated from the preceding trait_sm_binding
489
+ // ERROR. Detection: walk previous siblings of the dot's container looking
490
+ // for an enclosing `trait_sm_binding` (preferred) or an ERROR that
491
+ // contains the `<` and `as` tokens of an in-progress trait binding.
492
+ // Capture the SM name segment so the builder can resolve states.
493
+ // Topic 055 — capture SM name + remaining state-path prefix for the
494
+ // trait_sm_binding_state_target builder.
495
+ let traitSmBindingSmName;
496
+ let traitSmBindingStatePrefix;
497
+ if (prevLeaf?.type === "." && prevLeaf.parent) {
498
+ const dotContainer = prevLeaf.parent;
499
+ let sawAngle = false;
500
+ let sawAs = false;
501
+ let valuePathSegments;
502
+ // Helper: extract all identifier names from a qualified_name in document
503
+ // order. The first becomes the SM name, the rest the state prefix.
504
+ const collectSegments = (qn) => {
505
+ const out = [];
506
+ for (let i = 0; i < qn.childCount; i++) {
507
+ const c = qn.child(i);
508
+ if (c && c.type === "identifier")
509
+ out.push(c.text);
510
+ }
511
+ return out;
512
+ };
513
+ let sib = dotContainer.previousSibling;
514
+ let hops = 0;
515
+ while (sib && hops < 6) {
516
+ hops++;
517
+ if (sib.type === "trait_sm_binding") {
518
+ for (let i = 0; i < sib.childCount; i++) {
519
+ const c = sib.child(i);
520
+ if (!c)
521
+ continue;
522
+ const fieldName = sib.fieldNameForChild(i);
523
+ if (fieldName === "value" && c.type === "qualified_name") {
524
+ valuePathSegments = collectSegments(c);
525
+ }
526
+ }
527
+ sawAngle = sawAs = true;
528
+ break;
529
+ }
530
+ if (sib.type === "ERROR") {
531
+ for (let i = 0; i < sib.childCount; i++) {
532
+ const c = sib.child(i);
533
+ if (!c)
534
+ continue;
535
+ if (c.type === "<")
536
+ sawAngle = true;
537
+ if (c.type === "as")
538
+ sawAs = true;
539
+ if (c.type === "trait_sm_binding") {
540
+ for (let j = 0; j < c.childCount; j++) {
541
+ const cc = c.child(j);
542
+ if (!cc)
543
+ continue;
544
+ const fieldName = c.fieldNameForChild(j);
545
+ if (fieldName === "value" && cc.type === "qualified_name") {
546
+ valuePathSegments = collectSegments(cc);
547
+ }
548
+ }
549
+ sawAngle = sawAs = true;
550
+ }
551
+ }
552
+ if (sawAngle && sawAs && valuePathSegments && valuePathSegments.length > 0)
553
+ break;
554
+ }
555
+ sib = sib.previousSibling;
556
+ }
557
+ if (sawAngle &&
558
+ sawAs &&
559
+ valuePathSegments &&
560
+ valuePathSegments.length > 0) {
561
+ symbolKinds = "trait_sm_binding_state_target";
562
+ traitSmBindingSmName = valuePathSegments[0];
563
+ traitSmBindingStatePrefix = valuePathSegments.slice(1);
564
+ }
565
+ }
566
+ // Topic 055 — bare dot inside an already-formed `trait_sm_binding > value
567
+ // (qualified_name)`. This fires when the user types `<sm as Sm.|>;` (or
568
+ // when the parser otherwise reformed the qualified_name despite the dot
569
+ // being mid-edit). Detection: prevLeaf is `.`, prevLeaf.parent is
570
+ // qualified_name, qualified_name.parent is trait_sm_binding (value path).
571
+ // Capture the SM name and any preceding state-prefix segments so the
572
+ // builder can resolve `Sm` → top-level / class-local SM and descend.
573
+ if (prevLeaf?.type === "." &&
574
+ prevLeaf.parent?.type === "qualified_name" &&
575
+ prevLeaf.parent.parent?.type === "trait_sm_binding") {
576
+ const tsb = prevLeaf.parent.parent;
577
+ let isValuePath = false;
578
+ for (let i = 0; i < tsb.childCount; i++) {
579
+ const c = tsb.child(i);
580
+ if (!c)
581
+ continue;
582
+ const fieldName = tsb.fieldNameForChild(i);
583
+ if (fieldName === "value" && c.id === prevLeaf.parent.id) {
584
+ isValuePath = true;
585
+ break;
586
+ }
587
+ }
588
+ if (isValuePath) {
589
+ const qn = prevLeaf.parent;
590
+ const precedingIdentifiers = [];
591
+ for (let i = 0; i < qn.childCount; i++) {
592
+ const c = qn.child(i);
593
+ if (!c)
594
+ continue;
595
+ if (c.id === prevLeaf.id)
596
+ break;
597
+ if (c.type === "identifier")
598
+ precedingIdentifiers.push(c.text);
599
+ }
600
+ if (precedingIdentifiers.length > 0) {
601
+ symbolKinds = "trait_sm_binding_state_target";
602
+ traitSmBindingSmName = precedingIdentifiers[0];
603
+ traitSmBindingStatePrefix = precedingIdentifiers.slice(1);
604
+ }
605
+ }
606
+ }
607
+ // Topic 055 — typed dotted continuation: cursor on identifier inside
608
+ // `trait_sm_binding > qualified_name`. The query-based scope already routes
609
+ // first-identifier captures to `trait_sm_binding_target`; here we promote
610
+ // non-first identifiers (i.e., the dotted-state segment) to the dedicated
611
+ // state target. Detection: walk siblings of the cursor identifier inside
612
+ // the qualified_name; if any preceding sibling is a `.` token we're past
613
+ // the SM-name segment.
614
+ if (nodeAtCursor &&
615
+ nodeAtCursor.type === "identifier" &&
616
+ nodeAtCursor.parent?.type === "qualified_name" &&
617
+ nodeAtCursor.parent.parent?.type === "trait_sm_binding") {
618
+ const tsb = nodeAtCursor.parent.parent;
619
+ // Only consider the value path, not the param path.
620
+ let isValuePath = false;
621
+ for (let i = 0; i < tsb.childCount; i++) {
622
+ const c = tsb.child(i);
623
+ if (!c)
624
+ continue;
625
+ const fieldName = tsb.fieldNameForChild(i);
626
+ if (fieldName === "value" && c.id === nodeAtCursor.parent.id) {
627
+ isValuePath = true;
628
+ break;
629
+ }
630
+ }
631
+ if (isValuePath) {
632
+ const qn = nodeAtCursor.parent;
633
+ const precedingIdentifiers = [];
634
+ let pastDot = false;
635
+ for (let i = 0; i < qn.childCount; i++) {
636
+ const c = qn.child(i);
637
+ if (!c)
638
+ continue;
639
+ if (c.id === nodeAtCursor.id)
640
+ break;
641
+ if (c.type === ".")
642
+ pastDot = true;
643
+ else if (c.type === "identifier")
644
+ precedingIdentifiers.push(c.text);
645
+ }
646
+ if (pastDot) {
647
+ symbolKinds = "trait_sm_binding_state_target";
648
+ // First identifier is the SM name; subsequent are state-path segments
649
+ // BEFORE the cursor (state-prefix). The cursor identifier itself is
650
+ // the typed prefix the client filters on.
651
+ if (precedingIdentifiers.length > 0) {
652
+ traitSmBindingSmName = precedingIdentifiers[0];
653
+ traitSmBindingStatePrefix = precedingIdentifiers.slice(1);
654
+ }
655
+ }
656
+ else {
657
+ symbolKinds = "trait_sm_binding_target";
658
+ }
217
659
  }
218
660
  }
219
- if (prevLeaf?.type === "->" && prevLeaf.parent?.type === "ERROR") {
661
+ if (prevLeaf?.type === "->" &&
662
+ prevLeaf.parent?.type === "ERROR" &&
663
+ prevLeaf.endIndex < cursorOffset) {
220
664
  let n = prevLeaf.parent;
221
665
  while (n) {
222
666
  if (n.type === "state_machine" || n.type === "statemachine_definition") {
@@ -227,13 +671,45 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
227
671
  break;
228
672
  n = n.parent;
229
673
  }
674
+ // Topic 050 case 3 — `status { s1 -> }` parses as `enumerated_attribute
675
+ // < ERROR(->)` rather than reaching state_machine recovery, so the walk
676
+ // above bails at class_definition. Detect the `enumerated_attribute`
677
+ // ancestor and treat the position as transition_target. The completion
678
+ // builder's transition_target branch returns an empty list when no
679
+ // enclosingStateMachine is present — better empty than 48 wrong items.
680
+ if (symbolKinds !== "transition_target") {
681
+ let walk = prevLeaf.parent;
682
+ while (walk) {
683
+ if (walk.type === "enumerated_attribute") {
684
+ // Confirm class-like outer container.
685
+ let outer = walk.parent;
686
+ while (outer) {
687
+ if (CLASS_LIKE_DEF_TYPES.has(outer.type)) {
688
+ symbolKinds = "transition_target";
689
+ break;
690
+ }
691
+ if (outer.type === "source_file")
692
+ break;
693
+ outer = outer.parent;
694
+ }
695
+ break;
696
+ }
697
+ if (walk.type === "class_definition" || walk.type === "source_file")
698
+ break;
699
+ walk = walk.parent;
700
+ }
701
+ }
230
702
  }
231
- // --- Partial inline association completion ---
232
- // While the user is mid-typing an association inside a class-like body:
233
- // `1 -> |` ERROR wraps (multiplicity)(arrow) offer a right-
234
- // multiplicity curated list (1 / * / 0..1 / 1..* / 0..*).
235
- // `1 -> * |` ERROR wraps (multiplicity)(arrow)(multiplicity) offer
703
+ // --- Partial association completion ---
704
+ // While the user is mid-typing an inline association inside a class-like body:
705
+ // `1 -> |` -> ERROR wraps (multiplicity)(arrow); offer right-
706
+ // multiplicities (1 / * / 0..1 / 1..* / 0..*).
707
+ // `1 -> * |` -> ERROR wraps (multiplicity)(arrow)(multiplicity); offer
236
708
  // class symbols for the right_type slot.
709
+ // Standalone association blocks have a left_type slot after the left
710
+ // multiplicity:
711
+ // `association { 1 | }` -> offer class names, not arrows.
712
+ // `association { 1 Other | }` -> offer arrows.
237
713
  // Once the user starts typing the type identifier the parser forms a full
238
714
  // `association_inline`, and the existing (association_inline) @scope.* in
239
715
  // completions.scm takes over. This fallback only fires while the partial is
@@ -245,37 +721,53 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
245
721
  // $.arrow, association_definition doesn't), and prevLeaf can land inside a
246
722
  // multiplicity token or on the multiplicity node itself depending on width.
247
723
  {
248
- const CLASS_LIKE_ASSOC_CONTAINERS = new Set([
724
+ const INLINE_ASSOC_CONTAINERS = new Set([
249
725
  "class_definition", "trait_definition", "interface_definition",
250
726
  "association_class_definition", "mixset_definition",
251
- "association_definition",
252
727
  ]);
253
728
  const ASSOC_SM_STOPS = new Set([
254
729
  "state_machine", "statemachine_definition", "state", "transition",
255
730
  ]);
256
- const enclosingIsClassLike = (node) => {
731
+ const associationCompletionMode = (node) => {
257
732
  let n = node;
258
733
  while (n) {
259
734
  if (ASSOC_SM_STOPS.has(n.type))
260
- return false;
261
- if (CLASS_LIKE_ASSOC_CONTAINERS.has(n.type))
262
- return true;
735
+ return null;
736
+ if (n.type === "association_definition")
737
+ return "standalone";
738
+ if (INLINE_ASSOC_CONTAINERS.has(n.type))
739
+ return "inline";
263
740
  if (n.type === "source_file")
264
- return false;
741
+ return null;
265
742
  n = n.parent;
266
743
  }
267
- return false;
744
+ return null;
268
745
  };
269
746
  const findEnclosingError = (n) => {
747
+ let found = null;
270
748
  while (n) {
271
749
  if (n.type === "ERROR")
272
- return n;
750
+ found = n;
273
751
  n = n.parent;
274
752
  }
275
- return null;
753
+ return found;
754
+ };
755
+ const pushErrorFlattenedChildren = (node, out) => {
756
+ if (node.type !== "ERROR") {
757
+ if (!node.isExtra)
758
+ out.push(node);
759
+ return;
760
+ }
761
+ for (let i = 0; i < node.childCount; i++) {
762
+ const c = node.child(i);
763
+ if (c && (!c.isExtra || c.type === "ERROR")) {
764
+ pushErrorFlattenedChildren(c, out);
765
+ }
766
+ }
276
767
  };
277
768
  const errorNode = prevLeaf ? findEnclosingError(prevLeaf) : null;
278
- if (prevLeaf && errorNode && enclosingIsClassLike(errorNode)) {
769
+ const assocMode = errorNode ? associationCompletionMode(errorNode) : null;
770
+ if (prevLeaf && errorNode && assocMode) {
279
771
  // Anchor classification on prevLeaf's position within the ERROR's non-
280
772
  // extras children. Multiple in-progress associations in the same block
281
773
  // (`association { 1 ->\n 1 -> *\n 0..1 -> 1..* }`) collapse into one
@@ -295,6 +787,7 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
295
787
  // Letter-leading identifiers DON'T count, so `e ->` in class body
296
788
  // (attribute-shaped, no multiplicity) stays on class_body.
297
789
  const isArrow = (c) => c.type === "arrow" || c.type === "->";
790
+ const isLetterLeadingId = (c) => c.type === "identifier" && /^[A-Za-z_]/.test(c.text);
298
791
  const mightBeMult = (c) => c.type === "multiplicity" ||
299
792
  c.type === "*" ||
300
793
  c.type === ".." ||
@@ -303,8 +796,9 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
303
796
  const children = [];
304
797
  for (let i = 0; i < errorNode.childCount; i++) {
305
798
  const c = errorNode.child(i);
306
- if (c && !c.isExtra)
307
- children.push(c);
799
+ if (c && (!c.isExtra || c.type === "ERROR")) {
800
+ pushErrorFlattenedChildren(c, children);
801
+ }
308
802
  }
309
803
  let prevIdx = -1;
310
804
  for (let i = children.length - 1; i >= 0; i--) {
@@ -315,30 +809,121 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
315
809
  }
316
810
  }
317
811
  if (prevIdx >= 0) {
318
- if (isArrow(children[prevIdx])) {
319
- if (children.slice(0, prevIdx).some(mightBeMult)) {
320
- symbolKinds = "association_multiplicity";
812
+ // All slot detection is segment-bounded. The segment is the range of
813
+ // children since the last `;` boundary up to and including prevIdx
814
+ // i.e. the current association attempt only. Without this, cascades
815
+ // in `association { 1 -> * Other; 1 |}` would inherit the prior
816
+ // association's arrow and misclassify the new left-mult as slot 2.
817
+ let segStart = 0;
818
+ for (let i = prevIdx - 1; i >= 0; i--) {
819
+ if (children[i].type === ";") {
820
+ segStart = i + 1;
821
+ break;
822
+ }
823
+ if (children[i].endPosition.row < prevLeaf.startPosition.row) {
824
+ segStart = i + 1;
825
+ break;
321
826
  }
322
827
  }
323
- else {
324
- let arrowIdx = -1;
325
- for (let i = prevIdx - 1; i >= 0; i--) {
326
- if (isArrow(children[i])) {
327
- arrowIdx = i;
328
- break;
828
+ const segment = children.slice(segStart, prevIdx + 1);
829
+ const segArrowIdxInSeg = segment.findIndex(isArrow);
830
+ const prevInSeg = segment.length - 1; // prevIdx in segment terms
831
+ const last = segment[prevInSeg];
832
+ const onlyOneCompleteMultAtCursor = segment.length === 1 &&
833
+ last.type === "multiplicity" &&
834
+ last.endIndex === cursorOffset;
835
+ // `1 -> *|` is still the multiplicity token. The right-type slot starts
836
+ // after whitespace (`1 -> * |`) or after a typed identifier (`1 -> * O|`).
837
+ const isRightMultiplicityWithoutTypeBoundary = (node) => !isLetterLeadingId(node) && mightBeMult(node) && node.endIndex === cursorOffset;
838
+ if (assocMode === "standalone") {
839
+ const leftSide = segArrowIdxInSeg >= 0
840
+ ? segment.slice(0, segArrowIdxInSeg)
841
+ : segment;
842
+ const hasLeftMult = leftSide.some(mightBeMult);
843
+ const hasLeftType = leftSide.some(isLetterLeadingId);
844
+ if (isArrow(last)) {
845
+ // Standalone slot 2: `1 Other -> |` needs right multiplicity.
846
+ if (last.endIndex === cursorOffset) {
847
+ symbolKinds = null;
848
+ }
849
+ else if (hasLeftMult && hasLeftType) {
850
+ symbolKinds = "association_multiplicity";
329
851
  }
852
+ else if (hasLeftMult) {
853
+ // Recovery for malformed `association { 1 -> | }`: the left
854
+ // type is still the missing piece.
855
+ symbolKinds = "association_type";
856
+ }
857
+ }
858
+ else if (segArrowIdxInSeg >= 0 && hasLeftMult && hasLeftType) {
859
+ // Standalone slot 3: `1 Other -> * |` needs right type.
860
+ const child = segment[prevInSeg];
861
+ if (isRightMultiplicityWithoutTypeBoundary(child)) {
862
+ symbolKinds = null;
863
+ }
864
+ else {
865
+ symbolKinds = isLetterLeadingId(child)
866
+ ? "association_typed_prefix"
867
+ : "association_type";
868
+ }
869
+ }
870
+ else if (hasLeftMult && hasLeftType) {
871
+ // Standalone slot 1b: `1 Other |` or `1 Other -|` needs arrow.
872
+ symbolKinds = "association_arrow";
873
+ }
874
+ else {
875
+ // Standalone slot 1a: `1 |` needs a left type, not an arrow.
876
+ const PARTIAL_ARROW_TYPES = new Set(["-", "<", ">", "@"]);
877
+ const partial = mightBeMult(last) ||
878
+ PARTIAL_ARROW_TYPES.has(last.type) ||
879
+ last.type === "req_free_text_punct";
880
+ if (partial && hasLeftMult && !onlyOneCompleteMultAtCursor) {
881
+ symbolKinds = "association_type";
882
+ }
883
+ }
884
+ }
885
+ else if (isArrow(segment[prevInSeg])) {
886
+ // Inline slot 1: cursor IS an arrow -> right-multiplicity slot.
887
+ if (segment[prevInSeg].endIndex === cursorOffset) {
888
+ symbolKinds = null;
330
889
  }
331
- if (arrowIdx >= 0 && children.slice(0, arrowIdx).some(mightBeMult)) {
332
- // If prevLeaf sits inside a letter-leading identifier (cursor at
333
- // end of typed prefix WITH trailing whitespace), the user has
334
- // started the right_type name and wants type-symbol completion.
335
- const child = children[prevIdx];
336
- const isLetterLeadingId = child.type === "identifier" && /^[A-Za-z_]/.test(child.text);
337
- symbolKinds = isLetterLeadingId
890
+ else if (segment.slice(0, prevInSeg).some(mightBeMult)) {
891
+ symbolKinds = "association_multiplicity";
892
+ }
893
+ }
894
+ else if (segArrowIdxInSeg >= 0
895
+ && segment.slice(0, segArrowIdxInSeg).some(mightBeMult)) {
896
+ // Inline slot 2: arrow appears before prevLeaf in the segment -> right-type
897
+ // slot. typed-prefix vs blank-multiplicity disambiguation matches
898
+ // topic 043's heuristic.
899
+ const child = segment[prevInSeg];
900
+ if (isRightMultiplicityWithoutTypeBoundary(child)) {
901
+ symbolKinds = null;
902
+ }
903
+ else {
904
+ symbolKinds = isLetterLeadingId(child)
338
905
  ? "association_typed_prefix"
339
906
  : "association_type";
340
907
  }
341
908
  }
909
+ else {
910
+ // Inline slot 0: no arrow in this segment yet. If prevLeaf is
911
+ // mult-like OR a partial-arrow character (`-`, `<`, `>`, `@`,
912
+ // `req_free_text_punct` for things like `<@`), AND the segment has
913
+ // a mult-like -> arrow slot.
914
+ const PARTIAL_ARROW_TYPES = new Set(["-", "<", ">", "@"]);
915
+ const partial = mightBeMult(last) ||
916
+ PARTIAL_ARROW_TYPES.has(last.type) ||
917
+ last.type === "req_free_text_punct";
918
+ // Topic 050 case 2 — bare `0..*` (a single complete multiplicity
919
+ // node with cursor exactly at its end) is not yet starting an
920
+ // association. Without intervening whitespace there's no arrow
921
+ // intent. `1 |` (cursor after a space) still fires because the
922
+ // cursor offset is past the digit token's end.
923
+ if (partial && segment.some(mightBeMult) && !onlyOneCompleteMultAtCursor) {
924
+ symbolKinds = "association_arrow";
925
+ }
926
+ }
342
927
  }
343
928
  }
344
929
  // --- Typed-prefix on the right-type identifier (cursor INSIDE the
@@ -351,11 +936,13 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
351
936
  nodeAtCursor.type === "identifier" &&
352
937
  /^[A-Za-z_]/.test(nodeAtCursor.text)) {
353
938
  const errAnc = findEnclosingError(nodeAtCursor);
354
- if (errAnc && enclosingIsClassLike(errAnc)) {
939
+ const typedAssocMode = errAnc ? associationCompletionMode(errAnc) : null;
940
+ if (errAnc && typedAssocMode) {
355
941
  // Sanity: ERROR contains an arrow with a mult-like before it (i.e.
356
- // we're really in an association right-side identifier slot, not
357
- // some unrelated identifier under a recovery ERROR).
942
+ // we're really in an association type identifier slot, not some
943
+ // unrelated identifier under a recovery ERROR).
358
944
  const isArrow2 = (c) => c.type === "arrow" || c.type === "->";
945
+ const isLetterLeadingId2 = (c) => c.type === "identifier" && /^[A-Za-z_]/.test(c.text);
359
946
  const mightBeMult2 = (c) => c.type === "multiplicity" ||
360
947
  c.type === "*" ||
361
948
  c.type === ".." ||
@@ -364,24 +951,76 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
364
951
  const errChildren = [];
365
952
  for (let i = 0; i < errAnc.childCount; i++) {
366
953
  const c = errAnc.child(i);
367
- if (c && !c.isExtra)
368
- errChildren.push(c);
954
+ if (c && (!c.isExtra || c.type === "ERROR")) {
955
+ pushErrorFlattenedChildren(c, errChildren);
956
+ }
369
957
  }
370
- let arrowSeen = false;
371
- let multSeen = false;
372
- for (const c of errChildren) {
373
- if (c.id === nodeAtCursor.id)
958
+ let typedIdx = -1;
959
+ for (let i = 0; i < errChildren.length; i++) {
960
+ const c = errChildren[i];
961
+ if (c.id === nodeAtCursor.id ||
962
+ (c.startIndex <= nodeAtCursor.startIndex && nodeAtCursor.endIndex <= c.endIndex)) {
963
+ typedIdx = i;
374
964
  break;
375
- if (isArrow2(c))
376
- arrowSeen = true;
377
- if (!arrowSeen && mightBeMult2(c))
378
- multSeen = true;
965
+ }
966
+ }
967
+ if (typedIdx >= 0) {
968
+ let segStart = 0;
969
+ for (let i = typedIdx - 1; i >= 0; i--) {
970
+ if (errChildren[i].type === ";") {
971
+ segStart = i + 1;
972
+ break;
973
+ }
974
+ if (errChildren[i].endPosition.row < nodeAtCursor.startPosition.row) {
975
+ segStart = i + 1;
976
+ break;
977
+ }
978
+ }
979
+ const typedSegment = errChildren.slice(segStart, typedIdx + 1);
980
+ const arrowIdx = typedSegment.findIndex(isArrow2);
981
+ if (typedAssocMode === "standalone" && arrowIdx < 0) {
982
+ // Left_type typed prefix: `association { 1 O| }`.
983
+ if (typedSegment.slice(0, -1).some(mightBeMult2)) {
984
+ symbolKinds = "association_typed_prefix";
985
+ }
986
+ }
987
+ else if (arrowIdx >= 0) {
988
+ const beforeArrow = typedSegment.slice(0, arrowIdx);
989
+ const hasLeftMult = beforeArrow.some(mightBeMult2);
990
+ const hasLeftType = typedAssocMode === "inline" ||
991
+ beforeArrow.some(isLetterLeadingId2);
992
+ if (hasLeftMult && hasLeftType) {
993
+ symbolKinds = "association_typed_prefix";
994
+ }
995
+ }
379
996
  }
380
- if (arrowSeen && multSeen) {
997
+ }
998
+ }
999
+ // Corpus association syntax also permits standalone association ends:
1000
+ // association { 1 Person; * Employee role; }
1001
+ // That clean parse means `association { 1 O| }` and `1 Other |` are no
1002
+ // longer wrapped in ERROR, so preserve the pre-existing completion
1003
+ // behavior explicitly: typed identifiers complete class-like symbols,
1004
+ // and whitespace after the left type still offers arrows for users who
1005
+ // are building the arrow-form association member.
1006
+ const isSingleEndInAssociationDefinition = (node) => node?.type === "single_association_end" && node.parent?.type === "association_definition";
1007
+ if (nodeAtCursor && isLetterLeadingIdentifier(nodeAtCursor)) {
1008
+ let n = nodeAtCursor.parent;
1009
+ while (n) {
1010
+ if (isSingleEndInAssociationDefinition(n)) {
381
1011
  symbolKinds = "association_typed_prefix";
1012
+ break;
382
1013
  }
1014
+ if (n.type === "association_definition" || n.type === "source_file")
1015
+ break;
1016
+ n = n.parent;
383
1017
  }
384
1018
  }
1019
+ if (prevLeaf?.type === "identifier" &&
1020
+ isSingleEndInAssociationDefinition(prevLeaf.parent) &&
1021
+ cursorOffset > prevLeaf.endIndex) {
1022
+ symbolKinds = "association_arrow";
1023
+ }
385
1024
  }
386
1025
  // --- Structured req body: slot-ready starter completion ---
387
1026
  // The completions.scm query can't distinguish "cursor between tags" (slot-
@@ -450,17 +1089,35 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
450
1089
  symbolKinds = ["requirement"];
451
1090
  break;
452
1091
  }
1092
+ // Topic 051 item 1 — isA comma continuation. After `isA Person,|` the
1093
+ // user is starting another type name. Route to the same scalar
1094
+ // typed-prefix scope as `isa_typed_prefix` so completionBuilder
1095
+ // returns class/interface/trait symbols only (no class_body junk).
1096
+ if (n.type === "isa_declaration") {
1097
+ symbolKinds = "isa_typed_prefix";
1098
+ break;
1099
+ }
453
1100
  if (n.type === "ERROR") {
454
1101
  // Look for a sibling `implementsReq` keyword anywhere in this ERROR
455
1102
  // recovery region — that's the signature of a partial req_implementation.
1103
+ // Same scan also catches partial isa_declaration recovery.
1104
+ let foundReq = false;
1105
+ let foundIsA = false;
456
1106
  for (let i = 0; i < n.childCount; i++) {
457
- if (n.child(i)?.type === "implementsReq") {
458
- symbolKinds = ["requirement"];
459
- break;
460
- }
1107
+ const c = n.child(i);
1108
+ if (c?.type === "implementsReq")
1109
+ foundReq = true;
1110
+ if (c?.type === "isA")
1111
+ foundIsA = true;
1112
+ }
1113
+ if (foundReq) {
1114
+ symbolKinds = ["requirement"];
1115
+ break;
461
1116
  }
462
- if (symbolKinds && Array.isArray(symbolKinds) && symbolKinds[0] === "requirement")
1117
+ if (foundIsA) {
1118
+ symbolKinds = "isa_typed_prefix";
463
1119
  break;
1120
+ }
464
1121
  }
465
1122
  if (n.type === "class_definition" || n.type === "source_file")
466
1123
  break;
@@ -486,6 +1143,23 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
486
1143
  traitSmContext = { traitName };
487
1144
  }
488
1145
  }
1146
+ // Topic 053 — `isA T<-|` / `isA T<+|` recovery shape parses as a flat
1147
+ // ERROR under class_definition (no isa_declaration / type_list ancestor),
1148
+ // so the helpers above don't reach the trait name. Recover from the
1149
+ // ERROR's child sequence directly. Bare `isA T<|` (no -/+ marker yet)
1150
+ // suppresses to avoid the class_body keyword leak.
1151
+ if (!traitSmContext && nodeAtCursor) {
1152
+ const recovered = recoverTraitSmOpFromIsAError(nodeAtCursor);
1153
+ if (recovered) {
1154
+ if (recovered.mode === "op") {
1155
+ symbolKinds = "trait_sm_op_sm";
1156
+ traitSmContext = { traitName: recovered.traitName };
1157
+ }
1158
+ else {
1159
+ symbolKinds = "suppress";
1160
+ }
1161
+ }
1162
+ }
489
1163
  if (!traitSmContext && prevLeaf?.type === ".") {
490
1164
  // After dot in trait SM operation paths. The dot is inside ERROR,
491
1165
  // with the preceding path in a sibling node.
@@ -610,7 +1284,8 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
610
1284
  const traceStmt = findAncestorOfType(cursorNode, "trace_statement");
611
1285
  if (traceStmt) {
612
1286
  const STATE_PREFIXES = new Set(["entry", "exit"]);
613
- const ATTR_PREFIXES = new Set(["set", "get"]);
1287
+ const ATTR_PREFIXES = new Set(["set", "get", "onlyGet", "onlySet"]);
1288
+ const EVENT_PREFIXES = new Set(["transition"]);
614
1289
  const ASSOC_PREFIXES = new Set(["add", "remove", "cardinality"]);
615
1290
  let prefixType = null;
616
1291
  for (let i = 0; i < traceStmt.childCount; i++) {
@@ -627,6 +1302,10 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
627
1302
  prefixType = "attribute";
628
1303
  break;
629
1304
  }
1305
+ if (EVENT_PREFIXES.has(child.type)) {
1306
+ prefixType = "event";
1307
+ break;
1308
+ }
630
1309
  if (ASSOC_PREFIXES.has(child.type)) {
631
1310
  prefixType = "suppress";
632
1311
  break;
@@ -650,6 +1329,9 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
650
1329
  else if (prefixType === "attribute") {
651
1330
  symbolKinds = "trace_attribute";
652
1331
  }
1332
+ else if (prefixType === "event") {
1333
+ symbolKinds = "trace_event";
1334
+ }
653
1335
  else if (prefixType === "suppress") {
654
1336
  symbolKinds = "suppress";
655
1337
  }
@@ -667,6 +1349,8 @@ function analyzeCompletion(tree, language, completionsQuery, content, line, colu
667
1349
  dottedStatePrefix,
668
1350
  sortedKeyOwner,
669
1351
  traitSmContext,
1352
+ traitSmBindingSmName,
1353
+ traitSmBindingStatePrefix,
670
1354
  };
671
1355
  }
672
1356
  function findAncestorOfType(node, type) {
@@ -689,6 +1373,328 @@ function isInsideComment(node) {
689
1373
  }
690
1374
  return false;
691
1375
  }
1376
+ // ── Topic 050 suppression guards ────────────────────────────────────────────
1377
+ const CLASS_LIKE_DEF_TYPES = new Set([
1378
+ "class_definition",
1379
+ "trait_definition",
1380
+ "interface_definition",
1381
+ "association_class_definition",
1382
+ "mixset_definition",
1383
+ ]);
1384
+ /** Cursor sits inside a `java_annotation` subtree (e.g. `@Override`). */
1385
+ function isInsideJavaAnnotation(node) {
1386
+ let n = node;
1387
+ while (n) {
1388
+ if (n.type === "java_annotation")
1389
+ return true;
1390
+ n = n.parent;
1391
+ }
1392
+ return false;
1393
+ }
1394
+ /**
1395
+ * Cursor sits inside a method param list at a NON-completable position
1396
+ * (param-name slot, or whitespace after a complete param). Topic 052
1397
+ * item 4 narrowed this from "any position inside param list" to "only
1398
+ * positions that are NOT a parameter-type slot" — type slots route to
1399
+ * `param_type_typed_prefix` via `isInsideMethodParamTypeSlot` instead.
1400
+ *
1401
+ * Now suppresses ONLY:
1402
+ * - cursor on a param's NAME identifier (param has both type_name and
1403
+ * identifier; cursor is on the second one)
1404
+ * - cursor in `param_list` whitespace (no param ancestor)
1405
+ *
1406
+ * The two old broken-recovery shapes (`void f(|` no-closing-paren / `,`
1407
+ * in ERROR sibling of param_list) are now handled positively by
1408
+ * `isInsideMethodParamTypeSlot` and pre-empt this suppressor.
1409
+ */
1410
+ function isInsideMethodParamListStart(node, prevLeaf = null) {
1411
+ // If this is a param-type slot, the positive scope handles it; do NOT
1412
+ // suppress.
1413
+ if (isInsideMethodParamTypeSlot(node, prevLeaf))
1414
+ return false;
1415
+ // Otherwise, only suppress when cursor is genuinely inside a param ancestor
1416
+ // at a non-type-slot position (the param-name slot after a full
1417
+ // type_name + identifier pair) or inside param_list whitespace.
1418
+ let n = node;
1419
+ while (n) {
1420
+ if (n.type === "param") {
1421
+ let hasTypeName = false;
1422
+ let hasIdentifier = false;
1423
+ for (let i = 0; i < n.childCount; i++) {
1424
+ const c = n.child(i);
1425
+ if (!c)
1426
+ continue;
1427
+ if (c.type === "type_name")
1428
+ hasTypeName = true;
1429
+ if (c.type === "identifier")
1430
+ hasIdentifier = true;
1431
+ }
1432
+ // Suppress only when the param has both type_name and identifier
1433
+ // (cursor on the name slot). Single-identifier params are type-slots
1434
+ // and have already returned false above via isInsideMethodParamTypeSlot.
1435
+ return hasTypeName && hasIdentifier;
1436
+ }
1437
+ if (n.type === "param_list") {
1438
+ // Cursor in param_list whitespace after a complete param — suppress.
1439
+ return true;
1440
+ }
1441
+ if (n.type === "method_declaration" || n.type === "class_definition" ||
1442
+ n.type === "source_file")
1443
+ break;
1444
+ n = n.parent;
1445
+ }
1446
+ return false;
1447
+ }
1448
+ /**
1449
+ * Topic 052 item 4 — cursor sits at a parameter-type slot inside a method
1450
+ * declaration. Three recovery shapes:
1451
+ *
1452
+ * 1. Blank first slot `void f(|)` — cursor at `(` whose parent is
1453
+ * `method_declaration`. No `param_list` formed yet.
1454
+ * 2. Typed inside single-identifier param `void f(P|)` /
1455
+ * `void f(int a, P|)` — cursor on an `identifier` whose parent is
1456
+ * `param`, and the param has only one identifier child (no separate
1457
+ * type_name yet). The single identifier is the type-being-typed.
1458
+ * 3. Blank continuation `void f(int a, |)` — cursor sits between `,`
1459
+ * (in ERROR sibling of param_list) and `)`. Detection: prevLeaf is
1460
+ * `,` whose parent ERROR is a sibling of param_list under
1461
+ * method_declaration.
1462
+ *
1463
+ * Returns true if any of these match. Caller sets
1464
+ * `symbolKinds = "param_type_typed_prefix"`.
1465
+ */
1466
+ function isInsideMethodParamTypeSlot(node, prevLeaf = null) {
1467
+ // Shape 1 — cursor at `(` of method_declaration. Two parse states:
1468
+ // 1a. clean: `void f() {}` — `(` directly under method_declaration.
1469
+ // 1b. broken: `void f(` (no closing) — the entire method shape lives
1470
+ // under an ERROR. The ERROR contains a `type_name` (return type),
1471
+ // identifier (method name), and `(`. This is the topic-050
1472
+ // broken-`(` shape, now redirected from suppress to positive.
1473
+ if (node.type === "(") {
1474
+ if (node.parent?.type === "method_declaration")
1475
+ return true;
1476
+ if (node.parent?.type === "ERROR") {
1477
+ const err = node.parent;
1478
+ let hasTypeName = false;
1479
+ let hasNameAfterTypeName = false;
1480
+ for (let i = 0; i < err.childCount; i++) {
1481
+ const c = err.child(i);
1482
+ if (!c)
1483
+ continue;
1484
+ if (c.id === node.id)
1485
+ break;
1486
+ if (c.type === "type_name")
1487
+ hasTypeName = true;
1488
+ if (c.type === "identifier" && hasTypeName)
1489
+ hasNameAfterTypeName = true;
1490
+ }
1491
+ if (hasTypeName && hasNameAfterTypeName) {
1492
+ // Confirm class-like ancestor.
1493
+ let p = err.parent;
1494
+ while (p) {
1495
+ if (CLASS_LIKE_DEF_TYPES.has(p.type))
1496
+ return true;
1497
+ if (p.type === "source_file")
1498
+ return false;
1499
+ p = p.parent;
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+ // Shape 2 — cursor on an identifier inside a single-identifier param.
1505
+ if (node.type === "identifier") {
1506
+ const param = node.parent;
1507
+ if (param?.type === "param") {
1508
+ let typeNameCount = 0;
1509
+ let idCount = 0;
1510
+ for (let i = 0; i < param.childCount; i++) {
1511
+ const c = param.child(i);
1512
+ if (!c)
1513
+ continue;
1514
+ if (c.type === "type_name")
1515
+ typeNameCount++;
1516
+ if (c.type === "identifier")
1517
+ idCount++;
1518
+ }
1519
+ // Single identifier, no type_name → cursor is on the in-progress
1520
+ // type token.
1521
+ return typeNameCount === 0 && idCount === 1;
1522
+ }
1523
+ }
1524
+ // Shape 3 — cursor after a comma in the ERROR-sibling-of-param_list shape.
1525
+ if (prevLeaf?.type === "," && prevLeaf.parent?.type === "ERROR") {
1526
+ const err = prevLeaf.parent;
1527
+ if (err.parent?.type === "method_declaration")
1528
+ return true;
1529
+ }
1530
+ return false;
1531
+ }
1532
+ /**
1533
+ * Cursor sits inside an attribute-initializer expression after `=` and
1534
+ * before the terminating `;`. The grammar's `_attribute_value` is hidden,
1535
+ * and broken/incomplete expressions live entirely under an ERROR. Codex's
1536
+ * invariant: in a class-like container, enclosing ERROR has `=` before
1537
+ * cursor with no association arrow before cursor → suppress.
1538
+ */
1539
+ function isInsideAttributeInitializer(node) {
1540
+ let err = node;
1541
+ while (err && err.type !== "ERROR")
1542
+ err = err.parent;
1543
+ if (!err)
1544
+ return false;
1545
+ // ERROR must sit inside a class-like container.
1546
+ let p = err.parent;
1547
+ let inClassLike = false;
1548
+ while (p) {
1549
+ if (CLASS_LIKE_DEF_TYPES.has(p.type)) {
1550
+ inClassLike = true;
1551
+ break;
1552
+ }
1553
+ if (p.type === "source_file")
1554
+ return false;
1555
+ p = p.parent;
1556
+ }
1557
+ if (!inClassLike)
1558
+ return false;
1559
+ // Walk ERROR children before the cursor: must contain `=` and no arrow.
1560
+ let sawEquals = false;
1561
+ let sawArrow = false;
1562
+ for (let i = 0; i < err.childCount; i++) {
1563
+ const c = err.child(i);
1564
+ if (!c)
1565
+ continue;
1566
+ if (c.startIndex >= node.startIndex)
1567
+ break;
1568
+ if (c.type === "=")
1569
+ sawEquals = true;
1570
+ if (c.type === "->" || c.type === "arrow")
1571
+ sawArrow = true;
1572
+ }
1573
+ return sawEquals && !sawArrow;
1574
+ }
1575
+ /**
1576
+ * Cursor sits inside or at the end of a contiguous identifier-character run
1577
+ * (`[A-Za-z0-9_-]+`) that contains both a `-` and a letter — i.e. a
1578
+ * malformed identifier the parser couldn't classify. Examples: `req-|foo`,
1579
+ * `req|-foo`, `req-foo|`. Negatives: `Int|`, `isA P|`, `1 -> * O|`,
1580
+ * `1 -|` (no letter in the run).
1581
+ *
1582
+ * Mostly textual; the recovery AST is not stable enough at these positions
1583
+ * to drive the decision off node types alone. We do, however, exempt two
1584
+ * legitimate dash-bearing contexts:
1585
+ *
1586
+ * 1. AST: cursor inside a `req_id`, `req_implementation`, or
1587
+ * `requirement_definition` subtree — `req_id` allows hyphens, so
1588
+ * `implementsReq L01-|License` is a valid completion target.
1589
+ *
1590
+ * 2. Textual fallback (when the AST doesn't form because of a missing
1591
+ * `;`): scan the current line backward from the cursor; if we encounter
1592
+ * `implementsReq` or `req` keyword before any `;`, `}`, or line start,
1593
+ * the user is editing a requirement-id position — don't suppress.
1594
+ *
1595
+ * Codex-approved narrow form — does NOT suppress legitimate typed-prefix
1596
+ * completions because those scopes have their own positive entries above
1597
+ * this guard.
1598
+ */
1599
+ function isInsideMalformedDashIdentifier(content, line, column, nodeAtCursor) {
1600
+ const lines = content.split("\n");
1601
+ const lineText = lines[line] ?? "";
1602
+ const isRunChar = (c) => /[A-Za-z0-9_-]/.test(c);
1603
+ const isLetter = (c) => /[A-Za-z_]/.test(c);
1604
+ // Scan the contiguous run that contains (or is adjacent to) the cursor.
1605
+ let start = column;
1606
+ while (start > 0 && isRunChar(lineText[start - 1]))
1607
+ start--;
1608
+ let end = column;
1609
+ while (end < lineText.length && isRunChar(lineText[end]))
1610
+ end++;
1611
+ if (start === end)
1612
+ return false; // empty run on either side
1613
+ const run = lineText.slice(start, end);
1614
+ if (!run.includes("-") || ![...run].some(isLetter))
1615
+ return false;
1616
+ // Exempt #1 — AST shows the cursor is in a real requirement context.
1617
+ if (nodeAtCursor) {
1618
+ let n = nodeAtCursor;
1619
+ while (n) {
1620
+ if (n.type === "req_id" ||
1621
+ n.type === "req_implementation" ||
1622
+ n.type === "requirement_definition")
1623
+ return false;
1624
+ n = n.parent;
1625
+ }
1626
+ }
1627
+ // Exempt #2 — same-line back-scan for `implementsReq` / `req` keyword
1628
+ // before any statement boundary. Covers the AST-didn't-form case (no `;`
1629
+ // closing the partial req_implementation).
1630
+ const before = lineText.slice(0, start);
1631
+ // If before contains `;` or `}`, that closes any prior context — bail.
1632
+ if (/[;}]/.test(before))
1633
+ return true;
1634
+ if (/\b(?:implementsReq|req)\b/.test(before))
1635
+ return false;
1636
+ return true;
1637
+ }
1638
+ /**
1639
+ * Topic 050 case 2 — bare complete multiplicity sitting alone in a class
1640
+ * body or association block, with cursor at the exact end of the
1641
+ * multiplicity token. Examples: `0..*|`, `1|`, `0..1|` placed alone in a
1642
+ * class-body line. Without further context (arrow, type) this isn't a real
1643
+ * completion target — the partial-association refinement keeps it from
1644
+ * classifying as `association_arrow`, but the analyzer would still fall
1645
+ * through to `class_body` and surface 47 wrong keywords. Suppress.
1646
+ *
1647
+ * Negatives this guard does NOT match:
1648
+ * `0..* |` (cursor past whitespace) — segments differ; partial-association
1649
+ * logic upstream picks it up as a real arrow slot.
1650
+ * `1 -> *|` (after the right multiplicity of an in-progress association)
1651
+ * — segment has an arrow; this guard requires no arrow before cursor.
1652
+ */
1653
+ function isBareCompleteMultiplicityAtEnd(node, content, line, column) {
1654
+ if (node.type !== "multiplicity")
1655
+ return false;
1656
+ const lines = content.split("\n");
1657
+ let offset = 0;
1658
+ for (let i = 0; i < line; i++)
1659
+ offset += lines[i].length + 1;
1660
+ offset += column;
1661
+ if (node.endIndex !== offset)
1662
+ return false;
1663
+ const err = node.parent;
1664
+ if (!err || err.type !== "ERROR")
1665
+ return false;
1666
+ // No arrow or `;` before this multiplicity in the ERROR
1667
+ for (let i = 0; i < err.childCount; i++) {
1668
+ const c = err.child(i);
1669
+ if (!c)
1670
+ continue;
1671
+ if (c.id === node.id)
1672
+ break;
1673
+ if (c.type === "arrow" || c.type === "->")
1674
+ return false;
1675
+ if (c.type === ";")
1676
+ return false;
1677
+ }
1678
+ // ERROR must sit in a class-like or association container
1679
+ let p = err.parent;
1680
+ while (p) {
1681
+ if (CLASS_LIKE_DEF_TYPES.has(p.type) || p.type === "association_definition") {
1682
+ return true;
1683
+ }
1684
+ if (p.type === "source_file")
1685
+ return false;
1686
+ p = p.parent;
1687
+ }
1688
+ return false;
1689
+ }
1690
+ function offsetAt(content, line, column) {
1691
+ const lines = content.split("\n");
1692
+ let offset = 0;
1693
+ for (let i = 0; i < line && i < lines.length; i++) {
1694
+ offset += lines[i].length + 1;
1695
+ }
1696
+ return offset + Math.min(column, lines[line]?.length ?? 0);
1697
+ }
692
1698
  function findPreviousLeaf(tree, content, line, column) {
693
1699
  const lines = content.split("\n");
694
1700
  let offset = 0;
@@ -793,6 +1799,26 @@ function resolveCompletionScope(completionsQuery, tree, line, column) {
793
1799
  return "association_type";
794
1800
  if (kindStr === "association_typed_prefix")
795
1801
  return "association_typed_prefix";
1802
+ if (kindStr === "isa_typed_prefix")
1803
+ return "isa_typed_prefix";
1804
+ if (kindStr === "decl_type_typed_prefix")
1805
+ return "decl_type_typed_prefix";
1806
+ if (kindStr === "return_type_typed_prefix")
1807
+ return "return_type_typed_prefix";
1808
+ if (kindStr === "code_injection_method")
1809
+ return "code_injection_method";
1810
+ if (kindStr === "filter_include_target")
1811
+ return "filter_include_target";
1812
+ if (kindStr === "param_type_typed_prefix")
1813
+ return "param_type_typed_prefix";
1814
+ if (kindStr === "association_arrow")
1815
+ return "association_arrow";
1816
+ if (kindStr === "referenced_sm_target")
1817
+ return "referenced_sm_target";
1818
+ if (kindStr === "trait_sm_binding_target")
1819
+ return "trait_sm_binding_target";
1820
+ if (kindStr === "trait_sm_binding_state_target")
1821
+ return "trait_sm_binding_state_target";
796
1822
  if (kindStr === "none")
797
1823
  return null;
798
1824
  return kindStr.split("_");
@@ -922,11 +1948,132 @@ function isInsideTraitSmOpContext(node) {
922
1948
  }
923
1949
  return false;
924
1950
  }
1951
+ /**
1952
+ * Topic 053 — `isA T<` recovery shapes parse as a flat ERROR under
1953
+ * class_definition (no `isa_declaration` / `type_list` / `type_name`
1954
+ * ancestor). Returns the matched recovery info if the ERROR shape is
1955
+ * `[isA, qualified_name(trait), <, ...rest]` AND cursor sits at/after
1956
+ * the `<`. The `mode` field tells the caller whether we have a
1957
+ * trait-SM operation marker (`-` / `+`) or just the bare `<`.
1958
+ */
1959
+ function recoverTraitSmOpFromIsAError(nodeAtCursor) {
1960
+ // Bail when cursor is already inside a formed `trait_sm_binding` —
1961
+ // the parse has progressed past the `T<` recovery shape and the user
1962
+ // is editing a deeper position (e.g. `isA T<sm as S|`). Existing
1963
+ // logic for those positions should run.
1964
+ let walk = nodeAtCursor;
1965
+ while (walk) {
1966
+ if (walk.type === "trait_sm_binding")
1967
+ return null;
1968
+ if (walk.type === "ERROR")
1969
+ break;
1970
+ walk = walk.parent;
1971
+ }
1972
+ // Find the enclosing ERROR.
1973
+ let err = nodeAtCursor;
1974
+ while (err && err.type !== "ERROR")
1975
+ err = err.parent;
1976
+ if (!err)
1977
+ return null;
1978
+ // Must sit directly under a class-like container.
1979
+ let p = err.parent;
1980
+ let inClassLike = false;
1981
+ while (p) {
1982
+ if (CLASS_LIKE_DEF_TYPES.has(p.type)) {
1983
+ inClassLike = true;
1984
+ break;
1985
+ }
1986
+ if (p.type === "source_file")
1987
+ break;
1988
+ p = p.parent;
1989
+ }
1990
+ if (!inClassLike)
1991
+ return null;
1992
+ // Walk children: find the [isA, qualified_name, <] prefix BEFORE cursor.
1993
+ let sawIsA = false;
1994
+ let traitName;
1995
+ let sawAngle = false;
1996
+ let sawOpMarker = false;
1997
+ for (let i = 0; i < err.childCount; i++) {
1998
+ const c = err.child(i);
1999
+ if (!c || c.isExtra)
2000
+ continue;
2001
+ if (c.startIndex >= nodeAtCursor.startIndex && c.id !== nodeAtCursor.id)
2002
+ break;
2003
+ if (c.type === "isA") {
2004
+ sawIsA = true;
2005
+ continue;
2006
+ }
2007
+ if (sawIsA && !traitName && c.type === "qualified_name") {
2008
+ const lastId = c.namedChild(c.namedChildCount - 1);
2009
+ if (lastId?.type === "identifier")
2010
+ traitName = lastId.text;
2011
+ continue;
2012
+ }
2013
+ if (sawIsA && traitName && c.type === "<") {
2014
+ sawAngle = true;
2015
+ continue;
2016
+ }
2017
+ if (sawAngle && (c.type === "-" || c.type === "+")) {
2018
+ sawOpMarker = true;
2019
+ continue;
2020
+ }
2021
+ }
2022
+ if (!sawIsA || !traitName || !sawAngle)
2023
+ return null;
2024
+ return { traitName, mode: sawOpMarker ? "op" : "bare" };
2025
+ }
2026
+ /**
2027
+ * Topic 053 — broken method-name slot in a class body when the method has
2028
+ * no `{}` body. Parses as `class_definition < ERROR < [type_name,
2029
+ * identifier(name), (, )]`. Cursor on the name identifier is the broken
2030
+ * equivalent of `void method|() {}` which is normally caught by
2031
+ * isAtAttributeNamePosition. Suppress.
2032
+ */
2033
+ function isInsideBrokenMethodNameSlot(node) {
2034
+ if (node.type !== "identifier")
2035
+ return false;
2036
+ const err = node.parent;
2037
+ if (err?.type !== "ERROR")
2038
+ return false;
2039
+ // ERROR's children must include type_name BEFORE this identifier, and
2040
+ // `(` after — confirms the method-shape recovery.
2041
+ let sawTypeName = false;
2042
+ let sawNameAfterTypeName = false;
2043
+ let sawOpenParenAfterName = false;
2044
+ for (let i = 0; i < err.childCount; i++) {
2045
+ const c = err.child(i);
2046
+ if (!c || c.isExtra)
2047
+ continue;
2048
+ if (c.type === "type_name")
2049
+ sawTypeName = true;
2050
+ else if (c.id === node.id) {
2051
+ if (sawTypeName)
2052
+ sawNameAfterTypeName = true;
2053
+ }
2054
+ else if (c.type === "(") {
2055
+ if (sawNameAfterTypeName)
2056
+ sawOpenParenAfterName = true;
2057
+ }
2058
+ }
2059
+ if (!sawNameAfterTypeName || !sawOpenParenAfterName)
2060
+ return false;
2061
+ // Confirm class-like container.
2062
+ let p = err.parent;
2063
+ while (p) {
2064
+ if (CLASS_LIKE_DEF_TYPES.has(p.type))
2065
+ return true;
2066
+ if (p.type === "source_file")
2067
+ return false;
2068
+ p = p.parent;
2069
+ }
2070
+ return false;
2071
+ }
925
2072
  /** Check if a node is inside angle brackets of a type_name (trait type arguments). */
926
2073
  function isInsideTraitAngleBrackets(node) {
927
2074
  let n = node;
928
2075
  while (n) {
929
- if (n.type === "type_name" || n.type === "type_list")
2076
+ if (n.type === "type_name" || n.type === "type_list" || n.type === "isa_type_list")
930
2077
  return true;
931
2078
  // ERROR nodes inside isA declarations with angle brackets
932
2079
  if (n.type === "isa_declaration")
@@ -955,8 +2102,8 @@ function extractTraitNameFromAngleBrackets(node) {
955
2102
  const isa = n.parent;
956
2103
  for (let i = 0; i < isa.namedChildCount; i++) {
957
2104
  const child = isa.namedChild(i);
958
- if (child?.type === "type_list") {
959
- // type_list > type_name > qualified_name
2105
+ if (child?.type === "type_list" || child?.type === "isa_type_list") {
2106
+ // type_list/isa_type_list > type_name > qualified_name
960
2107
  const typeName = child.namedChild(0);
961
2108
  if (typeName?.type === "type_name") {
962
2109
  const qn = typeName.childForFieldName("name") ?? typeName.namedChild(0);