@adhisang/minecraft-modding-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/dist/access-widener-parser.d.ts +24 -0
- package/dist/access-widener-parser.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +178 -0
- package/dist/decompiler/vineflower.d.ts +15 -0
- package/dist/decompiler/vineflower.js +185 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.js +49 -0
- package/dist/hash.d.ts +1 -0
- package/dist/hash.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1447 -0
- package/dist/java-process.d.ts +16 -0
- package/dist/java-process.js +120 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +21 -0
- package/dist/mapping-pipeline-service.d.ts +18 -0
- package/dist/mapping-pipeline-service.js +60 -0
- package/dist/mapping-service.d.ts +161 -0
- package/dist/mapping-service.js +1706 -0
- package/dist/maven-resolver.d.ts +22 -0
- package/dist/maven-resolver.js +122 -0
- package/dist/minecraft-explorer-service.d.ts +43 -0
- package/dist/minecraft-explorer-service.js +562 -0
- package/dist/mixin-parser.d.ts +34 -0
- package/dist/mixin-parser.js +194 -0
- package/dist/mixin-validator.d.ts +59 -0
- package/dist/mixin-validator.js +274 -0
- package/dist/mod-analyzer.d.ts +23 -0
- package/dist/mod-analyzer.js +346 -0
- package/dist/mod-decompile-service.d.ts +39 -0
- package/dist/mod-decompile-service.js +136 -0
- package/dist/mod-remap-service.d.ts +17 -0
- package/dist/mod-remap-service.js +186 -0
- package/dist/mod-search-service.d.ts +28 -0
- package/dist/mod-search-service.js +174 -0
- package/dist/mojang-tiny-mapping-service.d.ts +13 -0
- package/dist/mojang-tiny-mapping-service.js +351 -0
- package/dist/nbt/java-nbt-codec.d.ts +3 -0
- package/dist/nbt/java-nbt-codec.js +385 -0
- package/dist/nbt/json-patch.d.ts +3 -0
- package/dist/nbt/json-patch.js +352 -0
- package/dist/nbt/pipeline.d.ts +39 -0
- package/dist/nbt/pipeline.js +173 -0
- package/dist/nbt/typed-json.d.ts +10 -0
- package/dist/nbt/typed-json.js +205 -0
- package/dist/nbt/types.d.ts +66 -0
- package/dist/nbt/types.js +2 -0
- package/dist/observability.d.ts +88 -0
- package/dist/observability.js +165 -0
- package/dist/path-converter.d.ts +12 -0
- package/dist/path-converter.js +161 -0
- package/dist/path-resolver.d.ts +19 -0
- package/dist/path-resolver.js +78 -0
- package/dist/registry-service.d.ts +29 -0
- package/dist/registry-service.js +214 -0
- package/dist/repo-downloader.d.ts +15 -0
- package/dist/repo-downloader.js +111 -0
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +154 -0
- package/dist/search-hit-accumulator.d.ts +38 -0
- package/dist/search-hit-accumulator.js +153 -0
- package/dist/source-jar-reader.d.ts +13 -0
- package/dist/source-jar-reader.js +216 -0
- package/dist/source-resolver.d.ts +14 -0
- package/dist/source-resolver.js +274 -0
- package/dist/source-service.d.ts +404 -0
- package/dist/source-service.js +2881 -0
- package/dist/storage/artifacts-repo.d.ts +45 -0
- package/dist/storage/artifacts-repo.js +209 -0
- package/dist/storage/db.d.ts +14 -0
- package/dist/storage/db.js +132 -0
- package/dist/storage/files-repo.d.ts +78 -0
- package/dist/storage/files-repo.js +437 -0
- package/dist/storage/index-meta-repo.d.ts +35 -0
- package/dist/storage/index-meta-repo.js +97 -0
- package/dist/storage/migrations.d.ts +11 -0
- package/dist/storage/migrations.js +71 -0
- package/dist/storage/schema.d.ts +1 -0
- package/dist/storage/schema.js +160 -0
- package/dist/storage/sqlite.d.ts +20 -0
- package/dist/storage/sqlite.js +111 -0
- package/dist/storage/symbols-repo.d.ts +63 -0
- package/dist/storage/symbols-repo.js +401 -0
- package/dist/symbols/symbol-extractor.d.ts +7 -0
- package/dist/symbols/symbol-extractor.js +64 -0
- package/dist/tiny-remapper-resolver.d.ts +1 -0
- package/dist/tiny-remapper-resolver.js +62 -0
- package/dist/tiny-remapper-service.d.ts +16 -0
- package/dist/tiny-remapper-service.js +73 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +2 -0
- package/dist/version-diff-service.d.ts +41 -0
- package/dist/version-diff-service.js +222 -0
- package/dist/version-service.d.ts +70 -0
- package/dist/version-service.js +411 -0
- package/dist/vineflower-resolver.d.ts +1 -0
- package/dist/vineflower-resolver.js +62 -0
- package/dist/workspace-mapping-service.d.ts +18 -0
- package/dist/workspace-mapping-service.js +89 -0
- package/package.json +61 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight regex-based parser for Fabric Mixin Java sources.
|
|
3
|
+
* No AST required — pattern-matches annotations and member declarations.
|
|
4
|
+
*/
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Regex patterns */
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
const CLASS_DECL_RE = /(?:public\s+)?(?:abstract\s+)?class\s+(\w+)/;
|
|
9
|
+
// @Mixin(Foo.class) or @Mixin({Foo.class, Bar.class}) or @Mixin(value = Foo.class)
|
|
10
|
+
// Also handles @Mixin(value = {Foo.class, Bar.class}, priority = 900)
|
|
11
|
+
const MIXIN_ANNOTATION_START_RE = /^\s*@Mixin\s*\(/;
|
|
12
|
+
const MIXIN_TARGET_RE = /(\w[\w.]*?)\.class/g;
|
|
13
|
+
const MIXIN_PRIORITY_RE = /priority\s*=\s*(\d+)/;
|
|
14
|
+
// Injection annotations: @Inject, @Redirect, @ModifyArg, @ModifyVariable, @ModifyConstant, @ModifyExpressionValue
|
|
15
|
+
const INJECTION_ANNOTATION_RE = /^\s*@(Inject|Redirect|ModifyArg|ModifyVariable|ModifyConstant|ModifyExpressionValue)\s*\(/;
|
|
16
|
+
const METHOD_ATTR_RE = /method\s*=\s*"([^"]+)"/;
|
|
17
|
+
// @Shadow field / method
|
|
18
|
+
const SHADOW_ANNOTATION_RE = /^\s*@Shadow\b/;
|
|
19
|
+
const FIELD_DECL_RE = /(?:private|protected|public)?\s*(?:static\s+)?(?:final\s+)?(\w[\w<>,\s]*?)\s+(\w+)\s*[;=]/;
|
|
20
|
+
const METHOD_DECL_RE = /(?:private|protected|public)?\s*(?:static\s+)?(?:abstract\s+)?(?:native\s+)?(\w[\w<>,\s]*?)\s+(\w+)\s*\(/;
|
|
21
|
+
// @Accessor / @Invoker
|
|
22
|
+
const ACCESSOR_ANNOTATION_RE = /^\s*@(Accessor|Invoker)\s*(?:\(\s*"([^"]+)"\s*\))?\s*$/;
|
|
23
|
+
const ACCESSOR_ANNOTATION_START_RE = /^\s*@(Accessor|Invoker)\s*\(/;
|
|
24
|
+
const ACCESSOR_EXPLICIT_RE = /"([^"]+)"/;
|
|
25
|
+
// Naming conventions for accessor/invoker target inference
|
|
26
|
+
const GETTER_PREFIX_RE = /^(?:get|is)([A-Z].*)/;
|
|
27
|
+
const SETTER_PREFIX_RE = /^set([A-Z].*)/;
|
|
28
|
+
const INVOKER_PREFIX_RE = /^(?:invoke|call)([A-Z].*)/;
|
|
29
|
+
/* ------------------------------------------------------------------ */
|
|
30
|
+
/* Helpers */
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
function collectMultilineAnnotation(lines, startIndex) {
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let text = "";
|
|
35
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
text += (i === startIndex ? "" : "\n") + line;
|
|
38
|
+
for (const ch of line) {
|
|
39
|
+
if (ch === "(")
|
|
40
|
+
depth++;
|
|
41
|
+
if (ch === ")")
|
|
42
|
+
depth--;
|
|
43
|
+
}
|
|
44
|
+
if (depth <= 0) {
|
|
45
|
+
return { text, endIndex: i };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { text, endIndex: lines.length - 1 };
|
|
49
|
+
}
|
|
50
|
+
function inferAccessorTarget(methodName) {
|
|
51
|
+
const getterMatch = GETTER_PREFIX_RE.exec(methodName);
|
|
52
|
+
if (getterMatch) {
|
|
53
|
+
return getterMatch[1].charAt(0).toLowerCase() + getterMatch[1].slice(1);
|
|
54
|
+
}
|
|
55
|
+
const setterMatch = SETTER_PREFIX_RE.exec(methodName);
|
|
56
|
+
if (setterMatch) {
|
|
57
|
+
return setterMatch[1].charAt(0).toLowerCase() + setterMatch[1].slice(1);
|
|
58
|
+
}
|
|
59
|
+
const invokerMatch = INVOKER_PREFIX_RE.exec(methodName);
|
|
60
|
+
if (invokerMatch) {
|
|
61
|
+
return invokerMatch[1].charAt(0).toLowerCase() + invokerMatch[1].slice(1);
|
|
62
|
+
}
|
|
63
|
+
return methodName;
|
|
64
|
+
}
|
|
65
|
+
/* ------------------------------------------------------------------ */
|
|
66
|
+
/* Main parser */
|
|
67
|
+
/* ------------------------------------------------------------------ */
|
|
68
|
+
export function parseMixinSource(source) {
|
|
69
|
+
const lines = source.split(/\r?\n/);
|
|
70
|
+
const parseWarnings = [];
|
|
71
|
+
const targets = [];
|
|
72
|
+
const injections = [];
|
|
73
|
+
const shadows = [];
|
|
74
|
+
const accessors = [];
|
|
75
|
+
let className = "";
|
|
76
|
+
let priority;
|
|
77
|
+
// --- Pass 1: find @Mixin annotation and class name ---
|
|
78
|
+
let i = 0;
|
|
79
|
+
while (i < lines.length) {
|
|
80
|
+
if (MIXIN_ANNOTATION_START_RE.test(lines[i])) {
|
|
81
|
+
const { text: mixinText, endIndex } = collectMultilineAnnotation(lines, i);
|
|
82
|
+
MIXIN_TARGET_RE.lastIndex = 0;
|
|
83
|
+
let match;
|
|
84
|
+
while ((match = MIXIN_TARGET_RE.exec(mixinText)) !== null) {
|
|
85
|
+
targets.push({ className: match[1] });
|
|
86
|
+
}
|
|
87
|
+
const priorityMatch = MIXIN_PRIORITY_RE.exec(mixinText);
|
|
88
|
+
if (priorityMatch) {
|
|
89
|
+
priority = parseInt(priorityMatch[1], 10);
|
|
90
|
+
}
|
|
91
|
+
i = endIndex + 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const classMatch = CLASS_DECL_RE.exec(lines[i]);
|
|
95
|
+
if (classMatch && !className) {
|
|
96
|
+
className = classMatch[1];
|
|
97
|
+
}
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
if (targets.length === 0) {
|
|
101
|
+
parseWarnings.push("No @Mixin annotation target found.");
|
|
102
|
+
}
|
|
103
|
+
// --- Pass 2: scan member annotations ---
|
|
104
|
+
i = 0;
|
|
105
|
+
while (i < lines.length) {
|
|
106
|
+
const line = lines[i];
|
|
107
|
+
const lineNum = i + 1;
|
|
108
|
+
// --- @Inject / @Redirect / @ModifyArg etc. ---
|
|
109
|
+
const injMatch = INJECTION_ANNOTATION_RE.exec(line);
|
|
110
|
+
if (injMatch) {
|
|
111
|
+
const annotation = injMatch[1];
|
|
112
|
+
const { text: fullAnnotation, endIndex } = collectMultilineAnnotation(lines, i);
|
|
113
|
+
const methodMatch = METHOD_ATTR_RE.exec(fullAnnotation);
|
|
114
|
+
if (methodMatch) {
|
|
115
|
+
injections.push({ annotation, method: methodMatch[1], line: lineNum });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
parseWarnings.push(`Line ${lineNum}: @${annotation} missing method attribute.`);
|
|
119
|
+
}
|
|
120
|
+
i = endIndex + 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// --- @Shadow ---
|
|
124
|
+
if (SHADOW_ANNOTATION_RE.test(line)) {
|
|
125
|
+
// Advance past @Shadow line to find the declaration
|
|
126
|
+
let declLine = i + 1;
|
|
127
|
+
// Skip additional annotations between @Shadow and declaration
|
|
128
|
+
while (declLine < lines.length && /^\s*@/.test(lines[declLine])) {
|
|
129
|
+
declLine++;
|
|
130
|
+
}
|
|
131
|
+
if (declLine < lines.length) {
|
|
132
|
+
const declText = lines[declLine];
|
|
133
|
+
const methodDeclMatch = METHOD_DECL_RE.exec(declText);
|
|
134
|
+
const fieldDeclMatch = FIELD_DECL_RE.exec(declText);
|
|
135
|
+
// Method if it has parentheses
|
|
136
|
+
if (declText.includes("(") && methodDeclMatch) {
|
|
137
|
+
shadows.push({ kind: "method", name: methodDeclMatch[2], line: lineNum });
|
|
138
|
+
}
|
|
139
|
+
else if (fieldDeclMatch) {
|
|
140
|
+
shadows.push({ kind: "field", name: fieldDeclMatch[2], line: lineNum });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
parseWarnings.push(`Line ${lineNum}: Could not parse @Shadow member declaration.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
i = declLine + 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// --- @Accessor / @Invoker ---
|
|
150
|
+
const accessorMatch = ACCESSOR_ANNOTATION_RE.exec(line);
|
|
151
|
+
const accessorStartMatch = !accessorMatch ? ACCESSOR_ANNOTATION_START_RE.exec(line) : null;
|
|
152
|
+
if (accessorMatch || accessorStartMatch) {
|
|
153
|
+
const annotation = (accessorMatch?.[1] ?? accessorStartMatch?.[1]);
|
|
154
|
+
let explicitTarget = accessorMatch?.[2];
|
|
155
|
+
if (!explicitTarget && accessorStartMatch) {
|
|
156
|
+
const { text: fullAnnotation, endIndex } = collectMultilineAnnotation(lines, i);
|
|
157
|
+
const explicitMatch = ACCESSOR_EXPLICIT_RE.exec(fullAnnotation);
|
|
158
|
+
if (explicitMatch) {
|
|
159
|
+
explicitTarget = explicitMatch[1];
|
|
160
|
+
}
|
|
161
|
+
i = endIndex;
|
|
162
|
+
}
|
|
163
|
+
// Find the method declaration following the annotation
|
|
164
|
+
let methodLine = i + 1;
|
|
165
|
+
while (methodLine < lines.length && /^\s*@/.test(lines[methodLine])) {
|
|
166
|
+
methodLine++;
|
|
167
|
+
}
|
|
168
|
+
if (methodLine < lines.length) {
|
|
169
|
+
const methodDeclMatch = METHOD_DECL_RE.exec(lines[methodLine]);
|
|
170
|
+
if (methodDeclMatch) {
|
|
171
|
+
const methodName = methodDeclMatch[2];
|
|
172
|
+
const targetName = explicitTarget ?? inferAccessorTarget(methodName);
|
|
173
|
+
accessors.push({ annotation, name: methodName, targetName, line: lineNum });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
parseWarnings.push(`Line ${lineNum}: Could not parse @${annotation} method declaration.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
i = methodLine + 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
className,
|
|
186
|
+
targets,
|
|
187
|
+
priority,
|
|
188
|
+
injections,
|
|
189
|
+
shadows,
|
|
190
|
+
accessors,
|
|
191
|
+
parseWarnings
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=mixin-parser.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation engine for parsed Mixin sources and Access Widener files.
|
|
3
|
+
* Compares parsed annotations against resolved Minecraft bytecode signatures.
|
|
4
|
+
*/
|
|
5
|
+
import type { SignatureMember } from "./minecraft-explorer-service.js";
|
|
6
|
+
import type { ParsedMixin } from "./mixin-parser.js";
|
|
7
|
+
import type { ParsedAccessWidener, AccessWidenerEntry } from "./access-widener-parser.js";
|
|
8
|
+
export type ValidationIssue = {
|
|
9
|
+
severity: "error" | "warning";
|
|
10
|
+
kind: "target-not-found" | "method-not-found" | "field-not-found" | "descriptor-mismatch" | "access-mismatch" | "unknown-annotation";
|
|
11
|
+
annotation: string;
|
|
12
|
+
target: string;
|
|
13
|
+
message: string;
|
|
14
|
+
suggestions?: string[];
|
|
15
|
+
line?: number;
|
|
16
|
+
};
|
|
17
|
+
export type ValidationSummary = {
|
|
18
|
+
injections: number;
|
|
19
|
+
shadows: number;
|
|
20
|
+
accessors: number;
|
|
21
|
+
total: number;
|
|
22
|
+
errors: number;
|
|
23
|
+
warnings: number;
|
|
24
|
+
};
|
|
25
|
+
export type MixinValidationResult = {
|
|
26
|
+
className: string;
|
|
27
|
+
targets: string[];
|
|
28
|
+
priority?: number;
|
|
29
|
+
valid: boolean;
|
|
30
|
+
issues: ValidationIssue[];
|
|
31
|
+
summary: ValidationSummary;
|
|
32
|
+
warnings: string[];
|
|
33
|
+
};
|
|
34
|
+
export type ResolvedTargetMembers = {
|
|
35
|
+
className: string;
|
|
36
|
+
constructors: SignatureMember[];
|
|
37
|
+
methods: SignatureMember[];
|
|
38
|
+
fields: SignatureMember[];
|
|
39
|
+
};
|
|
40
|
+
export type AccessWidenerValidationResult = {
|
|
41
|
+
headerVersion: string;
|
|
42
|
+
namespace: string;
|
|
43
|
+
valid: boolean;
|
|
44
|
+
entries: Array<AccessWidenerEntry & {
|
|
45
|
+
valid: boolean;
|
|
46
|
+
issue?: string;
|
|
47
|
+
suggestions?: string[];
|
|
48
|
+
}>;
|
|
49
|
+
summary: {
|
|
50
|
+
total: number;
|
|
51
|
+
valid: number;
|
|
52
|
+
invalid: number;
|
|
53
|
+
};
|
|
54
|
+
warnings: string[];
|
|
55
|
+
};
|
|
56
|
+
export declare function levenshteinDistance(a: string, b: string): number;
|
|
57
|
+
export declare function suggestSimilar(name: string, candidates: string[], maxDistance?: number, maxResults?: number): string[];
|
|
58
|
+
export declare function validateParsedMixin(parsed: ParsedMixin, targetMembers: Map<string, ResolvedTargetMembers>, warnings: string[]): MixinValidationResult;
|
|
59
|
+
export declare function validateParsedAccessWidener(parsed: ParsedAccessWidener, membersByClass: Map<string, ResolvedTargetMembers>, warnings: string[]): AccessWidenerValidationResult;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation engine for parsed Mixin sources and Access Widener files.
|
|
3
|
+
* Compares parsed annotations against resolved Minecraft bytecode signatures.
|
|
4
|
+
*/
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Levenshtein distance */
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
export function levenshteinDistance(a, b) {
|
|
9
|
+
const la = a.length;
|
|
10
|
+
const lb = b.length;
|
|
11
|
+
if (la === 0)
|
|
12
|
+
return lb;
|
|
13
|
+
if (lb === 0)
|
|
14
|
+
return la;
|
|
15
|
+
// Single-row DP
|
|
16
|
+
const prev = new Array(lb + 1);
|
|
17
|
+
for (let j = 0; j <= lb; j++)
|
|
18
|
+
prev[j] = j;
|
|
19
|
+
for (let i = 1; i <= la; i++) {
|
|
20
|
+
let diagPrev = prev[0];
|
|
21
|
+
prev[0] = i;
|
|
22
|
+
for (let j = 1; j <= lb; j++) {
|
|
23
|
+
const temp = prev[j];
|
|
24
|
+
if (a[i - 1] === b[j - 1]) {
|
|
25
|
+
prev[j] = diagPrev;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
prev[j] = 1 + Math.min(diagPrev, prev[j - 1], prev[j]);
|
|
29
|
+
}
|
|
30
|
+
diagPrev = temp;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return prev[lb];
|
|
34
|
+
}
|
|
35
|
+
export function suggestSimilar(name, candidates, maxDistance = 3, maxResults = 3) {
|
|
36
|
+
const scored = [];
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
const distance = levenshteinDistance(name.toLowerCase(), candidate.toLowerCase());
|
|
39
|
+
if (distance <= maxDistance && distance > 0) {
|
|
40
|
+
scored.push({ candidate, distance });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
scored.sort((a, b) => a.distance - b.distance);
|
|
44
|
+
return scored.slice(0, maxResults).map((s) => s.candidate);
|
|
45
|
+
}
|
|
46
|
+
/* ------------------------------------------------------------------ */
|
|
47
|
+
/* Mixin validation */
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
function allMethodNames(members) {
|
|
50
|
+
return [
|
|
51
|
+
...members.constructors.map((m) => m.name),
|
|
52
|
+
...members.methods.map((m) => m.name)
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
function allFieldNames(members) {
|
|
56
|
+
return members.fields.map((m) => m.name);
|
|
57
|
+
}
|
|
58
|
+
function allMemberNames(members) {
|
|
59
|
+
return [...allMethodNames(members), ...allFieldNames(members)];
|
|
60
|
+
}
|
|
61
|
+
function validateInjection(inj, targetMembers, targetNames, issues) {
|
|
62
|
+
for (const targetName of targetNames) {
|
|
63
|
+
const members = targetMembers.get(targetName);
|
|
64
|
+
if (!members)
|
|
65
|
+
continue;
|
|
66
|
+
const methodNames = allMethodNames(members);
|
|
67
|
+
// Support method references like "<init>" and simple names
|
|
68
|
+
const methodRef = inj.method.replace(/<init>/g, "<init>");
|
|
69
|
+
if (!methodNames.includes(methodRef)) {
|
|
70
|
+
const suggestions = suggestSimilar(methodRef, methodNames);
|
|
71
|
+
issues.push({
|
|
72
|
+
severity: "error",
|
|
73
|
+
kind: "method-not-found",
|
|
74
|
+
annotation: `@${inj.annotation}`,
|
|
75
|
+
target: `${targetName}#${inj.method}`,
|
|
76
|
+
message: `Method "${inj.method}" not found in target class "${targetName}".`,
|
|
77
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
78
|
+
line: inj.line
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function validateShadow(shadow, targetMembers, targetNames, issues) {
|
|
84
|
+
for (const targetName of targetNames) {
|
|
85
|
+
const members = targetMembers.get(targetName);
|
|
86
|
+
if (!members)
|
|
87
|
+
continue;
|
|
88
|
+
if (shadow.kind === "field") {
|
|
89
|
+
const fieldNames = allFieldNames(members);
|
|
90
|
+
if (!fieldNames.includes(shadow.name)) {
|
|
91
|
+
const suggestions = suggestSimilar(shadow.name, fieldNames);
|
|
92
|
+
issues.push({
|
|
93
|
+
severity: "error",
|
|
94
|
+
kind: "field-not-found",
|
|
95
|
+
annotation: "@Shadow",
|
|
96
|
+
target: `${targetName}#${shadow.name}`,
|
|
97
|
+
message: `Field "${shadow.name}" not found in target class "${targetName}".`,
|
|
98
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
99
|
+
line: shadow.line
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const methodNames = allMethodNames(members);
|
|
105
|
+
if (!methodNames.includes(shadow.name)) {
|
|
106
|
+
const suggestions = suggestSimilar(shadow.name, methodNames);
|
|
107
|
+
issues.push({
|
|
108
|
+
severity: "error",
|
|
109
|
+
kind: "method-not-found",
|
|
110
|
+
annotation: "@Shadow",
|
|
111
|
+
target: `${targetName}#${shadow.name}`,
|
|
112
|
+
message: `Method "${shadow.name}" not found in target class "${targetName}".`,
|
|
113
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
114
|
+
line: shadow.line
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function validateAccessor(accessor, targetMembers, targetNames, issues) {
|
|
121
|
+
for (const targetName of targetNames) {
|
|
122
|
+
const members = targetMembers.get(targetName);
|
|
123
|
+
if (!members)
|
|
124
|
+
continue;
|
|
125
|
+
const allNames = allMemberNames(members);
|
|
126
|
+
if (!allNames.includes(accessor.targetName)) {
|
|
127
|
+
const suggestions = suggestSimilar(accessor.targetName, allNames);
|
|
128
|
+
issues.push({
|
|
129
|
+
severity: "error",
|
|
130
|
+
kind: accessor.annotation === "Invoker" ? "method-not-found" : "field-not-found",
|
|
131
|
+
annotation: `@${accessor.annotation}`,
|
|
132
|
+
target: `${targetName}#${accessor.targetName}`,
|
|
133
|
+
message: `Target "${accessor.targetName}" (inferred from "${accessor.name}") not found in class "${targetName}".`,
|
|
134
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
135
|
+
line: accessor.line
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function validateParsedMixin(parsed, targetMembers, warnings) {
|
|
141
|
+
const issues = [];
|
|
142
|
+
const targetNames = parsed.targets.map((t) => t.className);
|
|
143
|
+
// Check target classes exist
|
|
144
|
+
for (const target of parsed.targets) {
|
|
145
|
+
if (!targetMembers.has(target.className)) {
|
|
146
|
+
issues.push({
|
|
147
|
+
severity: "error",
|
|
148
|
+
kind: "target-not-found",
|
|
149
|
+
annotation: "@Mixin",
|
|
150
|
+
target: target.className,
|
|
151
|
+
message: `Target class "${target.className}" not found in game jar.`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Only validate members against targets that were resolved
|
|
156
|
+
const resolvedTargetNames = targetNames.filter((t) => targetMembers.has(t));
|
|
157
|
+
for (const inj of parsed.injections) {
|
|
158
|
+
validateInjection(inj, targetMembers, resolvedTargetNames, issues);
|
|
159
|
+
}
|
|
160
|
+
for (const shadow of parsed.shadows) {
|
|
161
|
+
validateShadow(shadow, targetMembers, resolvedTargetNames, issues);
|
|
162
|
+
}
|
|
163
|
+
for (const accessor of parsed.accessors) {
|
|
164
|
+
validateAccessor(accessor, targetMembers, resolvedTargetNames, issues);
|
|
165
|
+
}
|
|
166
|
+
// Add parse warnings
|
|
167
|
+
warnings.push(...parsed.parseWarnings);
|
|
168
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
169
|
+
const warningCount = issues.filter((i) => i.severity === "warning").length;
|
|
170
|
+
return {
|
|
171
|
+
className: parsed.className,
|
|
172
|
+
targets: targetNames,
|
|
173
|
+
priority: parsed.priority,
|
|
174
|
+
valid: errorCount === 0,
|
|
175
|
+
issues,
|
|
176
|
+
summary: {
|
|
177
|
+
injections: parsed.injections.length,
|
|
178
|
+
shadows: parsed.shadows.length,
|
|
179
|
+
accessors: parsed.accessors.length,
|
|
180
|
+
total: parsed.injections.length + parsed.shadows.length + parsed.accessors.length,
|
|
181
|
+
errors: errorCount,
|
|
182
|
+
warnings: warningCount
|
|
183
|
+
},
|
|
184
|
+
warnings
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/* ------------------------------------------------------------------ */
|
|
188
|
+
/* Access Widener validation */
|
|
189
|
+
/* ------------------------------------------------------------------ */
|
|
190
|
+
export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
191
|
+
warnings.push(...parsed.parseWarnings);
|
|
192
|
+
const validatedEntries = [];
|
|
193
|
+
let validCount = 0;
|
|
194
|
+
let invalidCount = 0;
|
|
195
|
+
for (const entry of parsed.entries) {
|
|
196
|
+
const ownerFqn = entry.target.replace(/\//g, ".");
|
|
197
|
+
if (entry.targetKind === "class") {
|
|
198
|
+
if (membersByClass.has(ownerFqn)) {
|
|
199
|
+
validatedEntries.push({ ...entry, valid: true });
|
|
200
|
+
validCount++;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
validatedEntries.push({
|
|
204
|
+
...entry,
|
|
205
|
+
valid: false,
|
|
206
|
+
issue: `Class "${ownerFqn}" not found in game jar.`
|
|
207
|
+
});
|
|
208
|
+
invalidCount++;
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// method or field
|
|
213
|
+
const members = membersByClass.get(ownerFqn);
|
|
214
|
+
if (!members) {
|
|
215
|
+
validatedEntries.push({
|
|
216
|
+
...entry,
|
|
217
|
+
valid: false,
|
|
218
|
+
issue: `Owner class "${ownerFqn}" not found in game jar.`
|
|
219
|
+
});
|
|
220
|
+
invalidCount++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (entry.targetKind === "method") {
|
|
224
|
+
const methodNames = allMethodNames(members);
|
|
225
|
+
const found = members.methods.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor)) || members.constructors.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
|
|
226
|
+
if (found) {
|
|
227
|
+
validatedEntries.push({ ...entry, valid: true });
|
|
228
|
+
validCount++;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const suggestions = entry.name ? suggestSimilar(entry.name, methodNames) : [];
|
|
232
|
+
validatedEntries.push({
|
|
233
|
+
...entry,
|
|
234
|
+
valid: false,
|
|
235
|
+
issue: `Method "${entry.name}" not found in class "${ownerFqn}".`,
|
|
236
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined
|
|
237
|
+
});
|
|
238
|
+
invalidCount++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// field
|
|
243
|
+
const fieldNames = allFieldNames(members);
|
|
244
|
+
const found = members.fields.some((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
|
|
245
|
+
if (found) {
|
|
246
|
+
validatedEntries.push({ ...entry, valid: true });
|
|
247
|
+
validCount++;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const suggestions = entry.name ? suggestSimilar(entry.name, fieldNames) : [];
|
|
251
|
+
validatedEntries.push({
|
|
252
|
+
...entry,
|
|
253
|
+
valid: false,
|
|
254
|
+
issue: `Field "${entry.name}" not found in class "${ownerFqn}".`,
|
|
255
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined
|
|
256
|
+
});
|
|
257
|
+
invalidCount++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
headerVersion: parsed.headerVersion,
|
|
263
|
+
namespace: parsed.namespace,
|
|
264
|
+
valid: invalidCount === 0,
|
|
265
|
+
entries: validatedEntries,
|
|
266
|
+
summary: {
|
|
267
|
+
total: parsed.entries.length,
|
|
268
|
+
valid: validCount,
|
|
269
|
+
invalid: invalidCount
|
|
270
|
+
},
|
|
271
|
+
warnings
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=mixin-validator.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ModLoader = "fabric" | "quilt" | "forge" | "neoforge" | "unknown";
|
|
2
|
+
export interface ModDependency {
|
|
3
|
+
modId: string;
|
|
4
|
+
versionRange?: string;
|
|
5
|
+
kind: "required" | "optional" | "recommends" | "conflicts";
|
|
6
|
+
}
|
|
7
|
+
export interface ModAnalysisResult {
|
|
8
|
+
loader: ModLoader;
|
|
9
|
+
modId?: string;
|
|
10
|
+
modName?: string;
|
|
11
|
+
modVersion?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
entrypoints?: Record<string, string[]>;
|
|
14
|
+
mixinConfigs?: string[];
|
|
15
|
+
accessWidener?: string;
|
|
16
|
+
dependencies?: ModDependency[];
|
|
17
|
+
classCount: number;
|
|
18
|
+
classes?: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface AnalyzeModOptions {
|
|
21
|
+
includeClasses?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function analyzeModJar(jarPath: string, options?: AnalyzeModOptions): Promise<ModAnalysisResult>;
|