diff --git a/src/domain/functionComparison.test.ts b/src/domain/functionComparison.test.ts index ebc59a6ef2..226fbb7167 100644 --- a/src/domain/functionComparison.test.ts +++ b/src/domain/functionComparison.test.ts @@ -143,7 +143,7 @@ describe("function comparison normalized identity", () => { }, ); - it.each(["sub_deadbeef", "fcn.00401000"])( + it.each(["sub_deadbeef", "fcn.00401000", "FUN_00401000", "FUN_deadBEEF"])( "keeps %s as an address-derived name", (name) => { const generated = (base: "0x1000" | "0x2000") => @@ -169,6 +169,8 @@ describe("function comparison normalized identity", () => { it.each([ { name: "sub_deallocate", status: "unchanged" }, { name: "sub_deadbeef", status: "unknown" }, + { name: "FUN_00401000", status: "unknown" }, + { name: "FUN_initialize", status: "unchanged" }, ])("preserves the calls dimension for callee $name", ({ name, status }) => { const calling = (base: "0x1000" | "0x2000") => functionDossierSchema.parse({ diff --git a/src/domain/functionComparisonNormalization.ts b/src/domain/functionComparisonNormalization.ts index 5bf43a9726..a676e1e491 100644 --- a/src/domain/functionComparisonNormalization.ts +++ b/src/domain/functionComparisonNormalization.ts @@ -182,8 +182,9 @@ export const sorted = (values: readonly unknown[]): readonly unknown[] => .sort((left, right) => compareUnicodeCodePoints(left.json, right.json)) .map(({ value }) => value); +/** Recognize address-derived procedure names without excluding named FUN_ symbols. */ export const isAutoName = (name: string): boolean => - /^(?:sub_[0-9a-f]+|fcn\.[0-9a-f]+)$/iu.test(name); + /^(?:sub_[0-9a-f]+|fcn\.[0-9a-f]+|FUN_[0-9a-f]+)$/iu.test(name); const parseAddress = (value: string): bigint | null => /^0x[0-9a-f]+$/iu.test(value) ? BigInt(value) : null; diff --git a/src/domain/swiftSymbolClassification.ts b/src/domain/swiftSymbolClassification.ts index 4903a97bd4..58b9b0d261 100644 --- a/src/domain/swiftSymbolClassification.ts +++ b/src/domain/swiftSymbolClassification.ts @@ -42,7 +42,11 @@ export const classifySwiftSymbol = ( const kind = mangling.charAt(end); const nominal = NOMINAL_CATEGORIES.get(kind); if (nominal === undefined) { - if (kind === "E" && category !== null) category = "extensions"; + // Current swiftc emits protocol descriptors as `…Mp` without + // the nominal `P` marker (e.g. `_$s4main7ScoringMp`). Decode that exact + // suffix; unknown substitution encodings stay unclassified, never guessed. + if (mangling.slice(end) === "Mp") category = "protocols"; + else if (kind === "E" && category !== null) category = "extensions"; break; } category = nominal; diff --git a/src/domain/symbolAnalysis.test.ts b/src/domain/symbolAnalysis.test.ts index f663d4ec25..8ebb7b7626 100644 --- a/src/domain/symbolAnalysis.test.ts +++ b/src/domain/symbolAnalysis.test.ts @@ -12,6 +12,7 @@ describe("symbol analysis", () => { { address: "0x1", name: "_OBJC_CLASS_$_App" }, { address: "0x2", name: "_OBJC_CLASS_$_App" }, { address: "0x3", name: "_OBJC_PROTOCOL_$_Delegate" }, + { address: "0x4", name: "__OBJC_PROTOCOL_REFERENCE_$_Delegate" }, { address: "0x4", name: "_OBJC_PROTOCOL_$_Delegate" }, ]; expect(discoverObjcClasses(names, "App")).toMatchObject({ count: 1 }); @@ -36,6 +37,39 @@ describe("symbol analysis", () => { }); }); + it("recognizes fragile Apple Objective-C class records without alias duplication", () => { + // Apple clang -target i386-apple-macosx10.5 + // -fobjc-runtime=macosx-fragile-10.5 emits these local class records. + // The `.objc_class_name_*` absolute symbols alias the same class and are + // deliberately not selected. + const names = [ + { address: "0x1ac", name: "l_OBJC_CLASS_ProbeClass" }, + { address: "0x0", name: ".objc_class_name_ProbeClass" }, + // A class named `NAME_` produces this exact legacy local symbol. + { address: "0x0", name: "l_OBJC_CLASS_NAME_" }, + ]; + expect(discoverObjcClasses(names, "")).toEqual({ + count: 2, + classes: [ + { address: "0x1ac", name: "l_OBJC_CLASS_ProbeClass" }, + { address: "0x0", name: "l_OBJC_CLASS_NAME_" }, + ], + }); + }); + + it("discovers modern Swift protocol descriptors without treating methods as declarations", () => { + // Current swiftc emits `…Mp` descriptors without the nominal `P` marker. + const names = [ + { address: "0xf0", name: "_$s4main7ScoringMp" }, + { address: "0x100", name: "_$s4main7ScoringTL" }, + { address: "0x110", name: "_$s4main7ScoringP5scoreSiyFTq" }, + ]; + expect(discoverObjcProtocols(names)).toEqual({ + count: 1, + protocols: [{ address: "0xf0", name: "_$s4main7ScoringMp" }], + }); + }); + it("categorizes every Swift mangling family and deduplicates names", () => { const result = categorizeSwiftTypes([ { address: "1", name: "_TtCClass" }, diff --git a/src/domain/symbolAnalysis.ts b/src/domain/symbolAnalysis.ts index 9f2e899151..65e0fe8eb6 100644 --- a/src/domain/symbolAnalysis.ts +++ b/src/domain/symbolAnalysis.ts @@ -5,6 +5,18 @@ import { type SwiftTypeCategory, } from "./swiftSymbolClassification.js"; +/** Objective-C class definition markers emitted by modern Apple clang. */ +const OBJC_CLASS_MARKER = "OBJC_CLASS_$_" as const; + +/** Local class records emitted by Apple's fragile Objective-C ABI. */ +const LEGACY_OBJC_CLASS_PREFIX = "l_OBJC_CLASS_" as const; + +/** + * Objective-C protocol declaration marker. `PROTOCOL_REFERENCE` slots + * share the substring but are uses, not definitions. + */ +const OBJC_PROTOCOL_MARKER = "OBJC_PROTOCOL_$_" as const; + /** Select and deduplicate Objective-C class labels. */ export const discoverObjcClasses = ( names: readonly AddressedName[], @@ -12,8 +24,11 @@ export const discoverObjcClasses = ( ): JsonValue => { const classes = uniqueByName( names - .filter(({ name }) => - ["OBJC_CLASS", "OBJC_$_CLASS"].some((marker) => name.includes(marker)), + .filter( + ({ name }) => + name.includes(OBJC_CLASS_MARKER) || + (name.startsWith(LEGACY_OBJC_CLASS_PREFIX) && + name.length > LEGACY_OBJC_CLASS_PREFIX.length), ) .filter(({ name }) => pattern.length === 0 || name.includes(pattern)), ); @@ -29,7 +44,11 @@ export const discoverObjcProtocols = ( ): JsonValue => { const protocols = uniqueByName( names.filter( - ({ name }) => name.includes("OBJC_PROTOCOL") || name.includes("_TtP"), + ({ name }) => + name.includes(OBJC_PROTOCOL_MARKER) || + name.includes("_TtP") || + (name.endsWith("Mp") && + classifySwiftSymbol(name)?.category === "protocols"), ), ); return { diff --git a/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts b/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts index a8c9bf464c..661f336349 100644 --- a/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts +++ b/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts @@ -27,8 +27,17 @@ describe("enhanced MCP tools", () => { expect(name).toBe("list_names"); return Promise.resolve( ok([ - { address: "0x1", value: "_OBJC_CLASS_$_First" }, - { address: "0x2", value: "_OBJC_CLASS_$_Last" }, + // Apple clang modern Objective-C ABI `nm` output: the first row + // is a class definition; the remaining rows are method metadata, + // class_ro_t, and a class reference. + { address: "0x70", value: "_OBJC_CLASS_$_ProbeClass" }, + { address: "0x1a8", value: "__OBJC_$_CLASS_METHODS_ProbeClass" }, + { address: "0x230", value: "__OBJC_CLASS_RO_$_ProbeClass" }, + { address: "0x450", value: "_OBJC_CLASSLIST_REFERENCES_$_" }, + { + address: "0x298", + value: "__OBJC_$_CATEGORY_CLASS_METHODS_ProbeClass_$_Category", + }, ]), ); }, @@ -36,7 +45,10 @@ describe("enhanced MCP tools", () => { const result = jsonResult( await client.callTool({ name: "get_objc_classes", arguments: {} }), ); - expect(result).toMatchObject({ count: 2 }); + expect(result).toEqual({ + count: 1, + classes: [{ address: "0x70", name: "_OBJC_CLASS_$_ProbeClass" }], + }); }); it("returns the complete overview inline with exhaustive totals", async () => {