@adhisang/minecraft-modding-mcp 4.2.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +5 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/access-widener-parser.d.ts +1 -0
- package/dist/access-widener-parser.js +6 -3
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +50 -32
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/decompiler/vineflower.js +8 -2
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-mod-service.js +2 -1
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +139 -34
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/compare-minecraft-service.js +1 -1
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +4 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -2
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +3 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +32 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +3 -1
- package/dist/entry-tools/response-contract.js +3 -2
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +120 -29
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/index.js +147 -76
- 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 +24 -0
- package/dist/mapping/loaders/tiny-maven.js +46 -20
- package/dist/mapping/lookup.d.ts +13 -2
- package/dist/mapping/lookup.js +60 -2
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +212 -55
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +73 -9
- package/dist/mixin/parsed-validator.js +3 -2
- package/dist/mixin-parser.js +119 -7
- package/dist/mod-decompile-service.d.ts +2 -0
- package/dist/mod-decompile-service.js +21 -3
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +101 -45
- package/dist/mojang-tiny-mapping-service.js +5 -2
- package/dist/nbt/java-nbt-codec.js +46 -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/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/path-converter.js +10 -3
- package/dist/registry-service.d.ts +2 -0
- package/dist/registry-service.js +41 -2
- package/dist/repo-downloader.js +4 -3
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +54 -4
- package/dist/source/class-source-helpers.d.ts +4 -4
- package/dist/source/class-source-helpers.js +77 -20
- package/dist/source/class-source.d.ts +19 -0
- package/dist/source/class-source.js +144 -40
- package/dist/source/file-access.js +3 -2
- package/dist/source/indexer.js +29 -9
- package/dist/source/lifecycle/mapping-helpers.js +28 -3
- package/dist/source/lifecycle/runtime-check.js +20 -6
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +117 -30
- package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +17 -10
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-resolver.js +2 -2
- package/dist/source-service.d.ts +28 -7
- 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 +15 -0
- package/dist/storage/files-repo.js +67 -37
- package/dist/storage/symbols-repo.d.ts +15 -0
- package/dist/storage/symbols-repo.js +53 -19
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +6 -2
- package/dist/tool-guidance.js +63 -13
- package/dist/tool-schemas.d.ts +561 -496
- package/dist/tool-schemas.js +175 -139
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +30 -0
- package/dist/warning-details.js +80 -0
- package/dist/workspace-mapping-service.js +9 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +22 -17
- package/package.json +1 -1
package/dist/mapping-service.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
4
5
|
import { buildSuggestedCall } from "./build-suggested-call.js";
|
|
5
6
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
7
|
+
import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "./page-cursor.js";
|
|
6
8
|
import { resolveGradleUserHomePath } from "./gradle-paths.js";
|
|
7
9
|
import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
|
|
8
10
|
import { collectMatchedJarEntriesAsUtf8 } from "./source-jar-reader.js";
|
|
@@ -10,7 +12,7 @@ import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
|
|
|
10
12
|
import { buildSymbolKey, createClassSymbolRecord, mergeDirectionIndexes, normalizeMappedSymbolOutput } from "./mapping/parsers/symbol-records.js";
|
|
11
13
|
import { buildAdjacency, buildTargetRecordIndex, pairKey } from "./mapping/parsers/normalize.js";
|
|
12
14
|
import { MATCH_RANK, MAX_CANDIDATES } from "./mapping/internal-types.js";
|
|
13
|
-
import { applyDisambiguationHints, clampRowLimit, collectTargetRecords, effectiveLoomSearchProjectPath, inferAmbiguityReasons, invalidInputError, limitResolutionCandidates, lookupCandidates, mappingPriorityFromInput, mappingSourceOrder, namespacePath, normalizeIncludedKinds, normalizeQuerySymbol, pathToTransformChain, pathUsesSource, projectLookupCandidateDescriptor, requiresOnlyObfuscatedMojangGraph, toLookupCandidate, toResolutionCandidate } from "./mapping/lookup.js";
|
|
15
|
+
import { applyDisambiguationHints, clampRowLimit, collectExactRecordIndex, collectTargetRecords, effectiveLoomSearchProjectPath, inferAmbiguityReasons, invalidInputError, limitResolutionCandidates, lookupCandidates, lookupExactClassBySimpleName, lookupExactClassBySymbol, lookupExactMembers, lookupExactMembersByOwner, mappingPriorityFromInput, mappingSourceOrder, namespacePath, normalizeIncludedKinds, normalizeQuerySymbol, pathToTransformChain, pathUsesSource, projectLookupCandidateDescriptor, requiresOnlyObfuscatedMojangGraph, toLookupCandidate, toResolutionCandidate } from "./mapping/lookup.js";
|
|
14
16
|
import { loadMojangPairs } from "./mapping/loaders/mojang.js";
|
|
15
17
|
import { loadTinyPairsFromLoom } from "./mapping/loaders/tiny-loom.js";
|
|
16
18
|
import { loadTinyPairsFromMaven } from "./mapping/loaders/tiny-maven.js";
|
|
@@ -32,6 +34,8 @@ export class MappingService {
|
|
|
32
34
|
static RESOLUTION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
33
35
|
resolutionCacheHits = 0;
|
|
34
36
|
resolutionCacheMisses = 0;
|
|
37
|
+
classProjectionComputes = 0;
|
|
38
|
+
apiMatrixRowMaps = 0;
|
|
35
39
|
get resolutionCacheStats() {
|
|
36
40
|
return {
|
|
37
41
|
hits: this.resolutionCacheHits,
|
|
@@ -39,6 +43,14 @@ export class MappingService {
|
|
|
39
43
|
size: this.resolutionCache.size
|
|
40
44
|
};
|
|
41
45
|
}
|
|
46
|
+
/** Test seam: counts genuine class-projection computes (cache misses). */
|
|
47
|
+
get classProjectionStats() {
|
|
48
|
+
return { computes: this.classProjectionComputes };
|
|
49
|
+
}
|
|
50
|
+
/** Test seam: counts genuine per-row cross-namespace path walks in getClassApiMatrix. */
|
|
51
|
+
get apiMatrixStats() {
|
|
52
|
+
return { rowMaps: this.apiMatrixRowMaps };
|
|
53
|
+
}
|
|
42
54
|
constructor(config, versionService = new VersionService(config), fetchFn = globalThis.fetch) {
|
|
43
55
|
this.config = config;
|
|
44
56
|
this.versionService = versionService;
|
|
@@ -65,7 +77,7 @@ export class MappingService {
|
|
|
65
77
|
// explicitly.
|
|
66
78
|
const effectiveSignatureMode = input.signatureMode ?? "name-only";
|
|
67
79
|
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
|
|
68
|
-
allowShortClassName: input.kind === "class" && input.sourceMapping === "obfuscated"
|
|
80
|
+
allowShortClassName: input.kind === "class" && (input.sourceMapping === "obfuscated" || input.nameMode === "auto")
|
|
69
81
|
});
|
|
70
82
|
const cacheKey = this.buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode);
|
|
71
83
|
const cached = this.resolutionCache.get(cacheKey);
|
|
@@ -176,7 +188,7 @@ export class MappingService {
|
|
|
176
188
|
}
|
|
177
189
|
rawCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
|
|
178
190
|
}
|
|
179
|
-
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
|
|
191
|
+
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation, warnings);
|
|
180
192
|
if (rawCandidates.length > disambiguatedCandidates.length) {
|
|
181
193
|
warnings.push(`Disambiguation hints narrowed candidates from ${rawCandidates.length} to ${disambiguatedCandidates.length}.`);
|
|
182
194
|
}
|
|
@@ -364,7 +376,13 @@ export class MappingService {
|
|
|
364
376
|
const candidates = rawCandidates.map(toResolutionCandidate);
|
|
365
377
|
const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
|
|
366
378
|
const strictDescriptor = projectedDescriptor ?? descriptor;
|
|
367
|
-
const
|
|
379
|
+
const acceptedDescriptors = new Set([descriptor, strictDescriptor]);
|
|
380
|
+
const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
381
|
+
if (toObfuscatedPath) {
|
|
382
|
+
const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, descriptor);
|
|
383
|
+
acceptedDescriptors.add(obfuscatedProjection.descriptor);
|
|
384
|
+
}
|
|
385
|
+
const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
|
|
368
386
|
if (strictCandidates.length === 1) {
|
|
369
387
|
const resolved = toResolutionCandidate(strictCandidates[0]);
|
|
370
388
|
return {
|
|
@@ -499,21 +517,18 @@ export class MappingService {
|
|
|
499
517
|
warnings
|
|
500
518
|
};
|
|
501
519
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return false;
|
|
520
|
+
const exactIndex = collectExactRecordIndex(graph, baseMapping);
|
|
521
|
+
const baseRecords = [];
|
|
522
|
+
if (includeKinds.has("class")) {
|
|
523
|
+
for (const record of lookupExactClassBySymbol(exactIndex, baseClass.symbol)) {
|
|
524
|
+
baseRecords.push(record);
|
|
508
525
|
}
|
|
509
|
-
|
|
510
|
-
|
|
526
|
+
}
|
|
527
|
+
for (const record of lookupExactMembersByOwner(exactIndex, baseClass.symbol)) {
|
|
528
|
+
if (record.kind === "field" ? includeKinds.has("field") : includeKinds.has("method")) {
|
|
529
|
+
baseRecords.push(record);
|
|
511
530
|
}
|
|
512
|
-
|
|
513
|
-
});
|
|
514
|
-
const rows = [];
|
|
515
|
-
let ambiguousRowCount = 0;
|
|
516
|
-
const rowSeen = new Set();
|
|
531
|
+
}
|
|
517
532
|
const rowKindOrder = {
|
|
518
533
|
class: 0,
|
|
519
534
|
field: 1,
|
|
@@ -530,12 +545,47 @@ export class MappingService {
|
|
|
530
545
|
}
|
|
531
546
|
return left.symbol.localeCompare(right.symbol);
|
|
532
547
|
});
|
|
548
|
+
// Dedup up front using the base-only symbol key. The sort key and buildSymbolKey
|
|
549
|
+
// read only base-namespace fields, so this stable first-occurrence-wins order is
|
|
550
|
+
// fully determined WITHOUT any cross-namespace mapping. That lets rowCount /
|
|
551
|
+
// nextCursor / row order stay byte-identical while we map only the requested page.
|
|
552
|
+
const rowSeen = new Set();
|
|
553
|
+
const dedupedBase = [];
|
|
533
554
|
for (const baseRecord of sortedBase) {
|
|
534
555
|
const key = buildSymbolKey(baseRecord);
|
|
535
556
|
if (rowSeen.has(key)) {
|
|
536
557
|
continue;
|
|
537
558
|
}
|
|
538
559
|
rowSeen.add(key);
|
|
560
|
+
dedupedBase.push(baseRecord);
|
|
561
|
+
}
|
|
562
|
+
const rowCount = dedupedBase.length;
|
|
563
|
+
const rowLimit = clampRowLimit(input.maxRows);
|
|
564
|
+
if (input.maxRows != null && Number.isFinite(input.maxRows) && input.maxRows > 5000) {
|
|
565
|
+
warnings.push(`maxRows was clamped to 5000 from ${input.maxRows}.`);
|
|
566
|
+
}
|
|
567
|
+
// Offset cursor over the stable row order; the context key ties a cursor to
|
|
568
|
+
// this exact query so a stale cursor restarts from the first page.
|
|
569
|
+
const rowCursorContext = buildPageContextKey([
|
|
570
|
+
version,
|
|
571
|
+
className,
|
|
572
|
+
classNameMapping,
|
|
573
|
+
(input.includeKinds ?? []).join(","),
|
|
574
|
+
input.sourcePriority,
|
|
575
|
+
resolveGradleUserHomePath(input.gradleUserHome)
|
|
576
|
+
]);
|
|
577
|
+
const { offset: rowOffset, cursorIgnored: rowCursorIgnored } = resolveCursorOffset(input.cursor, rowCursorContext);
|
|
578
|
+
const windowBase = rowLimit != null ? dedupedBase.slice(rowOffset, rowOffset + rowLimit) : dedupedBase.slice(rowOffset);
|
|
579
|
+
const consumedRows = rowOffset + windowBase.length;
|
|
580
|
+
const rowsTruncated = consumedRows < rowCount;
|
|
581
|
+
const nextCursor = rowsTruncated ? encodeOffsetCursor(consumedRows, rowCursorContext) : undefined;
|
|
582
|
+
// Map ONLY the requested window. Per-row mapping cost is therefore O(window),
|
|
583
|
+
// not O(rowCount). warnings/ambiguousRowCount are accordingly page-scoped on
|
|
584
|
+
// paginated calls (caveat B1) and byte-identical to the old full-set values when
|
|
585
|
+
// no maxRows/cursor narrows the window.
|
|
586
|
+
const rows = [];
|
|
587
|
+
let ambiguousRowCount = 0;
|
|
588
|
+
for (const baseRecord of windowBase) {
|
|
539
589
|
let rowHadAmbiguity = false;
|
|
540
590
|
const row = {
|
|
541
591
|
kind: baseRecord.kind,
|
|
@@ -584,9 +634,6 @@ export class MappingService {
|
|
|
584
634
|
ambiguousRowCount += 1;
|
|
585
635
|
}
|
|
586
636
|
}
|
|
587
|
-
const rowCount = rows.length;
|
|
588
|
-
const rowLimit = clampRowLimit(input.maxRows);
|
|
589
|
-
const limitedRows = rowLimit != null && rowCount > rowLimit ? rows.slice(0, rowLimit) : rows;
|
|
590
637
|
return {
|
|
591
638
|
version,
|
|
592
639
|
className,
|
|
@@ -597,9 +644,11 @@ export class MappingService {
|
|
|
597
644
|
intermediary: classByMapping.intermediary?.symbol,
|
|
598
645
|
yarn: classByMapping.yarn?.symbol
|
|
599
646
|
},
|
|
600
|
-
rows
|
|
647
|
+
rows,
|
|
601
648
|
rowCount,
|
|
602
|
-
rowsTruncated:
|
|
649
|
+
rowsTruncated: rowsTruncated ? true : undefined,
|
|
650
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
651
|
+
...(rowCursorIgnored ? { cursorIgnored: true } : {}),
|
|
603
652
|
warnings,
|
|
604
653
|
ambiguousRowCount: ambiguousRowCount > 0 ? ambiguousRowCount : undefined
|
|
605
654
|
};
|
|
@@ -632,6 +681,11 @@ export class MappingService {
|
|
|
632
681
|
sourcePriorityApplied: priority
|
|
633
682
|
};
|
|
634
683
|
const classNameMode = input.nameMode === "auto" ? "auto" : "fqcn";
|
|
684
|
+
// Normalize the effective signatureMode exactly once (mirrors findMapping) so an omitted
|
|
685
|
+
// signatureMode reaching the service — direct/internal callers, MCP resource handlers, and
|
|
686
|
+
// batch-symbol-exists entries — defaults to name-only instead of taking the strict
|
|
687
|
+
// descriptor-required path. Callers needing strict matching pass signatureMode: "exact".
|
|
688
|
+
const effectiveSignatureMode = input.signatureMode ?? "name-only";
|
|
635
689
|
const normalizedQuery = input.kind === "class" && classNameMode === "auto"
|
|
636
690
|
? (() => {
|
|
637
691
|
const owner = input.owner?.trim();
|
|
@@ -663,7 +717,9 @@ export class MappingService {
|
|
|
663
717
|
};
|
|
664
718
|
})()
|
|
665
719
|
: (() => {
|
|
666
|
-
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input,
|
|
720
|
+
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
|
|
721
|
+
allowShortClassName: input.kind === "class" && sourceMapping === "obfuscated"
|
|
722
|
+
});
|
|
667
723
|
return {
|
|
668
724
|
mode: "strict",
|
|
669
725
|
queryRecord,
|
|
@@ -684,6 +740,7 @@ export class MappingService {
|
|
|
684
740
|
warnings
|
|
685
741
|
};
|
|
686
742
|
}
|
|
743
|
+
const exactIndex = collectExactRecordIndex(graph, sourceMapping);
|
|
687
744
|
const buildOutput = (querySymbol, matched, status) => {
|
|
688
745
|
const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
|
|
689
746
|
const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
|
|
@@ -705,11 +762,11 @@ export class MappingService {
|
|
|
705
762
|
if (normalizedQuery.mode === "auto-class") {
|
|
706
763
|
const autoClassName = normalizedQuery.className;
|
|
707
764
|
if (autoClassName.includes(".")) {
|
|
708
|
-
const matched =
|
|
765
|
+
const matched = lookupExactClassBySymbol(exactIndex, autoClassName);
|
|
709
766
|
const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
|
|
710
767
|
return buildOutput(normalizedQuery.querySymbol, matched, status);
|
|
711
768
|
}
|
|
712
|
-
const matched =
|
|
769
|
+
const matched = lookupExactClassBySimpleName(exactIndex, autoClassName);
|
|
713
770
|
const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
|
|
714
771
|
if (status === "ambiguous") {
|
|
715
772
|
warnings.push(`Multiple class symbols matched short name "${autoClassName}". Provide fully-qualified class name.`);
|
|
@@ -718,18 +775,18 @@ export class MappingService {
|
|
|
718
775
|
}
|
|
719
776
|
const { queryRecord, querySymbol } = normalizedQuery;
|
|
720
777
|
if (queryRecord.kind === "class") {
|
|
721
|
-
const matched =
|
|
778
|
+
const matched = lookupExactClassBySymbol(exactIndex, queryRecord.symbol);
|
|
722
779
|
const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
|
|
723
780
|
return buildOutput(querySymbol, matched, status);
|
|
724
781
|
}
|
|
725
782
|
if (queryRecord.kind === "field") {
|
|
726
|
-
const matched =
|
|
783
|
+
const matched = lookupExactMembers(exactIndex, "field", queryRecord.owner ?? "", queryRecord.name);
|
|
727
784
|
const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
|
|
728
785
|
return buildOutput(querySymbol, matched, status);
|
|
729
786
|
}
|
|
730
|
-
const methodCandidates =
|
|
787
|
+
const methodCandidates = lookupExactMembers(exactIndex, "method", queryRecord.owner ?? "", queryRecord.name);
|
|
731
788
|
// name-only mode: skip descriptor matching, resolve by owner+name
|
|
732
|
-
if (
|
|
789
|
+
if (effectiveSignatureMode === "name-only") {
|
|
733
790
|
const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
|
|
734
791
|
if (status === "ambiguous") {
|
|
735
792
|
// name-only discards any supplied descriptor, so telling the caller to "provide
|
|
@@ -754,18 +811,27 @@ export class MappingService {
|
|
|
754
811
|
// and produce false negatives in the most common lookup shape. When no class references
|
|
755
812
|
// exist at all (primitives-only descriptors such as `(I)V`) the projector marks
|
|
756
813
|
// `hadClassReferences === false` and we simply reuse the original descriptor.
|
|
814
|
+
// Stored descriptors carry whatever coordinate the tiny column used: obfuscated
|
|
815
|
+
// for merged graphs, but intermediary for a standalone Fabric yarn tiny
|
|
816
|
+
// (`tiny 2 0 intermediary named`). The descriptor namespace is not recorded
|
|
817
|
+
// per record, so project the query descriptor along every reachable base
|
|
818
|
+
// namespace and accept a match in any of them rather than assuming obfuscated.
|
|
757
819
|
const queryDescriptor = queryRecord.descriptor;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
820
|
+
const candidateDescriptors = new Set([queryDescriptor]);
|
|
821
|
+
for (const baseNamespace of ["obfuscated", "intermediary"]) {
|
|
822
|
+
if (sourceMapping === baseNamespace) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const projectionPath = namespacePath(graph, sourceMapping, baseNamespace);
|
|
826
|
+
if (!projectionPath) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const projection = this.projectMethodDescriptorToTarget(graph, projectionPath, queryDescriptor);
|
|
830
|
+
if (projection.hadClassReferences) {
|
|
831
|
+
candidateDescriptors.add(projection.descriptor);
|
|
766
832
|
}
|
|
767
833
|
}
|
|
768
|
-
const descriptorMatched = methodCandidates.filter((record) => record.descriptor
|
|
834
|
+
const descriptorMatched = methodCandidates.filter((record) => record.descriptor != null && candidateDescriptors.has(record.descriptor));
|
|
769
835
|
if (descriptorMatched.length === 1) {
|
|
770
836
|
return buildOutput(querySymbol, descriptorMatched, "resolved");
|
|
771
837
|
}
|
|
@@ -782,6 +848,9 @@ export class MappingService {
|
|
|
782
848
|
if (sourceMapping === targetMapping) {
|
|
783
849
|
return [record];
|
|
784
850
|
}
|
|
851
|
+
// Count genuine cross-namespace path walks (the per-row cost getClassApiMatrix
|
|
852
|
+
// now scopes to the requested window); test seam via apiMatrixStats.
|
|
853
|
+
this.apiMatrixRowMaps += 1;
|
|
785
854
|
const path = resolvedPath ?? namespacePath(graph, sourceMapping, targetMapping);
|
|
786
855
|
if (!path) {
|
|
787
856
|
return [];
|
|
@@ -886,22 +955,35 @@ export class MappingService {
|
|
|
886
955
|
projectMethodDescriptorToTarget(graph, path, descriptor) {
|
|
887
956
|
let hadClassReferences = false;
|
|
888
957
|
let complete = true;
|
|
889
|
-
|
|
958
|
+
// Class-to-class projection depends only on (graph, path, internalName) — not on
|
|
959
|
+
// the descriptor — so memoize it on the graph and share it across every member
|
|
960
|
+
// lookup on this graph instead of rebuilding a fresh cache per call. A `null`
|
|
961
|
+
// entry memoizes an unmapped/ambiguous class so a repeat reference stays
|
|
962
|
+
// `complete=false` without recomputing.
|
|
963
|
+
const projectionKey = path.join(">");
|
|
964
|
+
const classProjectionCache = graph.classProjectionCache;
|
|
890
965
|
const projectedDescriptor = descriptor.replace(/L([^;]+);/g, (fullMatch, internalName) => {
|
|
891
966
|
hadClassReferences = true;
|
|
892
|
-
const
|
|
893
|
-
|
|
967
|
+
const cacheKey = `${projectionKey}\0${internalName}`;
|
|
968
|
+
const cached = classProjectionCache.get(cacheKey);
|
|
969
|
+
if (cached !== undefined) {
|
|
970
|
+
if (cached === null) {
|
|
971
|
+
complete = false;
|
|
972
|
+
return fullMatch;
|
|
973
|
+
}
|
|
894
974
|
return `L${cached};`;
|
|
895
975
|
}
|
|
976
|
+
this.classProjectionComputes += 1;
|
|
896
977
|
const projectedClassCandidates = this
|
|
897
978
|
.mapCandidatesAlongPath(graph, path, createClassSymbolRecord(internalName.replace(/\//g, ".")))
|
|
898
979
|
.filter((candidate) => candidate.kind === "class");
|
|
899
980
|
if (projectedClassCandidates.length !== 1) {
|
|
900
981
|
complete = false;
|
|
982
|
+
classProjectionCache.set(cacheKey, null);
|
|
901
983
|
return fullMatch;
|
|
902
984
|
}
|
|
903
985
|
const projectedInternalName = projectedClassCandidates[0].symbol.replace(/\./g, "/");
|
|
904
|
-
classProjectionCache.set(
|
|
986
|
+
classProjectionCache.set(cacheKey, projectedInternalName);
|
|
905
987
|
return `L${projectedInternalName};`;
|
|
906
988
|
});
|
|
907
989
|
return {
|
|
@@ -1031,6 +1113,8 @@ export class MappingService {
|
|
|
1031
1113
|
adjacency: new Map(),
|
|
1032
1114
|
pathCache: new Map(),
|
|
1033
1115
|
recordsByTarget: new Map(),
|
|
1116
|
+
exactRecordIndex: new Map(),
|
|
1117
|
+
classProjectionCache: new Map(),
|
|
1034
1118
|
warnings: [
|
|
1035
1119
|
`Version ${version} is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names.`
|
|
1036
1120
|
]
|
|
@@ -1044,6 +1128,8 @@ export class MappingService {
|
|
|
1044
1128
|
adjacency: new Map(),
|
|
1045
1129
|
pathCache: new Map(),
|
|
1046
1130
|
recordsByTarget: new Map(),
|
|
1131
|
+
exactRecordIndex: new Map(),
|
|
1132
|
+
classProjectionCache: new Map(),
|
|
1047
1133
|
warnings: []
|
|
1048
1134
|
};
|
|
1049
1135
|
const mojangLoad = await this.loadMojangPairs(version);
|
|
@@ -1209,25 +1295,68 @@ async function extractTinyFromJar(jarPath, outputPath) {
|
|
|
1209
1295
|
return false;
|
|
1210
1296
|
}
|
|
1211
1297
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
1212
|
-
|
|
1298
|
+
const tempPath = `${outputPath}.${randomBytes(4).toString("hex")}.tmp`;
|
|
1299
|
+
await writeFile(tempPath, tinyEntry.content, "utf8");
|
|
1300
|
+
await rename(tempPath, outputPath);
|
|
1213
1301
|
return true;
|
|
1214
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* How long a resolved yarn build coordinate is trusted before re-checking the
|
|
1305
|
+
* Fabric Maven metadata for a newer build. Within this window, resolving a yarn
|
|
1306
|
+
* tiny is zero-network (it reuses the recorded coordinate); after it expires the
|
|
1307
|
+
* next resolve does ONE metadata fetch. This fence keeps the metadata fetch off
|
|
1308
|
+
* the remap cache-hit fast path while still picking up new yarn builds.
|
|
1309
|
+
*/
|
|
1310
|
+
const YARN_METADATA_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1311
|
+
function yarnMetaPath(cacheDir, version) {
|
|
1312
|
+
return join(cacheDir, "mappings", "yarn", `${version}.meta.json`);
|
|
1313
|
+
}
|
|
1314
|
+
function yarnCoordinateTinyPath(cacheDir, coordinate) {
|
|
1315
|
+
return join(cacheDir, "mappings", "yarn", `${coordinate}.tiny`);
|
|
1316
|
+
}
|
|
1317
|
+
function readYarnCoordinateMeta(metaPath) {
|
|
1318
|
+
if (!existsSync(metaPath)) {
|
|
1319
|
+
return undefined;
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
const parsed = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
1323
|
+
if (typeof parsed.coordinate === "string" && typeof parsed.fetchedAt === "number") {
|
|
1324
|
+
return { coordinate: parsed.coordinate, fetchedAt: parsed.fetchedAt };
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
// Corrupt marker -> treat as absent and re-resolve.
|
|
1329
|
+
}
|
|
1330
|
+
return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
function writeYarnCoordinateMeta(metaPath, meta) {
|
|
1333
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
1334
|
+
writeFileSync(metaPath, JSON.stringify(meta));
|
|
1335
|
+
}
|
|
1215
1336
|
/**
|
|
1216
1337
|
* Resolve and cache a Tiny v2 mapping file for the given Minecraft version.
|
|
1217
1338
|
*
|
|
1339
|
+
* Intermediary is version-keyed (immutable per release). Yarn is coordinate-keyed:
|
|
1340
|
+
* Fabric publishes incrementing yarn builds for the same MC version, so the tiny
|
|
1341
|
+
* is cached at `mappings/yarn/${coordinate}.tiny` and the newest coordinate is
|
|
1342
|
+
* re-checked once per {@link YARN_METADATA_TTL_MS}. The resolved yarn coordinate
|
|
1343
|
+
* is returned so callers can key their own caches on the actual mappings used.
|
|
1344
|
+
*
|
|
1218
1345
|
* @param version - Minecraft version (e.g. "1.20.4")
|
|
1219
1346
|
* @param mapping - "intermediary" or "yarn"
|
|
1220
1347
|
* @param cacheDir - The application cache directory
|
|
1221
1348
|
* @param fetchFn - Optional fetch implementation for testing
|
|
1222
|
-
* @
|
|
1349
|
+
* @param options - forceRefresh bypasses the yarn metadata TTL; now overrides the clock (testing)
|
|
1350
|
+
* @returns The extracted Tiny v2 path and, for yarn, the resolved build coordinate
|
|
1223
1351
|
*/
|
|
1224
|
-
export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn) {
|
|
1225
|
-
const cachedTiny = join(cacheDir, "mappings", `${version}-${mapping}.tiny`);
|
|
1226
|
-
if (existsSync(cachedTiny)) {
|
|
1227
|
-
return cachedTiny;
|
|
1228
|
-
}
|
|
1352
|
+
export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn, options) {
|
|
1229
1353
|
const effectiveFetch = fetchFn ?? globalThis.fetch;
|
|
1230
1354
|
if (mapping === "intermediary") {
|
|
1355
|
+
// Intermediary is immutable per MC release: version-keyed cache is correct.
|
|
1356
|
+
const cachedTiny = join(cacheDir, "mappings", `${version}-intermediary.tiny`);
|
|
1357
|
+
if (existsSync(cachedTiny)) {
|
|
1358
|
+
return { path: cachedTiny };
|
|
1359
|
+
}
|
|
1231
1360
|
const url = `${FABRIC_MAVEN}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`;
|
|
1232
1361
|
const jarDest = defaultDownloadPath(cacheDir, url);
|
|
1233
1362
|
const downloaded = await downloadToCache(url, jarDest, {
|
|
@@ -1250,11 +1379,30 @@ export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn
|
|
|
1250
1379
|
details: { version, jarPath: downloaded.path }
|
|
1251
1380
|
});
|
|
1252
1381
|
}
|
|
1253
|
-
return cachedTiny;
|
|
1382
|
+
return { path: cachedTiny };
|
|
1383
|
+
}
|
|
1384
|
+
// yarn: coordinate-keyed + metadata-TTL fenced.
|
|
1385
|
+
const nowFn = options?.now ?? Date.now;
|
|
1386
|
+
const metaPath = yarnMetaPath(cacheDir, version);
|
|
1387
|
+
const existingMeta = readYarnCoordinateMeta(metaPath);
|
|
1388
|
+
// Within-TTL fast path: reuse the recorded coordinate with zero network.
|
|
1389
|
+
if (!options?.forceRefresh && existingMeta && nowFn() - existingMeta.fetchedAt < YARN_METADATA_TTL_MS) {
|
|
1390
|
+
const cachedCoordTiny = yarnCoordinateTinyPath(cacheDir, existingMeta.coordinate);
|
|
1391
|
+
if (existsSync(cachedCoordTiny)) {
|
|
1392
|
+
return { path: cachedCoordTiny, coordinate: existingMeta.coordinate };
|
|
1393
|
+
}
|
|
1254
1394
|
}
|
|
1255
|
-
// yarn
|
|
1256
1395
|
const yarnCoordinates = await fetchYarnCoordinatesStandalone(version, effectiveFetch);
|
|
1396
|
+
// Network fallback: the metadata fetch failed/empty (fetchYarnCoordinatesStandalone
|
|
1397
|
+
// swallows errors -> []). Serve the last-known-good coordinate rather than failing,
|
|
1398
|
+
// so a transient Fabric Maven outage after TTL expiry does not break cache hits.
|
|
1257
1399
|
if (yarnCoordinates.length === 0) {
|
|
1400
|
+
if (existingMeta) {
|
|
1401
|
+
const cachedCoordTiny = yarnCoordinateTinyPath(cacheDir, existingMeta.coordinate);
|
|
1402
|
+
if (existsSync(cachedCoordTiny)) {
|
|
1403
|
+
return { path: cachedCoordTiny, coordinate: existingMeta.coordinate };
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1258
1406
|
throw createError({
|
|
1259
1407
|
code: ERROR_CODES.MAPPING_UNAVAILABLE,
|
|
1260
1408
|
message: `No yarn builds found for Minecraft ${version}.`,
|
|
@@ -1262,6 +1410,12 @@ export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn
|
|
|
1262
1410
|
});
|
|
1263
1411
|
}
|
|
1264
1412
|
for (const coordinate of yarnCoordinates) {
|
|
1413
|
+
const coordTiny = yarnCoordinateTinyPath(cacheDir, coordinate);
|
|
1414
|
+
if (existsSync(coordTiny)) {
|
|
1415
|
+
// Already have this build's tiny; just refresh the TTL marker.
|
|
1416
|
+
writeYarnCoordinateMeta(metaPath, { coordinate, fetchedAt: nowFn() });
|
|
1417
|
+
return { path: coordTiny, coordinate };
|
|
1418
|
+
}
|
|
1265
1419
|
const url = `${FABRIC_MAVEN}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`;
|
|
1266
1420
|
const jarDest = defaultDownloadPath(cacheDir, url);
|
|
1267
1421
|
const downloaded = await downloadToCache(url, jarDest, {
|
|
@@ -1272,9 +1426,12 @@ export async function resolveTinyMappingFile(version, mapping, cacheDir, fetchFn
|
|
|
1272
1426
|
if (!downloaded.ok || !downloaded.path) {
|
|
1273
1427
|
continue;
|
|
1274
1428
|
}
|
|
1275
|
-
const extracted = await extractTinyFromJar(downloaded.path,
|
|
1429
|
+
const extracted = await extractTinyFromJar(downloaded.path, coordTiny);
|
|
1276
1430
|
if (extracted) {
|
|
1277
|
-
|
|
1431
|
+
// Ordering matters: the tiny exists before the marker is written, so a
|
|
1432
|
+
// crash never leaves a TTL-valid marker pointing at a missing tiny.
|
|
1433
|
+
writeYarnCoordinateMeta(metaPath, { coordinate, fetchedAt: nowFn() });
|
|
1434
|
+
return { path: coordTiny, coordinate };
|
|
1278
1435
|
}
|
|
1279
1436
|
}
|
|
1280
1437
|
throw createError({
|
package/dist/mcp-helpers.d.ts
CHANGED
package/dist/mcp-helpers.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ERROR_CODES } from "./errors.js";
|
|
2
|
+
import { retryClassForErrorCode, issueOriginForErrorCode, extractAllowlistedContext } from "./error-mapping.js";
|
|
3
|
+
import { toHints, extractValidatedSuggestionAndExamples, extractFieldErrorsFromDetails } from "./tool-guidance.js";
|
|
2
4
|
export function objectResult(data, options = {}) {
|
|
3
5
|
return {
|
|
4
6
|
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
@@ -42,8 +44,18 @@ function statusForResourceErrorCode(code) {
|
|
|
42
44
|
return 500;
|
|
43
45
|
}
|
|
44
46
|
export function errorResource(uri, error) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
+
const isStr = typeof error === "string";
|
|
48
|
+
const detail = isStr ? error : error.message;
|
|
49
|
+
const code = isStr ? ERROR_CODES.INVALID_INPUT : error.code ?? ERROR_CODES.INTERNAL;
|
|
50
|
+
// Resource reads carry the same AppError as the equivalent tool call, so they
|
|
51
|
+
// get the same recovery metadata. Classifiers are always present; the rest is
|
|
52
|
+
// extracted from the AppError details (reusing the tool-error helpers as-is so
|
|
53
|
+
// the suggestedCall is validated through the single buildSuggestedCall gate).
|
|
54
|
+
const details = isStr ? undefined : error.details;
|
|
55
|
+
const hints = toHints(details);
|
|
56
|
+
const { suggestedCall, exampleCalls } = extractValidatedSuggestionAndExamples(details);
|
|
57
|
+
const fieldErrors = extractFieldErrorsFromDetails(details);
|
|
58
|
+
const context = extractAllowlistedContext(details);
|
|
47
59
|
return {
|
|
48
60
|
contents: [
|
|
49
61
|
{
|
|
@@ -51,12 +63,22 @@ export function errorResource(uri, error) {
|
|
|
51
63
|
mimeType: "application/json",
|
|
52
64
|
text: JSON.stringify({
|
|
53
65
|
error: {
|
|
66
|
+
// Keep the generic resource type/title (asserted by tests and
|
|
67
|
+
// intentionally distinct from the per-code tool ProblemDetails) so
|
|
68
|
+
// the two access paths stay distinguishable.
|
|
54
69
|
type: "https://minecraft-modding-mcp.dev/problems/resource",
|
|
55
70
|
title: "Resource read failed",
|
|
56
71
|
detail,
|
|
57
72
|
status: statusForResourceErrorCode(code),
|
|
58
73
|
code,
|
|
59
|
-
instance: uri
|
|
74
|
+
instance: uri,
|
|
75
|
+
retryClass: retryClassForErrorCode(code),
|
|
76
|
+
issueOrigin: issueOriginForErrorCode(code),
|
|
77
|
+
...(fieldErrors ? { fieldErrors } : {}),
|
|
78
|
+
...(hints ? { hints } : {}),
|
|
79
|
+
...(suggestedCall ? { suggestedCall } : {}),
|
|
80
|
+
...(exampleCalls ? { exampleCalls } : {}),
|
|
81
|
+
...(context ? { context } : {})
|
|
60
82
|
},
|
|
61
83
|
meta: { uri }
|
|
62
84
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
2
2
|
import { loadConfig } from "./config.js";
|
|
3
3
|
import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createJarEntryReader } from "./source-jar-reader.js";
|
|
5
5
|
const CLASSFILE_MAGIC = 0xcafebabe;
|
|
6
6
|
const MAX_INHERITANCE_DEPTH = 64;
|
|
7
7
|
const ACC_PUBLIC = 0x0001;
|
|
@@ -452,18 +452,28 @@ export class MinecraftExplorerService {
|
|
|
452
452
|
};
|
|
453
453
|
}
|
|
454
454
|
const classEntryPath = `${toInternalName(fqn)}.class`;
|
|
455
|
-
|
|
455
|
+
// Open the jar ONCE for this call; every hierarchy class is served from the
|
|
456
|
+
// same handle (see reader.close() after hierarchy construction below).
|
|
457
|
+
const reader = await createJarEntryReader(jarPath);
|
|
458
|
+
let parsed;
|
|
456
459
|
try {
|
|
457
|
-
classBuffer
|
|
460
|
+
let classBuffer;
|
|
461
|
+
try {
|
|
462
|
+
classBuffer = await reader.getEntryBuffer(classEntryPath);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
throw createError({
|
|
466
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
467
|
+
message: `Class "${fqn}" was not found in "${jarPath}".`,
|
|
468
|
+
details: { fqn, jarPath, classEntryPath }
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
parsed = parseClassFile(classBuffer);
|
|
458
472
|
}
|
|
459
|
-
catch {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
message: `Class "${fqn}" was not found in "${jarPath}".`,
|
|
463
|
-
details: { fqn, jarPath, classEntryPath }
|
|
464
|
-
});
|
|
473
|
+
catch (error) {
|
|
474
|
+
reader.close();
|
|
475
|
+
throw error;
|
|
465
476
|
}
|
|
466
|
-
const parsed = parseClassFile(classBuffer);
|
|
467
477
|
const parsedClassCache = new Map([[parsed.internalName, parsed]]);
|
|
468
478
|
const warnings = [];
|
|
469
479
|
const warnMissingInheritedClass = (internalName, relation) => {
|
|
@@ -476,7 +486,7 @@ export class MinecraftExplorerService {
|
|
|
476
486
|
}
|
|
477
487
|
const classPath = `${internalName}.class`;
|
|
478
488
|
try {
|
|
479
|
-
const classBytes = await
|
|
489
|
+
const classBytes = await reader.getEntryBuffer(classPath);
|
|
480
490
|
const parsedClass = parseClassFile(classBytes);
|
|
481
491
|
parsedClassCache.set(parsedClass.internalName, parsedClass);
|
|
482
492
|
return parsedClass;
|
|
@@ -540,6 +550,9 @@ export class MinecraftExplorerService {
|
|
|
540
550
|
}
|
|
541
551
|
hierarchyClasses.push(...interfaceClasses);
|
|
542
552
|
}
|
|
553
|
+
// All jar reads are done; release the file descriptor before member projection
|
|
554
|
+
// (which performs no IO but may throw on malformed descriptors).
|
|
555
|
+
reader.close();
|
|
543
556
|
const toSignatureMember = (ownerFqn, ownerSimpleClassName, member, category) => {
|
|
544
557
|
if (category === "field") {
|
|
545
558
|
const parsedField = parseFieldType(member.descriptor, 0, { allowVoid: false });
|