fix(native): distinguish undecoded Swift spelling adjustments

Recognize known undecoded producer spellings without treating them as successful demangles. Preserve actual producer output and derive symbol types from the domain contract instead of duplicating its validation schema.

Compare retained output with the real demangler through MCP, while preserving CLI malformed-input and line-ending coverage.

Refs: #4215, #4287

Co-authored-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com>
This commit is contained in:
morlutoandVinícius DS Andrade committed 2026-10-16 08:26:19 +08:00
1 parent 2f6b9ecdc6
commit 808339f0b1
3 files changed
+89 -21

No files matched your search

+37
View File
@@ -59,6 +59,43 @@ describe("parseDemangledSymbols", () => {
).toEqual([...Array<string>(8).fill("invalid"), "unchanged"]);
});
it("keeps the demangler's adjusted spelling of an undecoded name out of demangled", () => {
const inputs = [
"__T0Bogus",
"__$sBogus",
"___stack_chk_fail",
"strlen",
"Symbol",
"entry",
"__T04main3FooV",
"s4main3FooV",
];
const output = [
"_T0Bogus",
"_$sBogus",
"__stack_chk_fail",
"$strlen",
"$Symbol",
"$entry",
"main.Foo",
"main.Foo",
].join("\n");
expect(parseDemangledSymbols(inputs, `${output}\n`)).toEqual([
{ input: "__T0Bogus", output: "_T0Bogus", status: "invalid" },
{ input: "__$sBogus", output: "_$sBogus", status: "invalid" },
{
input: "___stack_chk_fail",
output: "__stack_chk_fail",
status: "unchanged",
},
{ input: "strlen", output: "$strlen", status: "unchanged" },
{ input: "Symbol", output: "$Symbol", status: "unchanged" },
{ input: "entry", output: "$entry", status: "unchanged" },
{ input: "__T04main3FooV", output: "main.Foo", status: "demangled" },
{ input: "s4main3FooV", output: "main.Foo", status: "demangled" },
]);
});
it("parses mixed batches preserving ordering", () => {
const inputs = ["$s4main4testyyF", "_printf", "$sInvalidMangle"];
const output = "main.test() -> ()\n_printf\n$sInvalidMangle\n";
+23 -16
View File
@@ -1,14 +1,19 @@
import { z } from "zod";
import type { z } from "zod";
import { isSwiftMangledSymbolName } from "../../domain/swiftSymbolClassification.js";
import type { demangleSwiftSchema } from "../../domain/native/nativeInspection.js";
const resultSchema = z.object({
input: z.string(),
output: z.string(),
status: z.enum(["demangled", "unchanged", "invalid"]),
});
type DemangledSymbol = z.infer<typeof demangleSwiftSchema>["symbols"][number];
export type DemangledSymbol = z.infer<typeof resultSchema>;
/**
* swift-demangle removes one underscore from a name starting with `__` before
* demangling, and prefixes `$` to an argument starting with S, s or e. A name
* it cannot decode is printed in that adjusted spelling.
*/
const linkageName = (input: string): string =>
input.startsWith("__") ? input.slice(1) : input;
const undecodedSpelling = (input: string): string =>
/^[Sse]/u.test(input) ? `$${input}` : linkageName(input);
/**
* Preserve input ordering while parsing `swift-demangle --compact` lines.
@@ -26,19 +31,21 @@ export const parseDemangledSymbols = (
const lines = output.replace(/\n$/u, "").split("\n");
if (lines.length !== inputs.length)
throw new TypeError("swift-demangle output count does not match input");
return inputs.map((input, index) => {
return inputs.map<DemangledSymbol>((input, index) => {
const value = lines[index];
if (value === undefined)
throw new TypeError("swift-demangle omitted an output line");
return resultSchema.parse({
const decoded = value !== input && value !== undecodedSpelling(input);
return {
input,
// Status describes decoding; retain the producer's exact spelling even
// when it adjusted an identifier that it could not decode.
output: value,
status:
value === input
? isSwiftMangledSymbolName(input)
? "invalid"
: "unchanged"
: "demangled",
});
status: decoded
? "demangled"
: isSwiftMangledSymbolName(linkageName(input))
? "invalid"
: "unchanged",
};
});
};
+29 -5
View File
@@ -1,5 +1,7 @@
import { execFile } from "node:child_process";
import { copyFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { describe, expect } from "vitest";
@@ -11,6 +13,7 @@ import { mcpTest } from "../../support/mcp/mcpFixture.js";
import { openProductionTarget } from "../../support/mcp/productionSession.js";
const demangledSchema = evidenceResultOf(demangleSwiftSchema);
const execute = promisify(execFile);
describe.skipIf(process.platform !== "darwin")(
"native Swift demangling through MCP",
@@ -86,19 +89,40 @@ describe.skipIf(process.platform !== "darwin")(
...decodable.map((prefix) => `${prefix}4main3FooV`),
...prefixes.map((prefix) => `${prefix}Bogus`),
"_Tprintf",
// Current swift-demangle prints these undecoded names as
// _T0Bogus, __stack_chk_fail and $strlen.
"__T0Bogus",
"___stack_chk_fail",
"strlen",
],
},
});
expect(called.isError, JSON.stringify(called.content)).not.toBe(true);
expect(
demangledSchema
.parse(called.structuredContent)
.normalized_result.symbols.map(({ status }) => status),
).toEqual([
const result = demangledSchema.parse(
called.structuredContent,
).normalized_result;
const { symbols } = result;
expect(symbols.map(({ status }) => status)).toEqual([
...decodable.map(() => "demangled"),
...prefixes.map(() => "invalid"),
"unchanged",
"invalid",
"unchanged",
"unchanged",
]);
// Toolchain versions can differ in their undecoded spelling. Compare
// retained Evidence with the actual producer instead of rewriting it
// to match the input or requiring one toolchain's adjustment.
const command = result.provenance.find(
({ tool }) => tool === "swift-demangle",
)?.command;
const executable = command?.[0];
if (executable === undefined || command === undefined)
throw new Error("Missing demangler command provenance");
const { stdout } = await execute(executable, command.slice(1));
expect(symbols.map(({ output }) => output)).toEqual(
stdout.replace(/\n$/u, "").split("\n"),
);
},
);
},