fix(native): share exact Objective-C class symbol interpretation

This commit is contained in:
morluto
2026-10-24 03:17:56 +08:00
parent f01f56156e
commit 782c6c6636
6 changed files with 89 additions and 21 deletions
+6
View File
@@ -506,6 +506,12 @@ complete observation.
comments do not change that handling. XML key names combine their text and
CDATA segments without trimming or expanding literal CDATA entities. Other
values and the original archive digest are preserved.
Symbol fallback accepts complete Objective-C class/metaclass symbol leaves,
including qualified names. Pointer/string labels that merely contain a class
marker do not establish classes. Provider namespace method labels are not
interpreted as Objective-C declarations; class definition versus import origin
remains unsupported coverage rather than an inferred classification.
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
compiled storyboard and nib resources into objects, outlets, actions,
connections and controller/class names. A full app graph is often several
@@ -214,3 +214,34 @@ describe("ObjC/Swift metadata", () => {
);
});
});
it("derives classes only from complete class symbol leaves", () => {
const result = inspectNativeDispatchMetadata(
[
"PTR__OBJC_CLASS_$_NSAlert_100028668",
"s__OBJC_CLASS_$_NSAlert_10002ed0d",
"s__OBJC_CLASS_$_NSAppleEventManage_10002ed23",
"_OBJC_CLASS_$_Local",
"/System/Library/Frameworks/AppKit::_OBJC_CLASS_$_NSAlert",
"_OBJC_METACLASS_$_Local",
"OwnController::viewDidLoad",
].map((name, index) => ({ address: `0x${String(index + 1)}`, name })),
);
expect(result.objc_classes.map(({ name }) => name)).toEqual([
"Local",
"NSAlert",
]);
expect(result.coverage).toContainEqual(
expect.objectContaining({
facet: "objc_class_definitions",
status: "unsupported",
reason: expect.stringContaining("namespace_declarations"),
}),
);
expect(result.coverage).toContainEqual(
expect.objectContaining({
facet: "objc_dispatch_implementations",
reason: "provider_name_inventory_has_no_supported_objc_method_symbols",
}),
);
});
+12 -5
View File
@@ -1,3 +1,4 @@
import { objcClassSymbol } from "../objcClassSymbol.js";
import { z } from "zod";
import {
BINARY_ARCHITECTURES,
@@ -500,10 +501,8 @@ export const inspectNativeDispatchMetadata = (
const parsedAddress = nativeAddressInputSchema.safeParse(entry.address);
const address = parsedAddress.success ? parsedAddress.data : null;
const sourceAddress = parsedAddress.success ? entry.address : null;
const classMatch = /(?:OBJC_CLASS_|OBJC_METACLASS_)\$_([^\s]+)/u.exec(
entry.name,
);
if (classMatch?.[1] !== undefined) classNames.add(classMatch[1]);
const classSymbol = objcClassSymbol(entry.name);
if (classSymbol !== undefined) classNames.add(classSymbol.name);
const methodMatch = /^([+-])\[([^\s\]]+)\s+([^\]]+)\]$/u.exec(entry.name);
if (methodMatch?.[1] !== undefined && methodMatch[2] && methodMatch[3]) {
@@ -605,10 +604,18 @@ export const inspectNativeDispatchMetadata = (
? "record_limit_reached_and_only_symbolized_methods_are_resolved"
: decodedImplementations.length > 0
? "only_symbolized_methods_are_resolved"
: "provider_name_inventory_has_no_objc_method_symbols",
: "provider_name_inventory_has_no_supported_objc_method_symbols",
examined: examinedNames.length,
decoded: decodedImplementations.length,
},
{
facet: "objc_class_definitions",
status: "unsupported",
reason:
"symbol_inventory_does_not_establish_class_definitions_import_origins_or_namespace_declarations",
examined: examinedNames.length,
decoded: 0,
},
{
facet: "objc_ivar_layouts",
status: "unsupported",
+20
View File
@@ -0,0 +1,20 @@
/** An Objective-C class symbol leaf, optionally qualified by a provider namespace. */
export const objcClassSymbol = (
name: string,
):
| {
readonly name: string;
readonly symbol: string;
readonly metaclass: boolean;
}
| undefined => {
const namespace = name.lastIndexOf("::");
const leaf = namespace < 0 ? name : name.slice(namespace + 2);
const match = /^_?(OBJC_(META)?CLASS_\$_([^\s]+))$/u.exec(leaf);
if (match?.[1] === undefined || match[3] === undefined) return undefined;
return {
name: match[3],
symbol: match[1],
metaclass: match[2] !== undefined,
};
};
@@ -224,3 +224,18 @@ it("keeps regular self-scope and weak rebinding records separate", () => {
],
});
});
it("excludes pointer and string labels that only contain a class marker", () => {
const result = discoverObjcClasses(
[
{ address: "0x1", name: "PTR__OBJC_CLASS_$_NSAlert_100028668" },
{ address: "0x2", name: "s__OBJC_CLASS_$_NSAlert_10002ed0d" },
{ address: "0x3", name: "AppKit::_OBJC_CLASS_$_NSAlert" },
],
"",
);
expect(result).toMatchObject({
count: 1,
classes: [{ address: "0x3", name: "AppKit::_OBJC_CLASS_$_NSAlert" }],
});
});
+5 -16
View File
@@ -1,3 +1,4 @@
import { objcClassSymbol } from "./objcClassSymbol.js";
import type { JsonValue } from "./jsonValue.js";
import type { AddressedName } from "./hopperValues.js";
import type {
@@ -10,9 +11,6 @@ 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;
@@ -44,7 +42,7 @@ export const selectObjcClassCandidates = (
names
.filter(
({ name, objc_class_label }) =>
name.includes(OBJC_CLASS_MARKER) ||
objcClassSymbol(name)?.metaclass === false ||
objc_class_label === "hopper_legacy_class_object" ||
(name.startsWith(LEGACY_OBJC_CLASS_PREFIX) &&
name.length > LEGACY_OBJC_CLASS_PREFIX.length &&
@@ -333,20 +331,11 @@ const coalesceBoundClassAliases = (
};
const classNameFromImport = (symbol: string): string | undefined => {
const canonical = symbol.startsWith("_") ? symbol.slice(1) : symbol;
return canonical.startsWith(OBJC_CLASS_MARKER) &&
canonical.length > OBJC_CLASS_MARKER.length
? canonical
: undefined;
const facts = objcClassSymbol(symbol);
return facts?.metaclass === false ? facts.symbol : undefined;
};
const classNameFromDisplayName = (name: string): string | undefined =>
name.startsWith(OBJC_CLASS_MARKER) && name.length > OBJC_CLASS_MARKER.length
? name
: name.startsWith(`_${OBJC_CLASS_MARKER}`) &&
name.length > OBJC_CLASS_MARKER.length + 1
? name.slice(1)
: undefined;
const classNameFromDisplayName = classNameFromImport;
const toObjcClassEntry = (entry: ObjcClassOutputEntry): JsonValue => ({
address: entry.address,