Files
rea/src/domain/swiftSymbolClassification.ts
T
Kaoru0822 ffe4441522 fix(swift): match legacy runtime names by format, not a _Tt substring
analyze_swift_types treated any symbol containing `_Tt` as a Swift
observation, and get_objc_protocols treated any symbol containing `_TtP` as a
protocol, so Objective-C method labels and message-send shims for selectors
such as `_TtReset` or `_TtPing` were reported as Swift names or protocols.
Recognize an embedded legacy runtime name only when `_Tt` is followed by
nominal kind markers and an identifier length or the `s` module, owned by
the Swift classifier and reused by protocol discovery.

Fixes #5576
Fixes #5580

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:14:02 +00:00

108 lines
3.6 KiB
TypeScript

const LEGACY_CATEGORIES = [
["_TtC", "classes"],
["_TtV", "structs"],
["_TtO", "enums"],
["_TtP", "protocols"],
["_TtE", "extensions"],
] as const;
/**
* A legacy Swift runtime name (`_TtC5Hello3Foo`, `_TtPs5Error_`, `_TtGC…`)
* embedded in a symbol: `_Tt`, an optional generic `G`, nominal kinds, then a
* decimal identifier length or the standard-library module `s`. Objective-C
* selectors that merely contain `_Tt` (`_TtReset`, `_TtPing`) do not match.
*/
const LEGACY_RUNTIME_NAME = /_TtG?[CVOPE]+(?:[1-9]|s)/u;
const LEGACY_PROTOCOL_NAME = /_TtP(?:[1-9]|s)/u;
/** Recognize a symbol that embeds a legacy Swift runtime protocol name. */
export const containsLegacySwiftProtocolName = (name: string): boolean =>
LEGACY_PROTOCOL_NAME.test(name);
/** Supported procedure categories derived from Swift symbol encodings. */
export type SwiftTypeCategory = (typeof LEGACY_CATEGORIES)[number][1] | "other";
const NOMINAL_CATEGORIES: ReadonlyMap<string, SwiftTypeCategory> = new Map([
["C", "classes"],
["V", "structs"],
["O", "enums"],
["P", "protocols"],
]);
/**
* Recognize Swift manglings and decode literal module/nominal contexts.
* Substitutions, generic contexts, and other encodings remain unknown.
*/
export const classifySwiftSymbol = (
name: string,
): { readonly category: SwiftTypeCategory | null } | undefined => {
const prefix = /^_?(?:\$[sS]|_T0)/u.exec(name);
if (prefix === null) {
if (!LEGACY_RUNTIME_NAME.test(name)) return undefined;
return {
category:
LEGACY_CATEGORIES.find(([prefix]) => name.startsWith(prefix))?.[1] ??
null,
};
}
const mangling = name.slice(prefix[0].length);
let offset = identifierEnd(mangling, 0);
if (offset === undefined) return { category: null };
let category: SwiftTypeCategory | null = null;
for (;;) {
const end = identifierEnd(mangling, offset);
if (end === undefined) break;
const kind = mangling.charAt(end);
const nominal = NOMINAL_CATEGORIES.get(kind);
if (nominal === undefined) {
// 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;
offset = end + 1;
}
return { category };
};
/**
* Prefixes accepted by Swift's demangler as manglings. This is deliberately
* narrower than visible Swift runtime class names (`_Tt…`): those names are
* useful metadata symbols but are not themselves demangler manglings.
*/
const SWIFT_MANGLING_PREFIXES = [
"_T0",
"$S",
"_$S",
"$s",
"_$s",
"@__swiftmacro_",
"$e",
"_$e",
] as const;
/** Recognize names the Swift demangler accepts as mangled symbols. */
export const isSwiftMangledSymbolName = (name: string): boolean =>
SWIFT_MANGLING_PREFIXES.some((prefix) => name.startsWith(prefix));
const identifierEnd = (
mangling: string,
offset: number,
): number | undefined => {
const lengthToken = /^[1-9][0-9]*/u.exec(mangling.slice(offset))?.[0];
if (lengthToken === undefined) return undefined;
const start = offset + lengthToken.length;
const end = start + Number(lengthToken);
// Swift identifiers with substitutions/punycode need a fuller decoder.
if (
!Number.isSafeInteger(end) ||
end > mangling.length ||
!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(mangling.slice(start, end))
)
return undefined;
return end;
};