@adhisang/minecraft-modding-mcp 7.0.0 → 7.1.1

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 (95) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +4 -3
  3. package/dist/access-transformer-parser.d.ts +8 -0
  4. package/dist/access-transformer-parser.js +8 -1
  5. package/dist/access-widener-parser.d.ts +17 -0
  6. package/dist/access-widener-parser.js +12 -1
  7. package/dist/cache-registry.js +19 -6
  8. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  10. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  11. package/dist/entry-tools/batch-class-members-service.js +20 -6
  12. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  13. package/dist/entry-tools/batch-class-source-service.js +10 -0
  14. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  15. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  16. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  17. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
  18. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  19. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  20. package/dist/entry-tools/manage-cache-service.js +10 -14
  21. package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
  22. package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
  23. package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
  24. package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
  25. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  26. package/dist/entry-tools/verify-mixin-target-service.js +19 -8
  27. package/dist/index.js +38 -15
  28. package/dist/java-process.d.ts +1 -0
  29. package/dist/java-process.js +14 -0
  30. package/dist/mapping/lookup.js +16 -1
  31. package/dist/mapping-service.d.ts +14 -0
  32. package/dist/mapping-service.js +35 -15
  33. package/dist/minecraft-explorer-service.js +70 -8
  34. package/dist/mixin/access-validators.js +38 -2
  35. package/dist/mixin/annotation-validators.js +137 -43
  36. package/dist/mixin/parsed-validator.js +21 -7
  37. package/dist/mixin-parser.d.ts +52 -0
  38. package/dist/mixin-parser.js +709 -130
  39. package/dist/mod-decompile-service.js +11 -1
  40. package/dist/mod-remap-service.js +6 -6
  41. package/dist/nbt/java-nbt-codec.js +7 -1
  42. package/dist/source/access-validate.js +10 -0
  43. package/dist/source/artifact-resolver.d.ts +27 -3
  44. package/dist/source/artifact-resolver.js +235 -30
  45. package/dist/source/class-source/members-builder.d.ts +7 -0
  46. package/dist/source/class-source/members-builder.js +4 -1
  47. package/dist/source/class-source.d.ts +9 -2
  48. package/dist/source/class-source.js +186 -27
  49. package/dist/source/indexer.js +69 -1
  50. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  51. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  52. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  53. package/dist/source/lifecycle/runtime-check.js +68 -39
  54. package/dist/source/nested-jars.d.ts +15 -1
  55. package/dist/source/nested-jars.js +14 -5
  56. package/dist/source/search.d.ts +10 -2
  57. package/dist/source/search.js +60 -13
  58. package/dist/source/symbol-resolver.js +88 -0
  59. package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
  60. package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
  61. package/dist/source/validate-mixin.d.ts +5 -0
  62. package/dist/source/validate-mixin.js +136 -21
  63. package/dist/source/workspace-target.js +75 -7
  64. package/dist/source-jar-reader.d.ts +48 -1
  65. package/dist/source-jar-reader.js +93 -3
  66. package/dist/source-resolver.d.ts +7 -0
  67. package/dist/source-resolver.js +22 -14
  68. package/dist/source-service.d.ts +5 -0
  69. package/dist/source-service.js +7 -0
  70. package/dist/stdio-supervisor.d.ts +35 -1
  71. package/dist/stdio-supervisor.js +77 -2
  72. package/dist/storage/db.d.ts +62 -2
  73. package/dist/storage/db.js +181 -20
  74. package/dist/storage/files-repo.d.ts +7 -0
  75. package/dist/storage/files-repo.js +17 -4
  76. package/dist/storage/sqlite.d.ts +31 -1
  77. package/dist/storage/sqlite.js +125 -16
  78. package/dist/tool-contract-manifest.js +2 -2
  79. package/dist/tool-execution-gate.js +2 -1
  80. package/dist/tool-guidance.js +4 -1
  81. package/dist/tool-schemas.d.ts +64 -52
  82. package/dist/tool-schemas.js +9 -7
  83. package/dist/types.d.ts +9 -0
  84. package/dist/v1-parity-schemas.js +36 -2
  85. package/dist/version-diff-service.d.ts +23 -0
  86. package/dist/version-diff-service.js +101 -0
  87. package/dist/version-service.d.ts +14 -0
  88. package/dist/version-service.js +52 -3
  89. package/dist/workspace-context-cache.d.ts +25 -0
  90. package/dist/workspace-context-cache.js +52 -2
  91. package/dist/workspace-mapping-service.d.ts +8 -0
  92. package/dist/workspace-mapping-service.js +151 -21
  93. package/docs/README-ja.md +3 -1
  94. package/docs/tool-reference.md +69 -22
  95. package/package.json +1 -1
@@ -5,12 +5,15 @@
5
5
  /* ------------------------------------------------------------------ */
6
6
  /* Regex patterns */
7
7
  /* ------------------------------------------------------------------ */
8
+ // A type declaration keyword and its name; `.class` literals and `@interface` are not declarations.
9
+ const TYPE_DECL_RE = /(?<![\w$.@])(?:class|interface|enum|record)\s+([\w$]+)/;
8
10
  const CLASS_DECL_RE = /^\s*(?:@[\w$.]+(?:\([^)]*\))?\s+)*(?:public\s+|final\s+|abstract\s+|sealed\s+)*\b(?:class|interface)\s+(\w+)/;
9
11
  // import statements for FQCN resolution
10
12
  const IMPORT_RE = /^\s*import\s+([\w.]+)\s*;/;
11
13
  // @Mixin(Foo.class) or @Mixin({Foo.class, Bar.class}) or @Mixin(value = Foo.class)
12
14
  // Also handles @Mixin(value = {Foo.class, Bar.class}, priority = 900)
13
- const MIXIN_ANNOTATION_START_RE = /^\s*@Mixin\s*\(/;
15
+ // A @Mixin annotation anywhere on a line; matched against comment- and literal-free text.
16
+ const MIXIN_ANNOTATION_ANYWHERE_RE = /@Mixin\s*\(/g;
14
17
  // `$` is a legal Java identifier character (e.g. generated classes like My$Class);
15
18
  // without it the capture would start mid-name and yield a wrong target.
16
19
  const MIXIN_TARGET_RE = /([\w$][\w.$]*?)\.class/g;
@@ -20,17 +23,20 @@ const MIXIN_TARGETS_STRING_RE = /targets\s*=\s*(?:\{([^}]+)\}|"([^"]+)")/;
20
23
  const MIXIN_TARGETS_STRING_ITEM_RE = /"([^"]+)"/g;
21
24
  // Injection annotations: @Inject, @Redirect, @ModifyArg, @ModifyVariable, @ModifyConstant, @ModifyExpressionValue
22
25
  // Also MixinExtras: @WrapOperation, @WrapWithCondition, @ModifyReturnValue
