From 808339f0b1cf3837445f392b5d5f1f362d64a27d Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:26:19 +0800 Subject: [PATCH] fix(native): distinguish undecoded Swift spelling adjustments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- src/native/parsers/demangle.test.ts | 37 +++++++++++++++++++ src/native/parsers/demangle.ts | 39 ++++++++++++-------- tests/boundary/mcp/nativeDemangleMcp.test.ts | 34 ++++++++++++++--- 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/native/parsers/demangle.test.ts b/src/native/parsers/demangle.test.ts index dd17e34aad..ab2e5b5a4e 100644 --- a/src/native/parsers/demangle.test.ts +++ b/src/native/parsers/demangle.test.ts @@ -59,6 +59,43 @@ describe("parseDemangledSymbols", () => { ).toEqual([...Array(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"; diff --git a/src/native/parsers/demangle.ts b/src/native/parsers/demangle.ts index 3c036d37b7..154490543a 100644 --- a/src/native/parsers/demangle.ts +++ b/src/native/parsers/demangle.ts @@ -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["symbols"][number]; -export type DemangledSymbol = z.infer; +/** + * 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((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", + }; }); }; diff --git a/tests/boundary/mcp/nativeDemangleMcp.test.ts b/tests/boundary/mcp/nativeDemangleMcp.test.ts index 3130d09bba..14c13e8d25 100644 --- a/tests/boundary/mcp/nativeDemangleMcp.test.ts +++ b/tests/boundary/mcp/nativeDemangleMcp.test.ts @@ -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"), + ); }, ); },