fix(native): distinguish symbol definitions from metadata

This commit is contained in:
morluto committed 2026-10-10 05:13:53 +08:00
1 parent 890bb66b35
commit 70bad53847
6 files changed
+81 -9

No files matched your search

+3 -1
View File
@@ -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({
@@ -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;
+5 -1
View File
@@ -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 `…<Nominal>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;
+34
View File
@@ -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" },
+22 -3
View File
@@ -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 {
@@ -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 () => {