@adhisang/minecraft-modding-mcp 5.0.0 → 6.1.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 +55 -0
- package/dist/access-widener-parser.d.ts +1 -0
- package/dist/access-widener-parser.js +6 -3
- package/dist/cache-registry.js +35 -31
- package/dist/config.js +1 -1
- package/dist/decompiler/vineflower.js +8 -2
- package/dist/entry-tools/analyze-mod-service.js +2 -1
- package/dist/entry-tools/analyze-symbol-service.js +18 -9
- package/dist/entry-tools/batch-class-members-service.d.ts +2 -0
- package/dist/entry-tools/batch-class-members-service.js +1 -0
- package/dist/entry-tools/compare-minecraft-service.js +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
- package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
- package/dist/entry-tools/response-contract.d.ts +2 -0
- package/dist/entry-tools/response-contract.js +2 -2
- package/dist/entry-tools/validate-project/cases/project-summary.js +30 -12
- package/dist/entry-tools/validate-project-service.d.ts +4 -4
- package/dist/entry-tools/verify-mixin-target-service.js +7 -7
- package/dist/index.js +21 -16
- package/dist/java-process.js +1 -1
- package/dist/json-rpc-framing.d.ts +4 -0
- package/dist/json-rpc-framing.js +23 -1
- package/dist/mapping/internal-types.d.ts +17 -0
- package/dist/mapping/loaders/tiny-maven.js +46 -20
- package/dist/mapping/lookup.d.ts +12 -1
- package/dist/mapping/lookup.js +53 -1
- package/dist/mapping-service.js +37 -22
- package/dist/minecraft-explorer-service.js +2 -1
- package/dist/mixin/annotation-validators.js +21 -3
- package/dist/mixin/parsed-validator.js +1 -0
- package/dist/mixin-parser.js +71 -3
- package/dist/mod-decompile-service.d.ts +2 -0
- package/dist/mod-decompile-service.js +19 -2
- package/dist/mod-remap-service.js +6 -5
- package/dist/mojang-tiny-mapping-service.js +5 -2
- package/dist/nbt/java-nbt-codec.js +40 -16
- package/dist/nbt/json-patch.js +51 -12
- package/dist/nbt/typed-json.d.ts +1 -0
- package/dist/nbt/typed-json.js +12 -2
- package/dist/nbt/types.d.ts +2 -2
- package/dist/path-converter.js +10 -3
- package/dist/registry-service.d.ts +2 -0
- package/dist/registry-service.js +16 -1
- package/dist/repo-downloader.js +4 -3
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/class-source/members-builder.d.ts +26 -0
- package/dist/source/class-source/members-builder.js +31 -4
- package/dist/source/class-source/snippet-builder.js +6 -0
- package/dist/source/class-source-helpers.d.ts +4 -4
- package/dist/source/class-source-helpers.js +12 -11
- package/dist/source/class-source.d.ts +19 -0
- package/dist/source/class-source.js +87 -26
- package/dist/source/file-access.js +3 -2
- package/dist/source/indexer.js +24 -7
- package/dist/source/lifecycle/mapping-helpers.js +28 -3
- package/dist/source/lifecycle/runtime-check.js +20 -6
- package/dist/source/member-pattern.d.ts +16 -0
- package/dist/source/member-pattern.js +27 -0
- package/dist/source/search.js +23 -8
- package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
- package/dist/source/workspace-target.js +2 -1
- package/dist/source-resolver.js +2 -2
- package/dist/source-service.d.ts +8 -4
- package/dist/source-service.js +9 -1
- package/dist/stdio-supervisor.d.ts +2 -2
- package/dist/stdio-supervisor.js +29 -9
- package/dist/storage/db.js +24 -1
- package/dist/storage/files-repo.d.ts +3 -0
- package/dist/storage/files-repo.js +43 -37
- package/dist/storage/symbols-repo.d.ts +9 -0
- package/dist/storage/symbols-repo.js +47 -19
- package/dist/symbols/symbol-extractor.js +1 -1
- package/dist/tool-guidance.d.ts +2 -1
- package/dist/tool-guidance.js +27 -2
- package/dist/tool-schemas.d.ts +15 -4
- package/dist/tool-schemas.js +102 -97
- package/dist/warning-details.d.ts +12 -0
- package/dist/warning-details.js +17 -0
- package/dist/workspace-mapping-service.js +9 -0
- package/docs/tool-reference.md +23 -0
- package/package.json +1 -1
|
@@ -5,10 +5,38 @@ import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
|
5
5
|
import * as artifactResolver from "./artifact-resolver.js";
|
|
6
6
|
import * as classSourceHelpers from "./class-source-helpers.js";
|
|
7
7
|
import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
|
|
8
|
-
import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire } from "./class-source/members-builder.js";
|
|
8
|
+
import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, projectMembersByLevel } from "./class-source/members-builder.js";
|
|
9
|
+
import { matchesMemberPattern } from "./member-pattern.js";
|
|
9
10
|
import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
|
|
10
11
|
import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
12
|
+
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
11
13
|
const MEMBERS_STATUS_LEGACY = process.env.MEMBERS_STATUS_LEGACY === "1";
|
|
14
|
+
/**
|
|
15
|
+
* Unobfuscated Minecraft versions (26.1+) ship their runtime and decompiled source
|
|
16
|
+
* in deobfuscated (mojang) names directly — there is no real obfuscated namespace to
|
|
17
|
+
* map to. An artifact whose stored namespace label is "obfuscated" is therefore a
|
|
18
|
+
* mislabel for a mojang request: the bytes already carry mojang names.
|
|
19
|
+
*
|
|
20
|
+
* Without this reconciliation, requestedMapping="mojang" !== mappingApplied="obfuscated"
|
|
21
|
+
* triggers a cascade of doomed work: resolveClassNameForLookup attempts a mojang->obfuscated
|
|
22
|
+
* class remap, remapSignatureMembers tries to remap every member obfuscated->mojang and drops
|
|
23
|
+
* them all (counts.total===0), which forces a spurious decompiledFallback, disables
|
|
24
|
+
* memberPattern, and floods the response with one "Could not remap ..." warning per member.
|
|
25
|
+
*
|
|
26
|
+
* Collapsing mappingApplied to the requested mojang namespace makes the whole pipeline an
|
|
27
|
+
* identity no-op: members are returned from bytecode with correct mojang names, memberPattern
|
|
28
|
+
* applies, and the per-member warning flood disappears. This is the single highest-impact
|
|
29
|
+
* token-efficiency fix for unobfuscated versions.
|
|
30
|
+
*/
|
|
31
|
+
export function reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied) {
|
|
32
|
+
if (version &&
|
|
33
|
+
requestedMapping === "mojang" &&
|
|
34
|
+
mappingApplied === "obfuscated" &&
|
|
35
|
+
isUnobfuscatedVersion(version)) {
|
|
36
|
+
return "mojang";
|
|
37
|
+
}
|
|
38
|
+
return mappingApplied;
|
|
39
|
+
}
|
|
12
40
|
function normalizeStrictPositiveInt(value, field) {
|
|
13
41
|
if (value == null) {
|
|
14
42
|
return undefined;
|
|
@@ -35,15 +63,6 @@ function normalizeMemberAccess(access) {
|
|
|
35
63
|
details: { access }
|
|
36
64
|
});
|
|
37
65
|
}
|
|
38
|
-
function buildResolveArtifactParams(target, extra = {}) {
|
|
39
|
-
return {
|
|
40
|
-
target: {
|
|
41
|
-
kind: target.kind,
|
|
42
|
-
value: target.value
|
|
43
|
-
},
|
|
44
|
-
...extra
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
66
|
function looksLikeDeobfuscatedClassName(value) {
|
|
48
67
|
const trimmed = value.trim();
|
|
49
68
|
if (!trimmed) {
|
|
@@ -221,13 +240,15 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
|
|
|
221
240
|
if (!memberPattern) {
|
|
222
241
|
return list;
|
|
223
242
|
}
|
|
224
|
-
|
|
225
|
-
return list.filter((entry) => entry.name.toLowerCase().includes(lower));
|
|
243
|
+
return list.filter((entry) => matchesMemberPattern(entry.name, memberPattern));
|
|
226
244
|
};
|
|
227
245
|
let constructors = filterByPattern(extracted.constructors);
|
|
228
246
|
let fields = filterByPattern(extracted.fields);
|
|
229
247
|
let methods = filterByPattern(extracted.methods);
|
|
230
|
-
const
|
|
248
|
+
const constructorsBefore = constructors.length;
|
|
249
|
+
const fieldsBefore = fields.length;
|
|
250
|
+
const methodsBefore = methods.length;
|
|
251
|
+
const totalBefore = constructorsBefore + fieldsBefore + methodsBefore;
|
|
231
252
|
if (totalBefore === 0) {
|
|
232
253
|
return undefined;
|
|
233
254
|
}
|
|
@@ -251,11 +272,31 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
|
|
|
251
272
|
origin: "source-extracted"
|
|
252
273
|
},
|
|
253
274
|
counts: {
|
|
254
|
-
constructors:
|
|
255
|
-
fields:
|
|
256
|
-
methods:
|
|
257
|
-
total:
|
|
258
|
-
}
|
|
275
|
+
constructors: constructorsBefore,
|
|
276
|
+
fields: fieldsBefore,
|
|
277
|
+
methods: methodsBefore,
|
|
278
|
+
total: totalBefore
|
|
279
|
+
},
|
|
280
|
+
truncated: totalBefore > maxMembers
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Apply a member projection to the decompiled fallback so it honors the same
|
|
285
|
+
* `projection` contract as the bytecode members block. The fallback only carries
|
|
286
|
+
* names + source line + kind (no signatures), so any non-"full" level reduces to
|
|
287
|
+
* the member name (the array it lives in already conveys the kind). Keeps the
|
|
288
|
+
* full shape for the default "full" level.
|
|
289
|
+
*/
|
|
290
|
+
function projectDecompiledFallback(fallback, level) {
|
|
291
|
+
if (level === "full") {
|
|
292
|
+
return fallback;
|
|
293
|
+
}
|
|
294
|
+
const reduce = (member) => ({ name: member.name });
|
|
295
|
+
return {
|
|
296
|
+
constructors: fallback.constructors.map(reduce),
|
|
297
|
+
fields: fallback.fields.map(reduce),
|
|
298
|
+
methods: fallback.methods.map(reduce),
|
|
299
|
+
origin: fallback.origin
|
|
259
300
|
};
|
|
260
301
|
}
|
|
261
302
|
// The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
|
|
@@ -444,7 +485,7 @@ export async function getClassSource(svc, input) {
|
|
|
444
485
|
const artifact = svc.getArtifact(artifactId);
|
|
445
486
|
artifactId = artifact.artifactId;
|
|
446
487
|
origin = artifact.origin;
|
|
447
|
-
requestedMapping = artifact.requestedMapping ?? requestedMapping;
|
|
488
|
+
requestedMapping = input.mapping != null ? requestedMapping : (artifact.requestedMapping ?? requestedMapping);
|
|
448
489
|
mappingApplied = artifact.mappingApplied ?? requestedMapping;
|
|
449
490
|
provenance = artifact.provenance;
|
|
450
491
|
qualityFlags = artifact.qualityFlags;
|
|
@@ -461,6 +502,7 @@ export async function getClassSource(svc, input) {
|
|
|
461
502
|
preferProjectVersion: input.preferProjectVersion,
|
|
462
503
|
warnings
|
|
463
504
|
});
|
|
505
|
+
mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
|
|
464
506
|
let activeArtifactId = artifactId;
|
|
465
507
|
let activeOrigin = origin;
|
|
466
508
|
let activeProvenance = provenance;
|
|
@@ -624,11 +666,25 @@ export async function getClassSource(svc, input) {
|
|
|
624
666
|
mode,
|
|
625
667
|
startLine: nextStartLine,
|
|
626
668
|
...(input.endLine != null ? { endLine: input.endLine } : {}),
|
|
627
|
-
...(
|
|
669
|
+
...(maxLines != null ? { maxLines } : {}),
|
|
628
670
|
...(input.maxChars != null ? { maxChars: input.maxChars } : {})
|
|
629
671
|
}
|
|
630
672
|
})
|
|
631
673
|
: undefined;
|
|
674
|
+
// Decompiled source text can use different method/accessor names than the jar
|
|
675
|
+
// the workspace actually compiles against (e.g. `getGameRenderState()` in the
|
|
676
|
+
// decompiled source vs `gameRenderState()` in the runtime jar). Flag it so
|
|
677
|
+
// callers verify names against get-class-members (bytecode-derived) before
|
|
678
|
+
// copying signatures out of this source.
|
|
679
|
+
if (activeOrigin === "decompiled") {
|
|
680
|
+
activeQualityFlags = dedupeQualityFlags([
|
|
681
|
+
...activeQualityFlags,
|
|
682
|
+
"decompiled-source-signatures-unverified"
|
|
683
|
+
]);
|
|
684
|
+
warnings.push("Source is decompiled: method/accessor names may differ from the jar the workspace "
|
|
685
|
+
+ "compiles against. Confirm signatures with get-class-members (bytecode-derived) "
|
|
686
|
+
+ "before copying names from this source.");
|
|
687
|
+
}
|
|
632
688
|
return {
|
|
633
689
|
className,
|
|
634
690
|
mode,
|
|
@@ -746,6 +802,7 @@ export async function getClassMembers(svc, input) {
|
|
|
746
802
|
preferProjectVersion: input.preferProjectVersion,
|
|
747
803
|
warnings
|
|
748
804
|
});
|
|
805
|
+
mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
|
|
749
806
|
if (requestedMapping !== "obfuscated" && !version) {
|
|
750
807
|
throw createError({
|
|
751
808
|
code: ERROR_CODES.MAPPING_NOT_APPLIED,
|
|
@@ -753,10 +810,7 @@ export async function getClassMembers(svc, input) {
|
|
|
753
810
|
details: {
|
|
754
811
|
mapping: requestedMapping,
|
|
755
812
|
nextAction: "Resolve with target: { kind: \"version\", value: ... } or specify a versioned coordinate.",
|
|
756
|
-
...buildSuggestedCall({
|
|
757
|
-
tool: "resolve-artifact",
|
|
758
|
-
params: buildResolveArtifactParams({ kind: "version", value: "latest" })
|
|
759
|
-
})
|
|
813
|
+
...buildSuggestedCall({ tool: "list-versions", params: {} })
|
|
760
814
|
}
|
|
761
815
|
});
|
|
762
816
|
}
|
|
@@ -868,7 +922,8 @@ export async function getClassMembers(svc, input) {
|
|
|
868
922
|
// Slim the wire member shape: hoist a shared ownerFqn, drop accessFlags, omit
|
|
869
923
|
// isSynthetic:false, and drop FIELD jvmDescriptor unless includeDescriptors.
|
|
870
924
|
// Internal SignatureMember arrays above stay intact.
|
|
871
|
-
const
|
|
925
|
+
const projection = input.projection ?? "full";
|
|
926
|
+
const projectedMembers = projectMembersByLevel(projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false), projection);
|
|
872
927
|
const truncated = sliced.truncated;
|
|
873
928
|
const nextCursor = sliced.nextOffset != null ? encodeOffsetCursor(sliced.nextOffset, memberCursorContext) : undefined;
|
|
874
929
|
const normalizedProvenance = provenance ??
|
|
@@ -886,7 +941,7 @@ export async function getClassMembers(svc, input) {
|
|
|
886
941
|
const fallbackPattern = namespaceMismatch ? undefined : memberPattern;
|
|
887
942
|
const sourceFallback = buildDecompiledFallback(svc, artifactId, lookupClassName, fallbackPattern, maxMembers);
|
|
888
943
|
if (sourceFallback) {
|
|
889
|
-
decompiledFallback = sourceFallback.fallback;
|
|
944
|
+
decompiledFallback = projectDecompiledFallback(sourceFallback.fallback, projection);
|
|
890
945
|
decompiledMemberCounts = sourceFallback.counts;
|
|
891
946
|
fallbackQualityFlags = dedupeQualityFlags([
|
|
892
947
|
...qualityFlags,
|
|
@@ -898,6 +953,12 @@ export async function getClassMembers(svc, input) {
|
|
|
898
953
|
warnings.push("Bytecode member enumeration returned zero; populated decompiledFallback from decompiled source. "
|
|
899
954
|
+ "Descriptors and access modifiers are unavailable — use get-class-source for full details."
|
|
900
955
|
+ namespaceNote);
|
|
956
|
+
if (sourceFallback.truncated) {
|
|
957
|
+
const returnedTotal = decompiledFallback.constructors.length
|
|
958
|
+
+ decompiledFallback.fields.length
|
|
959
|
+
+ decompiledFallback.methods.length;
|
|
960
|
+
warnings.push(`Member list was truncated to ${returnedTotal} entries (from ${sourceFallback.counts.total}).`);
|
|
961
|
+
}
|
|
901
962
|
if (namespaceMismatch && memberPattern) {
|
|
902
963
|
warnings.push(`memberPattern="${memberPattern}" was not applied to decompiledFallback because the artifact namespace (${mappingApplied}) differs from the requested namespace (${requestedMapping}); filter the response client-side after mapping.`);
|
|
903
964
|
}
|
|
@@ -71,12 +71,13 @@ export async function listArtifactFiles(svc, input) {
|
|
|
71
71
|
const artifact = svc.getArtifact(input.artifactId);
|
|
72
72
|
const limit = clampLimit(input.limit, 200, 2000);
|
|
73
73
|
const warnings = [];
|
|
74
|
+
const prefix = input.prefix === undefined ? undefined : normalizePathStyle(input.prefix);
|
|
74
75
|
const page = svc.filesRepo.listFiles(artifact.artifactId, {
|
|
75
76
|
limit,
|
|
76
77
|
cursor: input.cursor,
|
|
77
|
-
prefix
|
|
78
|
+
prefix
|
|
78
79
|
});
|
|
79
|
-
const normalizedPrefix = normalizeOptionalString(
|
|
80
|
+
const normalizedPrefix = normalizeOptionalString(prefix);
|
|
80
81
|
if (normalizedPrefix &&
|
|
81
82
|
page.items.length === 0 &&
|
|
82
83
|
(normalizedPrefix.startsWith("assets/") || normalizedPrefix.startsWith("data/"))) {
|
package/dist/source/indexer.js
CHANGED
|
@@ -210,17 +210,20 @@ export async function buildRebuiltArtifactData(svc, resolved) {
|
|
|
210
210
|
}
|
|
211
211
|
catch (caughtError) {
|
|
212
212
|
if (isAppError(caughtError) && caughtError.code === ERROR_CODES.DECOMPILER_FAILED) {
|
|
213
|
+
// Decompilation failed BEFORE the artifact was ever upserted, so there is no
|
|
214
|
+
// queryable artifact. Deliberately omit artifactId from the error: exposing the
|
|
215
|
+
// would-be id led callers to pass it to find-class/get-class-*, which then failed
|
|
216
|
+
// with "Artifact not found. Resolve context first." — a state inconsistency.
|
|
217
|
+
const priorDetails = { ...(caughtError.details ?? {}) };
|
|
218
|
+
delete priorDetails.artifactId;
|
|
213
219
|
throw createError({
|
|
214
220
|
code: ERROR_CODES.DECOMPILER_FAILED,
|
|
215
221
|
message: caughtError.message,
|
|
216
222
|
details: {
|
|
217
|
-
...
|
|
218
|
-
artifactId: resolved.artifactId,
|
|
223
|
+
...priorDetails,
|
|
219
224
|
binaryJarPath: resolved.binaryJarPath,
|
|
220
|
-
producedJavaCount: typeof
|
|
221
|
-
|
|
222
|
-
: 0,
|
|
223
|
-
nextAction: "Verify Java runtime and Vineflower availability, then retry. If available, prefer source-backed artifacts.",
|
|
225
|
+
producedJavaCount: typeof priorDetails.producedJavaCount === "number" ? priorDetails.producedJavaCount : 0,
|
|
226
|
+
nextAction: "Decompilation failed, so no artifact was created. Verify Java runtime and Vineflower availability, then retry; prefer source-backed artifacts when available.",
|
|
224
227
|
recommendedCommand: "echo $MCP_VINEFLOWER_JAR_PATH"
|
|
225
228
|
}
|
|
226
229
|
});
|
|
@@ -336,6 +339,13 @@ export async function ingestIfNeeded(svc, resolved) {
|
|
|
336
339
|
const inflight = svc.state.inflightArtifactIngests.get(resolved.artifactId);
|
|
337
340
|
if (inflight) {
|
|
338
341
|
await inflight;
|
|
342
|
+
const transformChain = resolved.provenance?.transformChain ?? [];
|
|
343
|
+
if (transformChain.includes("binary-remap:obf->mojang") && resolved.binaryJarPath) {
|
|
344
|
+
const reconciledBinaryJarPath = await maybeRemapBinaryForMojang(svc, resolved);
|
|
345
|
+
if (reconciledBinaryJarPath !== resolved.binaryJarPath) {
|
|
346
|
+
resolved.binaryJarPath = reconciledBinaryJarPath;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
339
349
|
return;
|
|
340
350
|
}
|
|
341
351
|
const ingestPromise = rebuildMissingArtifactIndex(svc, resolved, reason);
|
|
@@ -429,9 +439,16 @@ export async function maybeRemapBinaryForMojang(svc, resolved, deps = defaultBin
|
|
|
429
439
|
if (inflight) {
|
|
430
440
|
return inflight;
|
|
431
441
|
}
|
|
442
|
+
// When a prior index-artifact persisted the remapped jar back into the
|
|
443
|
+
// artifacts row, binaryJarPath already equals remappedJarPath. Remapping that
|
|
444
|
+
// missing/corrupt path onto itself would fail permanently, so re-resolve the
|
|
445
|
+
// original obfuscated client jar to recover from out-of-band cache loss.
|
|
446
|
+
const inputJar = binaryJarPath === remappedJarPath
|
|
447
|
+
? (await svc.versionService.resolveVersionJar(resolved.version)).jarPath
|
|
448
|
+
: binaryJarPath;
|
|
432
449
|
const remapPromise = runBinaryRemapWithDeps(svc, {
|
|
433
450
|
version: resolved.version,
|
|
434
|
-
inputJar
|
|
451
|
+
inputJar,
|
|
435
452
|
remappedDir,
|
|
436
453
|
remappedJarPath
|
|
437
454
|
}, deps);
|
|
@@ -25,6 +25,22 @@ function looksLikeClassSegment(name) {
|
|
|
25
25
|
const trimmed = name.trim();
|
|
26
26
|
return /^[A-Z_$]/.test(trimmed);
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Collapse N per-member remap failures into a single warning line. Preserves the
|
|
30
|
+
* original "Could not remap" / "Remap failed for" prefix so downstream classifiers
|
|
31
|
+
* (e.g. REMAP_WARNING_RE in validate-mixin, warningDetails coding) still recognise it,
|
|
32
|
+
* while bounding the per-member flood to one line plus a short sample of names.
|
|
33
|
+
*/
|
|
34
|
+
function pushAggregatedRemapWarning(warnings, prefix, names, kind, sourceMapping, targetMapping) {
|
|
35
|
+
if (names.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const unique = [...new Set(names)];
|
|
39
|
+
const sample = unique.slice(0, 3).join(", ");
|
|
40
|
+
const more = unique.length > 3 ? `, +${unique.length - 3} more` : "";
|
|
41
|
+
const plural = names.length === 1 ? "" : "s";
|
|
42
|
+
warnings.push(`${prefix} ${names.length} ${kind}${plural} from ${sourceMapping} to ${targetMapping} (${sample}${more}).`);
|
|
43
|
+
}
|
|
28
44
|
export function rejectLifecycleClassLikeInput(svc, input) {
|
|
29
45
|
void svc;
|
|
30
46
|
if (!looksLikeClassSegment(input.methodName)) {
|
|
@@ -234,6 +250,13 @@ export async function remapSignatureMembers(svc, members, kind, version, sourceM
|
|
|
234
250
|
"resolveMethodMappingExact" in svc.mappingService &&
|
|
235
251
|
typeof svc.mappingService.resolveMethodMappingExact === "function";
|
|
236
252
|
const memberEntries = [...memberKeyToRemapped.entries()];
|
|
253
|
+
// Collect per-member remap failures and emit a SINGLE aggregated warning each
|
|
254
|
+
// after the loop instead of one line per member. On namespace mismatches a large
|
|
255
|
+
// class can fail to remap every one of its ~150 members, which previously flooded
|
|
256
|
+
// both warnings[] and the 1:1 warningDetails[] with hundreds of near-identical
|
|
257
|
+
// lines (the dominant token sink for these responses).
|
|
258
|
+
const unremappedNames = [];
|
|
259
|
+
const remapErrorNames = [];
|
|
237
260
|
await Promise.all(memberEntries.map(async ([key, _sourceName]) => {
|
|
238
261
|
const [ownerFqn, name, descriptor] = key.split("\0");
|
|
239
262
|
try {
|
|
@@ -299,20 +322,22 @@ export async function remapSignatureMembers(svc, members, kind, version, sourceM
|
|
|
299
322
|
}
|
|
300
323
|
}
|
|
301
324
|
else {
|
|
302
|
-
|
|
325
|
+
unremappedNames.push(name);
|
|
303
326
|
failedNames.add(name);
|
|
304
327
|
}
|
|
305
328
|
}
|
|
306
329
|
else {
|
|
307
|
-
|
|
330
|
+
unremappedNames.push(name);
|
|
308
331
|
failedNames.add(name);
|
|
309
332
|
}
|
|
310
333
|
}
|
|
311
334
|
catch {
|
|
312
|
-
|
|
335
|
+
remapErrorNames.push(name);
|
|
313
336
|
failedNames.add(name);
|
|
314
337
|
}
|
|
315
338
|
}));
|
|
339
|
+
pushAggregatedRemapWarning(warnings, "Could not remap", unremappedNames, kind, sourceMapping, targetMapping);
|
|
340
|
+
pushAggregatedRemapWarning(warnings, "Remap failed for", remapErrorNames, kind, sourceMapping, targetMapping);
|
|
316
341
|
const isField = kind === "field";
|
|
317
342
|
return {
|
|
318
343
|
members: members.map((member) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ERROR_CODES, isAppError } from "../../errors.js";
|
|
1
2
|
export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbackBase) {
|
|
2
3
|
const version = input.version.trim();
|
|
3
4
|
const name = input.name.trim();
|
|
@@ -50,16 +51,25 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
50
51
|
signature = await svc.explorerService.getSignature({
|
|
51
52
|
fqn: targetClass,
|
|
52
53
|
jarPath,
|
|
53
|
-
access: "all"
|
|
54
|
+
access: "all",
|
|
55
|
+
// Method existence is asked relative to the owner type, so inherited methods count:
|
|
56
|
+
// `level.getGameTime()` exists on Level even though it is declared on a supertype.
|
|
57
|
+
// Scoped to methods only: fields already resolved correctly without an inheritance
|
|
58
|
+
// walk, and including inherited fields would let a shadowed field name match twice
|
|
59
|
+
// and flip a previously-resolved field to "ambiguous".
|
|
60
|
+
includeInherited: input.kind === "method"
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
|
-
catch {
|
|
63
|
+
catch (error) {
|
|
64
|
+
const classMissing = isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND;
|
|
57
65
|
return {
|
|
58
66
|
...fallbackBase,
|
|
59
67
|
querySymbol,
|
|
60
68
|
warnings: [
|
|
61
69
|
...fallbackBase.warnings,
|
|
62
|
-
|
|
70
|
+
classMissing
|
|
71
|
+
? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
|
|
72
|
+
: `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
|
|
63
73
|
]
|
|
64
74
|
};
|
|
65
75
|
}
|
|
@@ -114,9 +124,13 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
114
124
|
});
|
|
115
125
|
}
|
|
116
126
|
const methodCandidates = signature.methods.filter((method) => method.name === name);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
const signatureMode = input.signatureMode ?? "name-only";
|
|
128
|
+
if (signatureMode === "name-only") {
|
|
129
|
+
// Existence semantics: any overload with this name means the method exists. Multiple
|
|
130
|
+
// overloads are not "ambiguous" for a name-only existence check — only zero matches
|
|
131
|
+
// is not_found. (Use signatureMode: "exact" with a descriptor to pin one overload.)
|
|
132
|
+
if (methodCandidates.length === 0) {
|
|
133
|
+
return buildUnresolved("not_found");
|
|
120
134
|
}
|
|
121
135
|
return buildResolved({
|
|
122
136
|
kind: "method",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match a class-member name against a `memberPattern` filter.
|
|
3
|
+
*
|
|
4
|
+
* Matching is case-insensitive substring. A `|` splits the pattern into OR
|
|
5
|
+
* alternatives, so `"getStateForPlacement|canSurvive|setPlacedBy"` matches a
|
|
6
|
+
* name that contains ANY of those tokens. Before this, the pattern was matched
|
|
7
|
+
* as a single literal substring, so a piped pattern searched for a name
|
|
8
|
+
* containing a literal `|` and matched nothing — which made member listing
|
|
9
|
+
* return zero on large vanilla classes when callers used the natural OR syntax.
|
|
10
|
+
*
|
|
11
|
+
* Java member names never contain `|`, so treating `|` as OR never collides with
|
|
12
|
+
* a legitimate substring search. Empty or whitespace-only alternatives are
|
|
13
|
+
* dropped; a pattern with no usable alternative matches nothing (a non-empty
|
|
14
|
+
* filter that excludes everything), matching the prior empty-filter behavior.
|
|
15
|
+
*/
|
|
16
|
+
export declare function matchesMemberPattern(name: string, pattern: string): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match a class-member name against a `memberPattern` filter.
|
|
3
|
+
*
|
|
4
|
+
* Matching is case-insensitive substring. A `|` splits the pattern into OR
|
|
5
|
+
* alternatives, so `"getStateForPlacement|canSurvive|setPlacedBy"` matches a
|
|
6
|
+
* name that contains ANY of those tokens. Before this, the pattern was matched
|
|
7
|
+
* as a single literal substring, so a piped pattern searched for a name
|
|
8
|
+
* containing a literal `|` and matched nothing — which made member listing
|
|
9
|
+
* return zero on large vanilla classes when callers used the natural OR syntax.
|
|
10
|
+
*
|
|
11
|
+
* Java member names never contain `|`, so treating `|` as OR never collides with
|
|
12
|
+
* a legitimate substring search. Empty or whitespace-only alternatives are
|
|
13
|
+
* dropped; a pattern with no usable alternative matches nothing (a non-empty
|
|
14
|
+
* filter that excludes everything), matching the prior empty-filter behavior.
|
|
15
|
+
*/
|
|
16
|
+
export function matchesMemberPattern(name, pattern) {
|
|
17
|
+
const alternatives = pattern
|
|
18
|
+
.split("|")
|
|
19
|
+
.map((part) => part.trim().toLowerCase())
|
|
20
|
+
.filter((part) => part.length > 0);
|
|
21
|
+
if (alternatives.length === 0) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const lowerName = name.toLowerCase();
|
|
25
|
+
return alternatives.some((alt) => lowerName.includes(alt));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=member-pattern.js.map
|
package/dist/source/search.js
CHANGED
|
@@ -65,11 +65,13 @@ export function buildGlobRegex(pattern) {
|
|
|
65
65
|
while (i < pattern.length) {
|
|
66
66
|
const ch = pattern[i];
|
|
67
67
|
if (ch === "*" && pattern[i + 1] === "*") {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
if (pattern[i + 2] === "/") {
|
|
69
|
+
result += "(?:.*/)?";
|
|
70
|
+
i += 3;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
result += ".*";
|
|
74
|
+
i += 2;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
else if (ch === "*") {
|
|
@@ -301,6 +303,9 @@ export async function searchClassSource(svc, input) {
|
|
|
301
303
|
searchWarnings.push(`queryNamespace=${input.queryNamespace}: translation failed (${caughtError instanceof Error ? caughtError.message : String(caughtError)}); running literal search instead.`);
|
|
302
304
|
}
|
|
303
305
|
}
|
|
306
|
+
else if (intent === "symbol") {
|
|
307
|
+
searchWarnings.push(`queryNamespace=${input.queryNamespace} requires a fully-qualified class name for translation; running literal search in ${artifactMapping} instead. Resolve "${originalQuery}" to an FQCN with find-class or find-mapping first.`);
|
|
308
|
+
}
|
|
304
309
|
else if (intent === "text" || intent === "path") {
|
|
305
310
|
searchWarnings.push(`queryNamespace=${input.queryNamespace} has no effect when intent="${intent}" — ${intent} search is a literal match against the artifact's ${artifactMapping} index. Use intent="symbol" for namespace translation.`);
|
|
306
311
|
}
|
|
@@ -676,7 +681,9 @@ export function searchPathIntent(svc, artifactId, query, match, scope, regexPatt
|
|
|
676
681
|
}
|
|
677
682
|
export function findSymbolHits(svc, artifactId, query, match, scope, regexPattern) {
|
|
678
683
|
if (match !== "regex") {
|
|
679
|
-
const filePathLike = scope?.fileGlob
|
|
684
|
+
const filePathLike = scope?.fileGlob && !scope.fileGlob.includes("**")
|
|
685
|
+
? globToSqlLike(normalizePathStyle(scope.fileGlob))
|
|
686
|
+
: undefined;
|
|
680
687
|
const scoped = svc.symbolsRepo.findScopedSymbols({
|
|
681
688
|
artifactId,
|
|
682
689
|
query,
|
|
@@ -688,8 +695,12 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
|
|
|
688
695
|
});
|
|
689
696
|
svc.metrics.recordSearchDbRoundtrip();
|
|
690
697
|
svc.metrics.recordSearchRowsScanned(scoped.items.length);
|
|
698
|
+
const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
691
699
|
const result = [];
|
|
692
700
|
for (const symbol of scoped.items) {
|
|
701
|
+
if (globFilter && !globFilter.test(symbol.filePath)) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
693
704
|
if (!isSymbolKind(symbol.symbolKind)) {
|
|
694
705
|
continue;
|
|
695
706
|
}
|
|
@@ -705,12 +716,15 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
|
|
|
705
716
|
}
|
|
706
717
|
return result;
|
|
707
718
|
}
|
|
708
|
-
|
|
719
|
+
// Stream rows instead of materializing every symbol: regex scans on large
|
|
720
|
+
// artifacts only keep matching rows in memory this way.
|
|
721
|
+
const candidates = svc.symbolsRepo.iterateSymbolsForArtifact(artifactId, scope?.symbolKind);
|
|
709
722
|
svc.metrics.recordSearchDbRoundtrip();
|
|
710
|
-
|
|
723
|
+
let rowsScanned = 0;
|
|
711
724
|
const result = [];
|
|
712
725
|
const glob = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
713
726
|
for (const symbol of candidates) {
|
|
727
|
+
rowsScanned += 1;
|
|
714
728
|
if (!checkPackagePrefix(symbol.filePath, scope?.packagePrefix)) {
|
|
715
729
|
continue;
|
|
716
730
|
}
|
|
@@ -732,6 +746,7 @@ export function findSymbolHits(svc, artifactId, query, match, scope, regexPatter
|
|
|
732
746
|
matchIndex: index
|
|
733
747
|
});
|
|
734
748
|
}
|
|
749
|
+
svc.metrics.recordSearchRowsScanned(rowsScanned);
|
|
735
750
|
return result;
|
|
736
751
|
}
|
|
737
752
|
export function indexedCandidateLimit(svc) {
|
|
@@ -40,10 +40,22 @@ export async function runResolveStage(svc, ctx) {
|
|
|
40
40
|
scope: ctx.input.scope,
|
|
41
41
|
preferProjectVersion: false
|
|
42
42
|
});
|
|
43
|
-
ctx.jarPath = ctx.resolvedArtifact.binaryJarPath ?? (await svc.versionService.resolveVersionJar(ctx.version)).jarPath;
|
|
44
43
|
ctx.warnings.push(...ctx.resolvedArtifact.warnings);
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
if (ctx.resolvedArtifact.binaryJarPath) {
|
|
45
|
+
ctx.jarPath = ctx.resolvedArtifact.binaryJarPath;
|
|
46
|
+
ctx.mappingApplied = ctx.resolvedArtifact.mappingApplied;
|
|
47
|
+
ctx.signatureLookupMapping = ctx.resolvedArtifact.mappingApplied;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ctx.jarPath = (await svc.versionService.resolveVersionJar(ctx.version)).jarPath;
|
|
51
|
+
ctx.signatureLookupMapping = "obfuscated";
|
|
52
|
+
ctx.scopeFallback = {
|
|
53
|
+
requested: ctx.input.scope,
|
|
54
|
+
applied: "vanilla",
|
|
55
|
+
reason: "Resolved artifact had no binary jar (sources only); falling back to vanilla client jar."
|
|
56
|
+
};
|
|
57
|
+
ctx.warnings.push(`Scope "${ctx.input.scope}" resolved without a binary jar; falling back to vanilla client jar.`);
|
|
58
|
+
}
|
|
47
59
|
if (ctx.resolvedArtifact.version) {
|
|
48
60
|
ctx.version = ctx.resolvedArtifact.version;
|
|
49
61
|
}
|
|
@@ -59,6 +71,14 @@ export async function runResolveStage(svc, ctx) {
|
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
else {
|
|
74
|
+
if (ctx.input.scope && ctx.input.scope !== "vanilla") {
|
|
75
|
+
ctx.scopeFallback = {
|
|
76
|
+
requested: ctx.input.scope,
|
|
77
|
+
applied: "vanilla",
|
|
78
|
+
reason: "projectPath required for merged/loader scope; falling back to vanilla client jar."
|
|
79
|
+
};
|
|
80
|
+
ctx.warnings.push(`Scope "${ctx.input.scope}" requires projectPath; falling back to vanilla client jar.`);
|
|
81
|
+
}
|
|
62
82
|
ctx.jarPath = (await svc.versionService.resolveVersionJar(ctx.version)).jarPath;
|
|
63
83
|
}
|
|
64
84
|
if (ctx.jarPath.includes("-sources.jar")) {
|
|
@@ -37,6 +37,7 @@ export async function loadOrDetectWorkspaceContext(svc, projectPath) {
|
|
|
37
37
|
value: loaderResult.loader
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
+
const latest = svc.workspaceContextCache.read(projectPath);
|
|
40
41
|
const ctx = {
|
|
41
42
|
projectPath,
|
|
42
43
|
minecraftVersion,
|
|
@@ -44,7 +45,7 @@ export async function loadOrDetectWorkspaceContext(svc, projectPath) {
|
|
|
44
45
|
loader: loaderResult?.resolved ? loaderResult.loader : undefined,
|
|
45
46
|
detectedAt: Date.now(),
|
|
46
47
|
evidence,
|
|
47
|
-
dependencyVersions: cached?.dependencyVersions ?? new Map(),
|
|
48
|
+
dependencyVersions: latest?.dependencyVersions ?? cached?.dependencyVersions ?? new Map(),
|
|
48
49
|
partial: false
|
|
49
50
|
};
|
|
50
51
|
svc.workspaceContextCache.write(ctx);
|
package/dist/source-resolver.js
CHANGED
|
@@ -260,7 +260,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
260
260
|
? false
|
|
261
261
|
: download.statusCode !== 404 && isTransientFailure(download.statusCode);
|
|
262
262
|
sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
|
|
263
|
-
if (hasNextAttempt && transient) {
|
|
263
|
+
if (hasNextAttempt && (transient || download.ok)) {
|
|
264
264
|
options.onRepoFailover?.({
|
|
265
265
|
stage: "source",
|
|
266
266
|
repoUrl: sourceUrl,
|
|
@@ -320,7 +320,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
320
320
|
? false
|
|
321
321
|
: downloaded.statusCode !== 404 && isTransientFailure(downloaded.statusCode);
|
|
322
322
|
sawRemoteRepoFailure = sawRemoteRepoFailure || transient;
|
|
323
|
-
if (hasNextAttempt && transient) {
|
|
323
|
+
if (hasNextAttempt && (transient || downloaded.ok)) {
|
|
324
324
|
options.onRepoFailover?.({
|
|
325
325
|
stage: "binary",
|
|
326
326
|
repoUrl: binaryUrl,
|
package/dist/source-service.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import * as indexer from "./source/indexer.js";
|
|
|
11
11
|
import * as workspaceTarget from "./source/workspace-target.js";
|
|
12
12
|
import * as validateMixinModule from "./source/validate-mixin.js";
|
|
13
13
|
import * as artifactResolver from "./source/artifact-resolver.js";
|
|
14
|
-
import type {
|
|
14
|
+
import type { MemberProjection, ProjectedMembersBlock } from "./source/class-source/members-builder.js";
|
|
15
15
|
import { type StageEmitter } from "./stage-emitter.js";
|
|
16
16
|
import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
|
|
17
17
|
import { type WorkspaceContextCache } from "./workspace-context-cache.js";
|
|
@@ -266,11 +266,15 @@ export type GetClassMembersInput = {
|
|
|
266
266
|
strictVersion?: boolean;
|
|
267
267
|
/** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
|
|
268
268
|
includeDescriptors?: boolean;
|
|
269
|
+
/** Narrow the per-member fields returned: "names" | "signatures" | "full" (default "full"). */
|
|
270
|
+
projection?: MemberProjection;
|
|
269
271
|
};
|
|
270
272
|
export type DecompiledMember = {
|
|
271
273
|
name: string;
|
|
272
|
-
line
|
|
273
|
-
|
|
274
|
+
/** Source line of the declaration. Omitted under a non-"full" projection. */
|
|
275
|
+
line?: number;
|
|
276
|
+
/** Member kind. Omitted under a non-"full" projection (the array conveys it). */
|
|
277
|
+
kind?: "constructor" | "field" | "method";
|
|
274
278
|
};
|
|
275
279
|
export type DecompiledFallback = {
|
|
276
280
|
constructors: DecompiledMember[];
|
|
@@ -281,7 +285,7 @@ export type DecompiledFallback = {
|
|
|
281
285
|
export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
|
|
282
286
|
export type GetClassMembersOutput = {
|
|
283
287
|
className: string;
|
|
284
|
-
members:
|
|
288
|
+
members: ProjectedMembersBlock;
|
|
285
289
|
counts: {
|
|
286
290
|
constructors: number;
|
|
287
291
|
fields: number;
|
package/dist/source-service.js
CHANGED
|
@@ -141,7 +141,15 @@ export class SourceService {
|
|
|
141
141
|
}
|
|
142
142
|
async checkSymbolExists(input) {
|
|
143
143
|
const result = await this.mappingService.checkSymbolExists(input);
|
|
144
|
-
|
|
144
|
+
// On unobfuscated versions the mapping graph is empty, so the mapping service can
|
|
145
|
+
// only ever report symbols as missing. We then validate against runtime bytecode.
|
|
146
|
+
// Classes/fields surface as `mapping_unavailable` when the graph yields nothing, but
|
|
147
|
+
// a method query with a non-empty graph returns `not_found` (empty member lookup) —
|
|
148
|
+
// which previously skipped the runtime fallback and produced false negatives for
|
|
149
|
+
// methods that genuinely exist. Treat `not_found` methods as fallback-eligible.
|
|
150
|
+
const fallbackEligible = result.status === "mapping_unavailable" ||
|
|
151
|
+
(result.status === "not_found" && input.kind === "method");
|
|
152
|
+
if (!fallbackEligible ||
|
|
145
153
|
!isUnobfuscatedVersion(input.version) ||
|
|
146
154
|
(input.sourceMapping !== "mojang" && input.sourceMapping !== "obfuscated")) {
|
|
147
155
|
return result;
|