@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.
- package/CHANGELOG.md +72 -0
- package/README.md +4 -3
- package/dist/access-transformer-parser.d.ts +8 -0
- package/dist/access-transformer-parser.js +8 -1
- package/dist/access-widener-parser.d.ts +17 -0
- package/dist/access-widener-parser.js +12 -1
- package/dist/cache-registry.js +19 -6
- package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-members-service.js +20 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-source-service.js +10 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
- package/dist/entry-tools/compare-minecraft-service.js +65 -4
- package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
- package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.js +10 -14
- package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
- package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
- package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
- package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/entry-tools/verify-mixin-target-service.js +19 -8
- package/dist/index.js +38 -15
- package/dist/java-process.d.ts +1 -0
- package/dist/java-process.js +14 -0
- package/dist/mapping/lookup.js +16 -1
- package/dist/mapping-service.d.ts +14 -0
- package/dist/mapping-service.js +35 -15
- package/dist/minecraft-explorer-service.js +70 -8
- package/dist/mixin/access-validators.js +38 -2
- package/dist/mixin/annotation-validators.js +137 -43
- package/dist/mixin/parsed-validator.js +21 -7
- package/dist/mixin-parser.d.ts +52 -0
- package/dist/mixin-parser.js +709 -130
- package/dist/mod-decompile-service.js +11 -1
- package/dist/mod-remap-service.js +6 -6
- package/dist/nbt/java-nbt-codec.js +7 -1
- package/dist/source/access-validate.js +10 -0
- package/dist/source/artifact-resolver.d.ts +27 -3
- package/dist/source/artifact-resolver.js +235 -30
- package/dist/source/class-source/members-builder.d.ts +7 -0
- package/dist/source/class-source/members-builder.js +4 -1
- package/dist/source/class-source.d.ts +9 -2
- package/dist/source/class-source.js +186 -27
- package/dist/source/indexer.js +69 -1
- package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
- package/dist/source/lifecycle/mapping-helpers.js +29 -3
- package/dist/source/lifecycle/runtime-check.d.ts +25 -0
- package/dist/source/lifecycle/runtime-check.js +68 -39
- package/dist/source/nested-jars.d.ts +15 -1
- package/dist/source/nested-jars.js +14 -5
- package/dist/source/search.d.ts +10 -2
- package/dist/source/search.js +60 -13
- package/dist/source/symbol-resolver.js +88 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
- package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
- package/dist/source/validate-mixin.d.ts +5 -0
- package/dist/source/validate-mixin.js +136 -21
- package/dist/source/workspace-target.js +75 -7
- package/dist/source-jar-reader.d.ts +48 -1
- package/dist/source-jar-reader.js +93 -3
- package/dist/source-resolver.d.ts +7 -0
- package/dist/source-resolver.js +22 -14
- package/dist/source-service.d.ts +5 -0
- package/dist/source-service.js +7 -0
- package/dist/stdio-supervisor.d.ts +35 -1
- package/dist/stdio-supervisor.js +77 -2
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +181 -20
- package/dist/storage/files-repo.d.ts +7 -0
- package/dist/storage/files-repo.js +17 -4
- package/dist/storage/sqlite.d.ts +31 -1
- package/dist/storage/sqlite.js +125 -16
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +2 -1
- package/dist/tool-guidance.js +4 -1
- package/dist/tool-schemas.d.ts +64 -52
- package/dist/tool-schemas.js +9 -7
- package/dist/types.d.ts +9 -0
- package/dist/v1-parity-schemas.js +36 -2
- package/dist/version-diff-service.d.ts +23 -0
- package/dist/version-diff-service.js +101 -0
- package/dist/version-service.d.ts +14 -0
- package/dist/version-service.js +52 -3
- package/dist/workspace-context-cache.d.ts +25 -0
- package/dist/workspace-context-cache.js +52 -2
- package/dist/workspace-mapping-service.d.ts +8 -0
- package/dist/workspace-mapping-service.js +151 -21
- package/docs/README-ja.md +3 -1
- package/docs/tool-reference.md +69 -22
- package/package.json +1 -1
package/dist/mixin-parser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
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 =
|
|
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 =
|
|
33
|
-
const ACCESSOR_ANNOTATION_START_RE =
|
|
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
|
|
62
|
-
* while preserving line count and line numbers, so the annotation scans never
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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 (
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
|
302
|
-
if (
|
|
303
|
-
const annotation =
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let
|
|
317
|
-
|
|
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 (
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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(
|
|
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(
|
|
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
|
|
349
|
-
let declLine =
|
|
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
|
-
|
|
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(
|
|
811
|
+
shadows.push(makeShadow("method", methodDeclMatch[2], lineNum, prefix, aliases));
|
|
359
812
|
}
|
|
360
813
|
else if (fieldDeclMatch) {
|
|
361
|
-
shadows.push(
|
|
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
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|