@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
|
@@ -1,19 +1,70 @@
|
|
|
1
1
|
import { allFieldNames, allMethodNames, computeFalsePositiveRisk, extractMethodDescriptor, extractMethodName, suggestSimilar } from "./helpers.js";
|
|
2
|
+
// Mixin regex target selector: "/pattern/" or "owner=/…/ name=/…/ desc=/…/".
|
|
3
|
+
const REGEX_SELECTOR_RE = /^(?:(?:owner|name|desc)\s*=\s*)?\//;
|
|
4
|
+
// Trailing target-selector quantifier. For the brace form, group 1 is the minimum,
|
|
5
|
+
// group 2 the comma (open or explicit maximum) and group 3 the maximum.
|
|
6
|
+
const QUANTIFIER_RE = /(?:\*|\+|\{(\d*)(?:(,)(\d*))?\})$/;
|
|
2
7
|
export function validateInjection(inj, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
|
|
3
8
|
for (const targetName of targetNames) {
|
|
4
9
|
const members = targetMembers.get(targetName);
|
|
5
10
|
if (!members)
|
|
6
11
|
continue;
|
|
7
12
|
const methodNames = allMethodNames(members);
|
|
13
|
+
if (REGEX_SELECTOR_RE.test(inj.method.trim())) {
|
|
14
|
+
// Regex selectors ("/pattern/", "name=/pattern/ desc=/pattern/") match by pattern,
|
|
15
|
+
// which the name lookup below cannot evaluate; unverifiable.
|
|
16
|
+
resolvedMembers.push({
|
|
17
|
+
annotation: `@${inj.annotation}`,
|
|
18
|
+
name: inj.method,
|
|
19
|
+
line: inj.line,
|
|
20
|
+
status: "skipped"
|
|
21
|
+
});
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// Mixin strips all whitespace from a selector before parsing it ("tick{1, 3}").
|
|
25
|
+
const selector = inj.method.replace(/\s+/g, "");
|
|
8
26
|
// Strip owner prefix and JVM descriptor from the method reference
|
|
9
|
-
const rawMethodName = extractMethodName(
|
|
10
|
-
// Strip a trailing target-selector quantifier (e.g. "tick*", "tick+", "tick{2,3}")
|
|
11
|
-
|
|
27
|
+
const rawMethodName = extractMethodName(selector);
|
|
28
|
+
// Strip a trailing target-selector quantifier (e.g. "tick*", "tick+", "tick{2,3}").
|
|
29
|
+
// As in Mixin, the quantifier only bounds how many members the EXACT name (and
|
|
30
|
+
// descriptor, when given) must match; it never turns the name into a prefix glob.
|
|
31
|
+
const quantifierMatch = QUANTIFIER_RE.exec(rawMethodName);
|
|
32
|
+
const quantifier = quantifierMatch?.[0];
|
|
12
33
|
const methodName = quantifier
|
|
13
34
|
? rawMethodName.slice(0, rawMethodName.length - quantifier.length)
|
|
14
35
|
: rawMethodName;
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
let minMatches = quantifier === "+" ? 1 : 0;
|
|
37
|
+
if (quantifierMatch && quantifier !== "*" && quantifier !== "+") {
|
|
38
|
+
minMatches = quantifierMatch[1] ? Number.parseInt(quantifierMatch[1], 10) : 0;
|
|
39
|
+
const maxMatches = quantifierMatch[2] === undefined
|
|
40
|
+
? minMatches
|
|
41
|
+
: quantifierMatch[3]
|
|
42
|
+
? Number.parseInt(quantifierMatch[3], 10)
|
|
43
|
+
: Number.POSITIVE_INFINITY;
|
|
44
|
+
// Mixin clamps a max below the min up to the min ("{3,1}" acts as "{3}"), so only a
|
|
45
|
+
// quantifier whose clamped max is 0 ("{}", "{0}") allows no match; Mixin rejects it.
|
|
46
|
+
if (Math.max(minMatches, maxMatches) === 0) {
|
|
47
|
+
issues.push({
|
|
48
|
+
severity: "warning",
|
|
49
|
+
kind: "method-not-found",
|
|
50
|
+
annotation: `@${inj.annotation}`,
|
|
51
|
+
target: `${targetName}#${inj.method}`,
|
|
52
|
+
message: `Selector "${rawMethodName}" has a malformed quantifier "${quantifier}" (it allows no match); Mixin rejects it.`,
|
|
53
|
+
line: inj.line,
|
|
54
|
+
confidence: "likely"
|
|
55
|
+
});
|
|
56
|
+
resolvedMembers.push({
|
|
57
|
+
annotation: `@${inj.annotation}`,
|
|
58
|
+
name: rawMethodName,
|
|
59
|
+
line: inj.line,
|
|
60
|
+
status: "not-found"
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const anyName = quantifier !== undefined && methodName === "";
|
|
66
|
+
if (anyName && minMatches === 0 && !extractMethodDescriptor(selector)) {
|
|
67
|
+
// A bare "*" matches any method, or none at all; there is nothing to check.
|
|
17
68
|
resolvedMembers.push({
|
|
18
69
|
annotation: `@${inj.annotation}`,
|
|
19
70
|
name: rawMethodName,
|
|
@@ -22,12 +73,12 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
22
73
|
});
|
|
23
74
|
continue;
|
|
24
75
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
// An empty name with a quantifier ("*(I)V", "+", "{2}") matches any method name; its
|
|
77
|
+
// descriptor and count still apply below.
|
|
78
|
+
const matched = anyName || methodNames.includes(methodName);
|
|
28
79
|
if (!matched) {
|
|
29
80
|
const suggestions = suggestSimilar(methodName, methodNames);
|
|
30
|
-
const descriptor = extractMethodDescriptor(
|
|
81
|
+
const descriptor = extractMethodDescriptor(selector);
|
|
31
82
|
const descriptorHint = descriptor ? ` (descriptor: ${descriptor})` : "";
|
|
32
83
|
// Determine if this is a remap artifact or signature unavailability
|
|
33
84
|
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(methodName);
|
|
@@ -64,31 +115,39 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
64
115
|
// Name matched. If the reference carries a JVM descriptor, require an
|
|
65
116
|
// overload to match it; otherwise a wrong-signature target (e.g. tick(D)V
|
|
66
117
|
// when only tick(I)V exists) is silently reported as resolved.
|
|
67
|
-
const refDescriptor =
|
|
68
|
-
const sameNameMembers =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
const refDescriptor = extractMethodDescriptor(selector);
|
|
119
|
+
const sameNameMembers = anyName
|
|
120
|
+
? members.methods
|
|
121
|
+
: [...members.constructors, ...members.methods].filter((member) => member.name === methodName);
|
|
122
|
+
const displayName = anyName ? rawMethodName : methodName;
|
|
123
|
+
const matchCount = refDescriptor
|
|
124
|
+
? sameNameMembers.filter((member) => member.jvmDescriptor === refDescriptor).length
|
|
125
|
+
: sameNameMembers.length;
|
|
126
|
+
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(methodName);
|
|
127
|
+
const isSigFailed = signatureFailedTargets?.has(targetName);
|
|
128
|
+
// Only a healthy mapping lets us trust that the user-written descriptor (and the
|
|
129
|
+
// overload count) is byte-equal to the runtime one; otherwise downgrade to a warning.
|
|
130
|
+
const mappingDegraded = Boolean(isRemapFailed) || Boolean(isSigFailed) || healthReport?.overallHealthy === false;
|
|
131
|
+
const resolutionPath = isRemapFailed
|
|
132
|
+
? "member-remap-failed"
|
|
133
|
+
: isSigFailed
|
|
134
|
+
? "source-signature-unavailable"
|
|
135
|
+
: undefined;
|
|
136
|
+
const issueConfidence = mappingDegraded
|
|
137
|
+
? "uncertain"
|
|
138
|
+
: confidence ?? "definite";
|
|
139
|
+
if (refDescriptor && matchCount === 0 && (anyName || sameNameMembers.length > 0)) {
|
|
140
|
+
const available = anyName
|
|
141
|
+
? []
|
|
142
|
+
: [...new Set(sameNameMembers.map((member) => member.jvmDescriptor))];
|
|
86
143
|
issues.push({
|
|
87
144
|
severity: mappingDegraded ? "warning" : "error",
|
|
88
145
|
kind: "descriptor-mismatch",
|
|
89
146
|
annotation: `@${inj.annotation}`,
|
|
90
147
|
target: `${targetName}#${inj.method}`,
|
|
91
|
-
message:
|
|
148
|
+
message: `${anyName
|
|
149
|
+
? `No method in "${targetName}" matches descriptor ${refDescriptor}.`
|
|
150
|
+
: `Method "${methodName}" exists in "${targetName}" but no overload matches descriptor ${refDescriptor} (available: ${available.join(", ")}).`}${mappingDegraded ? " (mapping degraded; may be a remap artifact)" : ""}`,
|
|
92
151
|
suggestions: available.length > 0 ? available : undefined,
|
|
93
152
|
line: inj.line,
|
|
94
153
|
confidence: issueConfidence,
|
|
@@ -100,7 +159,31 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
100
159
|
});
|
|
101
160
|
resolvedMembers.push({
|
|
102
161
|
annotation: `@${inj.annotation}`,
|
|
103
|
-
name:
|
|
162
|
+
name: displayName,
|
|
163
|
+
line: inj.line,
|
|
164
|
+
status: "not-found"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (matchCount < minMatches) {
|
|
168
|
+
// "+" and "{n}"/"{n,}"/"{n,m}" demand at least n matching members. A shortfall stays
|
|
169
|
+
// uncertain: if a remap ran, a member whose name could not be remapped may not be counted.
|
|
170
|
+
issues.push({
|
|
171
|
+
severity: "warning",
|
|
172
|
+
kind: "method-not-found",
|
|
173
|
+
annotation: `@${inj.annotation}`,
|
|
174
|
+
target: `${targetName}#${inj.method}`,
|
|
175
|
+
message: `Selector "${rawMethodName}" requires at least ${minMatches} matching method(s)${anyName ? "" : ` named "${methodName}"`} in target class "${targetName}" but found ${matchCount}.${mappingDegraded ? " (mapping degraded; may be a remap artifact)" : ""}`,
|
|
176
|
+
line: inj.line,
|
|
177
|
+
confidence: "uncertain",
|
|
178
|
+
confidenceReason: mappingDegraded
|
|
179
|
+
? `Member remap/signature degraded; the overload count may be a tooling artifact.`
|
|
180
|
+
: `Counted from the listed members; a member whose name could not be remapped, if any, is not counted.`,
|
|
181
|
+
resolutionPath,
|
|
182
|
+
falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, "uncertain")
|
|
183
|
+
});
|
|
184
|
+
resolvedMembers.push({
|
|
185
|
+
annotation: `@${inj.annotation}`,
|
|
186
|
+
name: displayName,
|
|
104
187
|
line: inj.line,
|
|
105
188
|
status: "not-found"
|
|
106
189
|
});
|
|
@@ -108,9 +191,9 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
108
191
|
else {
|
|
109
192
|
resolvedMembers.push({
|
|
110
193
|
annotation: `@${inj.annotation}`,
|
|
111
|
-
name:
|
|
194
|
+
name: displayName,
|
|
112
195
|
line: inj.line,
|
|
113
|
-
resolvedTo: `${targetName}#${
|
|
196
|
+
resolvedTo: `${targetName}#${displayName}`,
|
|
114
197
|
status: "resolved"
|
|
115
198
|
});
|
|
116
199
|
}
|
|
@@ -118,11 +201,20 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
118
201
|
}
|
|
119
202
|
}
|
|
120
203
|
export function validateShadow(shadow, targetMembers, targetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport) {
|
|
204
|
+
// A @Shadow method declared as `<prefix>name` refers to `name` in the target; a
|
|
205
|
+
// @Shadow field's targetName is its declared name unchanged, since fields are not
|
|
206
|
+
// prefixed. `aliases` adds fallback names. Messages keep the declared name so they
|
|
207
|
+
// stay recognisable in the mixin source.
|
|
208
|
+
const effectiveName = shadow.targetName ?? shadow.name;
|
|
209
|
+
const candidateNames = [effectiveName, ...(shadow.aliases ?? [])];
|
|
210
|
+
const prefixHint = effectiveName !== shadow.name
|
|
211
|
+
? ` (resolved to "${effectiveName}" after @Shadow prefix removal)`
|
|
212
|
+
: "";
|
|
121
213
|
for (const targetName of targetNames) {
|
|
122
214
|
const members = targetMembers.get(targetName);
|
|
123
215
|
if (!members)
|
|
124
216
|
continue;
|
|
125
|
-
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(
|
|
217
|
+
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(effectiveName);
|
|
126
218
|
const isSigFailed = signatureFailedTargets?.has(targetName);
|
|
127
219
|
const issueConfidence = isRemapFailed ? "uncertain" : confidence;
|
|
128
220
|
const issueConfidenceReason = isRemapFailed
|
|
@@ -134,14 +226,15 @@ export function validateShadow(shadow, targetMembers, targetNames, issues, resol
|
|
|
134
226
|
const memberDegraded = isRemapFailed && healthReport?.memberRemapAvailable === false;
|
|
135
227
|
if (shadow.kind === "field") {
|
|
136
228
|
const fieldNames = allFieldNames(members);
|
|
137
|
-
|
|
138
|
-
|
|
229
|
+
const matchedName = candidateNames.find((candidate) => fieldNames.includes(candidate));
|
|
230
|
+
if (matchedName === undefined) {
|
|
231
|
+
const suggestions = suggestSimilar(effectiveName, fieldNames);
|
|
139
232
|
issues.push({
|
|
140
233
|
severity: memberDegraded ? "warning" : "error",
|
|
141
234
|
kind: "field-not-found",
|
|
142
235
|
annotation: "@Shadow",
|
|
143
|
-
target: `${targetName}#${
|
|
144
|
-
message: `Field "${shadow.name}" not found in target class "${targetName}" (${fieldNames.length} field(s) available).${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
236
|
+
target: `${targetName}#${effectiveName}`,
|
|
237
|
+
message: `Field "${shadow.name}" not found in target class "${targetName}" (${fieldNames.length} field(s) available).${prefixHint}${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
145
238
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
146
239
|
line: shadow.line,
|
|
147
240
|
confidence: issueConfidence,
|
|
@@ -152,19 +245,20 @@ export function validateShadow(shadow, targetMembers, targetNames, issues, resol
|
|
|
152
245
|
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, status: "not-found" });
|
|
153
246
|
}
|
|
154
247
|
else {
|
|
155
|
-
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, resolvedTo: `${targetName}#${
|
|
248
|
+
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, resolvedTo: `${targetName}#${matchedName}`, status: "resolved" });
|
|
156
249
|
}
|
|
157
250
|
}
|
|
158
251
|
else {
|
|
159
252
|
const methodNames = allMethodNames(members);
|
|
160
|
-
|
|
161
|
-
|
|
253
|
+
const matchedName = candidateNames.find((candidate) => methodNames.includes(candidate));
|
|
254
|
+
if (matchedName === undefined) {
|
|
255
|
+
const suggestions = suggestSimilar(effectiveName, methodNames);
|
|
162
256
|
issues.push({
|
|
163
257
|
severity: memberDegraded ? "warning" : "error",
|
|
164
258
|
kind: "method-not-found",
|
|
165
259
|
annotation: "@Shadow",
|
|
166
|
-
target: `${targetName}#${
|
|
167
|
-
message: `Method "${shadow.name}" not found in target class "${targetName}" (${methodNames.length} method(s) available).${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
260
|
+
target: `${targetName}#${effectiveName}`,
|
|
261
|
+
message: `Method "${shadow.name}" not found in target class "${targetName}" (${methodNames.length} method(s) available).${prefixHint}${memberDegraded ? " (infrastructure degraded; may be false positive)" : ""}`,
|
|
168
262
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
169
263
|
line: shadow.line,
|
|
170
264
|
confidence: issueConfidence,
|
|
@@ -175,7 +269,7 @@ export function validateShadow(shadow, targetMembers, targetNames, issues, resol
|
|
|
175
269
|
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, status: "not-found" });
|
|
176
270
|
}
|
|
177
271
|
else {
|
|
178
|
-
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, resolvedTo: `${targetName}#${
|
|
272
|
+
resolvedMembers.push({ annotation: "@Shadow", name: shadow.name, line: shadow.line, resolvedTo: `${targetName}#${matchedName}`, status: "resolved" });
|
|
179
273
|
}
|
|
180
274
|
}
|
|
181
275
|
}
|
|
@@ -4,6 +4,20 @@ import { validateAccessor, validateInjection, validateShadow } from "./annotatio
|
|
|
4
4
|
export function validateParsedMixin(parsed, targetMembers, warnings, provenance, confidence, mappingFailedTargets, explain, remapFailedMembers, signatureFailedTargets, suggestedCallContext, warningMode, healthReport, symbolExistsButSignatureFailed, deferredBudgetTargets) {
|
|
5
5
|
const issues = [];
|
|
6
6
|
const targetNames = parsed.targets.map((t) => t.className);
|
|
7
|
+
// A member is checked only against the targets of the @Mixin class that owns it; a member
|
|
8
|
+
// without an owner is checked against every target.
|
|
9
|
+
const ownerTargets = (parsed.mixins ?? []).map((mixin) => new Set(mixin.targets.map((t) => t.className)));
|
|
10
|
+
const targetsOf = (member, names) => {
|
|
11
|
+
const owned = member.mixinIndex === undefined ? undefined : ownerTargets[member.mixinIndex];
|
|
12
|
+
return owned ? names.filter((name) => owned.has(name)) : names;
|
|
13
|
+
};
|
|
14
|
+
// The members that a failed target leaves unvalidated: those whose owner targets it.
|
|
15
|
+
const membersOf = (targetName) => ({
|
|
16
|
+
...parsed,
|
|
17
|
+
injections: parsed.injections.filter((member) => targetsOf(member, [targetName]).length > 0),
|
|
18
|
+
shadows: parsed.shadows.filter((member) => targetsOf(member, [targetName]).length > 0),
|
|
19
|
+
accessors: parsed.accessors.filter((member) => targetsOf(member, [targetName]).length > 0)
|
|
20
|
+
});
|
|
7
21
|
const confidenceReason = confidence === "uncertain"
|
|
8
22
|
? `Mapping fallback: requested "${provenance?.requestedMapping}" but applied "${provenance?.mappingApplied}".`
|
|
9
23
|
: confidence === "likely"
|
|
@@ -27,7 +41,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
27
41
|
resolutionPath: "target-mapping-failed",
|
|
28
42
|
falsePositiveRisk: healthReport?.overallHealthy === false ? "high" : "medium"
|
|
29
43
|
});
|
|
30
|
-
addSkippedMembers(
|
|
44
|
+
addSkippedMembers(membersOf(target.className), resolvedMembers);
|
|
31
45
|
}
|
|
32
46
|
else if (symbolExistsButSignatureFailed?.has(target.className)) {
|
|
33
47
|
// Symbol exists in mapping graph but getSignature failed — tool limitation, not code issue
|
|
@@ -44,12 +58,12 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
44
58
|
issueOrigin: "tool_issue",
|
|
45
59
|
falsePositiveRisk: "high"
|
|
46
60
|
});
|
|
47
|
-
addSkippedMembers(
|
|
61
|
+
addSkippedMembers(membersOf(target.className), resolvedMembers);
|
|
48
62
|
}
|
|
49
63
|
else if (deferredBudgetTargets?.has(target.className)) {
|
|
50
64
|
// Deferred by stage budget; the targetOutcomes entry already records
|
|
51
65
|
// this, so skip member validation rather than emit a not-found error.
|
|
52
|
-
addSkippedMembers(
|
|
66
|
+
addSkippedMembers(membersOf(target.className), resolvedMembers);
|
|
53
67
|
}
|
|
54
68
|
else if (signatureFailedTargets?.has(target.className)) {
|
|
55
69
|
issues.push({
|
|
@@ -65,7 +79,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
65
79
|
issueOrigin: "tool_issue",
|
|
66
80
|
falsePositiveRisk: "high"
|
|
67
81
|
});
|
|
68
|
-
addSkippedMembers(
|
|
82
|
+
addSkippedMembers(membersOf(target.className), resolvedMembers);
|
|
69
83
|
}
|
|
70
84
|
else {
|
|
71
85
|
issues.push({
|
|
@@ -85,13 +99,13 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
85
99
|
// Only validate members against targets that were resolved
|
|
86
100
|
const resolvedTargetNames = targetNames.filter((t) => targetMembers.has(t));
|
|
87
101
|
for (const inj of parsed.injections) {
|
|
88
|
-
validateInjection(inj, targetMembers, resolvedTargetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
102
|
+
validateInjection(inj, targetMembers, targetsOf(inj, resolvedTargetNames), issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
89
103
|
}
|
|
90
104
|
for (const shadow of parsed.shadows) {
|
|
91
|
-
validateShadow(shadow, targetMembers, resolvedTargetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
105
|
+
validateShadow(shadow, targetMembers, targetsOf(shadow, resolvedTargetNames), issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
92
106
|
}
|
|
93
107
|
for (const accessor of parsed.accessors) {
|
|
94
|
-
validateAccessor(accessor, targetMembers, resolvedTargetNames, issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
108
|
+
validateAccessor(accessor, targetMembers, targetsOf(accessor, resolvedTargetNames), issues, resolvedMembers, confidence, confidenceReason, remapFailedMembers, signatureFailedTargets, healthReport);
|
|
95
109
|
}
|
|
96
110
|
// Add parse warnings — escalate @Accessor/@Invoker/@Shadow parse failures to issues
|
|
97
111
|
for (const pw of parsed.parseWarnings) {
|
package/dist/mixin-parser.d.ts
CHANGED
|
@@ -5,26 +5,71 @@
|
|
|
5
5
|
export type ParsedMixinTarget = {
|
|
6
6
|
className: string;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* One `@Mixin`-annotated class in the file. A file can hold several (nested or top-level),
|
|
10
|
+
* and each member is validated only against the targets of the class that owns it.
|
|
11
|
+
*/
|
|
12
|
+
export type ParsedMixinClass = {
|
|
13
|
+
/** Simple name of the annotated class. */
|
|
14
|
+
className: string;
|
|
15
|
+
/** Targets named by this class's own `@Mixin` annotation. */
|
|
16
|
+
targets: ParsedMixinTarget[];
|
|
17
|
+
/** 1-based line of the `@Mixin` annotation. */
|
|
18
|
+
startLine: number;
|
|
19
|
+
/** 1-based line of the brace that closes the class body. */
|
|
20
|
+
endLine: number;
|
|
21
|
+
/**
|
|
22
|
+
* Simple names from the file's top-level type down to this class (`["Outer", "Inner"]`, the
|
|
23
|
+
* nesting a mixins-config entry `Outer$Inner` names). Absent for a class declared inside a
|
|
24
|
+
* method, initializer or anonymous class body, which no such entry can name.
|
|
25
|
+
*/
|
|
26
|
+
classPath?: string[];
|
|
27
|
+
/** This class's own `@Mixin(priority = ...)`, when given. */
|
|
28
|
+
priority?: number;
|
|
29
|
+
};
|
|
8
30
|
export type ParsedInjection = {
|
|
9
31
|
annotation: string;
|
|
10
32
|
method: string;
|
|
11
33
|
line: number;
|
|
34
|
+
/**
|
|
35
|
+
* Index into `ParsedMixin.mixins` of the innermost `@Mixin` class enclosing this member
|
|
36
|
+
* (a member of a nested non-mixin class belongs to the nearest enclosing `@Mixin` class).
|
|
37
|
+
* Absent when no `@Mixin` class encloses it; such a member is checked against every target.
|
|
38
|
+
*/
|
|
39
|
+
mixinIndex?: number;
|
|
12
40
|
};
|
|
13
41
|
export type ParsedShadow = {
|
|
14
42
|
kind: "field" | "method";
|
|
15
43
|
name: string;
|
|
44
|
+
/**
|
|
45
|
+
* The member looked up in the target: for a method, the declared name with the @Shadow
|
|
46
|
+
* prefix removed; for a field, the declared name unchanged (fields are not prefixed).
|
|
47
|
+
*/
|
|
48
|
+
targetName?: string;
|
|
49
|
+
/** Alternative target names from `@Shadow(aliases = {...})`. */
|
|
50
|
+
aliases?: string[];
|
|
16
51
|
descriptor?: string;
|
|
17
52
|
line: number;
|
|
53
|
+
/** Owning `@Mixin` class; see `ParsedInjection.mixinIndex`. */
|
|
54
|
+
mixinIndex?: number;
|
|
18
55
|
};
|
|
19
56
|
export type ParsedAccessor = {
|
|
20
57
|
annotation: "Accessor" | "Invoker";
|
|
21
58
|
name: string;
|
|
22
59
|
targetName: string;
|
|
23
60
|
line: number;
|
|
61
|
+
/** Owning `@Mixin` class; see `ParsedInjection.mixinIndex`. */
|
|
62
|
+
mixinIndex?: number;
|
|
24
63
|
};
|
|
25
64
|
export type ParsedMixin = {
|
|
26
65
|
className: string;
|
|
66
|
+
/** Every target named by any `@Mixin` annotation in the file, first occurrence first. */
|
|
27
67
|
targets: ParsedMixinTarget[];
|
|
68
|
+
/**
|
|
69
|
+
* Each `@Mixin` class whose body could be located, in source order. Absent on a parse
|
|
70
|
+
* result built without ownership, whose members are then checked against every target.
|
|
71
|
+
*/
|
|
72
|
+
mixins?: ParsedMixinClass[];
|
|
28
73
|
priority?: number;
|
|
29
74
|
imports: Map<string, string>;
|
|
30
75
|
injections: ParsedInjection[];
|
|
@@ -33,3 +78,10 @@ export type ParsedMixin = {
|
|
|
33
78
|
parseWarnings: string[];
|
|
34
79
|
};
|
|
35
80
|
export declare function parseMixinSource(source: string): ParsedMixin;
|
|
81
|
+
/**
|
|
82
|
+
* `parsed` narrowed to the nested `@Mixin` class at `classPath` (see `ParsedMixinClass.classPath`;
|
|
83
|
+
* `["Outer", "Inner"]` for a config entry `Outer$Inner`): its own targets and priority, and only
|
|
84
|
+
* the members and line-numbered parse warnings it owns. Undefined when no `@Mixin` class sits at
|
|
85
|
+
* that path.
|
|
86
|
+
*/
|
|
87
|
+
export declare function selectNestedMixin(parsed: ParsedMixin, classPath: readonly string[]): ParsedMixin | undefined;
|