23
- const INJECTION_ANNOTATION_RE = /^\s*@(Inject|Redirect|ModifyArg|ModifyVariable|ModifyConstant|ModifyExpressionValue|WrapOperation|WrapWithCondition|ModifyReturnValue)\s*\(/;
24
- const METHOD_ATTR_RE = /method\s*=\s*"([^"]+)"/;
25
- const METHOD_ATTR_ARRAY_RE = /method\s*=\s*\{([^}]+)\}/;
26
- const METHOD_ATTR_ITEM_RE = /"([^"]+)"/g;
26
+ // Matched at an annotation boundary found by annotationStarts(), so an annotation that is
27
+ // not the first token on its line is still recognised.
28
+ const INJECTION_ANNOTATION_RE = /^@(Inject|Redirect|ModifyArg|ModifyVariable|ModifyConstant|ModifyExpressionValue|WrapOperation|WrapWithCondition|ModifyReturnValue)\s*\(/;
27
29
  // @Shadow field / method
28
- const SHADOW_ANNOTATION_RE = /^\s*@Shadow\b/;
30
+ const SHADOW_ANNOTATION_RE = /^@Shadow\b/;
31
+ // A @Shadow member declared as `<prefix>name` targets `name`; Mixin defaults the prefix
32
+ // to "shadow$" and lets `prefix = "..."` override it — including with "", which means
33
+ // strip nothing. `aliases` adds fallback names.
34
+ const SHADOW_DEFAULT_PREFIX = "shadow$";
29
35
  const FIELD_DECL_RE = /(?:private|protected|public)?\s*(?:static\s+)?(?:final\s+)?(?:volatile\s+)?([\w.][\w<>,.\s?\[\]]*?)\s+([\w$]+)\s*[;=]/;
30
36
  const METHOD_DECL_RE = /(?:private|protected|public)?\s*(?:default\s+)?(?:static\s+)?(?:synchronized\s+)?(?:abstract\s+)?(?:native\s+)?(?:<[\w<>,.\s?&\[\]]+>\s+)?([\w.][\w<>,.\s?\[\]]*?)\s+([\w$]+)\s*\(/;
31
37
  // @Accessor / @Invoker
32
- const ACCESSOR_ANNOTATION_RE = /^\s*@(Accessor|Invoker)\s*(?:\(\s*\)|\(\s*(?:value\s*=\s*)?"([^"]+)"\s*(?:,\s*\w+\s*=\s*(?:\w+|"[^"]*")\s*)*\))?(?:\s|$)/;
33
- const ACCESSOR_ANNOTATION_START_RE = /^\s*@(Accessor|Invoker)\s*\(/;
38
+ const ACCESSOR_ANNOTATION_RE = /^@(Accessor|Invoker)\s*(?:\(\s*\)|\(\s*(?:value\s*=\s*)?"([^"]+)"\s*(?:,\s*\w+\s*=\s*(?:\w+|"[^"]*")\s*)*\))?(?:\s|$)/;
39
+ const ACCESSOR_ANNOTATION_START_RE = /^@(Accessor|Invoker)\s*\(/;
34
40
  const ACCESSOR_EXPLICIT_RE = /"([^"]+)"/;
35
41
  // Naming conventions for accessor/invoker target inference
36
42
  const GETTER_PREFIX_RE = /^(?:get|is)([A-Z].*)/;
@@ -39,33 +45,18 @@ const INVOKER_PREFIX_RE = /^(?:invoke|call)([A-Z].*)/;
39
45
  /* ------------------------------------------------------------------ */
40
46
  /* Helpers */
41
47
  /* ------------------------------------------------------------------ */
42
- function collectMultilineAnnotation(lines, startIndex) {
43
- let depth = 0;
44
- let text = "";
45
- for (let i = startIndex; i < lines.length; i++) {
46
- const line = lines[i];
47
- text += (i === startIndex ? "" : "\n") + line;
48
- for (const ch of line) {
49
- if (ch === "(")
50
- depth++;
51
- if (ch === ")")
52
- depth--;
53
- }
54
- if (depth <= 0) {
55
- return { text, endIndex: i };
56
- }
57
- }
58
- return { text, endIndex: lines.length - 1 };
59
- }
60
48
  /**
61
- * Blank out `/* *\/` block comments and `//` line comments (outside string literals)
62
- * while preserving line count and line numbers, so the annotation scans never match
63
- * inside commented-out code.
49
+ * Blank out `/* *\/` block comments and `//` line comments (outside string and char
50
+ * literals) while preserving line count and line numbers, so the annotation scans never
51
+ * match inside commented-out code. A Java text block (`"""..."""`) spans lines, so its
52
+ * body is blanked too, behind an empty `""` literal: the line-based scans would otherwise
53
+ * read annotation-shaped text inside it as live code.
64
54
  */
