diff --git a/src/native/parsers/demangle.test.ts b/src/native/parsers/demangle.test.ts index 5b15229bd..dd17e34aa 100644 --- a/src/native/parsers/demangle.test.ts +++ b/src/native/parsers/demangle.test.ts @@ -40,21 +40,23 @@ describe("parseDemangledSymbols", () => { ]); }); - it("identifies invalid Swift symbols that fail demangling", () => { - const inputs = ["$sInvalidMangle", "_$sAlsoInvalid"]; - const output = "$sInvalidMangle\n_$sAlsoInvalid\n"; - expect(parseDemangledSymbols(inputs, output)).toEqual([ - { - input: "$sInvalidMangle", - output: "$sInvalidMangle", - status: "invalid", - }, - { - input: "_$sAlsoInvalid", - output: "_$sAlsoInvalid", - status: "invalid", - }, - ]); + it("identifies invalid Swift symbols for every demangler mangling prefix", () => { + const inputs = [ + "_T0Bogus", + "$SBogus", + "_$SBogus", + "$sBogus", + "_$sBogus", + "@__swiftmacro_Bogus", + "$eBogus", + "_$eBogus", + "_Tprintf", + ]; + expect( + parseDemangledSymbols(inputs, `${inputs.join("\n")}\n`).map( + ({ status }) => status, + ), + ).toEqual([...Array(8).fill("invalid"), "unchanged"]); }); it("parses mixed batches preserving ordering", () => { diff --git a/src/native/parsers/demangle.ts b/src/native/parsers/demangle.ts index afcb30b62..6f1e42d61 100644 --- a/src/native/parsers/demangle.ts +++ b/src/native/parsers/demangle.ts @@ -8,6 +8,22 @@ const resultSchema = z.object({ export type DemangledSymbol = z.infer; +/** + * Prefixes the Swift demangler accepts as mangled names (Swift 4, 4.x, 5+, + * macro file names and Embedded Swift). Legacy `_T` is omitted because it is + * also an ordinary C symbol spelling such as `_Tprintf`. + */ +const SWIFT_MANGLING_PREFIXES = [ + "_T0", + "$S", + "_$S", + "$s", + "_$s", + "@__swiftmacro_", + "$e", + "_$e", +] as const; + /** * Preserve input ordering while parsing `swift-demangle --compact` lines. * The demangler ends each record with LF, so a carriage return belongs to the @@ -33,7 +49,7 @@ export const parseDemangledSymbols = ( output: value, status: value === input - ? input.startsWith("$s") || input.startsWith("_$s") + ? SWIFT_MANGLING_PREFIXES.some((prefix) => input.startsWith(prefix)) ? "invalid" : "unchanged" : "demangled", diff --git a/tests/boundary/mcp/nativeDemangleMcp.test.ts b/tests/boundary/mcp/nativeDemangleMcp.test.ts index 8e66a27fa..b6ad1d02c 100644 --- a/tests/boundary/mcp/nativeDemangleMcp.test.ts +++ b/tests/boundary/mcp/nativeDemangleMcp.test.ts @@ -68,5 +68,39 @@ describe.skipIf(process.platform !== "darwin")( ); }, ); + + mcpTest( + "reports undecodable symbols with any Swift mangling prefix as invalid", + async ({ mcp, onTestFinished }) => { + const target = join( + await createTestTempDirectory("rea-demangle-mcp-"), + "tool", + ); + await copyFile("/usr/bin/true", target); + const client = await openProductionTarget(mcp, onTestFinished, target); + const prefixes = ["_T0", "$S", "_$S", "$e", "_$e"]; + + const called = await client.callTool({ + name: "demangle_swift", + arguments: { + symbols: [ + ...prefixes.map((prefix) => `${prefix}4main3FooV`), + ...prefixes.map((prefix) => `${prefix}Bogus`), + "_Tprintf", + ], + }, + }); + expect(called.isError, JSON.stringify(called.content)).not.toBe(true); + expect( + demangledSchema + .parse(called.structuredContent) + .normalized_result.symbols.map(({ status }) => status), + ).toEqual([ + ...prefixes.map(() => "demangled"), + ...prefixes.map(() => "invalid"), + "unchanged", + ]); + }, + ); }, );