@adhisang/minecraft-modding-mcp 7.0.0-rc.3 → 7.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 +43 -0
- package/README.md +3 -2
- package/dist/cache-registry.d.ts +16 -0
- package/dist/cache-registry.js +78 -10
- package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-members-service.js +20 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-source-service.js +10 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
- package/dist/entry-tools/compare-minecraft-service.js +65 -4
- package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
- package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.d.ts +2 -2
- package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/index.js +37 -15
- package/dist/json-rpc-framing.d.ts +20 -0
- package/dist/json-rpc-framing.js +80 -7
- package/dist/mapping/loaders/tiny-maven.d.ts +9 -0
- package/dist/mapping/loaders/tiny-maven.js +10 -2
- package/dist/repo-downloader.js +13 -2
- package/dist/source/artifact-resolver.d.ts +14 -0
- package/dist/source/artifact-resolver.js +106 -12
- package/dist/source/class-source/members-builder.d.ts +7 -0
- package/dist/source/class-source/members-builder.js +4 -1
- package/dist/source/class-source.d.ts +9 -2
- package/dist/source/class-source.js +229 -26
- package/dist/source/indexer.js +69 -1
- package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
- package/dist/source/lifecycle/mapping-helpers.js +29 -3
- package/dist/source/lifecycle/runtime-check.d.ts +25 -0
- package/dist/source/lifecycle/runtime-check.js +68 -39
- package/dist/source/symbol-resolver.js +88 -0
- package/dist/source-jar-reader.d.ts +33 -0
- package/dist/source-jar-reader.js +58 -0
- package/dist/source-resolver.d.ts +7 -0
- package/dist/source-resolver.js +20 -5
- package/dist/source-service.d.ts +5 -0
- package/dist/source-service.js +7 -0
- package/dist/stdio-supervisor.js +193 -34
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +186 -21
- package/dist/storage/sqlite.d.ts +31 -1
- package/dist/storage/sqlite.js +125 -16
- package/dist/tool-guidance.js +4 -1
- package/dist/tool-schemas.d.ts +64 -52
- package/dist/tool-schemas.js +9 -7
- package/dist/types.d.ts +9 -0
- package/dist/v1-parity-schemas.js +36 -2
- package/dist/version-diff-service.d.ts +23 -0
- package/dist/version-diff-service.js +101 -0
- package/dist/version-service.d.ts +14 -0
- package/dist/version-service.js +45 -3
- package/dist/workspace-mapping-service.d.ts +8 -0
- package/dist/workspace-mapping-service.js +35 -7
- package/docs/README-ja.md +2 -0
- package/docs/tool-reference.md +55 -11
- package/package.json +1 -1
|
@@ -3,12 +3,14 @@ import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
|
3
3
|
import { buildSuggestedCall } from "../build-suggested-call.js";
|
|
4
4
|
import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
5
5
|
import * as artifactResolver from "./artifact-resolver.js";
|
|
6
|
+
import { isUnobfuscatedIdentityPair } from "./lifecycle/mapping-helpers.js";
|
|
6
7
|
import * as classSourceHelpers from "./class-source-helpers.js";
|
|
7
8
|
import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
|
|
8
9
|
import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, projectMembersByLevel } from "./class-source/members-builder.js";
|
|
9
10
|
import { collectDidYouMeanCandidates } from "./did-you-mean.js";
|
|
10
11
|
import { matchesMemberPattern } from "./member-pattern.js";
|
|
11
12
|
import { findNestedJarClasses, resolveUniqueNestedJarForClass } from "./nested-jars.js";
|
|
13
|
+
import { extractSymbolsFromSource } from "../symbols/symbol-extractor.js";
|
|
12
14
|
import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
|
|
13
15
|
import { dedupeQualityFlags, inheritArtifactMapping, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
14
16
|
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
@@ -79,12 +81,33 @@ function looksLikeDeobfuscatedClassName(value) {
|
|
|
79
81
|
function obfuscatedNamespaceHint(className) {
|
|
80
82
|
return `Artifact is indexed in obfuscated runtime names. Deobfuscated names like "${className}" usually require mapping="mojang" or a find-mapping lookup to obfuscated names.`;
|
|
81
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The artifact's Minecraft version when its runtime ships unobfuscated (26.1+)
|
|
86
|
+
* names, else undefined. A native dependency's version is its own release number,
|
|
87
|
+
* not a Minecraft version, so it is never put to `isUnobfuscatedVersion` (the same
|
|
88
|
+
* rule as the `reconcileUnobfuscatedNamespace` call sites).
|
|
89
|
+
*/
|
|
90
|
+
function unobfuscatedMinecraftVersion(input) {
|
|
91
|
+
if (input.nativeDependency || !input.version || !isUnobfuscatedVersion(input.version)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return input.version;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Said where the obfuscated namespace hint would otherwise have been: on 26.1+
|
|
98
|
+
* the as-shipped names are the Mojang names, so no mapping choice finds another class.
|
|
99
|
+
*/
|
|
100
|
+
function unobfuscatedRuntimeNamesNote(version) {
|
|
101
|
+
return `Minecraft ${version} ships Mojang names at runtime, so retrying with a different mapping will not change the class names.`;
|
|
102
|
+
}
|
|
82
103
|
function hasPartialNetMinecraftCoverage(qualityFlags) {
|
|
83
104
|
return qualityFlags.includes("partial-source-no-net-minecraft");
|
|
84
105
|
}
|
|
85
106
|
/**
|
|
86
|
-
* Whether "this artifact is indexed in obfuscated names, ask
|
|
87
|
-
*
|
|
107
|
+
* Whether a miss has the shape "this artifact is indexed in obfuscated names, ask
|
|
108
|
+
* for mapping=mojang" answers, from the pieces of the artifact the caller sees.
|
|
109
|
+
* The claim can still be false on Minecraft 26.1+; `isObfuscatedNamespaceHintTrue`
|
|
110
|
+
* adds that gate.
|
|
88
111
|
*
|
|
89
112
|
* Two artifact kinds report `mappingApplied: "obfuscated"` without being an
|
|
90
113
|
* obfuscated Minecraft index, and the hint is simply false for them:
|
|
@@ -98,22 +121,41 @@ function hasPartialNetMinecraftCoverage(qualityFlags) {
|
|
|
98
121
|
* without these exclusions an ordinary library class such as "GameTest"
|
|
99
122
|
* qualifies and the caller is told to remap a jar that was never obfuscated.
|
|
100
123
|
*/
|
|
101
|
-
function
|
|
124
|
+
function isObfuscatedLabelMiss(input) {
|
|
102
125
|
return (input.mappingApplied === "obfuscated" &&
|
|
103
126
|
!input.nativeDependency &&
|
|
104
127
|
!input.qualityFlags.includes("shell-jar") &&
|
|
105
128
|
looksLikeDeobfuscatedClassName(input.className));
|
|
106
129
|
}
|
|
107
|
-
|
|
108
|
-
|
|
130
|
+
/**
|
|
131
|
+
* `isObfuscatedLabelMiss` is the shape of the miss the hint answers; this adds
|
|
132
|
+
* the one fact that makes its claim false even then. A Minecraft 26.1+ artifact
|
|
133
|
+
* labelled "obfuscated" is indexed in the as-shipped names, which ARE Mojang
|
|
134
|
+
* names: telling the caller to ask for mapping="mojang" sends them round a
|
|
135
|
+
* retry that returns the same names.
|
|
136
|
+
*/
|
|
137
|
+
function isObfuscatedNamespaceHintTrue(input) {
|
|
138
|
+
return (isObfuscatedLabelMiss(input) &&
|
|
139
|
+
unobfuscatedMinecraftVersion({
|
|
140
|
+
version: input.minecraftVersion,
|
|
141
|
+
nativeDependency: input.nativeDependency
|
|
142
|
+
}) === undefined);
|
|
143
|
+
}
|
|
144
|
+
function describeArtifactForNamespaceHint(artifact, className) {
|
|
145
|
+
return {
|
|
109
146
|
mappingApplied: artifact.mappingApplied,
|
|
110
147
|
qualityFlags: artifact.qualityFlags,
|
|
111
148
|
nativeDependency: artifactResolver.isDependencyLikeArtifact({
|
|
112
149
|
provenance: artifact.provenance,
|
|
113
150
|
coordinate: artifact.coordinate
|
|
114
151
|
}),
|
|
115
|
-
className
|
|
116
|
-
|
|
152
|
+
className,
|
|
153
|
+
minecraftVersion: artifactResolver.inferVersionFromContext({
|
|
154
|
+
version: artifact.version,
|
|
155
|
+
provenance: artifact.provenance,
|
|
156
|
+
coordinate: artifact.coordinate
|
|
157
|
+
})
|
|
158
|
+
};
|
|
117
159
|
}
|
|
118
160
|
function classNameToClassPath(className) {
|
|
119
161
|
const normalized = normalizePathStyle(className.trim()).replace(/\//g, ".");
|
|
@@ -148,10 +190,31 @@ export function resolveClassFilePath(svc, artifactId, className) {
|
|
|
148
190
|
const expectedPrefix = lastSlash < 0 ? "" : classPath.slice(0, lastSlash + 1);
|
|
149
191
|
return svc.filesRepo.findBestClassLookupPath(artifactId, [...candidates], normalizedClassName, simpleName, expectedPrefix);
|
|
150
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The class-name translation behind `svc.resolveClassNameForLookup`, whose callers
|
|
195
|
+
* (the lifecycle tools) pass a Minecraft version, so `version` also keys the 26.1+
|
|
196
|
+
* identity shortcut.
|
|
197
|
+
*/
|
|
151
198
|
export async function resolveClassNameForLookup(svc, input) {
|
|
199
|
+
return resolveArtifactClassNameForLookup(svc, { ...input, minecraftVersion: input.version });
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* `resolveClassNameForLookup` for an artifact the caller named, whose `version`
|
|
203
|
+
* need not be a Minecraft version: a dependency artifact's is its own release
|
|
204
|
+
* number. `version` still drives the mapping lookup, exactly as before; only
|
|
205
|
+
* `minecraftVersion`, which must be a proven Minecraft version, may take the 26.1+
|
|
206
|
+
* identity shortcut.
|
|
207
|
+
*/
|
|
208
|
+
async function resolveArtifactClassNameForLookup(svc, input) {
|
|
152
209
|
if (input.sourceMapping === input.targetMapping) {
|
|
153
210
|
return input.className;
|
|
154
211
|
}
|
|
212
|
+
// On 26.1+ the as-shipped ("obfuscated") names ARE the Mojang names and the
|
|
213
|
+
// mapping graph between them is empty by design, so the lookup can only miss
|
|
214
|
+
// and warn about a translation that is the identity.
|
|
215
|
+
if (isUnobfuscatedIdentityPair(input.minecraftVersion, input.sourceMapping, input.targetMapping)) {
|
|
216
|
+
return input.className;
|
|
217
|
+
}
|
|
155
218
|
if (!input.version) {
|
|
156
219
|
input.warnings.push(`Could not map class "${input.className}" from ${input.sourceMapping} to ${input.targetMapping} for ${input.context} because version is unavailable.`);
|
|
157
220
|
return input.className;
|
|
@@ -176,6 +239,38 @@ export async function resolveClassNameForLookup(svc, input) {
|
|
|
176
239
|
}
|
|
177
240
|
return input.className;
|
|
178
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* The provenance as returned, carrying `unobfuscatedRuntime` whenever the artifact
|
|
244
|
+
* it describes is a Minecraft 26.1+ runtime. The resolver stamps the flag on every
|
|
245
|
+
* fresh resolve, but a row indexed before the flag existed is served warm and never
|
|
246
|
+
* rewritten, and a row with no provenance gets `buildFallbackProvenance`. Only the
|
|
247
|
+
* provenance's OWN recorded version counts - never a project-derived one - and a
|
|
248
|
+
* dependency-like provenance is excluded, so a library's release number is never
|
|
249
|
+
* read as a Minecraft version. A jar row that predates the in-jar proof records no
|
|
250
|
+
* version and stays unflagged until a resolve proves the jar again, which backfills
|
|
251
|
+
* the row (`backfillResolvedVersion` in the indexer).
|
|
252
|
+
*/
|
|
253
|
+
function withUnobfuscatedRuntimeFlag(provenance) {
|
|
254
|
+
if (provenance.unobfuscatedRuntime === true ||
|
|
255
|
+
provenance.nestedJar ||
|
|
256
|
+
artifactResolver.isDependencyLikeArtifact({ provenance })) {
|
|
257
|
+
return provenance;
|
|
258
|
+
}
|
|
259
|
+
// A persisted or stubbed object can be partial; with nothing recorded there is
|
|
260
|
+
// nothing to derive the flag from.
|
|
261
|
+
const partial = provenance;
|
|
262
|
+
if (!partial.resolvedFrom || !partial.target) {
|
|
263
|
+
return provenance;
|
|
264
|
+
}
|
|
265
|
+
const version = unobfuscatedMinecraftVersion({
|
|
266
|
+
version: artifactResolver.inferVersionFromContext({
|
|
267
|
+
provenance,
|
|
268
|
+
coordinate: partial.resolvedFrom.coordinate
|
|
269
|
+
}),
|
|
270
|
+
nativeDependency: false
|
|
271
|
+
});
|
|
272
|
+
return version ? { ...provenance, unobfuscatedRuntime: true } : provenance;
|
|
273
|
+
}
|
|
179
274
|
export function buildFallbackProvenance(svc, input) {
|
|
180
275
|
const artifact = svc.getArtifact(input.artifactId);
|
|
181
276
|
const fallbackTarget = artifact.version
|
|
@@ -240,7 +335,21 @@ export function buildClassSourceNotFoundError(svc, input) {
|
|
|
240
335
|
// jar. Candidates from the artifact the caller did not name carry its id.
|
|
241
336
|
didYouMean: unionDidYouMeanCandidates(svc, requestedArtifactId, input.artifactId, input.className)
|
|
242
337
|
};
|
|
338
|
+
const unobfuscatedVersion = unobfuscatedMinecraftVersion({
|
|
339
|
+
version: input.version,
|
|
340
|
+
nativeDependency: input.nativeDependency === true
|
|
341
|
+
});
|
|
243
342
|
let nextAction = `Use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
|
|
343
|
+
// On 26.1+ there is no namespace to switch to, so the most useful thing to say
|
|
344
|
+
// first is the nearest class the index does hold.
|
|
345
|
+
const nearestCandidate = unobfuscatedVersion
|
|
346
|
+
? details.didYouMean[0]
|
|
347
|
+
: undefined;
|
|
348
|
+
if (nearestCandidate) {
|
|
349
|
+
nextAction =
|
|
350
|
+
`Did you mean "${nearestCandidate.className}"? didYouMean lists near-miss classes from the index; ` +
|
|
351
|
+
`otherwise use find-class to resolve the correct fully-qualified name for "${simpleName}".`;
|
|
352
|
+
}
|
|
244
353
|
let suggestionSpec = {
|
|
245
354
|
tool: "find-class",
|
|
246
355
|
params: { className: simpleName, artifactId: requestedArtifactId }
|
|
@@ -290,16 +399,26 @@ export function buildClassSourceNotFoundError(svc, input) {
|
|
|
290
399
|
// imperative "Provide/Pass mapping", and the sentence is concatenated into
|
|
291
400
|
// the same `nextAction` string as the find-class guidance, which is published
|
|
292
401
|
// as a single hint that no mid-string excision can repair.
|
|
402
|
+
//
|
|
403
|
+
// A third gate lives inside `isObfuscatedNamespaceHintTrue`: on Minecraft 26.1+
|
|
404
|
+
// the claim is false, and the same miss gets a note that no mapping will help.
|
|
293
405
|
const callerAskedForNonObfuscatedMapping = input.callerSuppliedMapping != null && input.callerSuppliedMapping !== "obfuscated";
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
406
|
+
const namespaceHintInput = {
|
|
407
|
+
mappingApplied: input.mappingApplied,
|
|
408
|
+
qualityFlags: input.qualityFlags,
|
|
409
|
+
nativeDependency: input.nativeDependency === true,
|
|
410
|
+
className: input.className,
|
|
411
|
+
minecraftVersion: input.version
|
|
412
|
+
};
|
|
413
|
+
if (!callerAskedForNonObfuscatedMapping && isObfuscatedNamespaceHintTrue(namespaceHintInput)) {
|
|
301
414
|
nextAction += ` ${obfuscatedNamespaceHint(input.className)}`;
|
|
302
415
|
}
|
|
416
|
+
else if (!callerAskedForNonObfuscatedMapping &&
|
|
417
|
+
unobfuscatedVersion &&
|
|
418
|
+
!hasPartialNetMinecraftCoverage(input.qualityFlags) &&
|
|
419
|
+
isObfuscatedLabelMiss(namespaceHintInput)) {
|
|
420
|
+
nextAction += ` ${unobfuscatedRuntimeNamesNote(unobfuscatedVersion)}`;
|
|
421
|
+
}
|
|
303
422
|
details.nextAction = nextAction;
|
|
304
423
|
Object.assign(details, buildSuggestedCall(suggestionSpec));
|
|
305
424
|
// Split-source workspaces can omit client-only classes from merged indexes;
|
|
@@ -407,6 +526,50 @@ function projectDecompiledFallback(fallback, level) {
|
|
|
407
526
|
// The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
|
|
408
527
|
// isTypeSymbol checks below; pushed down to SQL so non-type rows are never fetched.
|
|
409
528
|
const TYPE_SYMBOL_KINDS = ["class", "interface", "enum", "record"];
|
|
529
|
+
/**
|
|
530
|
+
* Reconstruct the full nesting chain between a file's top-level type and a
|
|
531
|
+
* type declared two or more levels deeper inside it. The extractor stores
|
|
532
|
+
* only ONE qualifiedName per FILE (the top-level type), so naively
|
|
533
|
+
* concatenating `<topLevelFQN>.<symbolName>` is only correct for exactly one
|
|
534
|
+
* level of nesting — it silently drops every intermediate enclosing type.
|
|
535
|
+
*
|
|
536
|
+
* Walks the file's own type declarations from the top-level type downward,
|
|
537
|
+
* using brace-range containment to find which declaration directly encloses
|
|
538
|
+
* `targetLine` at each level, collecting each intermediate simple name.
|
|
539
|
+
*
|
|
540
|
+
* Returns the intermediate simple names (outermost first, target excluded)
|
|
541
|
+
* or undefined when the chain cannot be reconstructed — e.g. the file's
|
|
542
|
+
* content is unavailable — in which case the caller falls back to the
|
|
543
|
+
* single-level formula.
|
|
544
|
+
*/
|
|
545
|
+
function resolveNestedTypeChain(svc, artifactId, filePath, topSimpleName, targetLine) {
|
|
546
|
+
const fileRow = svc.filesRepo.getFileContent(artifactId, filePath);
|
|
547
|
+
if (!fileRow)
|
|
548
|
+
return undefined;
|
|
549
|
+
const lines = fileRow.content.split(/\r?\n/);
|
|
550
|
+
const fileSymbols = extractSymbolsFromSource(filePath, fileRow.content);
|
|
551
|
+
let currentBody = classSourceHelpers.computeBraceRange(lines, fileSymbols, topSimpleName);
|
|
552
|
+
if (!currentBody)
|
|
553
|
+
return undefined;
|
|
554
|
+
const chain = [];
|
|
555
|
+
// Bounded by the number of type symbols in the file; guards against ever
|
|
556
|
+
// looping on a malformed structure.
|
|
557
|
+
for (let step = 0; step <= fileSymbols.length; step += 1) {
|
|
558
|
+
const childRanges = classSourceHelpers.computeNestedTypeRanges(lines, fileSymbols, currentBody);
|
|
559
|
+
if (childRanges.some((range) => range.declarationLine === targetLine)) {
|
|
560
|
+
return chain;
|
|
561
|
+
}
|
|
562
|
+
const containingChild = childRanges.find((range) => range.declarationLine < targetLine && targetLine <= range.endLine);
|
|
563
|
+
if (!containingChild)
|
|
564
|
+
return undefined;
|
|
565
|
+
const childSymbol = fileSymbols.find((symbol) => symbol.line === containingChild.declarationLine);
|
|
566
|
+
if (!childSymbol)
|
|
567
|
+
return undefined;
|
|
568
|
+
chain.push(childSymbol.symbolName);
|
|
569
|
+
currentBody = containingChild;
|
|
570
|
+
}
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
410
573
|
export function findClass(svc, input) {
|
|
411
574
|
const className = input.className.trim();
|
|
412
575
|
if (!className) {
|
|
@@ -500,8 +663,18 @@ export function findClass(svc, input) {
|
|
|
500
663
|
const enclosingQualifiedName = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
501
664
|
const enclosingSimpleName = enclosingQualifiedName.split(".").at(-1) ?? enclosingQualifiedName;
|
|
502
665
|
const nested = enclosingSimpleName !== row.symbolName;
|
|
666
|
+
let qualifiedName = nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName;
|
|
667
|
+
if (nested) {
|
|
668
|
+
// The formula above assumes exactly one level of nesting. When the type
|
|
669
|
+
// is nested two or more levels deep, reconstruct the real chain so the
|
|
670
|
+
// intermediate enclosing type(s) are not silently dropped.
|
|
671
|
+
const intermediateChain = resolveNestedTypeChain(svc, artifactId, row.filePath, enclosingSimpleName, row.line);
|
|
672
|
+
if (intermediateChain && intermediateChain.length > 0) {
|
|
673
|
+
qualifiedName = `${enclosingQualifiedName}.${[...intermediateChain, row.symbolName].join(".")}`;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
503
676
|
candidates.push({
|
|
504
|
-
qualifiedName
|
|
677
|
+
qualifiedName,
|
|
505
678
|
filePath: row.filePath,
|
|
506
679
|
line: row.line,
|
|
507
680
|
symbolKind: row.symbolKind,
|
|
@@ -554,8 +727,23 @@ function finishFindClass(svc, input) {
|
|
|
554
727
|
}
|
|
555
728
|
}).suggestedCall;
|
|
556
729
|
}
|
|
557
|
-
if (matches.length === 0
|
|
558
|
-
|
|
730
|
+
if (matches.length === 0) {
|
|
731
|
+
const hintInput = describeArtifactForNamespaceHint(artifact, className);
|
|
732
|
+
const unobfuscatedVersion = unobfuscatedMinecraftVersion({
|
|
733
|
+
version: hintInput.minecraftVersion,
|
|
734
|
+
nativeDependency: hintInput.nativeDependency
|
|
735
|
+
});
|
|
736
|
+
if (isObfuscatedNamespaceHintTrue(hintInput)) {
|
|
737
|
+
warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
|
|
738
|
+
}
|
|
739
|
+
else if (unobfuscatedVersion && isObfuscatedLabelMiss(hintInput)) {
|
|
740
|
+
const nearest = collectDidYouMeanCandidates(svc, artifactId, className)
|
|
741
|
+
.slice(0, 3)
|
|
742
|
+
.map((candidate) => `"${candidate.className}"`);
|
|
743
|
+
warnings.push(`No exact class symbol matched "${className}".` +
|
|
744
|
+
(nearest.length > 0 ? ` Did you mean ${nearest.join(", ")}?` : "") +
|
|
745
|
+
` ${unobfuscatedRuntimeNamesNote(unobfuscatedVersion)}`);
|
|
746
|
+
}
|
|
559
747
|
}
|
|
560
748
|
return {
|
|
561
749
|
matches,
|
|
@@ -702,6 +890,12 @@ export async function getClassSource(svc, input) {
|
|
|
702
890
|
if (!artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })) {
|
|
703
891
|
mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
|
|
704
892
|
}
|
|
893
|
+
// The same rule for the class-name lookup: its 26.1+ identity shortcut trusts only
|
|
894
|
+
// a Minecraft version, so a library's 26.x-shaped release number keeps the lookup
|
|
895
|
+
// (and its warnings) that any other library gets.
|
|
896
|
+
const minecraftVersion = artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })
|
|
897
|
+
? undefined
|
|
898
|
+
: version;
|
|
705
899
|
let activeArtifactId = artifactId;
|
|
706
900
|
let activeOrigin = origin;
|
|
707
901
|
let activeIsDecompiled = isDecompiled;
|
|
@@ -802,9 +996,10 @@ export async function getClassSource(svc, input) {
|
|
|
802
996
|
warnings.push(`Class "${className}" lives in nested jar "${match.entryName}" bundled by the shell jar; the lookup was redirected there automatically.`);
|
|
803
997
|
return true;
|
|
804
998
|
};
|
|
805
|
-
let activeLookupClassName = await svc
|
|
999
|
+
let activeLookupClassName = await resolveArtifactClassNameForLookup(svc, {
|
|
806
1000
|
className,
|
|
807
1001
|
version,
|
|
1002
|
+
minecraftVersion,
|
|
808
1003
|
sourceMapping: requestedMapping,
|
|
809
1004
|
targetMapping: activeMappingApplied,
|
|
810
1005
|
sourcePriority: input.sourcePriority,
|
|
@@ -817,9 +1012,10 @@ export async function getClassSource(svc, input) {
|
|
|
817
1012
|
filePath = resolveClassFilePath(svc, activeArtifactId, activeLookupClassName);
|
|
818
1013
|
}
|
|
819
1014
|
if (!filePath && (await tryBinaryFallback())) {
|
|
820
|
-
activeLookupClassName = await svc
|
|
1015
|
+
activeLookupClassName = await resolveArtifactClassNameForLookup(svc, {
|
|
821
1016
|
className,
|
|
822
1017
|
version,
|
|
1018
|
+
minecraftVersion,
|
|
823
1019
|
sourceMapping: requestedMapping,
|
|
824
1020
|
targetMapping: activeMappingApplied,
|
|
825
1021
|
sourcePriority: input.sourcePriority,
|
|
@@ -861,9 +1057,10 @@ export async function getClassSource(svc, input) {
|
|
|
861
1057
|
}
|
|
862
1058
|
}
|
|
863
1059
|
if (!row && (await tryBinaryFallback())) {
|
|
864
|
-
activeLookupClassName = await svc
|
|
1060
|
+
activeLookupClassName = await resolveArtifactClassNameForLookup(svc, {
|
|
865
1061
|
className,
|
|
866
1062
|
version,
|
|
1063
|
+
minecraftVersion,
|
|
867
1064
|
sourceMapping: requestedMapping,
|
|
868
1065
|
targetMapping: activeMappingApplied,
|
|
869
1066
|
sourcePriority: input.sourcePriority,
|
|
@@ -922,13 +1119,13 @@ export async function getClassSource(svc, input) {
|
|
|
922
1119
|
resolvedOutputFile = outputPath;
|
|
923
1120
|
sourceText = `[Written to ${outputPath}]`;
|
|
924
1121
|
}
|
|
925
|
-
const normalizedProvenance = activeProvenance ??
|
|
1122
|
+
const normalizedProvenance = withUnobfuscatedRuntimeFlag(activeProvenance ??
|
|
926
1123
|
buildFallbackProvenance(svc, {
|
|
927
1124
|
artifactId: activeArtifactId,
|
|
928
1125
|
origin: activeOrigin,
|
|
929
1126
|
requestedMapping,
|
|
930
1127
|
mappingApplied: activeMappingApplied
|
|
931
|
-
});
|
|
1128
|
+
}));
|
|
932
1129
|
const nextStartLine = snippet.nextStartLine;
|
|
933
1130
|
// Continuation guidance: when output was truncated and was not redirected to
|
|
934
1131
|
// a file, hand the caller the next line to read plus a replayable call that
|
|
@@ -1097,10 +1294,14 @@ export async function getClassMembers(svc, input) {
|
|
|
1097
1294
|
});
|
|
1098
1295
|
// Gated on the same predicate as the source path above, and for the same
|
|
1099
1296
|
// reason: a dependency's `version` is its own coordinate version, so the
|
|
1100
|
-
// unobfuscated-runtime relabel must not reach it.
|
|
1297
|
+
// unobfuscated-runtime relabel must not reach it - nor the 26.1+ identity
|
|
1298
|
+
// shortcut of the class and member lookups below.
|
|
1101
1299
|
if (!artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })) {
|
|
1102
1300
|
mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
|
|
1103
1301
|
}
|
|
1302
|
+
const minecraftVersion = artifactResolver.isDependencyLikeArtifact({ provenance, coordinate })
|
|
1303
|
+
? undefined
|
|
1304
|
+
: version;
|
|
1104
1305
|
if (requestedMapping !== "obfuscated" && !version) {
|
|
1105
1306
|
throw createError({
|
|
1106
1307
|
code: ERROR_CODES.MAPPING_NOT_APPLIED,
|
|
@@ -1167,9 +1368,10 @@ export async function getClassMembers(svc, input) {
|
|
|
1167
1368
|
}
|
|
1168
1369
|
});
|
|
1169
1370
|
}
|
|
1170
|
-
const lookupClassName = await svc
|
|
1371
|
+
const lookupClassName = await resolveArtifactClassNameForLookup(svc, {
|
|
1171
1372
|
className,
|
|
1172
1373
|
version,
|
|
1374
|
+
minecraftVersion,
|
|
1173
1375
|
sourceMapping: requestedMapping,
|
|
1174
1376
|
targetMapping: mappingApplied,
|
|
1175
1377
|
sourcePriority: input.sourcePriority,
|
|
@@ -1292,6 +1494,7 @@ export async function getClassMembers(svc, input) {
|
|
|
1292
1494
|
signatureFields,
|
|
1293
1495
|
signatureMethods,
|
|
1294
1496
|
version,
|
|
1497
|
+
minecraftVersion,
|
|
1295
1498
|
mappingApplied,
|
|
1296
1499
|
requestedMapping,
|
|
1297
1500
|
sourcePriority: input.sourcePriority,
|
|
@@ -1324,13 +1527,13 @@ export async function getClassMembers(svc, input) {
|
|
|
1324
1527
|
const projectedMembers = projectMembersByLevel(projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false), projection);
|
|
1325
1528
|
const truncated = sliced.truncated;
|
|
1326
1529
|
const nextCursor = sliced.nextOffset != null ? encodeOffsetCursor(sliced.nextOffset, memberCursorContext) : undefined;
|
|
1327
|
-
const baseProvenance = provenance ??
|
|
1530
|
+
const baseProvenance = withUnobfuscatedRuntimeFlag(provenance ??
|
|
1328
1531
|
buildFallbackProvenance(svc, {
|
|
1329
1532
|
artifactId,
|
|
1330
1533
|
origin,
|
|
1331
1534
|
requestedMapping,
|
|
1332
1535
|
mappingApplied
|
|
1333
|
-
});
|
|
1536
|
+
}));
|
|
1334
1537
|
const normalizedProvenance = nestedJarRedirect
|
|
1335
1538
|
? { ...baseProvenance, nestedJar: nestedJarRedirect }
|
|
1336
1539
|
: baseProvenance;
|
package/dist/source/indexer.js
CHANGED
|
@@ -415,8 +415,11 @@ export async function ingestIfNeeded(svc, resolved) {
|
|
|
415
415
|
if (resolved.artifactAlias && existing.alias !== resolved.artifactAlias) {
|
|
416
416
|
svc.artifactsRepo.setAlias(resolved.artifactId, resolved.artifactAlias);
|
|
417
417
|
}
|
|
418
|
-
svc.metrics.recordArtifactCacheHit();
|
|
419
418
|
const touchedAt = new Date().toISOString();
|
|
419
|
+
if (!existing.version) {
|
|
420
|
+
backfillResolvedVersion(svc, resolved, touchedAt);
|
|
421
|
+
}
|
|
422
|
+
svc.metrics.recordArtifactCacheHit();
|
|
420
423
|
svc.artifactsRepo.touchArtifact(resolved.artifactId, touchedAt);
|
|
421
424
|
touchCacheMetrics(svc, resolved.artifactId, touchedAt);
|
|
422
425
|
return;
|
|
@@ -444,6 +447,71 @@ export async function ingestIfNeeded(svc, resolved) {
|
|
|
444
447
|
}
|
|
445
448
|
}
|
|
446
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Writes the version a warm resolve derived onto a stored row that records none,
|
|
452
|
+
* with the two provenance fields that follow from it.
|
|
453
|
+
*
|
|
454
|
+
* The artifactId is a content hash, so a row indexed before its version could be
|
|
455
|
+
* derived - a Minecraft 26.1+ runtime jar indexed before the in-jar proof existed -
|
|
456
|
+
* is served warm by every later resolve and never rewritten. resolve-artifact then
|
|
457
|
+
* reports the version while the row lacks it, and every lookup by artifactId reads
|
|
458
|
+
* the row: get-class-members refuses mapping "mojang" for want of a version, and a
|
|
459
|
+
* miss still gets the legacy "use mapping=mojang" hint.
|
|
460
|
+
*
|
|
461
|
+
* Only gaps are filled. A stored version is never replaced, and the row keeps the
|
|
462
|
+
* label, chain and target it was indexed under. Only a version the fresh provenance
|
|
463
|
+
* records as resolved from is written: resolve-artifact records the version the
|
|
464
|
+
* target itself names (version target, coordinate or in-jar proof), which describes
|
|
465
|
+
* these bytes, while the binary fallback of get-class-source re-ingests with a
|
|
466
|
+
* caller version that may come from the project's gradle.properties, under the
|
|
467
|
+
* stored provenance, so it never qualifies. The row is re-read in the same
|
|
468
|
+
* synchronous turn as the write, so a concurrent writer since `existing` was read
|
|
469
|
+
* is not overwritten.
|
|
470
|
+
* `upsertArtifact` is the repo's only writer for these columns; on an existing row
|
|
471
|
+
* it updates in place and leaves `created_at` and the file index alone.
|
|
472
|
+
*/
|
|
473
|
+
function backfillResolvedVersion(svc, resolved, timestamp) {
|
|
474
|
+
const version = resolved.version;
|
|
475
|
+
if (!version || resolved.provenance?.resolvedFrom?.version !== version) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// Only a proven Minecraft 26.1+ runtime qualifies. A library version recorded by a
|
|
479
|
+
// dependency resolve must never land on a row whose stored provenance does not mark
|
|
480
|
+
// it as a dependency, or later lookups would read that version as Minecraft's.
|
|
481
|
+
if (resolved.provenance?.unobfuscatedRuntime !== true) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const current = svc.artifactsRepo.getArtifact(resolved.artifactId);
|
|
485
|
+
if (!current || current.version) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
svc.artifactsRepo.upsertArtifact({
|
|
489
|
+
artifactId: current.artifactId,
|
|
490
|
+
alias: current.alias,
|
|
491
|
+
origin: current.origin,
|
|
492
|
+
coordinate: current.coordinate,
|
|
493
|
+
version,
|
|
494
|
+
binaryJarPath: current.binaryJarPath,
|
|
495
|
+
sourceJarPath: current.sourceJarPath,
|
|
496
|
+
repoUrl: current.repoUrl,
|
|
497
|
+
requestedMapping: current.requestedMapping,
|
|
498
|
+
mappingApplied: current.mappingApplied,
|
|
499
|
+
provenance: current.provenance
|
|
500
|
+
? {
|
|
501
|
+
...current.provenance,
|
|
502
|
+
resolvedFrom: {
|
|
503
|
+
...current.provenance.resolvedFrom,
|
|
504
|
+
version: current.provenance.resolvedFrom?.version ?? version
|
|
505
|
+
},
|
|
506
|
+
unobfuscatedRuntime: true
|
|
507
|
+
}
|
|
508
|
+
: undefined,
|
|
509
|
+
qualityFlags: current.qualityFlags,
|
|
510
|
+
artifactSignature: current.artifactSignature,
|
|
511
|
+
isDecompiled: current.isDecompiled,
|
|
512
|
+
timestamp
|
|
513
|
+
});
|
|
514
|
+
}
|
|
447
515
|
async function rebuildMissingArtifactIndex(svc, resolved, reason) {
|
|
448
516
|
svc.metrics.recordArtifactCacheMiss();
|
|
449
517
|
svc.metrics.recordReindex();
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { SignatureMember } from "../../minecraft-explorer-service.js";
|
|
2
2
|
import type { SourceService } from "../../source-service.js";
|
|
3
3
|
import type { MappingSourcePriority, SourceMapping } from "../../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* On unobfuscated versions (26.1+) the as-shipped ("obfuscated") names are the Mojang
|
|
6
|
+
* names, so obfuscated<->mojang is the identity. The mapping graph is empty there, so
|
|
7
|
+
* asking it would only report every member as unmapped.
|
|
8
|
+
*
|
|
9
|
+
* `minecraftVersion` must be a proven Minecraft version. A dependency
|
|
10
|
+
* artifact's version is its own release number (`annotations:26.0.2`), so callers
|
|
11
|
+
* serving one pass undefined and keep the mapping lookup it always had.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isUnobfuscatedIdentityPair(minecraftVersion: string | undefined, sourceMapping: SourceMapping, targetMapping: SourceMapping): boolean;
|
|
4
14
|
export declare function normalizeMapping(mapping: SourceMapping | undefined): SourceMapping;
|
|
5
15
|
export declare function rejectLifecycleClassLikeInput(svc: SourceService, input: {
|
|
6
16
|
symbol: string;
|
|
@@ -16,7 +26,16 @@ export declare function resolveToObfuscatedMemberName(svc: SourceService, name:
|
|
|
16
26
|
name: string;
|
|
17
27
|
descriptor?: string;
|
|
18
28
|
}>;
|
|
19
|
-
export declare function remapSignatureMembers(svc: SourceService, members: SignatureMember[], kind: "field" | "method", version: string, sourceMapping: SourceMapping, targetMapping: SourceMapping, sourcePriority: MappingSourcePriority | undefined, warnings: string[], projectPath?: string, gradleUserHome?: string
|
|
29
|
+
export declare function remapSignatureMembers(svc: SourceService, members: SignatureMember[], kind: "field" | "method", version: string, sourceMapping: SourceMapping, targetMapping: SourceMapping, sourcePriority: MappingSourcePriority | undefined, warnings: string[], projectPath?: string, gradleUserHome?: string,
|
|
30
|
+
/**
|
|
31
|
+
* The Minecraft version the obfuscated<->mojang identity shortcut may trust.
|
|
32
|
+
* Defaults to `version`, which the lifecycle, validate-mixin and access-widener
|
|
33
|
+
* callers pass as a Minecraft version; get-class-members passes undefined for a
|
|
34
|
+
* dependency artifact, whose `version` is the library's own.
|
|
35
|
+
*/
|
|
36
|
+
identity?: {
|
|
37
|
+
minecraftVersion: string | undefined;
|
|
38
|
+
}): Promise<{
|
|
20
39
|
members: SignatureMember[];
|
|
21
40
|
failedNames: Set<string>;
|
|
22
41
|
}>;
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { buildSuggestedCall } from "../../build-suggested-call.js";
|
|
2
2
|
import { ERROR_CODES, createError } from "../../errors.js";
|
|
3
|
+
import { isUnobfuscatedVersion } from "../../version-service.js";
|
|
3
4
|
import { rebuildJavaSignature, remapJvmDescriptor } from "../descriptor-utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* On unobfuscated versions (26.1+) the as-shipped ("obfuscated") names are the Mojang
|
|
7
|
+
* names, so obfuscated<->mojang is the identity. The mapping graph is empty there, so
|
|
8
|
+
* asking it would only report every member as unmapped.
|
|
9
|
+
*
|
|
10
|
+
* `minecraftVersion` must be a proven Minecraft version. A dependency
|
|
11
|
+
* artifact's version is its own release number (`annotations:26.0.2`), so callers
|
|
12
|
+
* serving one pass undefined and keep the mapping lookup it always had.
|
|
13
|
+
*/
|
|
14
|
+
export function isUnobfuscatedIdentityPair(minecraftVersion, sourceMapping, targetMapping) {
|
|
15
|
+
const pair = new Set([sourceMapping, targetMapping]);
|
|
16
|
+
return (pair.size === 2 &&
|
|
17
|
+
pair.has("obfuscated") &&
|
|
18
|
+
pair.has("mojang") &&
|
|
19
|
+
minecraftVersion !== undefined &&
|
|
20
|
+
isUnobfuscatedVersion(minecraftVersion));
|
|
21
|
+
}
|
|
4
22
|
export function normalizeMapping(mapping) {
|
|
5
23
|
if (mapping == null) {
|
|
6
24
|
return "obfuscated";
|
|
@@ -87,7 +105,7 @@ export async function resolveToObfuscatedClassName(svc, className, version, mapp
|
|
|
87
105
|
});
|
|
88
106
|
}
|
|
89
107
|
export async function resolveToObfuscatedMemberName(svc, name, ownerInSourceMapping, descriptor, kind, version, mapping, sourcePriority, warnings, gradleUserHome) {
|
|
90
|
-
if (mapping === "obfuscated") {
|
|
108
|
+
if (mapping === "obfuscated" || isUnobfuscatedIdentityPair(version, mapping, "obfuscated")) {
|
|
91
109
|
return {
|
|
92
110
|
name,
|
|
93
111
|
descriptor: kind === "method" ? descriptor : undefined
|
|
@@ -167,9 +185,17 @@ export async function resolveToObfuscatedMemberName(svc, name, ownerInSourceMapp
|
|
|
167
185
|
descriptor: kind === "method" ? descriptor : undefined
|
|
168
186
|
};
|
|
169
187
|
}
|
|
170
|
-
export async function remapSignatureMembers(svc, members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings, projectPath, gradleUserHome
|
|
188
|
+
export async function remapSignatureMembers(svc, members, kind, version, sourceMapping, targetMapping, sourcePriority, warnings, projectPath, gradleUserHome,
|
|
189
|
+
/**
|
|
190
|
+
* The Minecraft version the obfuscated<->mojang identity shortcut may trust.
|
|
191
|
+
* Defaults to `version`, which the lifecycle, validate-mixin and access-widener
|
|
192
|
+
* callers pass as a Minecraft version; get-class-members passes undefined for a
|
|
193
|
+
* dependency artifact, whose `version` is the library's own.
|
|
194
|
+
*/
|
|
195
|
+
identity = { minecraftVersion: version }) {
|
|
171
196
|
const failedNames = new Set();
|
|
172
|
-
if (sourceMapping === targetMapping
|
|
197
|
+
if (sourceMapping === targetMapping ||
|
|
198
|
+
isUnobfuscatedIdentityPair(identity.minecraftVersion, sourceMapping, targetMapping)) {
|
|
173
199
|
return { members, failedNames };
|
|
174
200
|
}
|
|
175
201
|
const memberKeyToRemapped = new Map();
|
|
@@ -1,2 +1,27 @@
|
|
|
1
1
|
import type { CheckSymbolExistsInput, CheckSymbolExistsOutput, SourceService } from "../../source-service.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a runtime-bytecode existence check established.
|
|
4
|
+
*
|
|
5
|
+
* `verified` is true when the runtime jar answered: the class loaded and the member
|
|
6
|
+
* lookup ran to completion, or the class was confirmed missing (CLASS_NOT_FOUND). It
|
|
7
|
+
* is false when the check could not be performed - a short class name, a member query
|
|
8
|
+
* without an owner, or a class that failed to load for any other reason (unreadable
|
|
9
|
+
* jar, malformed class file) - and `result` is then the fallback base with the
|
|
10
|
+
* explanatory warning appended, so it still carries the caller's own status.
|
|
11
|
+
*/
|
|
12
|
+
export interface UnobfuscatedRuntimeCheck {
|
|
13
|
+
verified: boolean;
|
|
14
|
+
result: CheckSymbolExistsOutput;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* check-symbol-exists's runtime fallback: the result of `runUnobfuscatedRuntimeCheck`
|
|
18
|
+
* alone. Its callers keep the fallback base's status whenever the check could not be
|
|
19
|
+
* performed, so they need no `verified` flag.
|
|
20
|
+
*/
|
|
2
21
|
export declare function checkSymbolExistsInUnobfuscatedRuntime(svc: SourceService, input: CheckSymbolExistsInput, fallbackBase: CheckSymbolExistsOutput): Promise<CheckSymbolExistsOutput | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Checks a symbol against the Minecraft runtime jar of an unobfuscated version, whose
|
|
24
|
+
* names are the Mojang names. Undefined when the version or name is empty or the
|
|
25
|
+
* runtime jar cannot be resolved.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runUnobfuscatedRuntimeCheck(svc: SourceService, input: CheckSymbolExistsInput, fallbackBase: CheckSymbolExistsOutput): Promise<UnobfuscatedRuntimeCheck | undefined>;
|