65
55
  function stripComments(source) {
66
56
  let out = "";
67
57
  let inBlock = false;
68
58
  let inString = false;
59
+ let inTextBlock = false;
69
60
  for (let i = 0; i < source.length; i++) {
70
61
  const ch = source[i];
71
62
  const next = source[i + 1];
@@ -80,6 +71,22 @@ function stripComments(source) {
80
71
  }
81
72
  continue;
82
73
  }
74
+ if (inTextBlock) {
75
+ if (ch === "\\" && next !== undefined) {
76
+ // An escape (including `\"""`) never closes the block; `\<newline>` keeps its line.
77
+ out += next === "\n" ? " \n" : " ";
78
+ i++;
79
+ }
80
+ else if (ch === '"' && next === '"' && source[i + 2] === '"') {
81
+ out += " ";
82
+ i += 2;
83
+ inTextBlock = false;
84
+ }
85
+ else {
86
+ out += ch === "\n" ? "\n" : " ";
87
+ }
88
+ continue;
89
+ }
83
90
  if (inString) {
84
91
  out += ch;
85
92
  if (ch === "\\" && next !== undefined) {
@@ -91,11 +98,38 @@ function stripComments(source) {
91
98
  }
92
99
  continue;
93
100
  }
101
+ if (ch === '"' && next === '"' && source[i + 2] === '"') {
102
+ out += '"" ';
103
+ i += 2;
104
+ inTextBlock = true;
105
+ continue;
106
+ }
94
107
  if (ch === '"') {
95
108
  inString = true;
96
109
  out += ch;
97
110
  continue;
98
111
  }
112
+ if (ch === "'") {
113
+ // Copy a char literal through its closing quote, so '"' does not open a string.
114
+ out += ch;
115
+ i++;
116
+ while (i < source.length && source[i] !== "'" && source[i] !== "\n") {
117
+ if (source[i] === "\\" && i + 1 < source.length && source[i + 1] !== "\n") {
118
+ out += source[i] + source[i + 1];
119
+ i += 2;
120
+ continue;
121
+ }
122
+ out += source[i];
123
+ i++;
124
+ }
125
+ if (source[i] === "'") {
126
+ out += "'";
127
+ }
128
+ else {
129
+ i--; // unterminated on this line: let the newline be handled normally
130
+ }
131
+ continue;
132
+ }
99
133
  if (ch === "/" && next === "*") {
100
134
  out += " ";
101
135
  i++;
@@ -113,11 +147,131 @@ function stripComments(source) {
113
147
  }
114
148
  return out;
115
149
  }
150
+ /**
151
+ * `line` with the contents of its string and char literals blanked and its quotes kept, so a
152
+ * brace, parenthesis or keyword inside a literal is never read as code. Positions are kept.
153
+ * Applied to `stripComments` output, where no literal spans a line.
154
+ */
155
+ function blankLiterals(line) {
156
+ let out = "";
157
+ for (let i = 0; i < line.length; i++) {
158
+ const ch = line[i];
159
+ out += ch;
160
+ if (ch !== '"' && ch !== "'") {
161
+ continue;
162
+ }
163
+ i++;
164
+ while (i < line.length && line[i] !== ch) {
165
+ const width = line[i] === "\\" && i + 1 < line.length ? 2 : 1;
166
+ out += " ".repeat(width);
167
+ i += width;
168
+ }
169
+ if (i < line.length) {
170
+ out += ch;
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+ /**
176
+ * The class decorated by the annotation whose `@` sits at `codeLines[startIndex][offset]`:
177
+ * its simple name, the 0-based position of its declaration keyword and the 0-based line of
178
+ * the brace closing its body, found by skipping the
179
+ * annotation's argument list, then the declaration keyword, then balancing braces. `codeLines`
180
+ * carry no comments or literal contents, so only code braces count. Null when no class body
181
+ * follows the annotation.
182
+ */
183
+ function findAnnotatedClassBody(codeLines, startIndex, offset) {
184
+ let lineIndex = startIndex;
185
+ let col = offset;
186
+ let depth = 0;
187
+ let argsClosed = false;
188
+ argsScan: for (; lineIndex < codeLines.length; lineIndex++, col = 0) {
189
+ const text = codeLines[lineIndex];
190
+ for (; col < text.length; col++) {
191
+ if (text[col] === "(") {
192
+ depth++;
193
+ }
194
+ else if (text[col] === ")" && depth > 0) {
195
+ depth--;
196
+ if (depth === 0) {
197
+ col++;
198
+ argsClosed = true;
199
+ break argsScan;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ if (!argsClosed) {
205
+ return null;
206
+ }
207
+ let className;
208
+ let declColumn = 0;
209
+ for (; lineIndex < codeLines.length; lineIndex++, col = 0) {
210
+ const declMatch = TYPE_DECL_RE.exec(codeLines[lineIndex].slice(col));
211
+ if (declMatch) {
212
+ className = declMatch[1];
213
+ declColumn = col + declMatch.index;
214
+ col += declMatch.index + declMatch[0].length;
215
+ break;
216
+ }
217
+ }
218
+ if (className === undefined) {
219
+ return null;
220
+ }
221
+ const declLine = lineIndex;
222
+ // The body opens at the first `{` outside the header's parentheses (annotation arguments
223
+ // such as `extends @Mark({1, 2}) Base`) and type-argument brackets.
224
+ let parenDepth = 0;
225
+ let angleDepth = 0;
226
+ let bodyOpen = false;
227
+ headerScan: for (; lineIndex < codeLines.length; lineIndex++, col = 0) {
228
+ const text = codeLines[lineIndex];
229
+ for (; col < text.length; col++) {
230
+ const ch = text[col];
231
+ if (ch === "(") {
232
+ parenDepth++;
233
+ }
234
+ else if (ch === ")" && parenDepth > 0) {
235
+ parenDepth--;
236
+ }
237
+ else if (parenDepth === 0 && ch === "<") {
238
+ angleDepth++;
239
+ }
240
+ else if (parenDepth === 0 && ch === ">" && angleDepth > 0) {
241
+ angleDepth--;
242
+ }
243
+ else if (parenDepth === 0 && angleDepth === 0 && ch === "{") {
244
+ bodyOpen = true;
245
+ break headerScan;
246
+ }
247
+ }
248
+ }
249
+ if (!bodyOpen) {
250
+ return null;
251
+ }
252
+ depth = 0;
253
+ for (; lineIndex < codeLines.length; lineIndex++, col = 0) {
254
+ const text = codeLines[lineIndex];
255
+ for (; col < text.length; col++) {
256
+ if (text[col] === "{") {
257
+ depth++;
258
+ }
259
+ else if (text[col] === "}" && depth > 0) {
260
+ depth--;
261
+ if (depth === 0) {
262
+ return { className, declLine, declColumn, endIndex: lineIndex };
263
+ }
264
+ }
265
+ }
266
+ }
267
+ return null;
268
+ }
116
269
  /**
117
270
  * Skip past annotations (including multi-line ones) starting at `startIndex`.
118
271
  * Returns the index of the first non-annotation line.
119
272
  * Lines with inline annotation + declaration (e.g. `@Nullable private int x;`)
120
- * are treated as declaration lines (not skipped).
273
+ * are treated as declaration lines (not skipped); `declText` then carries the text after
274
+ * the annotations with arguments, which may end a wrapped argument list on that line.
121
275
  */
122
276
  function isUpperCaseLetter(ch) {
123
277
  return ch !== ch.toLowerCase() && ch === ch.toUpperCase();
@@ -152,28 +306,11 @@ function skipAnnotations(lines, startIndex) {
152
306
  break;
153
307
  }
154
308
  if (lines[idx].includes("(")) {
155
- // Check if parentheses are unbalanced (multi-line annotation) — always skip
156
- let depth = 0;
157
- for (const ch of lines[idx]) {
158
- if (ch === "(")
159
- depth++;
160
- if (ch === ")")
161
- depth--;
162
- }
163
- if (depth > 0) {
164
- // Multi-line annotation — skip to its end
165
- const { endIndex } = collectMultilineAnnotation(lines, idx);
166
- idx = endIndex + 1;
167
- continue;
168
- }
169
- // Single-line annotation with parens: check if declaration text remains
170
- const stripped = stripInlineAnnotations(lines[idx]);
171
- if (stripped === "") {
172
- idx++;
173
- }
174
- else {
175
- break; // declaration with inline annotation
309
+ const run = leadingAnnotationRun(lines, idx);
310
+ if (run.rest !== "") {
311
+ return { index: run.endIndex, declText: run.rest }; // declaration with inline annotation
176
312
  }
313
+ idx = run.endIndex + 1;
177
314
  }
178
315
  else {
179
316
  // Simple annotation like `@Final` — check for trailing declaration
@@ -186,7 +323,7 @@ function skipAnnotations(lines, startIndex) {
186
323
  }
187
324
  }
188
325
  }
189
- return idx;
326
+ return { index: idx };
190
327
  }
191
328
  /** Strip inline annotations (e.g. `@Final @Nullable`) from a declaration line. */
192
329
  const INLINE_ANNOTATION_RE = /\s*@[\w$.]+(?:\([^)]*\))?\s*/g;
@@ -208,6 +345,245 @@ function decapitalize(name) {
208
345
  }
209
346
  return name.charAt(0).toLowerCase() + name.slice(1);
210
347
  }
348
+ /**
349
+ * Index just past the `)` closing the argument list that opens at `open`, or -1 when it
350
+ * does not close on this line. String and char literals (with backslash escapes) are
351
+ * skipped, so a parenthesis inside `@SuppressWarnings("(")` does not shift the balance.
352
+ */
353
+ function skipAnnotationArgs(line, open) {
354
+ let depth = 0;
355
+ for (let i = open; i < line.length; i++) {
356
+ const ch = line[i];
357
+ if (ch === '"' || ch === "'") {
358
+ i++;
359
+ while (i < line.length && line[i] !== ch) {
360
+ i += line[i] === "\\" ? 2 : 1;
361
+ }
362
+ if (i >= line.length) {
363
+ return -1; // literal never closes on this line
364
+ }
365
+ continue;
366
+ }
367
+ if (ch === "(") {
368
+ depth++;
369
+ }
370
+ else if (ch === ")") {
371
+ depth--;
372
+ if (depth === 0) {
373
+ return i + 1;
374
+ }
375
+ }
376
+ }
377
+ return -1;
378
+ }
379
+ /**
380
+ * Offsets of the annotations that open `line`: `@Final @Shadow private int foo;` yields the
381
+ * offsets of `@Final` and `@Shadow`. Argument lists are skipped as a unit, so an annotation
382
+ * name inside a string argument is never reported and nested parentheses do not end the scan
383
+ * early. The scan is line-local: a leading annotation whose argument list does not close on
384
+ * this line ends it, and the annotations behind it are simply not offered.
385
+ */
386
+ function annotationStarts(line) {
387
+ const starts = [];
388
+ let i = 0;
389
+ while (i < line.length && /\s/.test(line[i])) {
390
+ i++;
391
+ }
392
+ while (line[i] === "@") {
393
+ starts.push(i);
394
+ i++;
395
+ while (i < line.length && /[\w$.]/.test(line[i])) {
396
+ i++;
397
+ }
398
+ let next = i;
399
+ while (next < line.length && /\s/.test(line[next])) {
400
+ next++;
401
+ }
402
+ if (line[next] === "(") {
403
+ const close = skipAnnotationArgs(line, next);
404
+ if (close < 0) {
405
+ return starts;
406
+ }
407
+ next = close;
408
+ while (next < line.length && /\s/.test(line[next])) {
409
+ next++;
410
+ }
411
+ }
412
+ i = next;
413
+ }
414
+ return starts;
415
+ }
416
+ /**
417
+ * Run an annotation pattern against every annotation that opens `line`, so a target
418
+ * annotation preceded by others on the same line is still found. The offset comes back with
419
+ * the match because the annotation's own arguments must be read from there, not from the
420
+ * line start — a preceding `@Meta(method = "other")` would otherwise win.
421
+ */
422
+ function matchLineAnnotation(line, pattern) {
423
+ for (const offset of annotationStarts(line)) {
424
+ const match = pattern.exec(line.slice(offset));
425
+ if (match) {
426
+ return { match, offset };
427
+ }
428
+ }
429
+ return null;
430
+ }
431
+ /**
432
+ * Argument list of the annotation that opens `annotationText` (the text between its
433
+ * parentheses), or undefined when it has none. String and char literals are skipped while
434
+ * looking for the closing parenthesis; an argument list that never closes yields the rest.
435
+ */
436
+ function annotationArguments(annotationText) {
437
+ const open = /^@[\w$.]+\s*\(/.exec(annotationText);
438
+ if (!open) {
439
+ return undefined;
440
+ }
441
+ const start = open[0].length;
442
+ const close = skipAnnotationArgs(annotationText, start - 1);
443
+ return close < 0 ? annotationText.slice(start) : annotationText.slice(start, close - 1);
444
+ }
445
+ /**
446
+ * Split `text` at its top-level commas. Commas inside string or char literals, `{...}`
447
+ * arrays and nested `(...)` annotations do not split.
448
+ */
449
+ function splitTopLevel(text) {
450
+ const parts = [];
451
+ let depth = 0;
452
+ let start = 0;
453
+ for (let i = 0; i < text.length; i++) {
454
+ const ch = text[i];
455
+ if (ch === '"' || ch === "'") {
456
+ i++;
457
+ while (i < text.length && text[i] !== ch) {
458
+ i += text[i] === "\\" ? 2 : 1;
459
+ }
460
+ continue;
461
+ }
462
+ if (ch === "(" || ch === "{") {
463
+ depth++;
464
+ }
465
+ else if (ch === ")" || ch === "}") {
466
+ depth--;
467
+ }
468
+ else if (ch === "," && depth === 0) {
469
+ parts.push(text.slice(start, i));
470
+ start = i + 1;
471
+ }
472
+ }
473
+ parts.push(text.slice(start));
474
+ return parts;
475
+ }
476
+ /**
477
+ * Top-level `name = value` pairs of an annotation argument list, so a selector such as
478
+ * `"tick{2}"` or `"a)b"` stays inside its attribute. A bare value is keyed `value`.
479
+ */
480
+ function annotationAttributes(args) {
481
+ const attributes = new Map();
482
+ for (const part of splitTopLevel(args)) {
483
+ const named = /^\s*([\w$]+)\s*=\s*([\s\S]*?)\s*$/.exec(part);
484
+ if (named) {
485
+ attributes.set(named[1], named[2]);
486
+ }
487
+ else if (part.trim() !== "") {
488
+ attributes.set("value", part.trim());
489
+ }
490
+ }
491
+ return attributes;
492
+ }
493
+ /** Contents of `text` when it is exactly one string literal (escapes left as written). */
494
+ function exactStringLiteral(text) {
495
+ return /^\s*"((?:[^"\\\n]|\\.)*)"\s*$/.exec(text)?.[1];
496
+ }
497
+ /**
498
+ * Read an attribute value that is one string literal or an array of them. Anything else —
499
+ * a constant, a concatenation, a method call — cannot be read statically and is counted in
500
+ * `unreadable` (per array element) instead of contributing a string.
501
+ */
502
+ function readStringValues(value) {
503
+ const trimmed = value.trim();
504
+ const isArray = trimmed.startsWith("{") && trimmed.endsWith("}");
505
+ const elements = isArray
506
+ ? splitTopLevel(trimmed.slice(1, -1)).filter((element) => element.trim() !== "")
507
+ : [trimmed];
508
+ const values = [];
509
+ let unreadable = 0;
510
+ for (const element of elements) {
511
+ const literal = exactStringLiteral(element);
512
+ if (literal === undefined) {
513
+ unreadable++;
514
+ }
515
+ else {
516
+ values.push(literal);
517
+ }
518
+ }
519
+ return { isArray, values, unreadable };
520
+ }
521
+ /**
522
+ * The annotation that starts at `offset` on `lines[startIndex]`, extended over later lines
523
+ * until its argument list closes; string and char literals are skipped while balancing the
524
+ * parentheses. `close` indexes just past the closing `)` in `text`. An annotation without
525
+ * an argument list, or one whose list never closes, spans only its own line (`close` -1).
526
+ */
527
+ function collectAnnotationSpan(lines, startIndex, offset) {
528
+ const first = lines[startIndex].slice(offset);
529
+ const open = /^@[\w$.]+\s*\(/.exec(first);
530
+ if (!open) {
531
+ return { text: first, endIndex: startIndex, close: -1 };
532
+ }
533
+ let text = first;
534
+ let end = startIndex;
535
+ let close = skipAnnotationArgs(text, open[0].length - 1);
536
+ while (close < 0 && end + 1 < lines.length) {
537
+ end++;
538
+ text += "\n" + lines[end];
539
+ close = skipAnnotationArgs(text, open[0].length - 1);
540
+ }
541
+ return close < 0
542
+ ? { text: first, endIndex: startIndex, close: -1 }
543
+ : { text, endIndex: end, close };
544
+ }
545
+ /**
546
+ * The annotations that open `lines[startIndex]`, each extended over the lines its argument
547
+ * list spans (literal-aware, as in `collectAnnotationSpan`): the 0-based line where the last
548
+ * one ends and the trimmed text that follows it there. An argument list that never closes
549
+ * runs to the end of the file, leaving no text behind it.
550
+ */
551
+ function leadingAnnotationRun(lines, startIndex) {
552
+ let lineIndex = startIndex;
553
+ let pos = lines[startIndex].search(/\S/);
554
+ while (pos >= 0 && lines[lineIndex][pos] === "@") {
555
+ const span = collectAnnotationSpan(lines, lineIndex, pos);
556
+ if (span.close >= 0) {
557
+ pos = span.endIndex === lineIndex
558
+ ? pos + span.close
559
+ : span.close - (span.text.lastIndexOf("\n") + 1);
560
+ lineIndex = span.endIndex;
561
+ }
562
+ else if (/^@[\w$.]+\s*\(/.test(span.text)) {
563
+ return { endIndex: lines.length - 1, rest: "" };
564
+ }
565
+ else {
566
+ pos += /^@[\w$.]*/.exec(span.text)[0].length;
567
+ }
568
+ while (pos < lines[lineIndex].length && /\s/.test(lines[lineIndex][pos])) {
569
+ pos++;
570
+ }
571
+ }
572
+ return { endIndex: lineIndex, rest: pos < 0 ? "" : lines[lineIndex].slice(pos).trim() };
573
+ }
574
+ /**
575
+ * Build a @Shadow entry, resolving the member it targets: for a method, the declared name
576
+ * with the annotation's prefix removed. A name that does not carry the prefix is left alone,
577
+ * and a name that is *only* the prefix would leave nothing to look up, so it stays too.
578
+ * Mixin strips the prefix from methods only (a prefixed field is rejected), so a field
579
+ * keeps its declared name.
580
+ */
581
+ function makeShadow(kind, name, line, prefix, aliases) {
582
+ const targetName = kind === "method" && prefix !== "" && name.length > prefix.length && name.startsWith(prefix)
583
+ ? name.slice(prefix.length)
584
+ : name;
585
+ return { kind, name, targetName, aliases, line };
586
+ }
211
587
  function inferAccessorTarget(methodName) {
212
588
  const getterMatch = GETTER_PREFIX_RE.exec(methodName);
213
589
  if (getterMatch) {
@@ -228,8 +604,11 @@ function inferAccessorTarget(methodName) {
228
604
  /* ------------------------------------------------------------------ */
229
605
  export function parseMixinSource(source) {
230
606
  const lines = stripComments(source).split(/\r?\n/);
607
+ const codeLines = lines.map(blankLiterals);
608
+ const typePaths = memberTypePaths(codeLines);
231
609
  const parseWarnings = [];
232
610
  const targets = [];
611
+ const mixins = [];
233
612
  const injections = [];
234
613
  const shadows = [];
235
614
  const accessors = [];
@@ -248,45 +627,93 @@ export function parseMixinSource(source) {
248
627
  }
249
628
  }
250
629
  }
251
- // --- Pass 1: find @Mixin annotation and class name ---
630
+ // --- Pass 1: find @Mixin annotations and class name ---
631
+ const recordMixin = (mixinText, lineIndex, offset) => {
632
+ // `value` (class literals) and `targets` (string names) are both contributed
633
+ // when both are present, so the string form is collected alongside the class
634
+ // literals rather than only as a fallback.
635
+ const annotationTargets = [];
636
+ // Class literals are read with string contents blanked, so `targets = "a.class_1"` adds no `a`.
637
+ const mixinCode = mixinText.split("\n").map(blankLiterals).join("\n");
638
+ MIXIN_TARGET_RE.lastIndex = 0;
639
+ let match;
640
+ while ((match = MIXIN_TARGET_RE.exec(mixinCode)) !== null) {
641
+ annotationTargets.push(match[1]);
642
+ }
643
+ // targets = "..." or targets = {"a", "b"} string form
644
+ const targetsStringMatch = MIXIN_TARGETS_STRING_RE.exec(mixinText);
645
+ if (targetsStringMatch) {
646
+ const arrayContent = targetsStringMatch[1]; // {..."..."...} content
647
+ const singleTarget = targetsStringMatch[2]; // single "..." content
648
+ if (arrayContent) {
649
+ MIXIN_TARGETS_STRING_ITEM_RE.lastIndex = 0;
650
+ let itemMatch;
651
+ while ((itemMatch = MIXIN_TARGETS_STRING_ITEM_RE.exec(arrayContent)) !== null) {
652
+ annotationTargets.push(itemMatch[1]);
653
+ }
654
+ }
655
+ else if (singleTarget) {
656
+ annotationTargets.push(singleTarget);
657
+ }
658
+ }
659
+ const ownTargets = [];
660
+ for (const targetName of annotationTargets) {
661
+ if (!targets.some((entry) => entry.className === targetName)) {
662
+ targets.push({ className: targetName });
663
+ }
664
+ if (!ownTargets.some((entry) => entry.className === targetName)) {
665
+ ownTargets.push({ className: targetName });
666
+ }
667
+ }
668
+ const priorityMatch = MIXIN_PRIORITY_RE.exec(mixinText);
669
+ const ownPriority = priorityMatch ? parseInt(priorityMatch[1], 10) : undefined;
670
+ if (ownPriority !== undefined) {
671
+ priority = ownPriority;
672
+ }
673
+ const body = findAnnotatedClassBody(codeLines, lineIndex, offset);
674
+ if (body) {
675
+ const classPath = typePaths.get(`${body.declLine}:${body.declColumn}`);
676
+ mixins.push({
677
+ className: body.className,
678
+ targets: ownTargets,
679
+ startLine: lineIndex + 1,
680
+ endLine: body.endIndex + 1,
681
+ ...(classPath ? { classPath } : {}),
682
+ ...(ownPriority !== undefined ? { priority: ownPriority } : {})
683
+ });
684
+ }
685
+ };
686
+ // Every @Mixin is found in the comment- and literal-free text and read from its own offset
687
+ // over the lines its argument list spans; the rest of the line where that list closes is
688
+ // then scanned for another one (`@Mixin(A.class) class A {} @Mixin(B.class) class B {}`).
252
689
  let i = 0;
690
+ let fromCol = 0;
253
691
  while (i < lines.length) {
254
- if (MIXIN_ANNOTATION_START_RE.test(lines[i])) {
255
- const { text: mixinText, endIndex } = collectMultilineAnnotation(lines, i);
256
- MIXIN_TARGET_RE.lastIndex = 0;
257
- let match;
258
- while ((match = MIXIN_TARGET_RE.exec(mixinText)) !== null) {
259
- targets.push({ className: match[1] });
260
- }
261
- // Fallback: parse targets = "..." or targets = {"a", "b"} string form
262
- if (targets.length === 0) {
263
- const targetsStringMatch = MIXIN_TARGETS_STRING_RE.exec(mixinText);
264
- if (targetsStringMatch) {
265
- const arrayContent = targetsStringMatch[1]; // {..."..."...} content
266
- const singleTarget = targetsStringMatch[2]; // single "..." content
267
- if (arrayContent) {
268
- MIXIN_TARGETS_STRING_ITEM_RE.lastIndex = 0;
269
- let itemMatch;
270
- while ((itemMatch = MIXIN_TARGETS_STRING_ITEM_RE.exec(arrayContent)) !== null) {
271
- targets.push({ className: itemMatch[1] });
272
- }
273
- }
274
- else if (singleTarget) {
275
- targets.push({ className: singleTarget });
276
- }
277
- }
692
+ if (fromCol === 0) {
693
+ const classMatch = CLASS_DECL_RE.exec(lines[i]);
694
+ if (classMatch && !className) {
695
+ className = classMatch[1];
278
696
  }
279
- const priorityMatch = MIXIN_PRIORITY_RE.exec(mixinText);
280
- if (priorityMatch) {
281
- priority = parseInt(priorityMatch[1], 10);
697
+ }
698
+ MIXIN_ANNOTATION_ANYWHERE_RE.lastIndex = fromCol;
699
+ const inlineMixin = MIXIN_ANNOTATION_ANYWHERE_RE.exec(codeLines[i]);
700
+ if (inlineMixin) {
701
+ const span = collectAnnotationSpan(lines, i, inlineMixin.index);
702
+ if (span.close >= 0) {
703
+ recordMixin(span.text.slice(0, span.close), i, inlineMixin.index);
704
+ fromCol = span.endIndex === i
705
+ ? inlineMixin.index + span.close
706
+ : span.close - (span.text.lastIndexOf("\n") + 1);
707
+ i = span.endIndex;
708
+ }
709
+ else {
710
+ recordMixin(span.text, i, inlineMixin.index);
711
+ fromCol = 0;
712
+ i++;
282
713
  }
283
- i = endIndex + 1;
284
714
  continue;
285
715
  }
286
- const classMatch = CLASS_DECL_RE.exec(lines[i]);
287
- if (classMatch && !className) {
288
- className = classMatch[1];
289
- }
716
+ fromCol = 0;
290
717
  i++;
291
718
  }
292
719
  if (targets.length === 0) {
@@ -298,31 +725,34 @@ export function parseMixinSource(source) {
298
725
  const line = lines[i];
299
726
  const lineNum = i + 1;
300
727
  // --- @Inject / @Redirect / @ModifyArg etc. ---
301
- const injMatch = INJECTION_ANNOTATION_RE.exec(line);
302
- if (injMatch) {
303
- const annotation = injMatch[1];
304
- const { text: fullAnnotation, endIndex } = collectMultilineAnnotation(lines, i);
305
- const methodMatch = METHOD_ATTR_RE.exec(fullAnnotation);
306
- if (methodMatch) {
307
- injections.push({ annotation, method: methodMatch[1], line: lineNum });
728
+ const injMatched = matchLineAnnotation(line, INJECTION_ANNOTATION_RE);
729
+ if (injMatched) {
730
+ const annotation = injMatched.match[1];
731
+ // Read the attributes from this annotation's own span; a preceding annotation on the
732
+ // same line may carry a `method` attribute of its own.
733
+ const { text: fullAnnotation, endIndex } = collectAnnotationSpan(lines, i, injMatched.offset);
734
+ const methodValue = annotationAttributes(annotationArguments(fullAnnotation) ?? "").get("method");
735
+ if (methodValue === undefined) {
736
+ parseWarnings.push(`Line ${lineNum}: @${annotation} missing method attribute.`);
308
737
  }
309
738
  else {
310
- // Try array form: method = {"m1", "m2"}
311
- const arrayMatch = METHOD_ATTR_ARRAY_RE.exec(fullAnnotation);
312
- if (arrayMatch) {
313
- const inner = arrayMatch[1];
314
- METHOD_ATTR_ITEM_RE.lastIndex = 0;
315
- let itemMatch;
316
- let found = false;
317
- while ((itemMatch = METHOD_ATTR_ITEM_RE.exec(inner)) !== null) {
318
- injections.push({ annotation, method: itemMatch[1], line: lineNum });
319
- found = true;
739
+ const { isArray, values, unreadable } = readStringValues(methodValue);
740
+ const selectors = values.filter((selector) => selector !== "");
741
+ for (const selector of selectors) {
742
+ injections.push({ annotation, method: selector, line: lineNum });
743
+ }
744
+ if (isArray) {
745
+ for (let n = 0; n < unreadable; n++) {
746
+ parseWarnings.push(`Line ${lineNum}: @${annotation} could not parse method array element (not a string literal).`);
320
747
  }
321
- if (!found) {
748
+ if (selectors.length === 0 && unreadable === 0) {
322
749
  parseWarnings.push(`Line ${lineNum}: @${annotation} method array is empty.`);
323
750
  }
324
751
  }
325
- else {
752
+ else if (unreadable > 0) {
753
+ parseWarnings.push(`Line ${lineNum}: @${annotation} could not parse method attribute (not a string literal).`);
754
+ }
755
+ else if (selectors.length === 0) {
326
756
  parseWarnings.push(`Line ${lineNum}: @${annotation} missing method attribute.`);
327
757
  }
328
758
  }
@@ -330,35 +760,58 @@ export function parseMixinSource(source) {
330
760
  continue;
331
761
  }
332
762
  // --- @Shadow ---
333
- if (SHADOW_ANNOTATION_RE.test(line)) {
334
- // Try same-line declaration first (e.g. `@Shadow @Final private int x;`)
335
- const inlineDecl = stripInlineAnnotations(line);
763
+ const shadowMatched = matchLineAnnotation(line, SHADOW_ANNOTATION_RE);
764
+ if (shadowMatched) {
765
+ let shadowText = line.slice(shadowMatched.offset);
766
+ // Declaration text sharing a line with the annotation (e.g. `@Shadow @Final private
767
+ // int x;`, or `) private int x;` closing a wrapped `@Shadow(` argument list).
768
+ let inlineDecl = stripInlineAnnotations(line);
769
+ let annotationEnd = i;
770
+ // prefix/aliases are read from the whole argument list, even when it wraps.
771
+ const span = collectAnnotationSpan(lines, i, shadowMatched.offset);
772
+ if (span.close >= 0) {
773
+ shadowText = span.text;
774
+ inlineDecl = stripInlineAnnotations(span.text.slice(span.close));
775
+ annotationEnd = span.endIndex;
776
+ }
777
+ // An explicit `prefix = ""` means strip nothing, so absent and empty must stay apart.
778
+ const shadowAttributes = annotationAttributes(annotationArguments(shadowText) ?? "");
779
+ const prefixValue = shadowAttributes.get("prefix");
780
+ const prefix = prefixValue === undefined
781
+ ? SHADOW_DEFAULT_PREFIX
782
+ : exactStringLiteral(prefixValue) ?? SHADOW_DEFAULT_PREFIX;
783
+ const aliasesValue = shadowAttributes.get("aliases");
784
+ const aliasList = aliasesValue === undefined
785
+ ? []
786
+ : readStringValues(aliasesValue).values.filter((alias) => alias !== "");
787
+ const aliases = aliasList.length > 0 ? aliasList : undefined;
336
788
  const inlineMethodMatch = inlineDecl.includes("(") ? METHOD_DECL_RE.exec(inlineDecl) : null;
337
789
  const inlineFieldMatch = !inlineMethodMatch ? FIELD_DECL_RE.exec(inlineDecl) : null;
338
790
  if (inlineMethodMatch) {
339
- shadows.push({ kind: "method", name: inlineMethodMatch[2], line: lineNum });
340
- i++;
791
+ shadows.push(makeShadow("method", inlineMethodMatch[2], lineNum, prefix, aliases));
792
+ i = annotationEnd + 1;
341
793
  continue;
342
794
  }
343
795
  else if (inlineFieldMatch) {
344
- shadows.push({ kind: "field", name: inlineFieldMatch[2], line: lineNum });
345
- i++;
796
+ shadows.push(makeShadow("field", inlineFieldMatch[2], lineNum, prefix, aliases));
797
+ i = annotationEnd + 1;
346
798
  continue;
347
799
  }
348
- // Advance past @Shadow line to find the declaration
349
- let declLine = i + 1;
800
+ // Advance past the @Shadow annotation to find the declaration
801
+ let declLine = annotationEnd + 1;
350
802
  // Skip additional annotations between @Shadow and declaration (including multi-line)
351
- declLine = skipAnnotations(lines, declLine);
803
+ const skipped = skipAnnotations(lines, declLine);
804
+ declLine = skipped.index;
352
805
  if (declLine < lines.length) {
353
- const declText = stripInlineAnnotations(lines[declLine]);
806
+ const declText = stripInlineAnnotations(skipped.declText ?? lines[declLine]);
354
807
  const methodDeclMatch = METHOD_DECL_RE.exec(declText);
355
808
  const fieldDeclMatch = FIELD_DECL_RE.exec(declText);
356
809
  // Method if it has parentheses
357
810
  if (declText.includes("(") && methodDeclMatch) {
358
- shadows.push({ kind: "method", name: methodDeclMatch[2], line: lineNum });
811
+ shadows.push(makeShadow("method", methodDeclMatch[2], lineNum, prefix, aliases));
359
812
  }
360
813
  else if (fieldDeclMatch) {
361
- shadows.push({ kind: "field", name: fieldDeclMatch[2], line: lineNum });
814
+ shadows.push(makeShadow("field", fieldDeclMatch[2], lineNum, prefix, aliases));
362
815
  }
363
816
  else {
364
817
  parseWarnings.push(`Line ${lineNum}: Could not parse @Shadow member declaration.`);
@@ -368,21 +821,30 @@ export function parseMixinSource(source) {
368
821
  continue;
369
822
  }
370
823
  // --- @Accessor / @Invoker ---
371
- const accessorMatch = ACCESSOR_ANNOTATION_RE.exec(line);
372
- const accessorStartMatch = !accessorMatch ? ACCESSOR_ANNOTATION_START_RE.exec(line) : null;
373
- if (accessorMatch || accessorStartMatch) {
374
- const annotation = (accessorMatch?.[1] ?? accessorStartMatch?.[1]);
375
- let explicitTarget = accessorMatch?.[2];
376
- if (!explicitTarget && accessorStartMatch) {
377
- const { text: fullAnnotation, endIndex } = collectMultilineAnnotation(lines, i);
378
- const explicitMatch = ACCESSOR_EXPLICIT_RE.exec(fullAnnotation);
824
+ const accessorMatched = matchLineAnnotation(line, ACCESSOR_ANNOTATION_RE);
825
+ const accessorStartMatched = !accessorMatched
826
+ ? matchLineAnnotation(line, ACCESSOR_ANNOTATION_START_RE)
827
+ : null;
828
+ if (accessorMatched || accessorStartMatched) {
829
+ const annotation = (accessorMatched?.match[1] ?? accessorStartMatched?.match[1]);
830
+ let explicitTarget = accessorMatched?.match[2];
831
+ // Declaration text sharing a line with the annotation (e.g. `@Accessor("x") int getX();`,
832
+ // or `) int getX();` closing a wrapped `@Accessor(` argument list).
833
+ let inlineDecl = stripInlineAnnotations(line);
834
+ if (!explicitTarget && accessorStartMatched) {
835
+ // Read from this annotation's own arguments so a string argument of a preceding
836
+ // annotation on the same line is not mistaken for the explicit target.
837
+ const span = collectAnnotationSpan(lines, i, accessorStartMatched.offset);
838
+ const explicitMatch = ACCESSOR_EXPLICIT_RE.exec(annotationArguments(span.text) ?? "");
379
839
  if (explicitMatch) {
380
840
  explicitTarget = explicitMatch[1];
381
841
  }
382
- i = endIndex;
842
+ if (span.close >= 0) {
843
+ inlineDecl = stripInlineAnnotations(span.text.slice(span.close));
844
+ }
845
+ i = span.endIndex;
383
846
  }
384
- // Try same-line declaration first (e.g. `@Accessor("x") int getX();`)
385
- const inlineDecl = stripInlineAnnotations(line);
847
+ // Try same-line declaration first
386
848
  const inlineMethodMatch = inlineDecl.includes("(") ? METHOD_DECL_RE.exec(inlineDecl) : null;
387
849
  if (inlineMethodMatch) {
388
850
  const methodName = inlineMethodMatch[2];
@@ -393,9 +855,10 @@ export function parseMixinSource(source) {
393
855
  }
394
856
  // Find the method declaration following the annotation (skip multi-line annotations)
395
857
  let methodLine = i + 1;
396
- methodLine = skipAnnotations(lines, methodLine);
858
+ const skipped = skipAnnotations(lines, methodLine);
859
+ methodLine = skipped.index;
397
860
  if (methodLine < lines.length) {
398
- const methodDeclMatch = METHOD_DECL_RE.exec(stripInlineAnnotations(lines[methodLine]));
861
+ const methodDeclMatch = METHOD_DECL_RE.exec(stripInlineAnnotations(skipped.declText ?? lines[methodLine]));
399
862
  if (methodDeclMatch) {
400
863
  const methodName = methodDeclMatch[2];
401
864
  const targetName = explicitTarget ?? inferAccessorTarget(methodName);
@@ -410,9 +873,28 @@ export function parseMixinSource(source) {
410
873
  }
411
874
  i++;
412
875
  }
876
+ // Ownership: the innermost @Mixin class whose span holds the member's annotation line.
877
+ // Nested class spans nest, so the containing span that starts last is the innermost.
878
+ const assignOwner = (member) => {
879
+ let owner;
880
+ mixins.forEach((mixin, index) => {
881
+ if (mixin.startLine <= member.line &&
882
+ member.line <= mixin.endLine &&
883
+ (owner === undefined || mixin.startLine > mixins[owner].startLine)) {
884
+ owner = index;
885
+ }
886
+ });
887
+ if (owner !== undefined) {
888
+ member.mixinIndex = owner;
889
+ }
890
+ };
891
+ injections.forEach(assignOwner);
892
+ shadows.forEach(assignOwner);
893
+ accessors.forEach(assignOwner);
413
894
  return {
414
895
  className,
415
896
  targets,
897
+ mixins,
416
898
  priority,
417
899
  imports,
418
900
  injections,
@@ -421,4 +903,101 @@ export function parseMixinSource(source) {
421
903
  parseWarnings
422
904
  };
423
905
  }
906
+ /**
907
+ * The nesting path of each type declared in `codeLines` (comment- and literal-free), keyed by
908
+ * the `line:column` of its declaration keyword: the simple names from the top-level type down
909
+ * to it. A type declared inside a method, initializer or anonymous class body has no such path
910
+ * (its binary name is `Outer$1Local`) and is left out, as is every type nested in it.
911
+ */
912
+ function memberTypePaths(codeLines) {
913
+ // TYPE_DECL_RE, plus annotation-type declarations (`@interface Name`), which enclose member
914
+ // types too; an annotation usage never has a declaration keyword after its `@`.
915
+ const declRe = /(?<![\w$.@])(?:@\s*)?(?:class|interface|enum|record)\s+([\w$]+)/g;
916
+ const paths = new Map();
917
+ // One entry per open brace: the path of the type whose body it opens, or null for any other
918
+ // block (and for the body of a type that has no path).
919
+ const braces = [];
920
+ let parenDepth = 0;
921
+ let pending;
922
+ codeLines.forEach((text, lineIndex) => {
923
+ const declStarts = new Map();
924
+ for (const match of text.matchAll(declRe)) {
925
+ declStarts.set(match.index, match);
926
+ }
927
+ for (let col = 0; col < text.length; col++) {
928
+ const decl = declStarts.get(col);
929
+ if (decl) {
930
+ pending = { key: `${lineIndex}:${col}`, name: decl[1], parenDepth };
931
+ col += decl[0].length - 1;
932
+ continue;
933
+ }
934
+ const ch = text[col];
935
+ if (ch === "(") {
936
+ parenDepth++;
937
+ }
938
+ else if (ch === ")" && parenDepth > 0) {
939
+ parenDepth--;
940
+ }
941
+ else if (ch === ";" && pending?.parenDepth === parenDepth) {
942
+ pending = undefined;
943
+ }
944
+ else if (ch === "{") {
945
+ let path = null;
946
+ if (pending?.parenDepth === parenDepth) {
947
+ if (braces.every((entry) => entry !== null)) {
948
+ path = [...(braces.at(-1) ?? []), pending.name];
949
+ paths.set(pending.key, path);
950
+ }
951
+ pending = undefined;
952
+ }
953
+ braces.push(path);
954
+ }
955
+ else if (ch === "}") {
956
+ braces.pop();
957
+ }
958
+ }
959
+ });
960
+ return paths;
961
+ }
962
+ /**
963
+ * `parsed` narrowed to the nested `@Mixin` class at `classPath` (see `ParsedMixinClass.classPath`;
964
+ * `["Outer", "Inner"]` for a config entry `Outer$Inner`): its own targets and priority, and only
965
+ * the members and line-numbered parse warnings it owns. Undefined when no `@Mixin` class sits at
966
+ * that path.
967
+ */
968
+ export function selectNestedMixin(parsed, classPath) {
969
+ const mixins = parsed.mixins ?? [];
970
+ const index = mixins.findIndex((mixin) => mixin.classPath?.length === classPath.length && mixin.classPath.every((name, n) => name === classPath[n]));
971
+ if (index < 0) {
972
+ return undefined;
973
+ }
974
+ const mixin = mixins[index];
975
+ // The innermost @Mixin class whose span holds `line`, as for member ownership.
976
+ const ownerOf = (line) => {
977
+ let owner;
978
+ mixins.forEach((candidate, candidateIndex) => {
979
+ if (candidate.startLine <= line &&
980
+ line <= candidate.endLine &&
981
+ (owner === undefined || candidate.startLine > mixins[owner].startLine)) {
982
+ owner = candidateIndex;
983
+ }
984
+ });
985
+ return owner;
986
+ };
987
+ const own = (members) => members.filter((member) => member.mixinIndex === index).map((member) => ({ ...member, mixinIndex: 0 }));
988
+ return {
989
+ ...parsed,
990
+ className: mixin.className,
991
+ targets: mixin.targets,
992
+ mixins: [mixin],
993
+ priority: mixin.priority,
994
+ injections: own(parsed.injections),
995
+ shadows: own(parsed.shadows),
996
+ accessors: own(parsed.accessors),
997
+ parseWarnings: parsed.parseWarnings.filter((warning) => {
998
+ const lineMatch = /^Line (\d+):/.exec(warning);
999
+ return !lineMatch || ownerOf(Number.parseInt(lineMatch[1], 10)) === index;
1000
+ })
1001
+ };
1002
+ }
424
1003
  //# sourceMappingURL=mixin-parser.js.map