@adhisang/minecraft-modding-mcp 3.1.0 → 3.2.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 +62 -34
- package/README.md +79 -100
- package/dist/access-transformer-parser.d.ts +17 -0
- package/dist/access-transformer-parser.js +97 -0
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +24 -0
- package/dist/config.js +19 -11
- package/dist/decompiler/vineflower.js +22 -21
- package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
- package/dist/entry-tools/analyze-symbol-service.d.ts +22 -20
- package/dist/entry-tools/analyze-symbol-service.js +6 -3
- package/dist/entry-tools/inspect-minecraft-service.d.ts +166 -149
- package/dist/entry-tools/inspect-minecraft-service.js +318 -55
- package/dist/entry-tools/validate-project-service.d.ts +153 -16
- package/dist/entry-tools/validate-project-service.js +360 -23
- package/dist/gradle-paths.d.ts +4 -0
- package/dist/gradle-paths.js +57 -0
- package/dist/index.js +274 -13
- package/dist/mapping-pipeline-service.d.ts +3 -1
- package/dist/mapping-pipeline-service.js +16 -1
- package/dist/mapping-service.d.ts +5 -0
- package/dist/mapping-service.js +200 -84
- package/dist/minecraft-explorer-service.d.ts +13 -0
- package/dist/minecraft-explorer-service.js +8 -4
- package/dist/mixin-validator.d.ts +33 -2
- package/dist/mixin-validator.js +197 -11
- package/dist/mod-analyzer.d.ts +1 -0
- package/dist/mod-analyzer.js +17 -1
- package/dist/mod-decompile-service.js +4 -4
- package/dist/mod-remap-service.js +1 -54
- package/dist/mod-search-service.d.ts +1 -0
- package/dist/mod-search-service.js +84 -51
- package/dist/response-utils.d.ts +35 -0
- package/dist/response-utils.js +113 -0
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +103 -1
- package/dist/source-resolver.js +9 -10
- package/dist/source-service.d.ts +24 -2
- package/dist/source-service.js +1052 -139
- package/dist/tool-contract-manifest.js +74 -74
- package/dist/types.d.ts +17 -0
- package/dist/workspace-mapping-service.d.ts +13 -0
- package/dist/workspace-mapping-service.js +146 -14
- package/package.json +1 -1
package/dist/source-service.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
5
4
|
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
|
|
6
5
|
import fastGlob from "fast-glob";
|
|
6
|
+
import { mapWithConcurrencyLimit } from "./concurrency.js";
|
|
7
7
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
8
8
|
import { loadConfig } from "./config.js";
|
|
9
9
|
import { decompileBinaryJar } from "./decompiler/vineflower.js";
|
|
10
10
|
import { resolveVineflowerJar } from "./vineflower-resolver.js";
|
|
11
11
|
import { parseCoordinate } from "./maven-resolver.js";
|
|
12
|
-
import { MinecraftExplorerService } from "./minecraft-explorer-service.js";
|
|
12
|
+
import { MinecraftExplorerService, modifierPrefix, parseFieldType, parseMethodDescriptor } from "./minecraft-explorer-service.js";
|
|
13
13
|
import { parseMixinSource } from "./mixin-parser.js";
|
|
14
14
|
import { parseAccessWidener } from "./access-widener-parser.js";
|
|
15
|
-
import {
|
|
15
|
+
import { parseAccessTransformer } from "./access-transformer-parser.js";
|
|
16
|
+
import { validateParsedMixin, refreshMixinValidationOutcome, validateParsedAccessWidener, validateParsedAccessTransformer } from "./mixin-validator.js";
|
|
16
17
|
import { resolveSourceTarget as resolveSourceTargetInternal } from "./source-resolver.js";
|
|
17
18
|
import { applyMappingPipeline } from "./mapping-pipeline-service.js";
|
|
18
19
|
import { MappingService } from "./mapping-service.js";
|
|
19
20
|
import { extractSymbolsFromSource } from "./symbols/symbol-extractor.js";
|
|
20
|
-
import { iterateJavaEntriesAsUtf8, listJavaEntries } from "./source-jar-reader.js";
|
|
21
|
+
import { detectFabricLikeInputNamespace, iterateJavaEntriesAsUtf8, listJavaEntries } from "./source-jar-reader.js";
|
|
21
22
|
import { openDatabase } from "./storage/db.js";
|
|
22
23
|
import { ArtifactsRepo } from "./storage/artifacts-repo.js";
|
|
23
24
|
import { FilesRepo } from "./storage/files-repo.js";
|
|
@@ -26,6 +27,7 @@ import { SymbolsRepo } from "./storage/symbols-repo.js";
|
|
|
26
27
|
import { RuntimeMetrics } from "./observability.js";
|
|
27
28
|
import { log } from "./logger.js";
|
|
28
29
|
import { normalizePathForHost } from "./path-converter.js";
|
|
30
|
+
import { buildLoaderRuntimeSearchRoots, buildVersionSourceSearchRoots, normalizeOptionalProjectPath } from "./gradle-paths.js";
|
|
29
31
|
import { createSearchHitAccumulator, decodeSearchCursor, encodeSearchCursor } from "./search-hit-accumulator.js";
|
|
30
32
|
import { WorkspaceMappingService } from "./workspace-mapping-service.js";
|
|
31
33
|
import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
|
|
@@ -159,6 +161,7 @@ function clampLimit(limit, fallback, max) {
|
|
|
159
161
|
}
|
|
160
162
|
const MAX_REGEX_QUERY_LENGTH = 200;
|
|
161
163
|
const MAX_REGEX_RESULT_LIMIT = 100;
|
|
164
|
+
const TRACE_LIFECYCLE_MAX_CONCURRENCY = 3;
|
|
162
165
|
function normalizePathStyle(path) {
|
|
163
166
|
return path.replaceAll("\\", "/");
|
|
164
167
|
}
|
|
@@ -177,6 +180,30 @@ function hasExactVersionToken(path, version) {
|
|
|
177
180
|
?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}([^0-9a-z]|$)`, "i"));
|
|
178
181
|
return pattern.test(normalizedPath);
|
|
179
182
|
}
|
|
183
|
+
function inferMergedRuntimeNamespaceHint(path) {
|
|
184
|
+
const normalizedPath = normalizePathStyle(path).toLowerCase();
|
|
185
|
+
if (normalizedPath.includes("merged-intermediary-v2") ||
|
|
186
|
+
normalizedPath.includes("merged-intermediary")) {
|
|
187
|
+
return "intermediary";
|
|
188
|
+
}
|
|
189
|
+
if (normalizedPath.includes("minecraft-merged-mojang") ||
|
|
190
|
+
normalizedPath.includes("merged-mojang")) {
|
|
191
|
+
return "mojang";
|
|
192
|
+
}
|
|
193
|
+
if (normalizedPath.includes("merged-named")) {
|
|
194
|
+
return "named";
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
function runtimeJarNamespaceHintScore(hint) {
|
|
199
|
+
if (hint === "intermediary" || hint === "mojang") {
|
|
200
|
+
return 8_000;
|
|
201
|
+
}
|
|
202
|
+
if (hint === "named") {
|
|
203
|
+
return 1_000;
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
180
207
|
function looksLikeDeobfuscatedClassName(value) {
|
|
181
208
|
const trimmed = value.trim();
|
|
182
209
|
if (!trimmed) {
|
|
@@ -203,39 +230,17 @@ function buildResolveArtifactParams(target, extra = {}) {
|
|
|
203
230
|
...extra
|
|
204
231
|
};
|
|
205
232
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
const trimmed = projectPath.trim();
|
|
211
|
-
if (!trimmed) {
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
const normalized = normalizePathForHost(trimmed, undefined, "projectPath");
|
|
215
|
-
return isAbsolute(normalized) ? normalized : resolvePath(process.cwd(), normalized);
|
|
233
|
+
function looksLikeClassSegment(name) {
|
|
234
|
+
const trimmed = name.trim();
|
|
235
|
+
return /^[A-Z_$]/.test(trimmed);
|
|
216
236
|
}
|
|
217
|
-
function
|
|
218
|
-
const
|
|
219
|
-
if (!
|
|
220
|
-
return
|
|
237
|
+
function looksLikeJvmMethodDescriptor(descriptor) {
|
|
238
|
+
const trimmed = descriptor?.trim();
|
|
239
|
+
if (!trimmed || !trimmed.startsWith("(")) {
|
|
240
|
+
return false;
|
|
221
241
|
}
|
|
222
|
-
const
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
function buildVersionSourceSearchRoots(projectPath) {
|
|
226
|
-
const roots = new Set();
|
|
227
|
-
if (projectPath) {
|
|
228
|
-
roots.add(resolvePath(projectPath, ".gradle", "loom-cache"));
|
|
229
|
-
roots.add(resolvePath(projectPath, ".gradle-user", "caches", "fabric-loom"));
|
|
230
|
-
roots.add(resolvePath(projectPath, ".gradle", "caches", "fabric-loom"));
|
|
231
|
-
const projectParent = dirname(projectPath);
|
|
232
|
-
roots.add(resolvePath(projectParent, ".gradle-user-home", "loom-cache"));
|
|
233
|
-
roots.add(resolvePath(projectParent, ".gradle-user-home", "caches", "fabric-loom"));
|
|
234
|
-
}
|
|
235
|
-
const homeGradle = resolveGradleUserHomePath();
|
|
236
|
-
roots.add(resolvePath(homeGradle, "loom-cache"));
|
|
237
|
-
roots.add(resolvePath(homeGradle, "caches", "fabric-loom"));
|
|
238
|
-
return [...roots];
|
|
242
|
+
const closing = trimmed.indexOf(")");
|
|
243
|
+
return closing > 0 && closing < trimmed.length - 1;
|
|
239
244
|
}
|
|
240
245
|
function looksLikeMinecraftSourceArtifact(path, hasMinecraftNamespace) {
|
|
241
246
|
if (hasMinecraftNamespace) {
|
|
@@ -278,16 +283,19 @@ function scopeToJarType(scope) {
|
|
|
278
283
|
}
|
|
279
284
|
function parseQualifiedMethodSymbol(symbol) {
|
|
280
285
|
const trimmed = symbol.trim();
|
|
281
|
-
const
|
|
282
|
-
|
|
286
|
+
const descriptorStart = trimmed.indexOf("(");
|
|
287
|
+
const qualifiedSymbol = descriptorStart >= 0 ? trimmed.slice(0, descriptorStart) : trimmed;
|
|
288
|
+
const inlineDescriptor = descriptorStart >= 0 ? trimmed.slice(descriptorStart).trim() : undefined;
|
|
289
|
+
const separator = qualifiedSymbol.lastIndexOf(".");
|
|
290
|
+
if (separator <= 0 || separator >= qualifiedSymbol.length - 1) {
|
|
283
291
|
throw createError({
|
|
284
292
|
code: ERROR_CODES.INVALID_INPUT,
|
|
285
293
|
message: `symbol must be in the form "fully.qualified.Class.method".`,
|
|
286
294
|
details: { symbol }
|
|
287
295
|
});
|
|
288
296
|
}
|
|
289
|
-
const className =
|
|
290
|
-
const methodName =
|
|
297
|
+
const className = qualifiedSymbol.slice(0, separator);
|
|
298
|
+
const methodName = qualifiedSymbol.slice(separator + 1);
|
|
291
299
|
if (!className ||
|
|
292
300
|
!methodName ||
|
|
293
301
|
className.includes("/") ||
|
|
@@ -299,7 +307,11 @@ function parseQualifiedMethodSymbol(symbol) {
|
|
|
299
307
|
details: { symbol }
|
|
300
308
|
});
|
|
301
309
|
}
|
|
302
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
className,
|
|
312
|
+
methodName,
|
|
313
|
+
...(inlineDescriptor ? { inlineDescriptor } : {})
|
|
314
|
+
};
|
|
303
315
|
}
|
|
304
316
|
function normalizeOptionalString(value) {
|
|
305
317
|
if (value == null) {
|
|
@@ -345,6 +357,15 @@ const MIXIN_PROJECT_DISCOVERY_IGNORES = [
|
|
|
345
357
|
"**/out/**",
|
|
346
358
|
"**/node_modules/**"
|
|
347
359
|
];
|
|
360
|
+
async function pathExists(filePath) {
|
|
361
|
+
try {
|
|
362
|
+
await access(filePath);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
348
369
|
function normalizeMapping(mapping) {
|
|
349
370
|
if (mapping == null) {
|
|
350
371
|
return "obfuscated";
|
|
@@ -381,6 +402,19 @@ function normalizeAccessWidenerNamespace(namespace) {
|
|
|
381
402
|
}
|
|
382
403
|
return undefined;
|
|
383
404
|
}
|
|
405
|
+
function normalizeAccessTransformerNamespace(namespace) {
|
|
406
|
+
const normalized = namespace?.trim().toLowerCase();
|
|
407
|
+
if (!normalized) {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
if (normalized === "srg" || normalized === "mojang" || normalized === "obfuscated") {
|
|
411
|
+
return normalized;
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
function isSourceMappingNamespace(namespace) {
|
|
416
|
+
return namespace === "obfuscated" || namespace === "mojang" || namespace === "intermediary" || namespace === "yarn";
|
|
417
|
+
}
|
|
384
418
|
function normalizeMemberAccess(access) {
|
|
385
419
|
if (access == null) {
|
|
386
420
|
return "public";
|
|
@@ -768,7 +802,7 @@ export class SourceService {
|
|
|
768
802
|
searchedPaths.push(root);
|
|
769
803
|
let discovered = [];
|
|
770
804
|
try {
|
|
771
|
-
discovered = fastGlob.
|
|
805
|
+
discovered = await fastGlob.glob("**/*sources.jar", {
|
|
772
806
|
cwd: root,
|
|
773
807
|
absolute: true,
|
|
774
808
|
onlyFiles: true
|
|
@@ -832,6 +866,371 @@ export class SourceService {
|
|
|
832
866
|
selectedHasMinecraftNamespace: selected?.hasMinecraftNamespace
|
|
833
867
|
};
|
|
834
868
|
}
|
|
869
|
+
async discoverAccessWidenerRuntimeCandidates(input) {
|
|
870
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
871
|
+
const normalizedProjectPathLower = normalizedProjectPath
|
|
872
|
+
? normalizePathStyle(normalizedProjectPath).toLowerCase()
|
|
873
|
+
: undefined;
|
|
874
|
+
const searchRoots = buildVersionSourceSearchRoots(normalizedProjectPath);
|
|
875
|
+
const searchedPaths = [];
|
|
876
|
+
const candidates = [];
|
|
877
|
+
const seen = new Set();
|
|
878
|
+
for (const root of searchRoots) {
|
|
879
|
+
searchedPaths.push(root);
|
|
880
|
+
let discovered = [];
|
|
881
|
+
try {
|
|
882
|
+
discovered = await fastGlob.glob(["**/*minecraft*.jar", "**/*merged*.jar"], {
|
|
883
|
+
cwd: root,
|
|
884
|
+
absolute: true,
|
|
885
|
+
onlyFiles: true,
|
|
886
|
+
ignore: ["**/*sources.jar", "**/node_modules/**", "**/.git/**", "**/build/**", "**/out/**"]
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
for (const candidatePath of discovered) {
|
|
893
|
+
const normalizedPath = normalizePathStyle(candidatePath);
|
|
894
|
+
if (seen.has(normalizedPath)) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
seen.add(normalizedPath);
|
|
898
|
+
const lower = normalizedPath.toLowerCase();
|
|
899
|
+
if (!lower.includes("minecraft")) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const exactVersionMatch = hasExactVersionToken(normalizedPath, input.version);
|
|
903
|
+
const looksMerged = lower.includes("minecraft-merged") || lower.includes("/merged/") || lower.includes("-merged");
|
|
904
|
+
const namespaceHint = inferMergedRuntimeNamespaceHint(normalizedPath);
|
|
905
|
+
const appliedScope = looksMerged
|
|
906
|
+
? "merged"
|
|
907
|
+
: input.requestedScope === "loader"
|
|
908
|
+
? "merged"
|
|
909
|
+
: input.requestedScope;
|
|
910
|
+
const score = (exactVersionMatch ? 5_000 : 0) +
|
|
911
|
+
(looksMerged ? 4_000 : 0) +
|
|
912
|
+
runtimeJarNamespaceHintScore(namespaceHint) +
|
|
913
|
+
(normalizedProjectPathLower && lower.startsWith(normalizedProjectPathLower) ? 2_000 : 0) +
|
|
914
|
+
(lower.includes("loom-cache") || lower.includes("/caches/fabric-loom/") ? 500 : 0) +
|
|
915
|
+
(lower.includes("minecraft-client") || lower.includes("client") ? 100 : 0);
|
|
916
|
+
candidates.push({
|
|
917
|
+
jarPath: normalizedPath,
|
|
918
|
+
score,
|
|
919
|
+
appliedScope,
|
|
920
|
+
origin: lower.includes("loom-cache") || lower.includes("/caches/fabric-loom/")
|
|
921
|
+
? "loom-cache"
|
|
922
|
+
: "local-jar",
|
|
923
|
+
namespaceHint
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
candidates.sort((left, right) => {
|
|
928
|
+
if (right.score !== left.score) {
|
|
929
|
+
return right.score - left.score;
|
|
930
|
+
}
|
|
931
|
+
return left.jarPath.localeCompare(right.jarPath);
|
|
932
|
+
});
|
|
933
|
+
return {
|
|
934
|
+
searchedPaths,
|
|
935
|
+
candidateArtifacts: candidates.slice(0, 20).map((candidate) => candidate.jarPath),
|
|
936
|
+
selected: candidates[0]
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
async discoverAccessTransformerRuntimeCandidates(input) {
|
|
940
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
941
|
+
const normalizedProjectPathLower = normalizedProjectPath
|
|
942
|
+
? normalizePathStyle(normalizedProjectPath).toLowerCase()
|
|
943
|
+
: undefined;
|
|
944
|
+
const searchRoots = buildLoaderRuntimeSearchRoots(normalizedProjectPath);
|
|
945
|
+
const searchedPaths = [];
|
|
946
|
+
const candidates = [];
|
|
947
|
+
const seen = new Set();
|
|
948
|
+
const globs = [
|
|
949
|
+
"**/*minecraft*.jar",
|
|
950
|
+
"**/*patched*.jar",
|
|
951
|
+
"**/*srg*.jar",
|
|
952
|
+
"**/*joined*.jar",
|
|
953
|
+
"**/*client-extra*.jar",
|
|
954
|
+
"**/*forge*.jar",
|
|
955
|
+
"**/*neoforge*.jar",
|
|
956
|
+
"**/*moddev*.jar",
|
|
957
|
+
"**/*neoform*.jar"
|
|
958
|
+
];
|
|
959
|
+
for (const root of searchRoots) {
|
|
960
|
+
searchedPaths.push(root);
|
|
961
|
+
if (!(await pathExists(root))) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
let discovered = [];
|
|
965
|
+
try {
|
|
966
|
+
discovered = await fastGlob.glob(globs, {
|
|
967
|
+
cwd: root,
|
|
968
|
+
absolute: true,
|
|
969
|
+
onlyFiles: true,
|
|
970
|
+
ignore: ["**/*sources.jar", "**/node_modules/**", "**/.git/**", "**/out/**"]
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
catch {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
for (const candidatePath of discovered) {
|
|
977
|
+
const normalizedPath = normalizePathStyle(candidatePath);
|
|
978
|
+
if (seen.has(normalizedPath)) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
seen.add(normalizedPath);
|
|
982
|
+
const lower = normalizedPath.toLowerCase();
|
|
983
|
+
if (!hasExactVersionToken(normalizedPath, input.version)) {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const looksMerged = lower.includes("merged");
|
|
987
|
+
const looksSrg = lower.includes("srg");
|
|
988
|
+
const looksForge = lower.includes("forge");
|
|
989
|
+
const looksNeoForge = lower.includes("neoforge") || lower.includes("moddev") || lower.includes("neoform");
|
|
990
|
+
const looksPatchedRuntime = lower.includes("patched") || lower.includes("client-extra") || lower.includes("joined");
|
|
991
|
+
const appliedScope = looksMerged
|
|
992
|
+
? "merged"
|
|
993
|
+
: "loader";
|
|
994
|
+
if (input.atNamespace === "srg" && !looksSrg) {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (input.loader === "forge" && !looksForge && !looksSrg && !looksPatchedRuntime) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (input.loader === "neoforge" && !looksNeoForge && !looksPatchedRuntime && !lower.includes("minecraft")) {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
const score = 10_000 +
|
|
1004
|
+
(normalizedProjectPathLower && lower.startsWith(normalizedProjectPathLower) ? 4_000 : 0) +
|
|
1005
|
+
(looksPatchedRuntime ? 3_000 : 0) +
|
|
1006
|
+
(looksSrg ? 2_500 : 0) +
|
|
1007
|
+
(input.loader === "forge" && looksForge ? 1_500 : 0) +
|
|
1008
|
+
(input.loader === "neoforge" && looksNeoForge ? 1_500 : 0) +
|
|
1009
|
+
(input.requestedScope === appliedScope ? 1_000 : 0) +
|
|
1010
|
+
(looksMerged ? -500 : 0);
|
|
1011
|
+
candidates.push({
|
|
1012
|
+
jarPath: normalizedPath,
|
|
1013
|
+
score,
|
|
1014
|
+
appliedScope,
|
|
1015
|
+
origin: "local-jar"
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
candidates.sort((left, right) => {
|
|
1020
|
+
if (right.score !== left.score) {
|
|
1021
|
+
return right.score - left.score;
|
|
1022
|
+
}
|
|
1023
|
+
return left.jarPath.localeCompare(right.jarPath);
|
|
1024
|
+
});
|
|
1025
|
+
return {
|
|
1026
|
+
searchedPaths,
|
|
1027
|
+
candidateArtifacts: candidates.slice(0, 20).map((candidate) => candidate.jarPath),
|
|
1028
|
+
selected: candidates[0]
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
async resolveAccessWidenerRuntimeArtifact(input) {
|
|
1032
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
1033
|
+
let version = input.version;
|
|
1034
|
+
if (input.preferProjectVersion && normalizedProjectPath) {
|
|
1035
|
+
const detected = await this.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
|
|
1036
|
+
version = detected ?? version;
|
|
1037
|
+
}
|
|
1038
|
+
const requestedScope = input.scope ?? (normalizedProjectPath ? "loader" : "vanilla");
|
|
1039
|
+
if (requestedScope === "vanilla") {
|
|
1040
|
+
const versionJar = await this.versionService.resolveVersionJar(version);
|
|
1041
|
+
return {
|
|
1042
|
+
version: versionJar.version,
|
|
1043
|
+
jarPath: versionJar.jarPath,
|
|
1044
|
+
requestedScope,
|
|
1045
|
+
appliedScope: "vanilla",
|
|
1046
|
+
requestedMapping: input.awNamespace,
|
|
1047
|
+
mappingApplied: "obfuscated",
|
|
1048
|
+
origin: "version-jar"
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
const discovery = await this.discoverAccessWidenerRuntimeCandidates({
|
|
1052
|
+
version,
|
|
1053
|
+
projectPath: normalizedProjectPath,
|
|
1054
|
+
requestedScope
|
|
1055
|
+
});
|
|
1056
|
+
if (!discovery.selected) {
|
|
1057
|
+
throw createError({
|
|
1058
|
+
code: ERROR_CODES.CONTEXT_UNRESOLVED,
|
|
1059
|
+
message: "Could not resolve a runtime jar for Access Widener validation.",
|
|
1060
|
+
details: {
|
|
1061
|
+
version,
|
|
1062
|
+
requestedScope,
|
|
1063
|
+
projectPath: normalizedProjectPath,
|
|
1064
|
+
searchedPaths: discovery.searchedPaths,
|
|
1065
|
+
candidateArtifacts: discovery.candidateArtifacts,
|
|
1066
|
+
nextAction: "Provide projectPath for a Loom workspace with generated runtime jars, or run Gradle tasks that populate the Loom cache before retrying.",
|
|
1067
|
+
suggestedCall: {
|
|
1068
|
+
tool: "validate-access-widener",
|
|
1069
|
+
params: {
|
|
1070
|
+
version,
|
|
1071
|
+
scope: requestedScope,
|
|
1072
|
+
...(normalizedProjectPath ? { projectPath: normalizedProjectPath } : {})
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
const appliedScope = discovery.selected.appliedScope;
|
|
1079
|
+
const scopeFallback = requestedScope !== appliedScope
|
|
1080
|
+
? {
|
|
1081
|
+
requested: requestedScope,
|
|
1082
|
+
applied: appliedScope,
|
|
1083
|
+
reason: requestedScope === "loader"
|
|
1084
|
+
? "Fabric loader runtime validation currently reuses the merged runtime jar."
|
|
1085
|
+
: "Selected runtime jar matched a nearby merged artifact."
|
|
1086
|
+
}
|
|
1087
|
+
: undefined;
|
|
1088
|
+
let detectedMapping;
|
|
1089
|
+
const notes = [];
|
|
1090
|
+
if (scopeFallback) {
|
|
1091
|
+
notes.push(scopeFallback.reason);
|
|
1092
|
+
}
|
|
1093
|
+
if (isUnobfuscatedVersion(version)) {
|
|
1094
|
+
detectedMapping = "obfuscated";
|
|
1095
|
+
}
|
|
1096
|
+
else if (discovery.selected.namespaceHint === "intermediary" ||
|
|
1097
|
+
discovery.selected.namespaceHint === "mojang") {
|
|
1098
|
+
detectedMapping = discovery.selected.namespaceHint;
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
const detection = await detectFabricLikeInputNamespace(discovery.selected.jarPath);
|
|
1102
|
+
detectedMapping = detection.fromNamespace;
|
|
1103
|
+
if (detection.warnings.length > 0) {
|
|
1104
|
+
notes.push(...detection.warnings);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
version,
|
|
1109
|
+
jarPath: discovery.selected.jarPath,
|
|
1110
|
+
requestedScope,
|
|
1111
|
+
appliedScope,
|
|
1112
|
+
requestedMapping: input.awNamespace,
|
|
1113
|
+
mappingApplied: detectedMapping,
|
|
1114
|
+
origin: discovery.selected.origin,
|
|
1115
|
+
resolutionNotes: notes.length > 0 ? notes : undefined,
|
|
1116
|
+
scopeFallback
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
async resolveAccessTransformerNamespace(input) {
|
|
1120
|
+
const explicit = normalizeAccessTransformerNamespace(input.atNamespace);
|
|
1121
|
+
if (explicit) {
|
|
1122
|
+
return explicit;
|
|
1123
|
+
}
|
|
1124
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
1125
|
+
if (!normalizedProjectPath) {
|
|
1126
|
+
throw createError({
|
|
1127
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1128
|
+
message: "atNamespace is required when projectPath is not provided.",
|
|
1129
|
+
details: {
|
|
1130
|
+
nextAction: "Pass atNamespace explicitly, or provide projectPath for a Forge/NeoForge workspace so the namespace can be inferred."
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
const loaderDetection = await this.workspaceMappingService.detectProjectLoader(normalizedProjectPath);
|
|
1135
|
+
if (loaderDetection.resolved && loaderDetection.loader === "forge") {
|
|
1136
|
+
return "srg";
|
|
1137
|
+
}
|
|
1138
|
+
if (loaderDetection.resolved && loaderDetection.loader === "neoforge") {
|
|
1139
|
+
return "mojang";
|
|
1140
|
+
}
|
|
1141
|
+
throw createError({
|
|
1142
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1143
|
+
message: "Could not infer atNamespace from the workspace.",
|
|
1144
|
+
details: {
|
|
1145
|
+
projectPath: normalizedProjectPath,
|
|
1146
|
+
evidence: loaderDetection.evidence,
|
|
1147
|
+
warnings: loaderDetection.warnings,
|
|
1148
|
+
nextAction: "Pass atNamespace explicitly, or point projectPath at a Forge/NeoForge workspace."
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
async resolveAccessTransformerRuntimeArtifact(input) {
|
|
1153
|
+
const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
|
|
1154
|
+
let version = input.version;
|
|
1155
|
+
if (input.preferProjectVersion && normalizedProjectPath) {
|
|
1156
|
+
const detected = await this.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
|
|
1157
|
+
version = detected ?? version;
|
|
1158
|
+
}
|
|
1159
|
+
const requestedScope = input.scope ?? (normalizedProjectPath ? "loader" : "vanilla");
|
|
1160
|
+
if (requestedScope === "vanilla") {
|
|
1161
|
+
if (input.atNamespace === "srg") {
|
|
1162
|
+
throw createError({
|
|
1163
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
1164
|
+
message: "atNamespace=srg requires projectPath and scope=loader so a Forge runtime jar can be resolved."
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
const versionJar = await this.versionService.resolveVersionJar(version);
|
|
1168
|
+
return {
|
|
1169
|
+
version: versionJar.version,
|
|
1170
|
+
jarPath: versionJar.jarPath,
|
|
1171
|
+
requestedScope,
|
|
1172
|
+
appliedScope: "vanilla",
|
|
1173
|
+
requestedMapping: input.atNamespace,
|
|
1174
|
+
mappingApplied: "obfuscated",
|
|
1175
|
+
origin: "version-jar"
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const loaderDetection = normalizedProjectPath
|
|
1179
|
+
? await this.workspaceMappingService.detectProjectLoader(normalizedProjectPath)
|
|
1180
|
+
: { resolved: false, loader: undefined, evidence: [], warnings: [] };
|
|
1181
|
+
const loader = loaderDetection.resolved ? loaderDetection.loader ?? "unknown" : "unknown";
|
|
1182
|
+
const discovery = await this.discoverAccessTransformerRuntimeCandidates({
|
|
1183
|
+
version,
|
|
1184
|
+
projectPath: normalizedProjectPath,
|
|
1185
|
+
requestedScope,
|
|
1186
|
+
atNamespace: input.atNamespace,
|
|
1187
|
+
loader
|
|
1188
|
+
});
|
|
1189
|
+
if (!discovery.selected) {
|
|
1190
|
+
throw createError({
|
|
1191
|
+
code: ERROR_CODES.CONTEXT_UNRESOLVED,
|
|
1192
|
+
message: "Could not resolve a runtime jar for Access Transformer validation.",
|
|
1193
|
+
details: {
|
|
1194
|
+
version,
|
|
1195
|
+
requestedScope,
|
|
1196
|
+
atNamespace: input.atNamespace,
|
|
1197
|
+
projectPath: normalizedProjectPath,
|
|
1198
|
+
searchedPaths: discovery.searchedPaths,
|
|
1199
|
+
candidateArtifacts: discovery.candidateArtifacts,
|
|
1200
|
+
loaderEvidence: loaderDetection.evidence,
|
|
1201
|
+
loaderWarnings: loaderDetection.warnings,
|
|
1202
|
+
nextAction: "Provide projectPath for a Forge/NeoForge workspace with generated runtime jars, or run the Gradle tasks that populate transformed runtime artifacts before retrying."
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
const selected = discovery.selected;
|
|
1207
|
+
const selectedLower = selected.jarPath.toLowerCase();
|
|
1208
|
+
const mappingApplied = input.atNamespace === "srg" || selectedLower.includes("srg")
|
|
1209
|
+
? "srg"
|
|
1210
|
+
: loader === "neoforge" || selectedLower.includes("moddev") || selectedLower.includes("neoforge")
|
|
1211
|
+
? "mojang"
|
|
1212
|
+
: "obfuscated";
|
|
1213
|
+
const scopeFallback = requestedScope !== selected.appliedScope
|
|
1214
|
+
? {
|
|
1215
|
+
requested: requestedScope,
|
|
1216
|
+
applied: selected.appliedScope,
|
|
1217
|
+
reason: selected.appliedScope === "merged"
|
|
1218
|
+
? "Resolved a nearby merged runtime jar because no transformed loader artifact was available."
|
|
1219
|
+
: "Resolved the closest transformed runtime artifact for validation."
|
|
1220
|
+
}
|
|
1221
|
+
: undefined;
|
|
1222
|
+
return {
|
|
1223
|
+
version,
|
|
1224
|
+
jarPath: selected.jarPath,
|
|
1225
|
+
requestedScope,
|
|
1226
|
+
appliedScope: selected.appliedScope,
|
|
1227
|
+
requestedMapping: input.atNamespace,
|
|
1228
|
+
mappingApplied,
|
|
1229
|
+
origin: selected.origin,
|
|
1230
|
+
resolutionNotes: scopeFallback ? [scopeFallback.reason] : undefined,
|
|
1231
|
+
scopeFallback
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
835
1234
|
buildVersionSourceRecoveryCommand(projectPath) {
|
|
836
1235
|
const normalizedProjectPath = normalizeOptionalProjectPath(projectPath);
|
|
837
1236
|
const prefix = normalizedProjectPath
|
|
@@ -931,9 +1330,11 @@ export class SourceService {
|
|
|
931
1330
|
let resolvedTarget = { kind, value };
|
|
932
1331
|
let resolvedVersion;
|
|
933
1332
|
let versionSourceDiscovery;
|
|
1333
|
+
let runtimeNamesUnobfuscated = false;
|
|
934
1334
|
if (kind === "version") {
|
|
935
1335
|
const versionJar = await this.versionService.resolveVersionJar(value);
|
|
936
1336
|
resolvedVersion = versionJar.version;
|
|
1337
|
+
runtimeNamesUnobfuscated = isUnobfuscatedVersion(resolvedVersion);
|
|
937
1338
|
resolvedTarget = {
|
|
938
1339
|
kind: "jar",
|
|
939
1340
|
value: versionJar.jarPath
|
|
@@ -948,6 +1349,9 @@ export class SourceService {
|
|
|
948
1349
|
// coordinate validity is validated by resolver, keep version undefined on parse failure.
|
|
949
1350
|
}
|
|
950
1351
|
}
|
|
1352
|
+
if (!runtimeNamesUnobfuscated && resolvedVersion && isUnobfuscatedVersion(resolvedVersion)) {
|
|
1353
|
+
runtimeNamesUnobfuscated = true;
|
|
1354
|
+
}
|
|
951
1355
|
// Unobfuscated versions (MC 26.1+) ship with deobfuscated runtime names; intermediary/yarn are not applicable.
|
|
952
1356
|
let effectiveMapping = mapping;
|
|
953
1357
|
if ((mapping === "intermediary" || mapping === "yarn") &&
|
|
@@ -956,7 +1360,11 @@ export class SourceService {
|
|
|
956
1360
|
warnings.push(`Version ${resolvedVersion} is unobfuscated; ${mapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
|
|
957
1361
|
effectiveMapping = "obfuscated";
|
|
958
1362
|
}
|
|
959
|
-
if (kind === "version" &&
|
|
1363
|
+
if (kind === "version" &&
|
|
1364
|
+
resolvedVersion &&
|
|
1365
|
+
effectiveMapping === "mojang" &&
|
|
1366
|
+
!runtimeNamesUnobfuscated &&
|
|
1367
|
+
scope !== "vanilla") {
|
|
960
1368
|
versionSourceDiscovery = await this.discoverVersionSourceJar({
|
|
961
1369
|
version: resolvedVersion,
|
|
962
1370
|
projectPath: input.projectPath
|
|
@@ -991,7 +1399,8 @@ export class SourceService {
|
|
|
991
1399
|
mappingDecision = applyMappingPipeline({
|
|
992
1400
|
requestedMapping: effectiveMapping,
|
|
993
1401
|
target: { kind, value },
|
|
994
|
-
resolved
|
|
1402
|
+
resolved,
|
|
1403
|
+
runtimeNamesUnobfuscated
|
|
995
1404
|
});
|
|
996
1405
|
}
|
|
997
1406
|
catch (caughtError) {
|
|
@@ -1420,7 +1829,14 @@ export class SourceService {
|
|
|
1420
1829
|
return this.mappingService.getClassApiMatrix(input);
|
|
1421
1830
|
}
|
|
1422
1831
|
async checkSymbolExists(input) {
|
|
1423
|
-
|
|
1832
|
+
const result = await this.mappingService.checkSymbolExists(input);
|
|
1833
|
+
if (result.status !== "mapping_unavailable" ||
|
|
1834
|
+
!isUnobfuscatedVersion(input.version) ||
|
|
1835
|
+
(input.sourceMapping !== "mojang" && input.sourceMapping !== "obfuscated")) {
|
|
1836
|
+
return result;
|
|
1837
|
+
}
|
|
1838
|
+
const runtimeFallback = await this.checkSymbolExistsInUnobfuscatedRuntime(input, result);
|
|
1839
|
+
return runtimeFallback ?? result;
|
|
1424
1840
|
}
|
|
1425
1841
|
async resolveWorkspaceSymbol(input) {
|
|
1426
1842
|
const projectPath = input.projectPath?.trim();
|
|
@@ -1642,10 +2058,152 @@ export class SourceService {
|
|
|
1642
2058
|
warnings: [...warnings, ...mapped.warnings]
|
|
1643
2059
|
};
|
|
1644
2060
|
}
|
|
2061
|
+
async checkSymbolExistsInUnobfuscatedRuntime(input, fallbackBase) {
|
|
2062
|
+
const version = input.version.trim();
|
|
2063
|
+
const name = input.name.trim();
|
|
2064
|
+
const owner = input.owner?.trim();
|
|
2065
|
+
if (!version || !name) {
|
|
2066
|
+
return undefined;
|
|
2067
|
+
}
|
|
2068
|
+
if (input.kind === "class" && input.nameMode !== "fqcn" && !name.includes(".")) {
|
|
2069
|
+
return {
|
|
2070
|
+
...fallbackBase,
|
|
2071
|
+
warnings: [
|
|
2072
|
+
...fallbackBase.warnings,
|
|
2073
|
+
`Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
|
|
2074
|
+
]
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
const querySymbol = input.kind === "class"
|
|
2078
|
+
? {
|
|
2079
|
+
kind: "class",
|
|
2080
|
+
name,
|
|
2081
|
+
symbol: name
|
|
2082
|
+
}
|
|
2083
|
+
: input.kind === "field"
|
|
2084
|
+
? {
|
|
2085
|
+
kind: "field",
|
|
2086
|
+
owner,
|
|
2087
|
+
name,
|
|
2088
|
+
symbol: `${owner}.${name}`
|
|
2089
|
+
}
|
|
2090
|
+
: {
|
|
2091
|
+
kind: "method",
|
|
2092
|
+
owner,
|
|
2093
|
+
name,
|
|
2094
|
+
descriptor: input.descriptor?.trim(),
|
|
2095
|
+
symbol: `${owner}.${name}${input.descriptor?.trim() ?? ""}`
|
|
2096
|
+
};
|
|
2097
|
+
const targetClass = input.kind === "class" ? name : owner;
|
|
2098
|
+
if (!targetClass) {
|
|
2099
|
+
return fallbackBase;
|
|
2100
|
+
}
|
|
2101
|
+
let jarPath;
|
|
2102
|
+
try {
|
|
2103
|
+
({ jarPath } = await this.versionService.resolveVersionJar(version));
|
|
2104
|
+
}
|
|
2105
|
+
catch {
|
|
2106
|
+
return undefined;
|
|
2107
|
+
}
|
|
2108
|
+
let signature;
|
|
2109
|
+
try {
|
|
2110
|
+
signature = await this.explorerService.getSignature({
|
|
2111
|
+
fqn: targetClass,
|
|
2112
|
+
jarPath,
|
|
2113
|
+
access: "all"
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
catch {
|
|
2117
|
+
return {
|
|
2118
|
+
...fallbackBase,
|
|
2119
|
+
querySymbol,
|
|
2120
|
+
warnings: [
|
|
2121
|
+
...fallbackBase.warnings,
|
|
2122
|
+
`Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
|
|
2123
|
+
]
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
const warnings = [
|
|
2127
|
+
...fallbackBase.warnings,
|
|
2128
|
+
...signature.warnings,
|
|
2129
|
+
`Version ${version} is unobfuscated; validated symbol existence against runtime bytecode.`
|
|
2130
|
+
];
|
|
2131
|
+
const buildResolved = (resolvedSymbol) => ({
|
|
2132
|
+
...fallbackBase,
|
|
2133
|
+
querySymbol,
|
|
2134
|
+
resolved: true,
|
|
2135
|
+
status: "resolved",
|
|
2136
|
+
resolvedSymbol,
|
|
2137
|
+
candidates: resolvedSymbol
|
|
2138
|
+
? [{
|
|
2139
|
+
...resolvedSymbol,
|
|
2140
|
+
matchKind: "exact",
|
|
2141
|
+
confidence: 1
|
|
2142
|
+
}]
|
|
2143
|
+
: [],
|
|
2144
|
+
candidateCount: resolvedSymbol ? 1 : 0,
|
|
2145
|
+
warnings
|
|
2146
|
+
});
|
|
2147
|
+
const buildUnresolved = (status) => ({
|
|
2148
|
+
...fallbackBase,
|
|
2149
|
+
querySymbol,
|
|
2150
|
+
resolved: false,
|
|
2151
|
+
status,
|
|
2152
|
+
resolvedSymbol: undefined,
|
|
2153
|
+
candidates: [],
|
|
2154
|
+
candidateCount: 0,
|
|
2155
|
+
warnings
|
|
2156
|
+
});
|
|
2157
|
+
if (input.kind === "class") {
|
|
2158
|
+
return buildResolved({
|
|
2159
|
+
kind: "class",
|
|
2160
|
+
name,
|
|
2161
|
+
symbol: name
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
if (input.kind === "field") {
|
|
2165
|
+
const matched = signature.fields.filter((field) => field.name === name);
|
|
2166
|
+
if (matched.length !== 1) {
|
|
2167
|
+
return buildUnresolved(matched.length > 1 ? "ambiguous" : "not_found");
|
|
2168
|
+
}
|
|
2169
|
+
return buildResolved({
|
|
2170
|
+
kind: "field",
|
|
2171
|
+
owner,
|
|
2172
|
+
name,
|
|
2173
|
+
symbol: `${owner}.${name}`
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
const methodCandidates = signature.methods.filter((method) => method.name === name);
|
|
2177
|
+
if (input.signatureMode === "name-only") {
|
|
2178
|
+
if (methodCandidates.length !== 1) {
|
|
2179
|
+
return buildUnresolved(methodCandidates.length > 1 ? "ambiguous" : "not_found");
|
|
2180
|
+
}
|
|
2181
|
+
return buildResolved({
|
|
2182
|
+
kind: "method",
|
|
2183
|
+
owner,
|
|
2184
|
+
name,
|
|
2185
|
+
descriptor: methodCandidates[0]?.jvmDescriptor,
|
|
2186
|
+
symbol: `${owner}.${name}${methodCandidates[0]?.jvmDescriptor ?? ""}`
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
const descriptor = input.descriptor?.trim();
|
|
2190
|
+
const matched = methodCandidates.filter((method) => method.jvmDescriptor === descriptor);
|
|
2191
|
+
if (matched.length !== 1) {
|
|
2192
|
+
return buildUnresolved(matched.length > 1 ? "ambiguous" : "not_found");
|
|
2193
|
+
}
|
|
2194
|
+
return buildResolved({
|
|
2195
|
+
kind: "method",
|
|
2196
|
+
owner,
|
|
2197
|
+
name,
|
|
2198
|
+
descriptor,
|
|
2199
|
+
symbol: `${owner}.${name}${descriptor ?? ""}`
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
1645
2202
|
async traceSymbolLifecycle(input) {
|
|
1646
2203
|
const mapping = normalizeMapping(input.mapping);
|
|
1647
|
-
const { className: userClassName, methodName: userMethodName } = parseQualifiedMethodSymbol(input.symbol);
|
|
1648
|
-
const descriptor = normalizeOptionalString(input.descriptor)
|
|
2204
|
+
const { className: userClassName, methodName: userMethodName, inlineDescriptor } = parseQualifiedMethodSymbol(input.symbol);
|
|
2205
|
+
const descriptor = normalizeOptionalString(input.descriptor)
|
|
2206
|
+
?? (looksLikeJvmMethodDescriptor(inlineDescriptor) ? normalizeOptionalString(inlineDescriptor) : undefined);
|
|
1649
2207
|
const includeTimeline = input.includeTimeline ?? false;
|
|
1650
2208
|
const includeSnapshots = input.includeSnapshots ?? false;
|
|
1651
2209
|
const maxVersions = clampLimit(input.maxVersions, 120, 400);
|
|
@@ -1701,60 +2259,75 @@ export class SourceService {
|
|
|
1701
2259
|
selectedVersions = selectedVersions.slice(selectedVersions.length - maxVersions);
|
|
1702
2260
|
warnings.push(`Version scan truncated to ${maxVersions} entries. Effective fromVersion is now "${selectedVersions[0]}".`);
|
|
1703
2261
|
}
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
|
|
2262
|
+
const referenceVersion = selectedVersions[selectedVersions.length - 1];
|
|
2263
|
+
await this.rejectLifecycleClassLikeInput({
|
|
2264
|
+
symbol: input.symbol,
|
|
2265
|
+
className: userClassName,
|
|
2266
|
+
methodName: userMethodName,
|
|
2267
|
+
mapping,
|
|
2268
|
+
version: referenceVersion,
|
|
2269
|
+
sourcePriority: input.sourcePriority
|
|
2270
|
+
});
|
|
2271
|
+
const scannedResults = await mapWithConcurrencyLimit(selectedVersions, TRACE_LIFECYCLE_MAX_CONCURRENCY, async (version) => {
|
|
2272
|
+
const versionWarnings = [];
|
|
1707
2273
|
try {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
]);
|
|
1714
|
-
resolvedSymbols = {
|
|
1715
|
-
className: obfuscatedClassName,
|
|
1716
|
-
methodName: obfuscatedMethod.name,
|
|
1717
|
-
methodDescriptor: obfuscatedMethod.descriptor
|
|
1718
|
-
};
|
|
1719
|
-
resolvedSymbolsByVersion.set(version, resolvedSymbols);
|
|
1720
|
-
}
|
|
1721
|
-
const resolvedJar = await this.versionService.resolveVersionJar(version);
|
|
2274
|
+
const [obfuscatedClassName, obfuscatedMethod, resolvedJar] = await Promise.all([
|
|
2275
|
+
this.resolveToObfuscatedClassName(userClassName, version, mapping, input.sourcePriority, versionWarnings),
|
|
2276
|
+
this.resolveToObfuscatedMemberName(userMethodName, userClassName, descriptor, "method", version, mapping, input.sourcePriority, versionWarnings),
|
|
2277
|
+
this.versionService.resolveVersionJar(version)
|
|
2278
|
+
]);
|
|
1722
2279
|
const signature = await this.explorerService.getSignature({
|
|
1723
|
-
fqn:
|
|
2280
|
+
fqn: obfuscatedClassName,
|
|
1724
2281
|
jarPath: resolvedJar.jarPath,
|
|
1725
2282
|
access: "all",
|
|
1726
2283
|
includeSynthetic: true
|
|
1727
2284
|
});
|
|
1728
|
-
const sameNameMethods = signature.methods.filter((method) => method.name ===
|
|
1729
|
-
const
|
|
1730
|
-
|
|
2285
|
+
const sameNameMethods = signature.methods.filter((method) => method.name === obfuscatedMethod.name);
|
|
2286
|
+
const effectiveDescriptor = obfuscatedMethod.descriptor ?? descriptor;
|
|
2287
|
+
const matchesDescriptor = effectiveDescriptor
|
|
2288
|
+
? sameNameMethods.some((method) => method.jvmDescriptor === effectiveDescriptor)
|
|
1731
2289
|
: sameNameMethods.length > 0;
|
|
1732
2290
|
const reason = !matchesDescriptor && descriptor && sameNameMethods.length > 0 ? "descriptor-mismatch" : undefined;
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2291
|
+
return {
|
|
2292
|
+
entry: {
|
|
2293
|
+
version,
|
|
2294
|
+
exists: matchesDescriptor,
|
|
2295
|
+
reason,
|
|
2296
|
+
determinate: true
|
|
2297
|
+
},
|
|
2298
|
+
warnings: versionWarnings
|
|
2299
|
+
};
|
|
1739
2300
|
}
|
|
1740
2301
|
catch (caughtError) {
|
|
1741
2302
|
if (isAppError(caughtError) && caughtError.code === ERROR_CODES.CLASS_NOT_FOUND) {
|
|
1742
|
-
|
|
2303
|
+
return {
|
|
2304
|
+
entry: {
|
|
2305
|
+
version,
|
|
2306
|
+
exists: false,
|
|
2307
|
+
reason: "class-not-found",
|
|
2308
|
+
determinate: true
|
|
2309
|
+
},
|
|
2310
|
+
warnings: versionWarnings
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
versionWarnings.push(`Failed to evaluate ${version}: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
|
|
2314
|
+
return {
|
|
2315
|
+
entry: {
|
|
1743
2316
|
version,
|
|
1744
2317
|
exists: false,
|
|
1745
|
-
reason: "
|
|
1746
|
-
determinate:
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
}
|
|
1750
|
-
scanned.push({
|
|
1751
|
-
version,
|
|
1752
|
-
exists: false,
|
|
1753
|
-
reason: "unresolved",
|
|
1754
|
-
determinate: false
|
|
1755
|
-
});
|
|
1756
|
-
warnings.push(`Failed to evaluate ${version}: ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
|
|
2318
|
+
reason: "unresolved",
|
|
2319
|
+
determinate: false
|
|
2320
|
+
},
|
|
2321
|
+
warnings: versionWarnings
|
|
2322
|
+
};
|
|
1757
2323
|
}
|
|
2324
|
+
finally {
|
|
2325
|
+
this.releaseLifecycleMappingGraph(version, input.sourcePriority);
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
const scanned = scannedResults.map((result) => result.entry);
|
|
2329
|
+
for (const result of scannedResults) {
|
|
2330
|
+
warnings.push(...result.warnings);
|
|
1758
2331
|
}
|
|
1759
2332
|
const determinate = scanned.filter((entry) => entry.determinate);
|
|
1760
2333
|
const present = determinate.filter((entry) => entry.exists);
|
|
@@ -2695,17 +3268,18 @@ export class SourceService {
|
|
|
2695
3268
|
path: this.resolveMixinInputPath(path, "path")
|
|
2696
3269
|
},
|
|
2697
3270
|
sourcePath: path
|
|
2698
|
-
})), input);
|
|
3271
|
+
})), input, []);
|
|
2699
3272
|
}
|
|
2700
3273
|
const resolvedInput = mode === "project"
|
|
2701
|
-
? this.createProjectValidateMixinConfigInput(input)
|
|
3274
|
+
? await this.createProjectValidateMixinConfigInput(input)
|
|
2702
3275
|
: input;
|
|
2703
|
-
const configSources = await this.resolveMixinConfigSources(resolvedInput);
|
|
3276
|
+
const { sources: configSources, warnings: configWarnings } = await this.resolveMixinConfigSources(resolvedInput);
|
|
2704
3277
|
if (configSources.length === 0) {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3278
|
+
const emptyOutput = this.buildValidateMixinOutput(mode, []);
|
|
3279
|
+
return this.applyValidateMixinOutputCompaction({
|
|
3280
|
+
...emptyOutput,
|
|
3281
|
+
warnings: [...new Set([...emptyOutput.warnings, ...configWarnings])]
|
|
3282
|
+
}, input);
|
|
2709
3283
|
}
|
|
2710
3284
|
return this.validateMixinMany(mode, configSources.map((entry) => ({
|
|
2711
3285
|
source: {
|
|
@@ -2715,19 +3289,19 @@ export class SourceService {
|
|
|
2715
3289
|
configPath: entry.configPath
|
|
2716
3290
|
},
|
|
2717
3291
|
sourcePath: entry.sourcePath
|
|
2718
|
-
})), resolvedInput);
|
|
3292
|
+
})), resolvedInput, configWarnings);
|
|
2719
3293
|
}
|
|
2720
|
-
createProjectValidateMixinConfigInput(input) {
|
|
3294
|
+
async createProjectValidateMixinConfigInput(input) {
|
|
2721
3295
|
if (input.input.mode !== "project") {
|
|
2722
3296
|
return input;
|
|
2723
3297
|
}
|
|
2724
3298
|
const resolvedProjectPath = this.resolveMixinInputPath(input.input.path, "path");
|
|
2725
|
-
const configPaths = fastGlob.
|
|
3299
|
+
const configPaths = (await fastGlob.glob(["**/*.mixins.json"], {
|
|
2726
3300
|
cwd: resolvedProjectPath,
|
|
2727
3301
|
absolute: true,
|
|
2728
3302
|
onlyFiles: true,
|
|
2729
3303
|
ignore: [...MIXIN_PROJECT_DISCOVERY_IGNORES]
|
|
2730
|
-
}).sort((left, right) => left.localeCompare(right));
|
|
3304
|
+
})).sort((left, right) => left.localeCompare(right));
|
|
2731
3305
|
if (configPaths.length === 0) {
|
|
2732
3306
|
throw createError({
|
|
2733
3307
|
code: ERROR_CODES.INVALID_INPUT,
|
|
@@ -2770,7 +3344,8 @@ export class SourceService {
|
|
|
2770
3344
|
name: input.className,
|
|
2771
3345
|
sourceMapping: input.sourceMapping,
|
|
2772
3346
|
targetMapping: input.targetMapping,
|
|
2773
|
-
sourcePriority: input.sourcePriority
|
|
3347
|
+
sourcePriority: input.sourcePriority,
|
|
3348
|
+
projectPath: input.projectPath
|
|
2774
3349
|
});
|
|
2775
3350
|
}
|
|
2776
3351
|
const cacheKey = [
|
|
@@ -2778,7 +3353,8 @@ export class SourceService {
|
|
|
2778
3353
|
input.className,
|
|
2779
3354
|
input.sourceMapping,
|
|
2780
3355
|
input.targetMapping,
|
|
2781
|
-
input.sourcePriority
|
|
3356
|
+
input.sourcePriority,
|
|
3357
|
+
input.projectPath ?? ""
|
|
2782
3358
|
].join("\0");
|
|
2783
3359
|
const cached = cache.get(cacheKey);
|
|
2784
3360
|
if (cached) {
|
|
@@ -2790,7 +3366,8 @@ export class SourceService {
|
|
|
2790
3366
|
name: input.className,
|
|
2791
3367
|
sourceMapping: input.sourceMapping,
|
|
2792
3368
|
targetMapping: input.targetMapping,
|
|
2793
|
-
sourcePriority: input.sourcePriority
|
|
3369
|
+
sourcePriority: input.sourcePriority,
|
|
3370
|
+
projectPath: input.projectPath
|
|
2794
3371
|
}).catch((error) => {
|
|
2795
3372
|
cache.delete(cacheKey);
|
|
2796
3373
|
throw error;
|
|
@@ -2971,6 +3548,7 @@ export class SourceService {
|
|
|
2971
3548
|
sourceMapping: requestedMapping,
|
|
2972
3549
|
targetMapping: signatureLookupMapping,
|
|
2973
3550
|
sourcePriority: currentSourcePriority,
|
|
3551
|
+
projectPath: input.projectPath,
|
|
2974
3552
|
batchCaches: input.batchCaches
|
|
2975
3553
|
});
|
|
2976
3554
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
@@ -3004,9 +3582,9 @@ export class SourceService {
|
|
|
3004
3582
|
if (requestedMapping !== signatureLookupMapping) {
|
|
3005
3583
|
try {
|
|
3006
3584
|
const [ctorResult, methodResult, fieldResult] = await Promise.all([
|
|
3007
|
-
this.remapSignatureMembers(sig.constructors, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
|
|
3008
|
-
this.remapSignatureMembers(sig.methods, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings),
|
|
3009
|
-
this.remapSignatureMembers(sig.fields, "field", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings)
|
|
3585
|
+
this.remapSignatureMembers(sig.constructors, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings, input.projectPath),
|
|
3586
|
+
this.remapSignatureMembers(sig.methods, "method", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings, input.projectPath),
|
|
3587
|
+
this.remapSignatureMembers(sig.fields, "field", version, signatureLookupMapping, requestedMapping, currentSourcePriority, warnings, input.projectPath)
|
|
3010
3588
|
]);
|
|
3011
3589
|
constructors = ctorResult.members;
|
|
3012
3590
|
methods = methodResult.members;
|
|
@@ -3245,9 +3823,13 @@ export class SourceService {
|
|
|
3245
3823
|
}
|
|
3246
3824
|
async resolveMixinConfigSources(input) {
|
|
3247
3825
|
if (input.input.mode !== "config") {
|
|
3248
|
-
return
|
|
3826
|
+
return {
|
|
3827
|
+
sources: [],
|
|
3828
|
+
warnings: []
|
|
3829
|
+
};
|
|
3249
3830
|
}
|
|
3250
3831
|
const results = [];
|
|
3832
|
+
const warnings = [];
|
|
3251
3833
|
for (const rawConfigPath of input.input.configPaths) {
|
|
3252
3834
|
const resolvedConfigPath = this.resolveMixinInputPath(rawConfigPath, "configPath");
|
|
3253
3835
|
let configJson;
|
|
@@ -3268,6 +3850,7 @@ export class SourceService {
|
|
|
3268
3850
|
...(configJson.server ?? [])
|
|
3269
3851
|
];
|
|
3270
3852
|
if (classNames.length === 0) {
|
|
3853
|
+
warnings.push(`Mixin config "${resolvedConfigPath}" contains no mixin class entries.`);
|
|
3271
3854
|
continue;
|
|
3272
3855
|
}
|
|
3273
3856
|
const projectBase = input.projectPath
|
|
@@ -3278,11 +3861,21 @@ export class SourceService {
|
|
|
3278
3861
|
sourceRootCandidates = input.sourceRoots;
|
|
3279
3862
|
}
|
|
3280
3863
|
else {
|
|
3281
|
-
const detected =
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3864
|
+
const detected = [];
|
|
3865
|
+
for (const candidateRoot of COMMON_SOURCE_ROOTS) {
|
|
3866
|
+
let foundInRoot = false;
|
|
3867
|
+
for (const className of classNames) {
|
|
3868
|
+
const fqcn = pkg ? `${pkg}.${className}` : className;
|
|
3869
|
+
const relative = fqcn.replace(/\./g, "/") + ".java";
|
|
3870
|
+
if (await pathExists(resolvePath(projectBase, candidateRoot, relative))) {
|
|
3871
|
+
foundInRoot = true;
|
|
3872
|
+
break;
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
if (foundInRoot) {
|
|
3876
|
+
detected.push(candidateRoot);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3286
3879
|
sourceRootCandidates = detected.length > 0 ? detected : ["src/main/java"];
|
|
3287
3880
|
}
|
|
3288
3881
|
for (const cls of classNames) {
|
|
@@ -3291,7 +3884,7 @@ export class SourceService {
|
|
|
3291
3884
|
let sourcePath = resolvePath(projectBase, sourceRootCandidates[0], relativePath);
|
|
3292
3885
|
for (const root of sourceRootCandidates) {
|
|
3293
3886
|
const candidate = resolvePath(projectBase, root, relativePath);
|
|
3294
|
-
if (
|
|
3887
|
+
if (await pathExists(candidate)) {
|
|
3295
3888
|
sourcePath = candidate;
|
|
3296
3889
|
break;
|
|
3297
3890
|
}
|
|
@@ -3302,9 +3895,12 @@ export class SourceService {
|
|
|
3302
3895
|
});
|
|
3303
3896
|
}
|
|
3304
3897
|
}
|
|
3305
|
-
return
|
|
3898
|
+
return {
|
|
3899
|
+
sources: results,
|
|
3900
|
+
warnings
|
|
3901
|
+
};
|
|
3306
3902
|
}
|
|
3307
|
-
async validateMixinMany(mode, entries, input) {
|
|
3903
|
+
async validateMixinMany(mode, entries, input, additionalWarnings) {
|
|
3308
3904
|
const results = [];
|
|
3309
3905
|
const batchWarningMode = input.warningMode ?? "aggregated";
|
|
3310
3906
|
const { input: _discardedInput, ...sharedInput } = input;
|
|
@@ -3331,7 +3927,13 @@ export class SourceService {
|
|
|
3331
3927
|
});
|
|
3332
3928
|
}
|
|
3333
3929
|
}
|
|
3334
|
-
|
|
3930
|
+
const output = this.buildValidateMixinOutput(mode, results);
|
|
3931
|
+
return this.applyValidateMixinOutputCompaction({
|
|
3932
|
+
...output,
|
|
3933
|
+
warnings: additionalWarnings.length === 0
|
|
3934
|
+
? output.warnings
|
|
3935
|
+
: [...new Set([...output.warnings, ...additionalWarnings])]
|
|
3936
|
+
}, input);
|
|
3335
3937
|
}
|
|
3336
3938
|
applyValidateMixinOutputCompaction(output, input) {
|
|
3337
3939
|
let nextOutput = output;
|
|
@@ -3473,7 +4075,6 @@ export class SourceService {
|
|
|
3473
4075
|
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
|
|
3474
4076
|
}
|
|
3475
4077
|
const warnings = [];
|
|
3476
|
-
const { jarPath } = await this.versionService.resolveVersionJar(version);
|
|
3477
4078
|
const parsed = parseAccessWidener(content);
|
|
3478
4079
|
const headerNamespaceRaw = normalizeOptionalString(parsed.namespace);
|
|
3479
4080
|
const overrideMapping = input.mapping ? normalizeMapping(input.mapping) : undefined;
|
|
@@ -3485,7 +4086,27 @@ export class SourceService {
|
|
|
3485
4086
|
if (overrideMapping && headerNamespace && overrideMapping !== headerNamespace) {
|
|
3486
4087
|
warnings.push(`Using mapping override "${overrideMapping}" instead of header namespace "${headerNamespaceRaw}".`);
|
|
3487
4088
|
}
|
|
3488
|
-
const
|
|
4089
|
+
const runtimeAware = input.projectPath != null || input.scope != null || input.preferProjectVersion === true;
|
|
4090
|
+
let resolvedVersion = version;
|
|
4091
|
+
let jarPath;
|
|
4092
|
+
let lookupMapping = "obfuscated";
|
|
4093
|
+
let provenance;
|
|
4094
|
+
if (runtimeAware) {
|
|
4095
|
+
provenance = await this.resolveAccessWidenerRuntimeArtifact({
|
|
4096
|
+
version,
|
|
4097
|
+
awNamespace,
|
|
4098
|
+
projectPath: input.projectPath,
|
|
4099
|
+
scope: input.scope,
|
|
4100
|
+
preferProjectVersion: input.preferProjectVersion
|
|
4101
|
+
});
|
|
4102
|
+
resolvedVersion = provenance.version;
|
|
4103
|
+
jarPath = provenance.jarPath;
|
|
4104
|
+
lookupMapping = provenance.mappingApplied;
|
|
4105
|
+
}
|
|
4106
|
+
else {
|
|
4107
|
+
({ jarPath } = await this.versionService.resolveVersionJar(version));
|
|
4108
|
+
}
|
|
4109
|
+
const needsLookupMapping = awNamespace !== lookupMapping;
|
|
3489
4110
|
// Collect unique class FQNs from entries
|
|
3490
4111
|
const classFqns = new Set();
|
|
3491
4112
|
for (const entry of parsed.entries) {
|
|
@@ -3494,22 +4115,23 @@ export class SourceService {
|
|
|
3494
4115
|
}
|
|
3495
4116
|
const membersByClass = new Map();
|
|
3496
4117
|
for (const fqn of classFqns) {
|
|
3497
|
-
let
|
|
3498
|
-
if (
|
|
4118
|
+
let lookupFqn = fqn;
|
|
4119
|
+
if (needsLookupMapping) {
|
|
3499
4120
|
try {
|
|
3500
4121
|
const mapped = await this.mappingService.findMapping({
|
|
3501
|
-
version,
|
|
4122
|
+
version: resolvedVersion,
|
|
3502
4123
|
kind: "class",
|
|
3503
4124
|
name: fqn,
|
|
3504
4125
|
sourceMapping: awNamespace,
|
|
3505
|
-
targetMapping:
|
|
3506
|
-
sourcePriority: input.sourcePriority
|
|
4126
|
+
targetMapping: lookupMapping,
|
|
4127
|
+
sourcePriority: input.sourcePriority,
|
|
4128
|
+
projectPath: input.projectPath
|
|
3507
4129
|
});
|
|
3508
4130
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
3509
|
-
|
|
4131
|
+
lookupFqn = mapped.resolvedSymbol.name;
|
|
3510
4132
|
}
|
|
3511
4133
|
else {
|
|
3512
|
-
warnings.push(`Could not map class "${fqn}" from ${awNamespace} to
|
|
4134
|
+
warnings.push(`Could not map class "${fqn}" from ${awNamespace} to ${lookupMapping}.`);
|
|
3513
4135
|
}
|
|
3514
4136
|
}
|
|
3515
4137
|
catch {
|
|
@@ -3518,23 +4140,156 @@ export class SourceService {
|
|
|
3518
4140
|
}
|
|
3519
4141
|
try {
|
|
3520
4142
|
const sig = await this.explorerService.getSignature({
|
|
3521
|
-
fqn:
|
|
4143
|
+
fqn: lookupFqn,
|
|
3522
4144
|
jarPath,
|
|
3523
4145
|
access: "all"
|
|
3524
4146
|
});
|
|
3525
4147
|
warnings.push(...sig.warnings);
|
|
4148
|
+
let constructors = sig.constructors;
|
|
4149
|
+
let methods = sig.methods;
|
|
4150
|
+
let fields = sig.fields;
|
|
4151
|
+
if (needsLookupMapping) {
|
|
4152
|
+
const [ctorResult, methodResult, fieldResult] = await Promise.all([
|
|
4153
|
+
this.remapSignatureMembers(sig.constructors, "method", resolvedVersion, lookupMapping, awNamespace, input.sourcePriority, warnings, input.projectPath),
|
|
4154
|
+
this.remapSignatureMembers(sig.methods, "method", resolvedVersion, lookupMapping, awNamespace, input.sourcePriority, warnings, input.projectPath),
|
|
4155
|
+
this.remapSignatureMembers(sig.fields, "field", resolvedVersion, lookupMapping, awNamespace, input.sourcePriority, warnings, input.projectPath)
|
|
4156
|
+
]);
|
|
4157
|
+
constructors = ctorResult.members;
|
|
4158
|
+
methods = methodResult.members;
|
|
4159
|
+
fields = fieldResult.members;
|
|
4160
|
+
}
|
|
3526
4161
|
membersByClass.set(fqn, {
|
|
3527
4162
|
className: fqn,
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
4163
|
+
classAccessFlags: sig.classAccessFlags,
|
|
4164
|
+
constructors,
|
|
4165
|
+
methods,
|
|
4166
|
+
fields
|
|
3531
4167
|
});
|
|
3532
4168
|
}
|
|
3533
4169
|
catch {
|
|
3534
|
-
warnings.push(`Could not load signature for class "${
|
|
4170
|
+
warnings.push(`Could not load signature for class "${lookupFqn}".`);
|
|
3535
4171
|
}
|
|
3536
4172
|
}
|
|
3537
|
-
|
|
4173
|
+
const result = validateParsedAccessWidener(parsed, membersByClass, warnings, {
|
|
4174
|
+
includeRuntimeEvidence: runtimeAware
|
|
4175
|
+
});
|
|
4176
|
+
if (provenance) {
|
|
4177
|
+
result.provenance = provenance;
|
|
4178
|
+
}
|
|
4179
|
+
return result;
|
|
4180
|
+
}
|
|
4181
|
+
async validateAccessTransformer(input) {
|
|
4182
|
+
const version = input.version.trim();
|
|
4183
|
+
if (!version) {
|
|
4184
|
+
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "version must be non-empty." });
|
|
4185
|
+
}
|
|
4186
|
+
const content = input.content;
|
|
4187
|
+
if (!content.trim()) {
|
|
4188
|
+
throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
|
|
4189
|
+
}
|
|
4190
|
+
const warnings = [];
|
|
4191
|
+
const parsed = parseAccessTransformer(content);
|
|
4192
|
+
const atNamespace = await this.resolveAccessTransformerNamespace({
|
|
4193
|
+
atNamespace: input.atNamespace,
|
|
4194
|
+
projectPath: input.projectPath
|
|
4195
|
+
});
|
|
4196
|
+
const runtimeAware = input.projectPath != null || input.scope != null || input.preferProjectVersion === true;
|
|
4197
|
+
let resolvedVersion = version;
|
|
4198
|
+
let jarPath;
|
|
4199
|
+
let lookupMapping = "obfuscated";
|
|
4200
|
+
let provenance;
|
|
4201
|
+
if (runtimeAware) {
|
|
4202
|
+
provenance = await this.resolveAccessTransformerRuntimeArtifact({
|
|
4203
|
+
version,
|
|
4204
|
+
atNamespace,
|
|
4205
|
+
projectPath: input.projectPath,
|
|
4206
|
+
scope: input.scope,
|
|
4207
|
+
preferProjectVersion: input.preferProjectVersion
|
|
4208
|
+
});
|
|
4209
|
+
resolvedVersion = provenance.version;
|
|
4210
|
+
jarPath = provenance.jarPath;
|
|
4211
|
+
lookupMapping = provenance.mappingApplied;
|
|
4212
|
+
}
|
|
4213
|
+
else {
|
|
4214
|
+
if (atNamespace === "srg") {
|
|
4215
|
+
throw createError({
|
|
4216
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
4217
|
+
message: "atNamespace=srg requires projectPath and scope=loader so a Forge runtime jar can be resolved."
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
({ jarPath } = await this.versionService.resolveVersionJar(version));
|
|
4221
|
+
}
|
|
4222
|
+
const needsLookupMapping = atNamespace !== lookupMapping;
|
|
4223
|
+
const classFqns = new Set(parsed.entries.map((entry) => entry.owner));
|
|
4224
|
+
const membersByClass = new Map();
|
|
4225
|
+
for (const fqn of classFqns) {
|
|
4226
|
+
let lookupFqn = fqn;
|
|
4227
|
+
if (needsLookupMapping) {
|
|
4228
|
+
if (!isSourceMappingNamespace(atNamespace) || !isSourceMappingNamespace(lookupMapping)) {
|
|
4229
|
+
warnings.push(`Could not map class "${fqn}" from ${atNamespace} to ${lookupMapping}.`);
|
|
4230
|
+
}
|
|
4231
|
+
else {
|
|
4232
|
+
try {
|
|
4233
|
+
const mapped = await this.mappingService.findMapping({
|
|
4234
|
+
version: resolvedVersion,
|
|
4235
|
+
kind: "class",
|
|
4236
|
+
name: fqn,
|
|
4237
|
+
sourceMapping: atNamespace,
|
|
4238
|
+
targetMapping: lookupMapping,
|
|
4239
|
+
sourcePriority: input.sourcePriority,
|
|
4240
|
+
projectPath: input.projectPath
|
|
4241
|
+
});
|
|
4242
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
4243
|
+
lookupFqn = mapped.resolvedSymbol.name;
|
|
4244
|
+
}
|
|
4245
|
+
else {
|
|
4246
|
+
warnings.push(`Could not map class "${fqn}" from ${atNamespace} to ${lookupMapping}.`);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
catch {
|
|
4250
|
+
warnings.push(`Mapping lookup failed for class "${fqn}".`);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
try {
|
|
4255
|
+
const sig = await this.explorerService.getSignature({
|
|
4256
|
+
fqn: lookupFqn,
|
|
4257
|
+
jarPath,
|
|
4258
|
+
access: "all"
|
|
4259
|
+
});
|
|
4260
|
+
warnings.push(...sig.warnings);
|
|
4261
|
+
let constructors = sig.constructors;
|
|
4262
|
+
let methods = sig.methods;
|
|
4263
|
+
let fields = sig.fields;
|
|
4264
|
+
if (needsLookupMapping && isSourceMappingNamespace(atNamespace) && isSourceMappingNamespace(lookupMapping)) {
|
|
4265
|
+
const [ctorResult, methodResult, fieldResult] = await Promise.all([
|
|
4266
|
+
this.remapSignatureMembers(sig.constructors, "method", resolvedVersion, lookupMapping, atNamespace, input.sourcePriority, warnings, input.projectPath),
|
|
4267
|
+
this.remapSignatureMembers(sig.methods, "method", resolvedVersion, lookupMapping, atNamespace, input.sourcePriority, warnings, input.projectPath),
|
|
4268
|
+
this.remapSignatureMembers(sig.fields, "field", resolvedVersion, lookupMapping, atNamespace, input.sourcePriority, warnings, input.projectPath)
|
|
4269
|
+
]);
|
|
4270
|
+
constructors = ctorResult.members;
|
|
4271
|
+
methods = methodResult.members;
|
|
4272
|
+
fields = fieldResult.members;
|
|
4273
|
+
}
|
|
4274
|
+
membersByClass.set(fqn, {
|
|
4275
|
+
className: fqn,
|
|
4276
|
+
classAccessFlags: sig.classAccessFlags,
|
|
4277
|
+
constructors,
|
|
4278
|
+
methods,
|
|
4279
|
+
fields
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4282
|
+
catch {
|
|
4283
|
+
warnings.push(`Could not load signature for class "${lookupFqn}".`);
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
const result = validateParsedAccessTransformer(parsed, membersByClass, warnings, {
|
|
4287
|
+
includeRuntimeEvidence: runtimeAware
|
|
4288
|
+
});
|
|
4289
|
+
if (provenance) {
|
|
4290
|
+
result.provenance = provenance;
|
|
4291
|
+
}
|
|
4292
|
+
return result;
|
|
3538
4293
|
}
|
|
3539
4294
|
getRuntimeMetrics() {
|
|
3540
4295
|
return this.metrics.snapshot();
|
|
@@ -4045,6 +4800,38 @@ export class SourceService {
|
|
|
4045
4800
|
details
|
|
4046
4801
|
});
|
|
4047
4802
|
}
|
|
4803
|
+
rejectLifecycleClassLikeInput(input) {
|
|
4804
|
+
if (!looksLikeClassSegment(input.methodName)) {
|
|
4805
|
+
return;
|
|
4806
|
+
}
|
|
4807
|
+
const classLikeSymbol = `${input.className}.${input.methodName}`;
|
|
4808
|
+
throw createError({
|
|
4809
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
4810
|
+
message: `symbol must be in the form "fully.qualified.Class.method".`,
|
|
4811
|
+
details: {
|
|
4812
|
+
symbol: input.symbol,
|
|
4813
|
+
classLikeSymbol,
|
|
4814
|
+
nextAction: "Pass lifecycle input as Class.method and use the separate descriptor field for exact overload matching.",
|
|
4815
|
+
suggestedCall: input.version
|
|
4816
|
+
? {
|
|
4817
|
+
tool: "check-symbol-exists",
|
|
4818
|
+
params: {
|
|
4819
|
+
version: input.version,
|
|
4820
|
+
kind: "class",
|
|
4821
|
+
name: classLikeSymbol,
|
|
4822
|
+
sourceMapping: input.mapping
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
: undefined
|
|
4826
|
+
}
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
releaseLifecycleMappingGraph(version, sourcePriority) {
|
|
4830
|
+
if ("releaseGraphCacheEntry" in this.mappingService &&
|
|
4831
|
+
typeof this.mappingService.releaseGraphCacheEntry === "function") {
|
|
4832
|
+
this.mappingService.releaseGraphCacheEntry(version, sourcePriority);
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4048
4835
|
async resolveToObfuscatedClassName(className, version, mapping, sourcePriority, warnings) {
|
|
4049
4836
|
return this.resolveClassNameForLookup({
|
|
4050
4837
|
className,
|
|
@@ -4106,21 +4893,22 @@ export class SourceService {
|
|
|
4106
4893
|
descriptor: kind === "method" ? descriptor : undefined
|
|
4107
4894
|
};
|
|
4108
4895
|
}
|
|
4109
|
-
async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings) {
|
|
4896
|
+
async remapSignatureMembers(members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings, projectPath) {
|
|
4110
4897
|
const failedNames = new Set();
|
|
4111
4898
|
if (sourceMapping === targetMapping) {
|
|
4112
4899
|
return { members, failedNames };
|
|
4113
4900
|
}
|
|
4114
4901
|
// Build deduplicated lookup tables for member names and owner FQNs
|
|
4115
4902
|
const memberKeyToRemapped = new Map();
|
|
4903
|
+
const memberDescriptorRemapped = new Map();
|
|
4116
4904
|
const ownerToRemapped = new Map();
|
|
4117
4905
|
for (const member of members) {
|
|
4118
4906
|
const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
|
|
4119
4907
|
if (!memberKeyToRemapped.has(memberKey)) {
|
|
4120
|
-
memberKeyToRemapped.set(memberKey, member.name); // default =
|
|
4908
|
+
memberKeyToRemapped.set(memberKey, member.name); // default = source name
|
|
4121
4909
|
}
|
|
4122
4910
|
if (!ownerToRemapped.has(member.ownerFqn)) {
|
|
4123
|
-
ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default =
|
|
4911
|
+
ownerToRemapped.set(member.ownerFqn, member.ownerFqn); // default = source FQN
|
|
4124
4912
|
}
|
|
4125
4913
|
}
|
|
4126
4914
|
// Phase 1: Remap owner FQNs first (needed for member disambiguation)
|
|
@@ -4133,22 +4921,98 @@ export class SourceService {
|
|
|
4133
4921
|
name: obfuscatedFqn,
|
|
4134
4922
|
sourceMapping,
|
|
4135
4923
|
targetMapping,
|
|
4136
|
-
sourcePriority
|
|
4924
|
+
sourcePriority,
|
|
4925
|
+
projectPath
|
|
4137
4926
|
});
|
|
4138
4927
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
4139
4928
|
ownerToRemapped.set(obfuscatedFqn, mapped.resolvedSymbol.name);
|
|
4140
4929
|
}
|
|
4141
4930
|
}
|
|
4142
4931
|
catch {
|
|
4143
|
-
// keep
|
|
4932
|
+
// keep source FQN as fallback
|
|
4144
4933
|
}
|
|
4145
4934
|
}));
|
|
4146
|
-
// Phase
|
|
4935
|
+
// Phase 1.5: Collect class references from descriptors and remap them
|
|
4936
|
+
const descriptorClassRefs = new Set();
|
|
4937
|
+
for (const member of members) {
|
|
4938
|
+
for (const match of member.jvmDescriptor.matchAll(/L([^;]+);/g)) {
|
|
4939
|
+
const dotFqn = match[1].replace(/\//g, ".");
|
|
4940
|
+
if (!ownerToRemapped.has(dotFqn)) {
|
|
4941
|
+
descriptorClassRefs.add(dotFqn);
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
if (descriptorClassRefs.size > 0) {
|
|
4946
|
+
const refs = [...descriptorClassRefs];
|
|
4947
|
+
for (const ref of refs) {
|
|
4948
|
+
ownerToRemapped.set(ref, ref); // default = source name
|
|
4949
|
+
}
|
|
4950
|
+
await Promise.all(refs.map(async (dotFqn) => {
|
|
4951
|
+
try {
|
|
4952
|
+
const mapped = await this.mappingService.findMapping({
|
|
4953
|
+
version,
|
|
4954
|
+
kind: "class",
|
|
4955
|
+
name: dotFqn,
|
|
4956
|
+
sourceMapping,
|
|
4957
|
+
targetMapping,
|
|
4958
|
+
sourcePriority,
|
|
4959
|
+
projectPath
|
|
4960
|
+
});
|
|
4961
|
+
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
4962
|
+
ownerToRemapped.set(dotFqn, mapped.resolvedSymbol.name);
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
catch {
|
|
4966
|
+
// keep source name as fallback
|
|
4967
|
+
}
|
|
4968
|
+
}));
|
|
4969
|
+
}
|
|
4970
|
+
// Build a class map for descriptor remapping (dot-FQN → dot-FQN)
|
|
4971
|
+
const classMap = new Map();
|
|
4972
|
+
for (const [src, tgt] of ownerToRemapped) {
|
|
4973
|
+
if (src !== tgt) {
|
|
4974
|
+
classMap.set(src, tgt);
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
// Phase 2: Remap member names (and descriptors for methods) using resolved owners
|
|
4978
|
+
const canResolveMethodExactly = kind === "method" &&
|
|
4979
|
+
"resolveMethodMappingExact" in this.mappingService &&
|
|
4980
|
+
typeof this.mappingService.resolveMethodMappingExact === "function";
|
|
4147
4981
|
const memberEntries = [...memberKeyToRemapped.entries()];
|
|
4148
|
-
await Promise.all(memberEntries.map(async ([key,
|
|
4982
|
+
await Promise.all(memberEntries.map(async ([key, _sourceName]) => {
|
|
4149
4983
|
const [ownerFqn, name, descriptor] = key.split("\0");
|
|
4150
4984
|
try {
|
|
4151
4985
|
const targetOwner = ownerToRemapped.get(ownerFqn) ?? ownerFqn;
|
|
4986
|
+
// For methods with descriptors, try exact resolution first
|
|
4987
|
+
if (canResolveMethodExactly && descriptor) {
|
|
4988
|
+
try {
|
|
4989
|
+
const exactResult = await this.mappingService.resolveMethodMappingExact({
|
|
4990
|
+
version,
|
|
4991
|
+
owner: ownerFqn,
|
|
4992
|
+
name: name,
|
|
4993
|
+
descriptor,
|
|
4994
|
+
sourceMapping,
|
|
4995
|
+
targetMapping,
|
|
4996
|
+
sourcePriority,
|
|
4997
|
+
projectPath
|
|
4998
|
+
});
|
|
4999
|
+
if (exactResult.resolved && exactResult.resolvedSymbol) {
|
|
5000
|
+
memberKeyToRemapped.set(key, exactResult.resolvedSymbol.name);
|
|
5001
|
+
if (exactResult.resolvedSymbol.descriptor) {
|
|
5002
|
+
memberDescriptorRemapped.set(key, exactResult.resolvedSymbol.descriptor);
|
|
5003
|
+
}
|
|
5004
|
+
return; // exact resolution succeeded
|
|
5005
|
+
}
|
|
5006
|
+
// Fall through to findMapping with descriptorHint
|
|
5007
|
+
}
|
|
5008
|
+
catch (exactError) {
|
|
5009
|
+
warnings.push(`Exact method resolution failed for "${name}" (falling back to name-based lookup): ${exactError instanceof Error ? exactError.message : String(exactError)}`);
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
// Fallback: findMapping with descriptorHint for overload disambiguation
|
|
5013
|
+
const remappedDescriptorHint = kind === "method" && descriptor
|
|
5014
|
+
? remapJvmDescriptor(descriptor, classMap)
|
|
5015
|
+
: undefined;
|
|
4152
5016
|
const mapped = await this.mappingService.findMapping({
|
|
4153
5017
|
version,
|
|
4154
5018
|
kind,
|
|
@@ -4158,10 +5022,17 @@ export class SourceService {
|
|
|
4158
5022
|
sourceMapping,
|
|
4159
5023
|
targetMapping,
|
|
4160
5024
|
sourcePriority,
|
|
4161
|
-
|
|
5025
|
+
projectPath,
|
|
5026
|
+
disambiguation: {
|
|
5027
|
+
ownerHint: targetOwner,
|
|
5028
|
+
descriptorHint: remappedDescriptorHint
|
|
5029
|
+
}
|
|
4162
5030
|
});
|
|
4163
5031
|
if (mapped.resolved && mapped.resolvedSymbol) {
|
|
4164
5032
|
memberKeyToRemapped.set(key, mapped.resolvedSymbol.name);
|
|
5033
|
+
if (kind === "method" && mapped.resolvedSymbol.descriptor) {
|
|
5034
|
+
memberDescriptorRemapped.set(key, mapped.resolvedSymbol.descriptor);
|
|
5035
|
+
}
|
|
4165
5036
|
}
|
|
4166
5037
|
else if (mapped.status === "ambiguous" && mapped.candidates && mapped.candidates.length > 0) {
|
|
4167
5038
|
// Disambiguate: filter by target owner and pick the best candidate
|
|
@@ -4189,13 +5060,20 @@ export class SourceService {
|
|
|
4189
5060
|
failedNames.add(name);
|
|
4190
5061
|
}
|
|
4191
5062
|
}));
|
|
5063
|
+
const isField = kind === "field";
|
|
4192
5064
|
return {
|
|
4193
5065
|
members: members.map((member) => {
|
|
4194
5066
|
const memberKey = `${member.ownerFqn}\0${member.name}\0${member.jvmDescriptor}`;
|
|
5067
|
+
const remappedName = memberKeyToRemapped.get(memberKey) ?? member.name;
|
|
5068
|
+
const remappedOwner = ownerToRemapped.get(member.ownerFqn) ?? member.ownerFqn;
|
|
5069
|
+
const remappedDescriptor = memberDescriptorRemapped.get(memberKey)
|
|
5070
|
+
?? remapJvmDescriptor(member.jvmDescriptor, classMap);
|
|
4195
5071
|
return {
|
|
4196
5072
|
...member,
|
|
4197
|
-
name:
|
|
4198
|
-
ownerFqn:
|
|
5073
|
+
name: remappedName,
|
|
5074
|
+
ownerFqn: remappedOwner,
|
|
5075
|
+
jvmDescriptor: remappedDescriptor,
|
|
5076
|
+
javaSignature: rebuildJavaSignature({ name: remappedName, ownerFqn: remappedOwner, accessFlags: member.accessFlags }, remappedDescriptor, isField)
|
|
4199
5077
|
};
|
|
4200
5078
|
}),
|
|
4201
5079
|
failedNames
|
|
@@ -4540,4 +5418,39 @@ export class SourceService {
|
|
|
4540
5418
|
this.metrics.setCacheArtifactByteAccountingRef(this.cacheMetricsState.lru);
|
|
4541
5419
|
}
|
|
4542
5420
|
}
|
|
5421
|
+
function remapJvmDescriptor(descriptor, classMap) {
|
|
5422
|
+
if (classMap.size === 0) {
|
|
5423
|
+
return descriptor;
|
|
5424
|
+
}
|
|
5425
|
+
return descriptor.replace(/L([^;]+);/g, (match, ref) => {
|
|
5426
|
+
const dotFqn = ref.replace(/\//g, ".");
|
|
5427
|
+
const remapped = classMap.get(dotFqn);
|
|
5428
|
+
return remapped ? `L${remapped.replace(/\./g, "/")};` : match;
|
|
5429
|
+
});
|
|
5430
|
+
}
|
|
5431
|
+
function rebuildJavaSignature(member, remappedDescriptor, isField) {
|
|
5432
|
+
const modifiers = modifierPrefix(member.accessFlags, isField ? "field" : "method");
|
|
5433
|
+
const prefix = modifiers ? `${modifiers} ` : "";
|
|
5434
|
+
if (isField) {
|
|
5435
|
+
try {
|
|
5436
|
+
const { type } = parseFieldType(remappedDescriptor, 0, { allowVoid: false });
|
|
5437
|
+
return `${prefix}${type} ${member.name}`.trim();
|
|
5438
|
+
}
|
|
5439
|
+
catch {
|
|
5440
|
+
return `${prefix}${member.name}`.trim();
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
try {
|
|
5444
|
+
const { args, returnType } = parseMethodDescriptor(remappedDescriptor);
|
|
5445
|
+
const argStr = args.join(", ");
|
|
5446
|
+
if (member.name === "<init>") {
|
|
5447
|
+
const ownerSimple = member.ownerFqn.split(".").pop();
|
|
5448
|
+
return `${prefix}${ownerSimple}(${argStr})`.trim();
|
|
5449
|
+
}
|
|
5450
|
+
return `${prefix}${returnType} ${member.name}(${argStr})`.trim();
|
|
5451
|
+
}
|
|
5452
|
+
catch {
|
|
5453
|
+
return `${prefix}${member.name}`.trim();
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
4543
5456
|
//# sourceMappingURL=source-service.js.map
|