@adhisang/minecraft-modding-mcp 7.0.0 → 7.1.1
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 +72 -0
- package/README.md +4 -3
- package/dist/access-transformer-parser.d.ts +8 -0
- package/dist/access-transformer-parser.js +8 -1
- package/dist/access-widener-parser.d.ts +17 -0
- package/dist/access-widener-parser.js +12 -1
- package/dist/cache-registry.js +19 -6
- 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/handlers/versions.js +15 -9
- 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/manage-cache-service.js +10 -14
- package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
- package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
- package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
- package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/entry-tools/verify-mixin-target-service.js +19 -8
- package/dist/index.js +38 -15
- package/dist/java-process.d.ts +1 -0
- package/dist/java-process.js +14 -0
- package/dist/mapping/lookup.js +16 -1
- package/dist/mapping-service.d.ts +14 -0
- package/dist/mapping-service.js +35 -15
- package/dist/minecraft-explorer-service.js +70 -8
- package/dist/mixin/access-validators.js +38 -2
- package/dist/mixin/annotation-validators.js +137 -43
- package/dist/mixin/parsed-validator.js +21 -7
- package/dist/mixin-parser.d.ts +52 -0
- package/dist/mixin-parser.js +709 -130
- package/dist/mod-decompile-service.js +11 -1
- package/dist/mod-remap-service.js +6 -6
- package/dist/nbt/java-nbt-codec.js +7 -1
- package/dist/source/access-validate.js +10 -0
- package/dist/source/artifact-resolver.d.ts +27 -3
- package/dist/source/artifact-resolver.js +235 -30
- 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 +186 -27
- 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/nested-jars.d.ts +15 -1
- package/dist/source/nested-jars.js +14 -5
- package/dist/source/search.d.ts +10 -2
- package/dist/source/search.js +60 -13
- package/dist/source/symbol-resolver.js +88 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
- package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
- package/dist/source/validate-mixin.d.ts +5 -0
- package/dist/source/validate-mixin.js +136 -21
- package/dist/source/workspace-target.js +75 -7
- package/dist/source-jar-reader.d.ts +48 -1
- package/dist/source-jar-reader.js +93 -3
- package/dist/source-resolver.d.ts +7 -0
- package/dist/source-resolver.js +22 -14
- package/dist/source-service.d.ts +5 -0
- package/dist/source-service.js +7 -0
- package/dist/stdio-supervisor.d.ts +35 -1
- package/dist/stdio-supervisor.js +77 -2
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +181 -20
- package/dist/storage/files-repo.d.ts +7 -0
- package/dist/storage/files-repo.js +17 -4
- package/dist/storage/sqlite.d.ts +31 -1
- package/dist/storage/sqlite.js +125 -16
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +2 -1
- 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 +52 -3
- package/dist/workspace-context-cache.d.ts +25 -0
- package/dist/workspace-context-cache.js +52 -2
- package/dist/workspace-mapping-service.d.ts +8 -0
- package/dist/workspace-mapping-service.js +151 -21
- package/docs/README-ja.md +3 -1
- package/docs/tool-reference.md +69 -22
- package/package.json +1 -1
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { ERROR_CODES, isAppError } from "../../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* check-symbol-exists's runtime fallback: the result of `runUnobfuscatedRuntimeCheck`
|
|
4
|
+
* alone. Its callers keep the fallback base's status whenever the check could not be
|
|
5
|
+
* performed, so they need no `verified` flag.
|
|
6
|
+
*/
|
|
2
7
|
export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbackBase) {
|
|
8
|
+
return (await runUnobfuscatedRuntimeCheck(svc, input, fallbackBase))?.result;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks a symbol against the Minecraft runtime jar of an unobfuscated version, whose
|
|
12
|
+
* names are the Mojang names. Undefined when the version or name is empty or the
|
|
13
|
+
* runtime jar cannot be resolved.
|
|
14
|
+
*/
|
|
15
|
+
export async function runUnobfuscatedRuntimeCheck(svc, input, fallbackBase) {
|
|
3
16
|
const version = input.version.trim();
|
|
4
17
|
const name = input.name.trim();
|
|
5
18
|
const owner = input.owner?.trim();
|
|
@@ -8,11 +21,14 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
8
21
|
}
|
|
9
22
|
if (input.kind === "class" && input.nameMode !== "fqcn" && !name.includes(".")) {
|
|
10
23
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
...fallbackBase
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
verified: false,
|
|
25
|
+
result: {
|
|
26
|
+
...fallbackBase,
|
|
27
|
+
warnings: [
|
|
28
|
+
...fallbackBase.warnings,
|
|
29
|
+
`Version ${version} is unobfuscated, but short class name "${name}" could not be checked against runtime bytecode without a fully-qualified name.`
|
|
30
|
+
]
|
|
31
|
+
}
|
|
16
32
|
};
|
|
17
33
|
}
|
|
18
34
|
const querySymbol = input.kind === "class"
|
|
@@ -37,7 +53,7 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
37
53
|
};
|
|
38
54
|
const targetClass = input.kind === "class" ? name : owner;
|
|
39
55
|
if (!targetClass) {
|
|
40
|
-
return fallbackBase;
|
|
56
|
+
return { verified: false, result: fallbackBase };
|
|
41
57
|
}
|
|
42
58
|
let jarPath;
|
|
43
59
|
try {
|
|
@@ -61,16 +77,20 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
61
77
|
});
|
|
62
78
|
}
|
|
63
79
|
catch (error) {
|
|
80
|
+
// Only CLASS_NOT_FOUND is an answer; any other failure means the lookup never completed.
|
|
64
81
|
const classMissing = isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND;
|
|
65
82
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
verified: classMissing,
|
|
84
|
+
result: {
|
|
85
|
+
...fallbackBase,
|
|
86
|
+
querySymbol,
|
|
87
|
+
warnings: [
|
|
88
|
+
...fallbackBase.warnings,
|
|
89
|
+
classMissing
|
|
90
|
+
? `Class "${targetClass}" was not found in the Minecraft ${version} runtime jar; it does not exist (or is not in this jar).`
|
|
91
|
+
: `Version ${version} is unobfuscated; runtime bytecode lookup could not load class "${targetClass}".`
|
|
92
|
+
]
|
|
93
|
+
}
|
|
74
94
|
};
|
|
75
95
|
}
|
|
76
96
|
const warnings = [...fallbackBase.warnings, ...signature.warnings];
|
|
@@ -80,33 +100,40 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
80
100
|
...fallbackBase.mappingContext,
|
|
81
101
|
runtimeValidated: true
|
|
82
102
|
};
|
|
103
|
+
// From here on the class loaded, so every answer below is a completed lookup.
|
|
83
104
|
const buildResolved = (resolvedSymbol) => ({
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
verified: true,
|
|
106
|
+
result: {
|
|
107
|
+
...fallbackBase,
|
|
108
|
+
mappingContext: runtimeValidatedContext,
|
|
109
|
+
querySymbol,
|
|
110
|
+
resolved: true,
|
|
111
|
+
status: "resolved",
|
|
112
|
+
resolvedSymbol,
|
|
113
|
+
candidates: resolvedSymbol
|
|
114
|
+
? [{
|
|
115
|
+
...resolvedSymbol,
|
|
116
|
+
matchKind: "exact",
|
|
117
|
+
confidence: 1
|
|
118
|
+
}]
|
|
119
|
+
: [],
|
|
120
|
+
candidateCount: resolvedSymbol ? 1 : 0,
|
|
121
|
+
warnings
|
|
122
|
+
}
|
|
99
123
|
});
|
|
100
124
|
const buildUnresolved = (status) => ({
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
verified: true,
|
|
126
|
+
result: {
|
|
127
|
+
...fallbackBase,
|
|
128
|
+
mappingContext: runtimeValidatedContext,
|
|
129
|
+
querySymbol,
|
|
130
|
+
resolved: false,
|
|
131
|
+
status,
|
|
132
|
+
resolvedSymbol: undefined,
|
|
133
|
+
candidates: [],
|
|
134
|
+
candidateCount: 0,
|
|
135
|
+
warnings
|
|
136
|
+
}
|
|
110
137
|
});
|
|
111
138
|
if (input.kind === "class") {
|
|
112
139
|
return buildResolved({
|
|
@@ -127,7 +154,9 @@ export async function checkSymbolExistsInUnobfuscatedRuntime(svc, input, fallbac
|
|
|
127
154
|
symbol: `${owner}.${name}`
|
|
128
155
|
});
|
|
129
156
|
}
|
|
130
|
-
|
|
157
|
+
// The bytecode reader lists constructors apart from methods (and only the owner's
|
|
158
|
+
// own: constructors are not inherited), so "<init>" is answered from them.
|
|
159
|
+
const methodCandidates = (name === "<init>" ? signature.constructors : signature.methods).filter((method) => method.name === name);
|
|
131
160
|
const signatureMode = input.signatureMode ?? "name-only";
|
|
132
161
|
if (signatureMode === "name-only") {
|
|
133
162
|
// Existence semantics: any overload with this name means the method exists. Multiple
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rename } from "node:fs/promises";
|
|
1
2
|
/**
|
|
2
3
|
* A jar with at most this many own `.class` entries can qualify as a
|
|
3
4
|
* Jar-in-Jar shell. Real Fabric API umbrella jars carry zero to a handful of
|
|
@@ -23,6 +24,10 @@ export interface NestedClassMatch {
|
|
|
23
24
|
filePath: string;
|
|
24
25
|
line: number;
|
|
25
26
|
symbolKind: "class";
|
|
27
|
+
/** Set when the match is a type declared INSIDE another type. */
|
|
28
|
+
nested?: boolean;
|
|
29
|
+
/** The enclosing top-level type, present only on a nested match. */
|
|
30
|
+
enclosingClass?: string;
|
|
26
31
|
}
|
|
27
32
|
/**
|
|
28
33
|
* Finds exact class-name matches across a shell's nested bytecode inventory.
|
|
@@ -50,6 +55,15 @@ export declare function detectShellJarInventory(jarPath: string): Promise<string
|
|
|
50
55
|
* shell always maps to the same extracted file and re-resolution reuses it.
|
|
51
56
|
*/
|
|
52
57
|
export declare function nestedJarCachePath(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Seam for injecting the temp->final rename primitive so tests can simulate a
|
|
60
|
+
* non-race rename failure (e.g. EBUSY/EPERM/ENOSPC), which cannot be provoked
|
|
61
|
+
* for real against a plain filesystem under the allowed test commands.
|
|
62
|
+
* Defaults to the module-level `rename` from node:fs/promises.
|
|
63
|
+
*/
|
|
64
|
+
export interface ExtractNestedJarDeps {
|
|
65
|
+
rename?: typeof rename;
|
|
66
|
+
}
|
|
53
67
|
/**
|
|
54
68
|
* Extracts one nested jar to the content-addressed cache (no-op when already
|
|
55
69
|
* present). Entry-name safety is enforced by readJarEntryAsBuffer, and the
|
|
@@ -62,7 +76,7 @@ export declare function nestedJarCachePath(cacheDir: string, outerJarPath: strin
|
|
|
62
76
|
* one extraction with ERR_LIMIT_EXCEEDED — `loadNestedJarClassSet` swallows it
|
|
63
77
|
* and keeps resolving the shell's other nested jars.
|
|
64
78
|
*/
|
|
65
|
-
export declare function extractNestedJar(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string, maxEntryBytes?: number): Promise<string>;
|
|
79
|
+
export declare function extractNestedJar(cacheDir: string, outerJarPath: string, outerSignature: string, entryName: string, maxEntryBytes?: number, deps?: ExtractNestedJarDeps): Promise<string>;
|
|
66
80
|
/**
|
|
67
81
|
* Finds every nested jar of a shell that contains the class. Zero matches
|
|
68
82
|
* means the class genuinely is not bundled; more than one means the caller
|
|
@@ -98,11 +98,17 @@ function nestedClassMatch(entry) {
|
|
|
98
98
|
if (innerSegments.some((segment) => segment.length === 0 || /^\d/.test(segment))) {
|
|
99
99
|
return undefined;
|
|
100
100
|
}
|
|
101
|
+
// Mirror the indexed find-class branch's shape (class-source.ts): a `$`
|
|
102
|
+
// inner-class match reports `nested`/`enclosingClass` against its top-level
|
|
103
|
+
// enclosing type, exactly like an indexed lookup would.
|
|
104
|
+
const outerBinaryName = internalName.split("$")[0];
|
|
105
|
+
const isNested = innerSegments.length > 0;
|
|
101
106
|
return {
|
|
102
107
|
qualifiedName: internalName.replaceAll("/", ".").replaceAll("$", "."),
|
|
103
|
-
filePath: `${
|
|
108
|
+
filePath: `${outerBinaryName}.java`,
|
|
104
109
|
line: 1,
|
|
105
|
-
symbolKind: "class"
|
|
110
|
+
symbolKind: "class",
|
|
111
|
+
...(isNested ? { nested: true, enclosingClass: outerBinaryName.replaceAll("/", ".") } : {})
|
|
106
112
|
};
|
|
107
113
|
}
|
|
108
114
|
/**
|
|
@@ -206,7 +212,8 @@ export function nestedJarCachePath(cacheDir, outerJarPath, outerSignature, entry
|
|
|
206
212
|
* one extraction with ERR_LIMIT_EXCEEDED — `loadNestedJarClassSet` swallows it
|
|
207
213
|
* and keeps resolving the shell's other nested jars.
|
|
208
214
|
*/
|
|
209
|
-
export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, entryName, maxEntryBytes) {
|
|
215
|
+
export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, entryName, maxEntryBytes, deps = {}) {
|
|
216
|
+
const renameFn = deps.rename ?? rename;
|
|
210
217
|
const finalPath = nestedJarCachePath(cacheDir, outerJarPath, outerSignature, entryName);
|
|
211
218
|
try {
|
|
212
219
|
await access(finalPath);
|
|
@@ -243,16 +250,18 @@ export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, e
|
|
|
243
250
|
const tempPath = `${finalPath}.tmp.${process.pid}.${Date.now()}.${randomBytes(6).toString("hex")}`;
|
|
244
251
|
await writeFile(tempPath, bytes);
|
|
245
252
|
try {
|
|
246
|
-
await
|
|
253
|
+
await renameFn(tempPath, finalPath);
|
|
247
254
|
}
|
|
248
255
|
catch (renameError) {
|
|
249
256
|
// A concurrent extraction of the same entry may have won the rename.
|
|
250
257
|
// The content-addressed final file being present makes this call a
|
|
251
|
-
// success; anything else is a real failure.
|
|
258
|
+
// success; anything else is a real failure. Either way, best-effort
|
|
259
|
+
// remove the orphaned temp file before returning/rethrowing.
|
|
252
260
|
try {
|
|
253
261
|
await access(finalPath);
|
|
254
262
|
}
|
|
255
263
|
catch {
|
|
264
|
+
await rm(tempPath, { force: true }).catch(() => { });
|
|
256
265
|
throw renameError;
|
|
257
266
|
}
|
|
258
267
|
await rm(tempPath, { force: true });
|
package/dist/source/search.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export declare function canUseIndexedSearchPath(indexedSearchEnabled: boolean, i
|
|
|
22
22
|
export declare function buildGlobRegex(pattern: string): RegExp;
|
|
23
23
|
export declare function globToSqlLike(pattern: string): string;
|
|
24
24
|
export declare function checkPackagePrefix(filePath: string, packagePrefix?: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* A literal file_path prefix every path accepted by the scope starts with, or
|
|
27
|
+
* undefined when the scope implies none. The indexed candidate query matches it
|
|
28
|
+
* with an ASCII case-insensitive LIKE, a superset of checkPackagePrefix and the
|
|
29
|
+
* glob regex (indexed paths are stored with `/` separators), so both exact
|
|
30
|
+
* checks still run on every candidate.
|
|
31
|
+
*/
|
|
32
|
+
export declare function indexedScopePathPrefix(scope: SearchScope | undefined): string | undefined;
|
|
25
33
|
export declare function buildSearchCursorContext(input: {
|
|
26
34
|
artifactId: string;
|
|
27
35
|
query: string;
|
|
@@ -42,8 +50,8 @@ export declare function scorePathMatch(match: SearchMatch, index: number): numbe
|
|
|
42
50
|
export declare function matchRegexIndex(target: string, regex: RegExp): number;
|
|
43
51
|
export declare function searchClassSource(svc: SourceService, input: SearchClassSourceInput): Promise<SearchClassSourceOutput>;
|
|
44
52
|
export declare function searchSymbolIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
45
|
-
export declare function searchTextIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void):
|
|
46
|
-
export declare function searchPathIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void):
|
|
53
|
+
export declare function searchTextIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): boolean;
|
|
54
|
+
export declare function searchPathIntentIndexed(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, onHit: (hit: SearchSourceHit) => void): boolean;
|
|
47
55
|
export declare function searchTextIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void, onWarning?: (warning: string) => void): void;
|
|
48
56
|
export declare function searchPathIntent(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined, onHit: (hit: SearchSourceHit) => void): void;
|
|
49
57
|
export declare function findSymbolHits(svc: SourceService, artifactId: string, query: string, match: SearchMatch, scope: SearchScope | undefined, regexPattern: RegExp | undefined): IndexedSymbolHit[];
|
package/dist/source/search.js
CHANGED
|
@@ -49,7 +49,9 @@ export function canUseIndexedSearchPath(indexedSearchEnabled, intent, match, _sc
|
|
|
49
49
|
if (match === "regex") {
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
|
-
// packagePrefix and fileGlob
|
|
52
|
+
// packagePrefix and a fileGlob's literal directory prefix narrow the indexed
|
|
53
|
+
// candidate query (see indexedScopePathPrefix); both scope filters are also
|
|
54
|
+
// checked exactly on every candidate.
|
|
53
55
|
return true;
|
|
54
56
|
}
|
|
55
57
|
export function buildGlobRegex(pattern) {
|
|
@@ -108,12 +110,36 @@ export function globToSqlLike(pattern) {
|
|
|
108
110
|
}
|
|
109
111
|
return result;
|
|
110
112
|
}
|
|
113
|
+
function packagePrefixPath(packagePrefix) {
|
|
114
|
+
return `${packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "")}/`;
|
|
115
|
+
}
|
|
111
116
|
export function checkPackagePrefix(filePath, packagePrefix) {
|
|
112
117
|
if (!packagePrefix) {
|
|
113
118
|
return true;
|
|
114
119
|
}
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
return normalizePathStyle(filePath).startsWith(packagePrefixPath(packagePrefix));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A literal file_path prefix every path accepted by the scope starts with, or
|
|
124
|
+
* undefined when the scope implies none. The indexed candidate query matches it
|
|
125
|
+
* with an ASCII case-insensitive LIKE, a superset of checkPackagePrefix and the
|
|
126
|
+
* glob regex (indexed paths are stored with `/` separators), so both exact
|
|
127
|
+
* checks still run on every candidate.
|
|
128
|
+
*/
|
|
129
|
+
export function indexedScopePathPrefix(scope) {
|
|
130
|
+
const packagePath = scope?.packagePrefix ? packagePrefixPath(scope.packagePrefix) : undefined;
|
|
131
|
+
let globPath;
|
|
132
|
+
if (scope?.fileGlob) {
|
|
133
|
+
// buildGlobRegex treats everything before the first `*`/`?` as literal.
|
|
134
|
+
const glob = normalizePathStyle(scope.fileGlob);
|
|
135
|
+
const wildcardIndex = glob.search(/[*?]/);
|
|
136
|
+
globPath = (wildcardIndex < 0 ? glob : glob.slice(0, wildcardIndex)) || undefined;
|
|
137
|
+
}
|
|
138
|
+
if (packagePath && globPath) {
|
|
139
|
+
// Keep the longer when one extends the other; otherwise either is a superset.
|
|
140
|
+
return globPath.startsWith(packagePath) ? globPath : packagePath;
|
|
141
|
+
}
|
|
142
|
+
return packagePath ?? globPath;
|
|
117
143
|
}
|
|
118
144
|
export function buildSearchCursorContext(input) {
|
|
119
145
|
return JSON.stringify({
|
|
@@ -363,6 +389,7 @@ export async function searchClassSource(svc, input) {
|
|
|
363
389
|
searchWarnings.push(warning);
|
|
364
390
|
};
|
|
365
391
|
const tokenOnlyTextIntent = intent === "text" && queryMode === "token";
|
|
392
|
+
let indexedWindowFull = false;
|
|
366
393
|
if (intent === "symbol") {
|
|
367
394
|
searchSymbolIntent(svc, artifact.artifactId, query, match, scope, regexPattern, recordHit);
|
|
368
395
|
}
|
|
@@ -384,12 +411,9 @@ export async function searchClassSource(svc, input) {
|
|
|
384
411
|
}
|
|
385
412
|
else if (canUseIndexedSearchPath(indexedSearchEnabled, intent, match, scope)) {
|
|
386
413
|
try {
|
|
387
|
-
|
|
388
|
-
searchPathIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit)
|
|
389
|
-
|
|
390
|
-
else {
|
|
391
|
-
searchTextIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit);
|
|
392
|
-
}
|
|
414
|
+
indexedWindowFull = intent === "path"
|
|
415
|
+
? searchPathIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit)
|
|
416
|
+
: searchTextIntentIndexed(svc, artifact.artifactId, query, match, scope, recordHit);
|
|
393
417
|
svc.metrics.recordSearchIndexedHit();
|
|
394
418
|
}
|
|
395
419
|
catch (caughtError) {
|
|
@@ -424,6 +448,9 @@ export async function searchClassSource(svc, input) {
|
|
|
424
448
|
svc.metrics.recordSearchIntentDuration(intent, Date.now() - intentStartedAt);
|
|
425
449
|
const finalizedHits = accumulator.finalize();
|
|
426
450
|
const page = finalizedHits.page;
|
|
451
|
+
if (indexedWindowFull && page.length < limit) {
|
|
452
|
+
searchWarnings.push(indexedCandidateWindowWarning(intent, indexedCandidateLimitForMatch(svc, match)));
|
|
453
|
+
}
|
|
427
454
|
svc.metrics.recordSearchRowsReturned(page.length);
|
|
428
455
|
const nextCursor = finalizedHits.nextCursorHit
|
|
429
456
|
? encodeSearchCursor(finalizedHits.nextCursorHit, cursorContext)
|
|
@@ -468,17 +495,19 @@ export function searchSymbolIntent(svc, artifactId, query, match, scope, regexPa
|
|
|
468
495
|
}
|
|
469
496
|
export function searchTextIntentIndexed(svc, artifactId, query, match, scope, onHit) {
|
|
470
497
|
const candidateLimit = indexedCandidateLimitForMatch(svc, match);
|
|
498
|
+
const pathPrefix = indexedScopePathPrefix(scope);
|
|
471
499
|
const indexed = svc.filesRepo.searchFileCandidates(artifactId, {
|
|
472
500
|
query,
|
|
473
501
|
match,
|
|
474
502
|
limit: candidateLimit,
|
|
475
|
-
mode: "text"
|
|
503
|
+
mode: "text",
|
|
504
|
+
...(pathPrefix ? { pathPrefix } : {})
|
|
476
505
|
});
|
|
477
506
|
svc.metrics.recordSearchDbRoundtrip(indexed.dbRoundtrips);
|
|
478
507
|
svc.metrics.recordSearchRowsScanned(indexed.scannedRows);
|
|
479
508
|
if (indexed.items.length === 0) {
|
|
480
509
|
svc.metrics.recordSearchIndexedZeroShortcircuit();
|
|
481
|
-
return;
|
|
510
|
+
return false;
|
|
482
511
|
}
|
|
483
512
|
const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
484
513
|
const candidatePaths = indexed.items
|
|
@@ -508,19 +537,22 @@ export function searchTextIntentIndexed(svc, artifactId, query, match, scope, on
|
|
|
508
537
|
reasonCodes: ["content_match", `text_${match}`, "indexed"]
|
|
509
538
|
});
|
|
510
539
|
}
|
|
540
|
+
return isCandidateWindowFull(indexed);
|
|
511
541
|
}
|
|
512
542
|
export function searchPathIntentIndexed(svc, artifactId, query, match, scope, onHit) {
|
|
513
543
|
const candidateLimit = indexedCandidateLimitForMatch(svc, match);
|
|
544
|
+
const pathPrefix = indexedScopePathPrefix(scope);
|
|
514
545
|
const indexed = svc.filesRepo.searchFileCandidates(artifactId, {
|
|
515
546
|
query,
|
|
516
547
|
limit: candidateLimit,
|
|
517
|
-
mode: "path"
|
|
548
|
+
mode: "path",
|
|
549
|
+
...(pathPrefix ? { pathPrefix } : {})
|
|
518
550
|
});
|
|
519
551
|
svc.metrics.recordSearchDbRoundtrip(indexed.dbRoundtrips);
|
|
520
552
|
svc.metrics.recordSearchRowsScanned(indexed.scannedRows);
|
|
521
553
|
if (indexed.items.length === 0) {
|
|
522
554
|
svc.metrics.recordSearchIndexedZeroShortcircuit();
|
|
523
|
-
return;
|
|
555
|
+
return false;
|
|
524
556
|
}
|
|
525
557
|
const globFilter = scope?.fileGlob ? buildGlobRegex(normalizePathStyle(scope.fileGlob)) : undefined;
|
|
526
558
|
const candidateRows = [];
|
|
@@ -551,6 +583,21 @@ export function searchPathIntentIndexed(svc, artifactId, query, match, scope, on
|
|
|
551
583
|
reasonCodes: ["path_match", `path_${match}`, "indexed"]
|
|
552
584
|
});
|
|
553
585
|
}
|
|
586
|
+
return isCandidateWindowFull(indexed);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* True when the index held more candidates than the window it returned: the
|
|
590
|
+
* repository reports that with a continuation cursor, which the single-window
|
|
591
|
+
* indexed search never follows.
|
|
592
|
+
*/
|
|
593
|
+
function isCandidateWindowFull(indexed) {
|
|
594
|
+
return indexed.nextCursor !== undefined;
|
|
595
|
+
}
|
|
596
|
+
function indexedCandidateWindowWarning(intent, candidateLimit) {
|
|
597
|
+
const fullScan = intent === "text"
|
|
598
|
+
? 'queryMode="literal" for a full substring scan'
|
|
599
|
+
: 'match="regex" for a full path scan';
|
|
600
|
+
return `indexed ${intent} search examined only its candidate window (the first ${candidateLimit} index candidates) and more matched the index, so results may be incomplete. Use ${fullScan}, or narrow the scope with packagePrefix or a fileGlob that starts with a literal directory.`;
|
|
554
601
|
}
|
|
555
602
|
const ASCII_RE = /^[\x00-\x7F]*$/;
|
|
556
603
|
/** True when every code unit is ASCII, so SQLite LIKE folds case identically to JS. */
|
|
@@ -1,4 +1,83 @@
|
|
|
1
1
|
import { ERROR_CODES, createError } from "../errors.js";
|
|
2
|
+
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
3
|
+
import { runUnobfuscatedRuntimeCheck } from "./lifecycle/runtime-check.js";
|
|
4
|
+
/**
|
|
5
|
+
* Minecraft 26.1+ ships its runtime in Mojang names, so a Loom project for it has no
|
|
6
|
+
* `mappings` declaration: it compiles against the runtime names directly. Resolving a
|
|
7
|
+
* workspace symbol there is an identity lookup, checked against runtime bytecode the
|
|
8
|
+
* same way checkSymbolExists validates unobfuscated versions.
|
|
9
|
+
*/
|
|
10
|
+
async function resolveUnobfuscatedWorkspaceSymbol(svc, input, context) {
|
|
11
|
+
const { version, querySymbol, sourcePriorityApplied } = context;
|
|
12
|
+
const workspaceDetection = {
|
|
13
|
+
resolved: true,
|
|
14
|
+
mappingApplied: "mojang",
|
|
15
|
+
evidence: [],
|
|
16
|
+
warnings: [
|
|
17
|
+
`Minecraft ${version} is unobfuscated; no mappings declaration is needed — symbols are resolved against runtime (Mojang) names.`
|
|
18
|
+
]
|
|
19
|
+
};
|
|
20
|
+
// The requested/defaulted sourceMapping label is echoed unchanged: on 26.1+ an
|
|
21
|
+
// "obfuscated" label already means the as-shipped Mojang names, and rewriting it to
|
|
22
|
+
// "mojang" would break callers that compare namespace labels for equality.
|
|
23
|
+
const base = {
|
|
24
|
+
querySymbol,
|
|
25
|
+
mappingContext: {
|
|
26
|
+
version,
|
|
27
|
+
sourceMapping: input.sourceMapping,
|
|
28
|
+
targetMapping: "mojang",
|
|
29
|
+
sourcePriorityApplied,
|
|
30
|
+
unobfuscatedRuntime: true
|
|
31
|
+
},
|
|
32
|
+
resolved: false,
|
|
33
|
+
status: "not_found",
|
|
34
|
+
candidates: [],
|
|
35
|
+
candidateCount: 0,
|
|
36
|
+
warnings: [...workspaceDetection.warnings]
|
|
37
|
+
};
|
|
38
|
+
// The runtime names are both the "obfuscated" (as-shipped) and the Mojang names;
|
|
39
|
+
// intermediary and yarn names do not exist for these versions.
|
|
40
|
+
if (input.sourceMapping !== "obfuscated" && input.sourceMapping !== "mojang") {
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
status: "mapping_unavailable",
|
|
44
|
+
workspaceDetection,
|
|
45
|
+
warnings: [
|
|
46
|
+
...base.warnings,
|
|
47
|
+
`sourceMapping "${input.sourceMapping}" has no names on unobfuscated Minecraft ${version}; pass sourceMapping "mojang" (or "obfuscated") to resolve against runtime names.`
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const runtime = await runUnobfuscatedRuntimeCheck(svc, {
|
|
52
|
+
version,
|
|
53
|
+
kind: querySymbol.kind,
|
|
54
|
+
name: querySymbol.name,
|
|
55
|
+
owner: querySymbol.owner,
|
|
56
|
+
descriptor: querySymbol.descriptor,
|
|
57
|
+
sourceMapping: input.sourceMapping,
|
|
58
|
+
signatureMode: "exact"
|
|
59
|
+
}, base);
|
|
60
|
+
if (!runtime) {
|
|
61
|
+
// Mirrors checkSymbolExists: a symbol that could not be checked is not reported missing.
|
|
62
|
+
return {
|
|
63
|
+
...base,
|
|
64
|
+
status: "mapping_unavailable",
|
|
65
|
+
workspaceDetection,
|
|
66
|
+
warnings: [
|
|
67
|
+
...base.warnings,
|
|
68
|
+
`Minecraft ${version} runtime jar could not be resolved; symbol existence was not verified.`
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (!runtime.verified) {
|
|
73
|
+
// The jar was reached but did not answer (short class name, class that failed to
|
|
74
|
+
// load for a reason other than being absent): the result carries the reason, and
|
|
75
|
+
// an unanswered lookup is not a definitive miss. `not_found` stays reserved for a
|
|
76
|
+
// class confirmed missing or a member lookup that completed without a match.
|
|
77
|
+
return { ...runtime.result, status: "mapping_unavailable", workspaceDetection };
|
|
78
|
+
}
|
|
79
|
+
return { ...runtime.result, workspaceDetection };
|
|
80
|
+
}
|
|
2
81
|
export async function resolveWorkspaceSymbol(svc, input) {
|
|
3
82
|
const projectPath = input.projectPath?.trim();
|
|
4
83
|
const version = input.version?.trim();
|
|
@@ -77,6 +156,15 @@ export async function resolveWorkspaceSymbol(svc, input) {
|
|
|
77
156
|
const workspaceDetection = await svc.workspaceMappingService.detectCompileMapping({
|
|
78
157
|
projectPath
|
|
79
158
|
});
|
|
159
|
+
// No evidence also means no build script was found at all (a missing, mistyped or
|
|
160
|
+
// empty projectPath). Only a build that was read and declares no mappings is a
|
|
161
|
+
// 26.1+ Loom project compiling against the runtime names.
|
|
162
|
+
if (!workspaceDetection.resolved &&
|
|
163
|
+
workspaceDetection.evidence.length === 0 &&
|
|
164
|
+
isUnobfuscatedVersion(version) &&
|
|
165
|
+
(await svc.workspaceMappingService.hasReadableBuildScript(projectPath))) {
|
|
166
|
+
return resolveUnobfuscatedWorkspaceSymbol(svc, input, { version, querySymbol, sourcePriorityApplied });
|
|
167
|
+
}
|
|
80
168
|
const warnings = [...workspaceDetection.warnings];
|
|
81
169
|
if (!workspaceDetection.resolved || !workspaceDetection.mappingApplied) {
|
|
82
170
|
return {
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the mapping data required by `requestedMapping` is present.
|
|
4
|
+
*
|
|
5
|
+
* Mojang mappings are only on the resolution path for `"mojang"` requests.
|
|
6
|
+
* `"yarn"`/`"intermediary"` requests resolve through Tiny mappings instead
|
|
7
|
+
* and never touch Mojang mappings. `"obfuscated"` requests need neither —
|
|
8
|
+
* the jar itself (checked separately via `jarAvailable`) is sufficient.
|
|
9
|
+
*/
|
|
10
|
+
function requiredMappingHealthy(requestedMapping, health) {
|
|
11
|
+
switch (requestedMapping) {
|
|
12
|
+
case "mojang":
|
|
13
|
+
return health.mojangMappingsAvailable;
|
|
14
|
+
case "yarn":
|
|
15
|
+
case "intermediary":
|
|
16
|
+
return health.tinyMappingsAvailable;
|
|
17
|
+
case "obfuscated":
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
2
21
|
export async function runMappingHealthStage(svc, ctx) {
|
|
3
22
|
const mappingHealthStartedAt = await ctx.enterStage("mapping-health");
|
|
4
23
|
try {
|
|
@@ -15,7 +34,7 @@ export async function runMappingHealthStage(svc, ctx) {
|
|
|
15
34
|
mojangMappingsAvailable: health.mojangMappingsAvailable,
|
|
16
35
|
tinyMappingsAvailable: health.tinyMappingsAvailable,
|
|
17
36
|
memberRemapAvailable: health.memberRemapAvailable,
|
|
18
|
-
overallHealthy: jarAvailable &&
|
|
37
|
+
overallHealthy: jarAvailable && requiredMappingHealthy(ctx.requestedMapping, health),
|
|
19
38
|
degradations: [
|
|
20
39
|
...(jarAvailable ? [] : ["Game jar not found."]),
|
|
21
40
|
...health.degradations
|
|
@@ -101,10 +101,13 @@ export async function processSingleMixinTarget(svc, ctx, target, targetIndex) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
try {
|
|
104
|
+
// Mixin can target synthetic members (lambda bodies, bridges, $VALUES, this$0) and counts
|
|
105
|
+
// synthetic methods in quantifiers.
|
|
104
106
|
const sig = await svc.explorerService.getSignature({
|
|
105
107
|
fqn: obfuscatedName,
|
|
106
108
|
jarPath: ctx.jarPath,
|
|
107
|
-
access: "all"
|
|
109
|
+
access: "all",
|
|
110
|
+
includeSynthetic: true
|
|
108
111
|
});
|
|
109
112
|
ctx.warnings.push(...sig.warnings);
|
|
110
113
|
ctx.resolutionTrace?.push({ target: target.className, step: "signature", input: obfuscatedName, output: `${sig.methods.length} methods, ${sig.fields.length} fields`, success: true });
|
|
@@ -112,15 +115,21 @@ export async function processSingleMixinTarget(svc, ctx, target, targetIndex) {
|
|
|
112
115
|
let methods = sig.methods;
|
|
113
116
|
let fields = sig.fields;
|
|
114
117
|
if (ctx.requestedMapping !== ctx.signatureLookupMapping) {
|
|
118
|
+
const remap = (members, kind, warnings) => svc.remapSignatureMembers(members, kind, ctx.version, ctx.signatureLookupMapping, ctx.requestedMapping, ctx.currentSourcePriority, warnings, ctx.input.projectPath, ctx.input.gradleUserHome);
|
|
119
|
+
// Unnamed lambdas routinely have no mapping; a synthetic member that fails to remap
|
|
120
|
+
// stays listed under its source name without degrading the target or the result.
|
|
121
|
+
const syntheticWarnings = [];
|
|
115
122
|
try {
|
|
116
|
-
const [ctorResult, methodResult, fieldResult] = await Promise.all([
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
const [ctorResult, methodResult, fieldResult, syntheticMethodResult, syntheticFieldResult] = await Promise.all([
|
|
124
|
+
remap(sig.constructors, "method", ctx.warnings),
|
|
125
|
+
remap(sig.methods.filter((member) => !member.isSynthetic), "method", ctx.warnings),
|
|
126
|
+
remap(sig.fields.filter((member) => !member.isSynthetic), "field", ctx.warnings),
|
|
127
|
+
remap(sig.methods.filter((member) => member.isSynthetic), "method", syntheticWarnings),
|
|
128
|
+
remap(sig.fields.filter((member) => member.isSynthetic), "field", syntheticWarnings)
|
|
120
129
|
]);
|
|
121
130
|
constructors = ctorResult.members;
|
|
122
|
-
methods = methodResult.members;
|
|
123
|
-
fields = fieldResult.members;
|
|
131
|
+
methods = [...methodResult.members, ...syntheticMethodResult.members];
|
|
132
|
+
fields = [...fieldResult.members, ...syntheticFieldResult.members];
|
|
124
133
|
const targetFailed = new Set();
|
|
125
134
|
for (const n of ctorResult.failedNames)
|
|
126
135
|
targetFailed.add(n);
|
|
@@ -8,6 +8,11 @@ export type ValidateMixinSingleInput = Omit<ValidateMixinInput, "input" | "versi
|
|
|
8
8
|
version: string;
|
|
9
9
|
source?: string;
|
|
10
10
|
sourcePath?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Validate only the `@Mixin` class at this simple-name path from the source's top-level class
|
|
13
|
+
* down (`["Outer"]` for a config entry `Outer`, `["Outer", "Inner"]` for `Outer$Inner`).
|
|
14
|
+
*/
|
|
15
|
+
nestedClassPath?: string[];
|
|
11
16
|
batchCaches?: {
|
|
12
17
|
classMappings: Map<string, Promise<MappingFindMappingOutput>>;
|
|
13
18
|
};
|