objcjs-types 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/objcjs-types.ts +31 -0
- package/dist/AVFoundation/functions.d.ts +21 -0
- package/dist/AVFoundation/functions.js +32 -0
- package/dist/AVFoundation/index.d.ts +1 -0
- package/dist/AVFoundation/index.js +1 -0
- package/dist/Accessibility/functions.d.ts +16 -0
- package/dist/Accessibility/functions.js +35 -0
- package/dist/Accessibility/index.d.ts +1 -0
- package/dist/Accessibility/index.js +1 -0
- package/dist/AddressBook/functions.d.ts +98 -0
- package/dist/AddressBook/functions.js +290 -0
- package/dist/AddressBook/index.d.ts +1 -0
- package/dist/AddressBook/index.js +1 -0
- package/dist/AppKit/functions.d.ts +112 -0
- package/dist/AppKit/functions.js +272 -0
- package/dist/AppKit/index.d.ts +1 -0
- package/dist/AppKit/index.js +1 -0
- package/dist/AudioToolbox/functions.d.ts +377 -0
- package/dist/AudioToolbox/functions.js +1124 -0
- package/dist/AudioToolbox/index.d.ts +1 -0
- package/dist/AudioToolbox/index.js +1 -0
- package/dist/AuthenticationServices/functions.d.ts +2 -0
- package/dist/AuthenticationServices/functions.js +5 -0
- package/dist/AuthenticationServices/index.d.ts +1 -0
- package/dist/AuthenticationServices/index.js +1 -0
- package/dist/BrowserEngineCore/functions.d.ts +3 -0
- package/dist/BrowserEngineCore/functions.js +11 -0
- package/dist/BrowserEngineCore/index.d.ts +1 -0
- package/dist/BrowserEngineCore/index.js +1 -0
- package/dist/CoreAudio/functions.d.ts +60 -0
- package/dist/CoreAudio/functions.js +173 -0
- package/dist/CoreAudio/index.d.ts +1 -0
- package/dist/CoreAudio/index.js +1 -0
- package/dist/CoreMIDI/functions.d.ts +96 -0
- package/dist/CoreMIDI/functions.js +287 -0
- package/dist/CoreMIDI/index.d.ts +1 -0
- package/dist/CoreMIDI/index.js +1 -0
- package/dist/CoreML/functions.d.ts +2 -0
- package/dist/CoreML/functions.js +5 -0
- package/dist/CoreML/index.d.ts +1 -0
- package/dist/CoreML/index.js +1 -0
- package/dist/CoreMediaIO/functions.d.ts +38 -0
- package/dist/CoreMediaIO/functions.js +107 -0
- package/dist/CoreMediaIO/index.d.ts +1 -0
- package/dist/CoreMediaIO/index.js +1 -0
- package/dist/CoreText/functions.d.ts +209 -0
- package/dist/CoreText/functions.js +611 -0
- package/dist/CoreText/index.d.ts +1 -0
- package/dist/CoreText/index.js +1 -0
- package/dist/CoreWLAN/functions.d.ts +23 -0
- package/dist/CoreWLAN/functions.js +56 -0
- package/dist/CoreWLAN/index.d.ts +1 -0
- package/dist/CoreWLAN/index.js +1 -0
- package/dist/DeviceDiscoveryExtension/functions.d.ts +11 -0
- package/dist/DeviceDiscoveryExtension/functions.js +17 -0
- package/dist/DeviceDiscoveryExtension/index.d.ts +1 -0
- package/dist/DeviceDiscoveryExtension/index.js +1 -0
- package/dist/DiscRecording/functions.d.ts +97 -0
- package/dist/DiscRecording/functions.js +290 -0
- package/dist/DiscRecording/index.d.ts +1 -0
- package/dist/DiscRecording/index.js +1 -0
- package/dist/DiscRecordingUI/functions.d.ts +13 -0
- package/dist/DiscRecordingUI/functions.js +38 -0
- package/dist/DiscRecordingUI/index.d.ts +1 -0
- package/dist/DiscRecordingUI/index.js +1 -0
- package/dist/ExceptionHandling/functions.d.ts +1 -0
- package/dist/ExceptionHandling/functions.js +5 -0
- package/dist/ExceptionHandling/index.d.ts +1 -0
- package/dist/ExceptionHandling/index.js +1 -0
- package/dist/FSKit/functions.d.ts +4 -0
- package/dist/FSKit/functions.js +11 -0
- package/dist/FSKit/index.d.ts +1 -0
- package/dist/FSKit/index.js +1 -0
- package/dist/Foundation/functions.d.ts +145 -0
- package/dist/Foundation/functions.js +386 -0
- package/dist/Foundation/index.d.ts +1 -0
- package/dist/Foundation/index.js +1 -0
- package/dist/GLKit/functions.d.ts +51 -0
- package/dist/GLKit/functions.js +146 -0
- package/dist/GLKit/index.d.ts +1 -0
- package/dist/GLKit/index.js +1 -0
- package/dist/GameController/functions.d.ts +18 -0
- package/dist/GameController/functions.js +44 -0
- package/dist/GameController/index.d.ts +1 -0
- package/dist/GameController/index.js +1 -0
- package/dist/HealthKit/functions.d.ts +19 -0
- package/dist/HealthKit/functions.js +35 -0
- package/dist/HealthKit/index.d.ts +1 -0
- package/dist/HealthKit/index.js +1 -0
- package/dist/IOSurface/functions.d.ts +53 -0
- package/dist/IOSurface/functions.js +155 -0
- package/dist/IOSurface/index.d.ts +1 -0
- package/dist/IOSurface/index.js +1 -0
- package/dist/IOUSBHost/functions.d.ts +44 -0
- package/dist/IOUSBHost/functions.js +131 -0
- package/dist/IOUSBHost/index.d.ts +1 -0
- package/dist/IOUSBHost/index.js +1 -0
- package/dist/InstantMessage/functions.d.ts +1 -0
- package/dist/InstantMessage/functions.js +5 -0
- package/dist/InstantMessage/index.d.ts +1 -0
- package/dist/InstantMessage/index.js +1 -0
- package/dist/JavaRuntimeSupport/functions.d.ts +40 -0
- package/dist/JavaRuntimeSupport/functions.js +113 -0
- package/dist/JavaRuntimeSupport/index.d.ts +1 -0
- package/dist/JavaRuntimeSupport/index.js +1 -0
- package/dist/JavaScriptCore/functions.d.ts +120 -0
- package/dist/JavaScriptCore/functions.js +359 -0
- package/dist/JavaScriptCore/index.d.ts +1 -0
- package/dist/JavaScriptCore/index.js +1 -0
- package/dist/MLCompute/functions.d.ts +27 -0
- package/dist/MLCompute/functions.js +41 -0
- package/dist/MLCompute/index.d.ts +1 -0
- package/dist/MLCompute/index.js +1 -0
- package/dist/MapKit/functions.d.ts +23 -0
- package/dist/MapKit/functions.js +56 -0
- package/dist/MapKit/index.d.ts +1 -0
- package/dist/MapKit/index.js +1 -0
- package/dist/Matter/functions.d.ts +17 -0
- package/dist/Matter/functions.js +26 -0
- package/dist/Matter/index.d.ts +1 -0
- package/dist/Matter/index.js +1 -0
- package/dist/MediaAccessibility/functions.d.ts +28 -0
- package/dist/MediaAccessibility/functions.js +83 -0
- package/dist/MediaAccessibility/index.d.ts +1 -0
- package/dist/MediaAccessibility/index.js +1 -0
- package/dist/MediaPlayer/functions.d.ts +3 -0
- package/dist/MediaPlayer/functions.js +11 -0
- package/dist/MediaPlayer/index.d.ts +1 -0
- package/dist/MediaPlayer/index.js +1 -0
- package/dist/Metal/functions.d.ts +14 -0
- package/dist/Metal/functions.js +26 -0
- package/dist/Metal/index.d.ts +1 -0
- package/dist/Metal/index.js +1 -0
- package/dist/MetalKit/functions.d.ts +11 -0
- package/dist/MetalKit/functions.js +20 -0
- package/dist/MetalKit/index.d.ts +1 -0
- package/dist/MetalKit/index.js +1 -0
- package/dist/MetalPerformanceShaders/functions.d.ts +7 -0
- package/dist/MetalPerformanceShaders/functions.js +14 -0
- package/dist/MetalPerformanceShaders/index.d.ts +1 -0
- package/dist/MetalPerformanceShaders/index.js +1 -0
- package/dist/NearbyInteraction/functions.d.ts +3 -0
- package/dist/NearbyInteraction/functions.js +5 -0
- package/dist/NearbyInteraction/index.d.ts +1 -0
- package/dist/NearbyInteraction/index.js +1 -0
- package/dist/ParavirtualizedGraphics/functions.d.ts +7 -0
- package/dist/ParavirtualizedGraphics/functions.js +14 -0
- package/dist/ParavirtualizedGraphics/index.d.ts +1 -0
- package/dist/ParavirtualizedGraphics/index.js +1 -0
- package/dist/QuartzCore/functions.d.ts +19 -0
- package/dist/QuartzCore/functions.js +50 -0
- package/dist/QuartzCore/index.d.ts +1 -0
- package/dist/QuartzCore/index.js +1 -0
- package/dist/SceneKit/functions.d.ts +17 -0
- package/dist/SceneKit/functions.js +38 -0
- package/dist/SceneKit/index.d.ts +1 -0
- package/dist/SceneKit/index.js +1 -0
- package/dist/SensorKit/functions.d.ts +4 -0
- package/dist/SensorKit/functions.js +14 -0
- package/dist/SensorKit/index.d.ts +1 -0
- package/dist/SensorKit/index.js +1 -0
- package/dist/ServiceManagement/functions.d.ts +7 -0
- package/dist/ServiceManagement/functions.js +20 -0
- package/dist/ServiceManagement/index.d.ts +1 -0
- package/dist/ServiceManagement/index.js +1 -0
- package/dist/StoreKit/functions.d.ts +1 -0
- package/dist/StoreKit/functions.js +5 -0
- package/dist/StoreKit/index.d.ts +1 -0
- package/dist/StoreKit/index.js +1 -0
- package/dist/VideoToolbox/functions.d.ts +81 -0
- package/dist/VideoToolbox/functions.js +236 -0
- package/dist/VideoToolbox/index.d.ts +1 -0
- package/dist/VideoToolbox/index.js +1 -0
- package/dist/Vision/functions.d.ts +16 -0
- package/dist/Vision/functions.js +38 -0
- package/dist/Vision/index.d.ts +1 -0
- package/dist/Vision/index.js +1 -0
- package/generator/ast-parser.ts +1368 -0
- package/generator/clang.ts +167 -0
- package/generator/custom.ts +936 -0
- package/generator/discover.ts +111 -0
- package/generator/emitter.ts +2026 -0
- package/generator/frameworks.ts +135 -0
- package/generator/index.ts +1334 -0
- package/generator/parse-worker.ts +263 -0
- package/generator/resolve-strings.ts +121 -0
- package/generator/struct-fields.ts +46 -0
- package/generator/templates/bind.ts +100 -0
- package/generator/templates/helpers.ts +70 -0
- package/generator/templates/nsdata.ts +97 -0
- package/generator/templates/osversion.ts +91 -0
- package/generator/type-mapper.ts +615 -0
- package/generator/worker-pool.ts +309 -0
- package/package.json +13 -4
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses clang AST JSON to extract Objective-C class/protocol declarations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClangASTNode } from "./clang.ts";
|
|
6
|
+
|
|
7
|
+
export interface ObjCMethod {
|
|
8
|
+
selector: string;
|
|
9
|
+
returnType: string;
|
|
10
|
+
parameters: { name: string; type: string }[];
|
|
11
|
+
isClassMethod: boolean;
|
|
12
|
+
isDeprecated: boolean;
|
|
13
|
+
deprecationMessage?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ObjCProperty {
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
readonly: boolean;
|
|
21
|
+
isClassProperty: boolean;
|
|
22
|
+
isDeprecated: boolean;
|
|
23
|
+
deprecationMessage?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ObjCClass {
|
|
28
|
+
name: string;
|
|
29
|
+
superclass: string | null;
|
|
30
|
+
protocols: string[];
|
|
31
|
+
instanceMethods: ObjCMethod[];
|
|
32
|
+
classMethods: ObjCMethod[];
|
|
33
|
+
properties: ObjCProperty[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ObjCProtocol {
|
|
37
|
+
name: string;
|
|
38
|
+
extendedProtocols: string[];
|
|
39
|
+
instanceMethods: ObjCMethod[];
|
|
40
|
+
classMethods: ObjCMethod[];
|
|
41
|
+
properties: ObjCProperty[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Struct types ---
|
|
45
|
+
|
|
46
|
+
export interface ObjCStructField {
|
|
47
|
+
/** The C field name from the struct definition */
|
|
48
|
+
name: string;
|
|
49
|
+
/** The C type string (e.g., "CGFloat", "CGPoint") */
|
|
50
|
+
type: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ObjCStruct {
|
|
54
|
+
/** The public typedef name (e.g., "CGPoint", "NSRange", "NSDecimal") */
|
|
55
|
+
name: string;
|
|
56
|
+
/** The internal struct name if different (e.g., "_NSRange" for NSRange) */
|
|
57
|
+
internalName?: string;
|
|
58
|
+
/** Fields from the struct definition */
|
|
59
|
+
fields: ObjCStructField[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A typedef alias from one struct name to another (e.g., NSPoint → CGPoint).
|
|
64
|
+
*/
|
|
65
|
+
export interface ObjCStructAlias {
|
|
66
|
+
/** The alias name (e.g., "NSPoint") */
|
|
67
|
+
name: string;
|
|
68
|
+
/** The target type name (e.g., "CGPoint") */
|
|
69
|
+
target: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Enum types ---
|
|
73
|
+
|
|
74
|
+
export interface ObjCEnumValue {
|
|
75
|
+
/** The constant name (e.g., "ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirementRequired") */
|
|
76
|
+
name: string;
|
|
77
|
+
/** The integer value as a string (from ConstantExpr.value), or null if implicit */
|
|
78
|
+
value: string | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ObjCIntegerEnum {
|
|
82
|
+
kind: "integer";
|
|
83
|
+
/** The enum type name (e.g., "ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirement") */
|
|
84
|
+
name: string;
|
|
85
|
+
/** The underlying integer type (e.g., "NSInteger", "NSUInteger") */
|
|
86
|
+
underlyingType: string;
|
|
87
|
+
/** Whether this is an NS_OPTIONS (bitfield) vs NS_ENUM */
|
|
88
|
+
isOptions: boolean;
|
|
89
|
+
/** Ordered list of enum constants with their values */
|
|
90
|
+
values: ObjCEnumValue[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ObjCStringEnumValue {
|
|
94
|
+
/** The full extern symbol name (e.g., "ASAuthorizationPublicKeyCredentialUserVerificationPreferencePreferred") */
|
|
95
|
+
symbolName: string;
|
|
96
|
+
/** Short key after stripping the enum name prefix (e.g., "Preferred") */
|
|
97
|
+
shortName: string;
|
|
98
|
+
/** The resolved string value from the framework binary (e.g., "preferred"), or null if unresolved */
|
|
99
|
+
value: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ObjCStringEnum {
|
|
103
|
+
kind: "string";
|
|
104
|
+
/** The enum type name (e.g., "ASAuthorizationPublicKeyCredentialUserVerificationPreference") */
|
|
105
|
+
name: string;
|
|
106
|
+
/** Extern NSString * constants with their resolved values */
|
|
107
|
+
values: ObjCStringEnumValue[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- C function types ---
|
|
111
|
+
|
|
112
|
+
export interface ObjCFunction {
|
|
113
|
+
/** The C function name (e.g., "NSHomeDirectory", "CGRectMake") */
|
|
114
|
+
name: string;
|
|
115
|
+
/** The return type qualType string (e.g., "NSString *", "void") */
|
|
116
|
+
returnType: string;
|
|
117
|
+
/** Ordered parameter list */
|
|
118
|
+
parameters: { name: string; type: string }[];
|
|
119
|
+
/** Whether the function accepts variadic arguments (e.g., NSLog) */
|
|
120
|
+
isVariadic: boolean;
|
|
121
|
+
/** The framework this function belongs to */
|
|
122
|
+
frameworkName: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Helpers for extracting doc comments and deprecation info ---
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Recursively extract text from a FullComment AST node.
|
|
129
|
+
* Joins all TextComment leaf nodes into a single string.
|
|
130
|
+
*/
|
|
131
|
+
function extractCommentText(node: ClangASTNode): string {
|
|
132
|
+
const texts: string[] = [];
|
|
133
|
+
|
|
134
|
+
function walk(n: ClangASTNode): void {
|
|
135
|
+
if (n.kind === "TextComment") {
|
|
136
|
+
const text = (n as any).text as string | undefined;
|
|
137
|
+
if (text) texts.push(text);
|
|
138
|
+
}
|
|
139
|
+
if (n.inner) {
|
|
140
|
+
for (const child of n.inner) {
|
|
141
|
+
walk(child);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
walk(node);
|
|
147
|
+
|
|
148
|
+
// Join and clean up whitespace
|
|
149
|
+
return texts
|
|
150
|
+
.map((t) => t.trim())
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.join(" ")
|
|
153
|
+
.replace(/\s+/g, " ")
|
|
154
|
+
.trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extract the description string from a declaration node's FullComment children.
|
|
159
|
+
*/
|
|
160
|
+
function extractDescription(node: ClangASTNode): string | undefined {
|
|
161
|
+
if (!node.inner) return undefined;
|
|
162
|
+
for (const child of node.inner) {
|
|
163
|
+
if (child.kind === "FullComment") {
|
|
164
|
+
const text = extractCommentText(child);
|
|
165
|
+
if (text) return text;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Propagate loc.file throughout the AST tree.
|
|
173
|
+
*
|
|
174
|
+
* Clang's JSON AST only emits `loc.file` when the file changes from the
|
|
175
|
+
* previous node. Subsequent nodes in the same file omit the field. This
|
|
176
|
+
* function walks the tree in order and fills in the missing `file` fields
|
|
177
|
+
* so every node has a resolvable file path. This is critical for batched
|
|
178
|
+
* mode where multiple headers are parsed at once and `scanForDeprecation`
|
|
179
|
+
* needs to look up the correct header's lines.
|
|
180
|
+
*
|
|
181
|
+
* IMPORTANT: In batched mode (multiple headers #included in a temp file),
|
|
182
|
+
* clang's file context bleeds across unrelated subtrees, so propagated
|
|
183
|
+
* values may be incorrect. However, nodes whose loc.file WAS explicitly
|
|
184
|
+
* set by clang are always correct. After propagation, callers should
|
|
185
|
+
* prefer extracting file info from child AvailabilityAttr nodes (whose
|
|
186
|
+
* range.begin.expansionLoc.file is always correct) over relying on
|
|
187
|
+
* propagated loc.file values.
|
|
188
|
+
*/
|
|
189
|
+
export function propagateLocFile(root: ClangASTNode): void {
|
|
190
|
+
let currentFile: string | undefined;
|
|
191
|
+
|
|
192
|
+
function walk(node: ClangASTNode): void {
|
|
193
|
+
const loc = node.loc as Record<string, any> | undefined;
|
|
194
|
+
if (loc) {
|
|
195
|
+
// Direct location
|
|
196
|
+
if (typeof loc.file === "string") {
|
|
197
|
+
currentFile = loc.file;
|
|
198
|
+
} else if (currentFile && typeof loc.line === "number") {
|
|
199
|
+
loc.file = currentFile;
|
|
200
|
+
}
|
|
201
|
+
// Expansion location (for macro-expanded nodes)
|
|
202
|
+
if (loc.expansionLoc) {
|
|
203
|
+
const exp = loc.expansionLoc as Record<string, any>;
|
|
204
|
+
if (typeof exp.file === "string") {
|
|
205
|
+
currentFile = exp.file;
|
|
206
|
+
} else if (currentFile && typeof exp.line === "number") {
|
|
207
|
+
exp.file = currentFile;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (node.inner) {
|
|
213
|
+
for (const child of node.inner) {
|
|
214
|
+
walk(child);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
walk(root);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Regex patterns that indicate a deprecated API in Objective-C header source. */
|
|
223
|
+
const DEPRECATION_PATTERNS = [
|
|
224
|
+
{ regex: /API_DEPRECATED\s*\(\s*"([^"]*)"/, group: 1 },
|
|
225
|
+
{ regex: /API_DEPRECATED_WITH_REPLACEMENT\s*\(\s*"([^"]*)"/, group: 1 },
|
|
226
|
+
{ regex: /NS_DEPRECATED_MAC\s*\(/, group: -1 },
|
|
227
|
+
{ regex: /NS_DEPRECATED\s*\(/, group: -1 },
|
|
228
|
+
{ regex: /DEPRECATED_ATTRIBUTE/, group: -1 },
|
|
229
|
+
{ regex: /__deprecated_msg\s*\(\s*"([^"]*)"/, group: 1 }
|
|
230
|
+
] as const;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get the line number from a ClangASTNode's loc field.
|
|
234
|
+
* Handles both direct locations and expansion locations.
|
|
235
|
+
*/
|
|
236
|
+
function getLocLine(node: ClangASTNode): number | undefined {
|
|
237
|
+
const loc = node.loc as Record<string, any> | undefined;
|
|
238
|
+
if (!loc) return undefined;
|
|
239
|
+
if (typeof loc.line === "number") return loc.line;
|
|
240
|
+
if (loc.expansionLoc && typeof loc.expansionLoc.line === "number") {
|
|
241
|
+
return loc.expansionLoc.line;
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get the source file path from a ClangASTNode's loc field.
|
|
248
|
+
* Handles both direct locations and expansion locations.
|
|
249
|
+
*
|
|
250
|
+
* In batched mode, loc.file may be missing or incorrectly propagated.
|
|
251
|
+
* As a fallback, checks child AvailabilityAttr nodes whose
|
|
252
|
+
* range.begin.expansionLoc.file reliably points to the correct header.
|
|
253
|
+
*/
|
|
254
|
+
function getLocFile(node: ClangASTNode): string | undefined {
|
|
255
|
+
const loc = node.loc as Record<string, any> | undefined;
|
|
256
|
+
if (loc) {
|
|
257
|
+
if (typeof loc.file === "string") return loc.file;
|
|
258
|
+
if (loc.expansionLoc && typeof loc.expansionLoc.file === "string") {
|
|
259
|
+
return loc.expansionLoc.file;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fallback: extract file from child AvailabilityAttr range
|
|
264
|
+
if (node.inner) {
|
|
265
|
+
for (const child of node.inner) {
|
|
266
|
+
if (child.kind === "AvailabilityAttr") {
|
|
267
|
+
const range = child.range as Record<string, any> | undefined;
|
|
268
|
+
if (range?.begin?.expansionLoc?.file) {
|
|
269
|
+
return range.begin.expansionLoc.file as string;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Resolve the header lines array for a given AST node.
|
|
280
|
+
* In single-header mode, returns headerLines directly.
|
|
281
|
+
* In batched mode, uses getLocFile() to look up the correct file's lines from the map.
|
|
282
|
+
* Falls back to contextFile if the node's own loc doesn't have file info.
|
|
283
|
+
*/
|
|
284
|
+
function resolveHeaderLines(
|
|
285
|
+
node: ClangASTNode,
|
|
286
|
+
headerLines: string[] | undefined,
|
|
287
|
+
headerLinesMap?: Map<string, string[]>,
|
|
288
|
+
contextFile?: string
|
|
289
|
+
): string[] | undefined {
|
|
290
|
+
if (headerLines) return headerLines;
|
|
291
|
+
if (headerLinesMap) {
|
|
292
|
+
const file = getLocFile(node) ?? contextFile;
|
|
293
|
+
if (file) return headerLinesMap.get(file);
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Scan the header source lines around a declaration to extract a doc comment.
|
|
300
|
+
* Looks backward from the declaration line for:
|
|
301
|
+
* - Block comments: `/** ... * /`, `/*! ... * /`, `/* ... * /`
|
|
302
|
+
* - Line comments: `/// ...`
|
|
303
|
+
* Also checks for inline `//` comments on the same line as the declaration.
|
|
304
|
+
*
|
|
305
|
+
* Returns the cleaned comment text, or undefined if no comment found.
|
|
306
|
+
*/
|
|
307
|
+
function scanForDocComment(
|
|
308
|
+
node: ClangASTNode,
|
|
309
|
+
headerLines: string[] | undefined,
|
|
310
|
+
headerLinesMap?: Map<string, string[]>,
|
|
311
|
+
contextFile?: string
|
|
312
|
+
): string | undefined {
|
|
313
|
+
const lines = resolveHeaderLines(node, headerLines, headerLinesMap, contextFile);
|
|
314
|
+
if (!lines) return undefined;
|
|
315
|
+
|
|
316
|
+
const line = getLocLine(node);
|
|
317
|
+
if (!line || line < 1) return undefined;
|
|
318
|
+
|
|
319
|
+
// --- Check for inline comment on the declaration line itself ---
|
|
320
|
+
// Handles patterns like: `+ (NSExpression *)foo:(id)bar; // Description`
|
|
321
|
+
// and enum values like: `NSConstantValueExpressionType = 0, // Description`
|
|
322
|
+
const declLine = lines[line - 1];
|
|
323
|
+
if (declLine) {
|
|
324
|
+
// Match // comment at end of line, after a ; or , or )
|
|
325
|
+
const inlineMatch = declLine.match(/[;,)]\s*\/\/\s*(.+?)\s*$/);
|
|
326
|
+
if (inlineMatch && inlineMatch[1]) {
|
|
327
|
+
const text = inlineMatch[1].trim();
|
|
328
|
+
if (text.length > 0) return text;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (line < 2) return undefined;
|
|
333
|
+
|
|
334
|
+
// Start scanning from the line just above the declaration (0-indexed: line - 2)
|
|
335
|
+
let scanEnd = line - 2; // inclusive, 0-indexed
|
|
336
|
+
|
|
337
|
+
// Skip blank lines and preprocessor directives between comment and declaration
|
|
338
|
+
while (scanEnd >= 0) {
|
|
339
|
+
const trimmed = lines[scanEnd]!.trim();
|
|
340
|
+
if (
|
|
341
|
+
trimmed === "" ||
|
|
342
|
+
trimmed.startsWith("#") ||
|
|
343
|
+
trimmed.startsWith("NS_HEADER_AUDIT") ||
|
|
344
|
+
trimmed.startsWith("API_AVAILABLE") ||
|
|
345
|
+
trimmed.startsWith("API_UNAVAILABLE") ||
|
|
346
|
+
trimmed.startsWith("NS_AVAILABLE") ||
|
|
347
|
+
trimmed.startsWith("NS_SWIFT_") ||
|
|
348
|
+
trimmed.startsWith("NS_REFINED_FOR_SWIFT") ||
|
|
349
|
+
trimmed.startsWith("NS_ASSUME_NONNULL") ||
|
|
350
|
+
trimmed.startsWith("__attribute__")
|
|
351
|
+
) {
|
|
352
|
+
scanEnd--;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
if (scanEnd < 0) return undefined;
|
|
358
|
+
|
|
359
|
+
const lastLine = lines[scanEnd]!;
|
|
360
|
+
|
|
361
|
+
// --- Case 1: Block comment ending with */
|
|
362
|
+
if (lastLine.trimEnd().endsWith("*/")) {
|
|
363
|
+
// Scan backward to find the opening /* or /** or /*!
|
|
364
|
+
let scanStart = scanEnd;
|
|
365
|
+
while (scanStart >= 0) {
|
|
366
|
+
const l = lines[scanStart]!;
|
|
367
|
+
if (/\/\*/.test(l)) break;
|
|
368
|
+
scanStart--;
|
|
369
|
+
}
|
|
370
|
+
if (scanStart < 0) return undefined;
|
|
371
|
+
|
|
372
|
+
const commentLines = lines.slice(scanStart, scanEnd + 1);
|
|
373
|
+
return cleanBlockComment(commentLines);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// --- Case 2: Line comments (///, //!, or plain // used as doc comments)
|
|
377
|
+
const trimmedLast = lastLine.trimStart();
|
|
378
|
+
if (trimmedLast.startsWith("//")) {
|
|
379
|
+
let scanStart = scanEnd;
|
|
380
|
+
while (scanStart > 0) {
|
|
381
|
+
const prev = lines[scanStart - 1]!.trimStart();
|
|
382
|
+
if (prev.startsWith("//") && !prev.startsWith("//#")) {
|
|
383
|
+
scanStart--;
|
|
384
|
+
} else {
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const commentLines = lines.slice(scanStart, scanEnd + 1);
|
|
390
|
+
return cleanLineComment(commentLines);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Clean a block comment (/* ... * /) into plain text.
|
|
398
|
+
* Strips the comment delimiters and leading * characters.
|
|
399
|
+
*/
|
|
400
|
+
function cleanBlockComment(lines: string[]): string | undefined {
|
|
401
|
+
const cleaned: string[] = [];
|
|
402
|
+
for (let i = 0; i < lines.length; i++) {
|
|
403
|
+
let line = lines[i]!;
|
|
404
|
+
// Remove opening delimiter
|
|
405
|
+
if (i === 0) {
|
|
406
|
+
line = line.replace(/\/\*[*!]?\s?/, "");
|
|
407
|
+
}
|
|
408
|
+
// Remove closing delimiter
|
|
409
|
+
if (i === lines.length - 1) {
|
|
410
|
+
line = line.replace(/\s*\*\/\s*$/, "");
|
|
411
|
+
}
|
|
412
|
+
// Remove leading whitespace + optional * + optional space
|
|
413
|
+
line = line.replace(/^\s*\*?\s?/, "");
|
|
414
|
+
cleaned.push(line);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const text = cleaned
|
|
418
|
+
.map((l) => l.trimEnd())
|
|
419
|
+
.join(" ")
|
|
420
|
+
.replace(/\s+/g, " ")
|
|
421
|
+
.trim();
|
|
422
|
+
|
|
423
|
+
return text || undefined;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Clean line comments (///, //!, or plain //) into plain text.
|
|
428
|
+
* Strips the leading comment prefix and trims.
|
|
429
|
+
*/
|
|
430
|
+
function cleanLineComment(lines: string[]): string | undefined {
|
|
431
|
+
const cleaned = lines.map((l) => {
|
|
432
|
+
const trimmed = l.trimStart();
|
|
433
|
+
// Strip ///, //!, or plain // prefix and optional single space
|
|
434
|
+
return trimmed.replace(/^\/\/[\/!]?\s?/, "");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const text = cleaned
|
|
438
|
+
.map((l) => l.trimEnd())
|
|
439
|
+
.join(" ")
|
|
440
|
+
.replace(/\s+/g, " ")
|
|
441
|
+
.trim();
|
|
442
|
+
|
|
443
|
+
return text || undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Scan the header source lines around a declaration's location for
|
|
448
|
+
* deprecation macros. Returns { isDeprecated, message } if found.
|
|
449
|
+
*
|
|
450
|
+
* Supports two modes:
|
|
451
|
+
* - Single header: `headerLines` is an array of lines from one file
|
|
452
|
+
* - Batched: `headerLinesMap` maps file paths to their lines
|
|
453
|
+
*/
|
|
454
|
+
function scanForDeprecation(
|
|
455
|
+
node: ClangASTNode,
|
|
456
|
+
headerLines: string[] | undefined,
|
|
457
|
+
headerLinesMap?: Map<string, string[]>,
|
|
458
|
+
contextFile?: string
|
|
459
|
+
): { isDeprecated: boolean; message?: string } {
|
|
460
|
+
const lines = resolveHeaderLines(node, headerLines, headerLinesMap, contextFile);
|
|
461
|
+
if (!lines) return { isDeprecated: false };
|
|
462
|
+
|
|
463
|
+
const line = getLocLine(node);
|
|
464
|
+
if (!line || line < 1 || line > lines.length) {
|
|
465
|
+
return { isDeprecated: false };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Scan the declaration line and a few lines after (macros can wrap to next lines)
|
|
469
|
+
const startLine = Math.max(0, line - 1);
|
|
470
|
+
const endLine = Math.min(lines.length, line + 5);
|
|
471
|
+
const sourceChunk = lines.slice(startLine, endLine).join(" ");
|
|
472
|
+
|
|
473
|
+
for (const pattern of DEPRECATION_PATTERNS) {
|
|
474
|
+
const match = pattern.regex.exec(sourceChunk);
|
|
475
|
+
if (match) {
|
|
476
|
+
const message = pattern.group >= 0 ? match[pattern.group]?.trim() : undefined;
|
|
477
|
+
return {
|
|
478
|
+
isDeprecated: true,
|
|
479
|
+
message: message || undefined
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { isDeprecated: false };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Parse a clang AST root node and extract all ObjC class declarations.
|
|
489
|
+
* Merges categories into their base class.
|
|
490
|
+
*
|
|
491
|
+
* @param headerLines - Split lines of the header file for deprecation scanning.
|
|
492
|
+
* Pass undefined to skip header-based deprecation detection.
|
|
493
|
+
* @param headerLinesMap - Map of file path → lines for batched mode deprecation scanning.
|
|
494
|
+
*/
|
|
495
|
+
export function parseAST(
|
|
496
|
+
root: ClangASTNode,
|
|
497
|
+
targetClasses: Set<string>,
|
|
498
|
+
headerLines?: string[],
|
|
499
|
+
headerLinesMap?: Map<string, string[]>
|
|
500
|
+
): Map<string, ObjCClass> {
|
|
501
|
+
const classes = new Map<string, ObjCClass>();
|
|
502
|
+
|
|
503
|
+
function getOrCreateClass(name: string): ObjCClass {
|
|
504
|
+
let cls = classes.get(name);
|
|
505
|
+
if (!cls) {
|
|
506
|
+
cls = {
|
|
507
|
+
name,
|
|
508
|
+
superclass: null,
|
|
509
|
+
protocols: [],
|
|
510
|
+
instanceMethods: [],
|
|
511
|
+
classMethods: [],
|
|
512
|
+
properties: []
|
|
513
|
+
};
|
|
514
|
+
classes.set(name, cls);
|
|
515
|
+
}
|
|
516
|
+
return cls;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function isDeprecated(node: ClangASTNode): boolean {
|
|
520
|
+
if (!node.inner) return false;
|
|
521
|
+
return node.inner.some((child) => child.kind === "DeprecatedAttr");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function isUnavailable(node: ClangASTNode): boolean {
|
|
525
|
+
if (!node.inner) return false;
|
|
526
|
+
return node.inner.some((child) => child.kind === "UnavailableAttr");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function extractMethod(node: ClangASTNode, contextFile?: string): ObjCMethod | null {
|
|
530
|
+
if (node.kind !== "ObjCMethodDecl") return null;
|
|
531
|
+
if (node.isImplicit) return null;
|
|
532
|
+
if (isUnavailable(node)) return null;
|
|
533
|
+
|
|
534
|
+
const selector = node.name ?? "";
|
|
535
|
+
const returnType = node.returnType?.qualType ?? "void";
|
|
536
|
+
const isClassMethod = node.instance === false;
|
|
537
|
+
const attrDeprecated = isDeprecated(node);
|
|
538
|
+
const sourceDeprecation = scanForDeprecation(node, headerLines, headerLinesMap, contextFile);
|
|
539
|
+
const deprecated = attrDeprecated || sourceDeprecation.isDeprecated;
|
|
540
|
+
const description = extractDescription(node) ?? scanForDocComment(node, headerLines, headerLinesMap, contextFile);
|
|
541
|
+
|
|
542
|
+
const parameters: { name: string; type: string }[] = [];
|
|
543
|
+
if (node.inner) {
|
|
544
|
+
for (const child of node.inner) {
|
|
545
|
+
if (child.kind === "ParmVarDecl") {
|
|
546
|
+
parameters.push({
|
|
547
|
+
name: child.name ?? "arg",
|
|
548
|
+
type: child.type?.qualType ?? "id"
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
selector,
|
|
556
|
+
returnType,
|
|
557
|
+
parameters,
|
|
558
|
+
isClassMethod,
|
|
559
|
+
isDeprecated: deprecated,
|
|
560
|
+
deprecationMessage: sourceDeprecation.message,
|
|
561
|
+
description
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function extractProperty(node: ClangASTNode, contextFile?: string): ObjCProperty | null {
|
|
566
|
+
if (node.kind !== "ObjCPropertyDecl") return null;
|
|
567
|
+
if (node.isImplicit) return null;
|
|
568
|
+
|
|
569
|
+
const name = node.name ?? "";
|
|
570
|
+
const type = node.type?.qualType ?? "id";
|
|
571
|
+
// ObjC properties are readwrite by default; only explicitly readonly ones are read-only
|
|
572
|
+
const readonly = node.readonly === true;
|
|
573
|
+
// Class properties have "class": true in the AST node
|
|
574
|
+
const isClassProperty = (node as any)["class"] === true;
|
|
575
|
+
const attrDeprecated = isDeprecated(node);
|
|
576
|
+
const sourceDeprecation = scanForDeprecation(node, headerLines, headerLinesMap, contextFile);
|
|
577
|
+
const deprecated = attrDeprecated || sourceDeprecation.isDeprecated;
|
|
578
|
+
const description = extractDescription(node) ?? scanForDocComment(node, headerLines, headerLinesMap, contextFile);
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
name,
|
|
582
|
+
type,
|
|
583
|
+
readonly,
|
|
584
|
+
isClassProperty,
|
|
585
|
+
isDeprecated: deprecated,
|
|
586
|
+
deprecationMessage: sourceDeprecation.message,
|
|
587
|
+
description
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function processInterfaceOrCategory(node: ClangASTNode, className: string, walkContextFile?: string): void {
|
|
592
|
+
if (!targetClasses.has(className)) return;
|
|
593
|
+
|
|
594
|
+
const cls = getOrCreateClass(className);
|
|
595
|
+
|
|
596
|
+
// Set superclass from interface declarations
|
|
597
|
+
if (node.kind === "ObjCInterfaceDecl" && node.super) {
|
|
598
|
+
cls.superclass = node.super.name;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Set protocols
|
|
602
|
+
if (node.protocols) {
|
|
603
|
+
for (const proto of node.protocols) {
|
|
604
|
+
if (!cls.protocols.includes(proto.name)) {
|
|
605
|
+
cls.protocols.push(proto.name);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!node.inner) return;
|
|
611
|
+
|
|
612
|
+
// Resolve the file path from the parent node for child context.
|
|
613
|
+
// Fall back to walkContextFile (the running file tracker from walk()).
|
|
614
|
+
const parentFile = getLocFile(node) ?? walkContextFile;
|
|
615
|
+
|
|
616
|
+
// Track selectors we've already added (to handle categories extending the same class)
|
|
617
|
+
const existingInstanceSelectors = new Set(cls.instanceMethods.map((m) => m.selector));
|
|
618
|
+
const existingClassSelectors = new Set(cls.classMethods.map((m) => m.selector));
|
|
619
|
+
const existingProperties = new Set(cls.properties.map((p) => p.name));
|
|
620
|
+
|
|
621
|
+
for (const child of node.inner) {
|
|
622
|
+
if (child.kind === "ObjCMethodDecl") {
|
|
623
|
+
const method = extractMethod(child, parentFile);
|
|
624
|
+
if (!method) continue;
|
|
625
|
+
|
|
626
|
+
if (method.isClassMethod) {
|
|
627
|
+
if (!existingClassSelectors.has(method.selector)) {
|
|
628
|
+
cls.classMethods.push(method);
|
|
629
|
+
existingClassSelectors.add(method.selector);
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
if (!existingInstanceSelectors.has(method.selector)) {
|
|
633
|
+
cls.instanceMethods.push(method);
|
|
634
|
+
existingInstanceSelectors.add(method.selector);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} else if (child.kind === "ObjCPropertyDecl") {
|
|
638
|
+
const prop = extractProperty(child, parentFile);
|
|
639
|
+
if (prop && !existingProperties.has(prop.name)) {
|
|
640
|
+
cls.properties.push(prop);
|
|
641
|
+
existingProperties.add(prop.name);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Running file tracker: clang only emits `loc.file` when it changes between
|
|
648
|
+
// nodes. As we walk in document order, we track the last-seen file so that
|
|
649
|
+
// nodes without an explicit `loc.file` can inherit the correct context.
|
|
650
|
+
let currentFile: string | undefined;
|
|
651
|
+
|
|
652
|
+
function walk(node: ClangASTNode): void {
|
|
653
|
+
// Update running file tracker from this node's loc (if present)
|
|
654
|
+
const nodeFile = getLocFile(node);
|
|
655
|
+
if (nodeFile) currentFile = nodeFile;
|
|
656
|
+
|
|
657
|
+
if (node.kind === "ObjCInterfaceDecl" && node.name) {
|
|
658
|
+
processInterfaceOrCategory(node, node.name, currentFile);
|
|
659
|
+
} else if (node.kind === "ObjCCategoryDecl" && node.interface?.name) {
|
|
660
|
+
processInterfaceOrCategory(node, node.interface.name, currentFile);
|
|
661
|
+
} else if (node.kind === "ObjCProtocolDecl" && node.name) {
|
|
662
|
+
// Merge protocol methods into matching class (e.g., NSObject protocol → NSObject class).
|
|
663
|
+
// The NSObject class conforms to the NSObject protocol, which defines
|
|
664
|
+
// isEqual:, isKindOfClass:, respondsToSelector:, performSelector:, etc.
|
|
665
|
+
if (targetClasses.has(node.name)) {
|
|
666
|
+
processInterfaceOrCategory(node, node.name, currentFile);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (node.inner) {
|
|
671
|
+
for (const child of node.inner) {
|
|
672
|
+
walk(child);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
walk(root);
|
|
678
|
+
return classes;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Parse a clang AST root node and extract ObjC protocol declarations.
|
|
683
|
+
* Returns a map of protocol name → ObjCProtocol for protocols in the target set.
|
|
684
|
+
*
|
|
685
|
+
* @param headerLines - Split lines of the header file for deprecation scanning.
|
|
686
|
+
* @param headerLinesMap - Map of file path → lines for batched mode deprecation scanning.
|
|
687
|
+
*/
|
|
688
|
+
export function parseProtocols(
|
|
689
|
+
root: ClangASTNode,
|
|
690
|
+
targetProtocols: Set<string>,
|
|
691
|
+
headerLines?: string[],
|
|
692
|
+
headerLinesMap?: Map<string, string[]>
|
|
693
|
+
): Map<string, ObjCProtocol> {
|
|
694
|
+
const protocols = new Map<string, ObjCProtocol>();
|
|
695
|
+
|
|
696
|
+
function isDeprecated(node: ClangASTNode): boolean {
|
|
697
|
+
if (!node.inner) return false;
|
|
698
|
+
return node.inner.some((child) => child.kind === "DeprecatedAttr");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function isUnavailable(node: ClangASTNode): boolean {
|
|
702
|
+
if (!node.inner) return false;
|
|
703
|
+
return node.inner.some((child) => child.kind === "UnavailableAttr");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function extractMethod(node: ClangASTNode, contextFile?: string): ObjCMethod | null {
|
|
707
|
+
if (node.kind !== "ObjCMethodDecl") return null;
|
|
708
|
+
if (node.isImplicit) return null;
|
|
709
|
+
if (isUnavailable(node)) return null;
|
|
710
|
+
|
|
711
|
+
const selector = node.name ?? "";
|
|
712
|
+
const returnType = node.returnType?.qualType ?? "void";
|
|
713
|
+
const isClassMethod = node.instance === false;
|
|
714
|
+
const attrDeprecated = isDeprecated(node);
|
|
715
|
+
const sourceDeprecation = scanForDeprecation(node, headerLines, headerLinesMap, contextFile);
|
|
716
|
+
const deprecated = attrDeprecated || sourceDeprecation.isDeprecated;
|
|
717
|
+
const description = extractDescription(node) ?? scanForDocComment(node, headerLines, headerLinesMap, contextFile);
|
|
718
|
+
|
|
719
|
+
const parameters: { name: string; type: string }[] = [];
|
|
720
|
+
if (node.inner) {
|
|
721
|
+
for (const child of node.inner) {
|
|
722
|
+
if (child.kind === "ParmVarDecl") {
|
|
723
|
+
parameters.push({
|
|
724
|
+
name: child.name ?? "arg",
|
|
725
|
+
type: child.type?.qualType ?? "id"
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
selector,
|
|
733
|
+
returnType,
|
|
734
|
+
parameters,
|
|
735
|
+
isClassMethod,
|
|
736
|
+
isDeprecated: deprecated,
|
|
737
|
+
deprecationMessage: sourceDeprecation.message,
|
|
738
|
+
description
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function extractProperty(node: ClangASTNode, contextFile?: string): ObjCProperty | null {
|
|
743
|
+
if (node.kind !== "ObjCPropertyDecl") return null;
|
|
744
|
+
if (node.isImplicit) return null;
|
|
745
|
+
|
|
746
|
+
const name = node.name ?? "";
|
|
747
|
+
const type = node.type?.qualType ?? "id";
|
|
748
|
+
const readonly = node.readonly === true;
|
|
749
|
+
const isClassProperty = (node as any)["class"] === true;
|
|
750
|
+
const attrDeprecated = isDeprecated(node);
|
|
751
|
+
const sourceDeprecation = scanForDeprecation(node, headerLines, headerLinesMap, contextFile);
|
|
752
|
+
const deprecated = attrDeprecated || sourceDeprecation.isDeprecated;
|
|
753
|
+
const description = extractDescription(node) ?? scanForDocComment(node, headerLines, headerLinesMap, contextFile);
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
name,
|
|
757
|
+
type,
|
|
758
|
+
readonly,
|
|
759
|
+
isClassProperty,
|
|
760
|
+
isDeprecated: deprecated,
|
|
761
|
+
deprecationMessage: sourceDeprecation.message,
|
|
762
|
+
description
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function getOrCreateProtocol(name: string): ObjCProtocol {
|
|
767
|
+
let proto = protocols.get(name);
|
|
768
|
+
if (!proto) {
|
|
769
|
+
proto = {
|
|
770
|
+
name,
|
|
771
|
+
extendedProtocols: [],
|
|
772
|
+
instanceMethods: [],
|
|
773
|
+
classMethods: [],
|
|
774
|
+
properties: []
|
|
775
|
+
};
|
|
776
|
+
protocols.set(name, proto);
|
|
777
|
+
}
|
|
778
|
+
return proto;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function processProtocolDecl(node: ClangASTNode, walkContextFile?: string): void {
|
|
782
|
+
const name = node.name ?? "";
|
|
783
|
+
if (!targetProtocols.has(name)) return;
|
|
784
|
+
|
|
785
|
+
const proto = getOrCreateProtocol(name);
|
|
786
|
+
|
|
787
|
+
// Collect extended protocols
|
|
788
|
+
if (node.protocols) {
|
|
789
|
+
for (const p of node.protocols) {
|
|
790
|
+
if (!proto.extendedProtocols.includes(p.name)) {
|
|
791
|
+
proto.extendedProtocols.push(p.name);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (!node.inner) return;
|
|
797
|
+
|
|
798
|
+
// Resolve the file path from the parent node for child context.
|
|
799
|
+
// Fall back to walkContextFile (the running file tracker from walk()).
|
|
800
|
+
const parentFile = getLocFile(node) ?? walkContextFile;
|
|
801
|
+
|
|
802
|
+
// Track selectors we've already added (to handle multiple declarations of the same protocol)
|
|
803
|
+
const existingInstanceSelectors = new Set(proto.instanceMethods.map((m) => m.selector));
|
|
804
|
+
const existingClassSelectors = new Set(proto.classMethods.map((m) => m.selector));
|
|
805
|
+
const existingProperties = new Set(proto.properties.map((p) => p.name));
|
|
806
|
+
|
|
807
|
+
for (const child of node.inner) {
|
|
808
|
+
if (child.kind === "ObjCMethodDecl") {
|
|
809
|
+
const method = extractMethod(child, parentFile);
|
|
810
|
+
if (!method) continue;
|
|
811
|
+
|
|
812
|
+
if (method.isClassMethod) {
|
|
813
|
+
if (!existingClassSelectors.has(method.selector)) {
|
|
814
|
+
proto.classMethods.push(method);
|
|
815
|
+
existingClassSelectors.add(method.selector);
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
if (!existingInstanceSelectors.has(method.selector)) {
|
|
819
|
+
proto.instanceMethods.push(method);
|
|
820
|
+
existingInstanceSelectors.add(method.selector);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
} else if (child.kind === "ObjCPropertyDecl") {
|
|
824
|
+
const prop = extractProperty(child, parentFile);
|
|
825
|
+
if (prop && !existingProperties.has(prop.name)) {
|
|
826
|
+
proto.properties.push(prop);
|
|
827
|
+
existingProperties.add(prop.name);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Running file tracker for batched mode (same approach as parseAST).
|
|
834
|
+
let currentFile: string | undefined;
|
|
835
|
+
|
|
836
|
+
function walk(node: ClangASTNode): void {
|
|
837
|
+
// Update running file tracker from this node's loc (if present)
|
|
838
|
+
const nodeFile = getLocFile(node);
|
|
839
|
+
if (nodeFile) currentFile = nodeFile;
|
|
840
|
+
|
|
841
|
+
if (node.kind === "ObjCProtocolDecl" && node.name) {
|
|
842
|
+
processProtocolDecl(node, currentFile);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (node.inner) {
|
|
846
|
+
for (const child of node.inner) {
|
|
847
|
+
walk(child);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
walk(root);
|
|
853
|
+
return protocols;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Parse a clang AST root node and extract integer enums (NS_ENUM / NS_OPTIONS).
|
|
858
|
+
*
|
|
859
|
+
* Integer enums appear as EnumDecl nodes with a fixedUnderlyingType.
|
|
860
|
+
* NS_OPTIONS enums additionally have a FlagEnumAttr child.
|
|
861
|
+
* Each EnumConstantDecl child represents a constant; its value comes from
|
|
862
|
+
* a nested ConstantExpr node (always the fully-computed integer as a string).
|
|
863
|
+
*
|
|
864
|
+
* @param targetEnums - Set of enum names to extract (from discovery)
|
|
865
|
+
*/
|
|
866
|
+
export function parseIntegerEnums(root: ClangASTNode, targetEnums: Set<string>): Map<string, ObjCIntegerEnum> {
|
|
867
|
+
const enums = new Map<string, ObjCIntegerEnum>();
|
|
868
|
+
|
|
869
|
+
function walk(node: ClangASTNode): void {
|
|
870
|
+
if (node.kind === "EnumDecl" && node.name && targetEnums.has(node.name)) {
|
|
871
|
+
if (node.fixedUnderlyingType) {
|
|
872
|
+
const hasConstants = node.inner?.some((child) => child.kind === "EnumConstantDecl") ?? false;
|
|
873
|
+
|
|
874
|
+
// Skip forward declarations (no EnumConstantDecl children).
|
|
875
|
+
// The full definition with constants may appear later with previousDecl set.
|
|
876
|
+
if (!hasConstants) {
|
|
877
|
+
// Only record if we haven't seen a definition with values yet
|
|
878
|
+
if (!enums.has(node.name)) {
|
|
879
|
+
enums.set(node.name, {
|
|
880
|
+
kind: "integer",
|
|
881
|
+
name: node.name,
|
|
882
|
+
underlyingType: node.fixedUnderlyingType.qualType,
|
|
883
|
+
isOptions: false,
|
|
884
|
+
values: []
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
const isOptions = node.inner?.some((child) => child.kind === "FlagEnumAttr") ?? false;
|
|
889
|
+
|
|
890
|
+
const values: ObjCEnumValue[] = [];
|
|
891
|
+
let implicitNextValue = 0;
|
|
892
|
+
|
|
893
|
+
for (const child of node.inner!) {
|
|
894
|
+
if (child.kind === "EnumConstantDecl" && child.name) {
|
|
895
|
+
// Extract value from ConstantExpr if present
|
|
896
|
+
let value: string | null = null;
|
|
897
|
+
if (child.inner) {
|
|
898
|
+
const constExpr = findConstantExpr(child);
|
|
899
|
+
if (constExpr?.value !== undefined) {
|
|
900
|
+
value = constExpr.value;
|
|
901
|
+
implicitNextValue = parseInt(value, 10) + 1;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (value === null) {
|
|
905
|
+
value = String(implicitNextValue);
|
|
906
|
+
implicitNextValue++;
|
|
907
|
+
}
|
|
908
|
+
values.push({ name: child.name, value });
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Always overwrite — the definition with constants is authoritative
|
|
913
|
+
enums.set(node.name, {
|
|
914
|
+
kind: "integer",
|
|
915
|
+
name: node.name,
|
|
916
|
+
underlyingType: node.fixedUnderlyingType.qualType,
|
|
917
|
+
isOptions,
|
|
918
|
+
values
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (node.inner) {
|
|
925
|
+
for (const child of node.inner) {
|
|
926
|
+
walk(child);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
walk(root);
|
|
932
|
+
return enums;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Recursively find the first ConstantExpr node in a tree.
|
|
937
|
+
* ConstantExpr may be nested under ImplicitCastExpr or other intermediate nodes.
|
|
938
|
+
*/
|
|
939
|
+
function findConstantExpr(node: ClangASTNode): ClangASTNode | null {
|
|
940
|
+
if (node.kind === "ConstantExpr" && node.value !== undefined) {
|
|
941
|
+
return node;
|
|
942
|
+
}
|
|
943
|
+
if (node.inner) {
|
|
944
|
+
for (const child of node.inner) {
|
|
945
|
+
const found = findConstantExpr(child);
|
|
946
|
+
if (found) return found;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Parse a clang AST root node and extract string enums (NS_TYPED_EXTENSIBLE_ENUM etc.).
|
|
954
|
+
*
|
|
955
|
+
* String enums appear as TypedefDecl nodes with a SwiftNewTypeAttr child
|
|
956
|
+
* and a type.qualType of "NSString *". The individual values are VarDecl
|
|
957
|
+
* nodes with storageClass "extern" whose type.typeAliasDeclId links back
|
|
958
|
+
* to the TypedefDecl's id.
|
|
959
|
+
*
|
|
960
|
+
* @param targetEnums - Set of string enum names to extract (from discovery)
|
|
961
|
+
*/
|
|
962
|
+
export function parseStringEnums(root: ClangASTNode, targetEnums: Set<string>): Map<string, ObjCStringEnum> {
|
|
963
|
+
const enums = new Map<string, ObjCStringEnum>();
|
|
964
|
+
// Map from TypedefDecl id → enum name, for linking VarDecls
|
|
965
|
+
const typedefIdToName = new Map<string, string>();
|
|
966
|
+
|
|
967
|
+
function walkForTypedefs(node: ClangASTNode): void {
|
|
968
|
+
if (node.kind === "TypedefDecl" && node.name && targetEnums.has(node.name)) {
|
|
969
|
+
// Check for SwiftNewTypeAttr in inner
|
|
970
|
+
const hasSwiftNewType = node.inner?.some((child) => child.kind === "SwiftNewTypeAttr") ?? false;
|
|
971
|
+
|
|
972
|
+
// Check that the underlying type is NSString *
|
|
973
|
+
const qualType = node.type?.qualType ?? "";
|
|
974
|
+
const isStringType =
|
|
975
|
+
qualType === "NSString *" ||
|
|
976
|
+
qualType.includes("NSString") ||
|
|
977
|
+
(node.type?.desugaredQualType ?? "").includes("NSString");
|
|
978
|
+
|
|
979
|
+
if (hasSwiftNewType && isStringType) {
|
|
980
|
+
typedefIdToName.set(node.id, node.name);
|
|
981
|
+
if (!enums.has(node.name)) {
|
|
982
|
+
enums.set(node.name, {
|
|
983
|
+
kind: "string",
|
|
984
|
+
name: node.name,
|
|
985
|
+
values: []
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (node.inner) {
|
|
992
|
+
for (const child of node.inner) {
|
|
993
|
+
walkForTypedefs(child);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function walkForVarDecls(node: ClangASTNode): void {
|
|
999
|
+
if (node.kind === "VarDecl" && node.name && node.storageClass === "extern" && node.type?.typeAliasDeclId) {
|
|
1000
|
+
const enumName = typedefIdToName.get(node.type.typeAliasDeclId);
|
|
1001
|
+
if (enumName) {
|
|
1002
|
+
const enumDef = enums.get(enumName);
|
|
1003
|
+
if (enumDef) {
|
|
1004
|
+
// Strip the enum name prefix to get the short name
|
|
1005
|
+
let shortName = node.name;
|
|
1006
|
+
if (shortName.startsWith(enumName)) {
|
|
1007
|
+
shortName = shortName.slice(enumName.length);
|
|
1008
|
+
}
|
|
1009
|
+
if (!shortName || /^\d/.test(shortName)) {
|
|
1010
|
+
shortName = node.name;
|
|
1011
|
+
}
|
|
1012
|
+
enumDef.values.push({
|
|
1013
|
+
symbolName: node.name,
|
|
1014
|
+
shortName,
|
|
1015
|
+
value: null // Resolved later by resolve-strings.ts
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (node.inner) {
|
|
1022
|
+
for (const child of node.inner) {
|
|
1023
|
+
walkForVarDecls(child);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// First pass: find TypedefDecl nodes
|
|
1029
|
+
walkForTypedefs(root);
|
|
1030
|
+
// Second pass: find VarDecl nodes that reference the typedefs
|
|
1031
|
+
walkForVarDecls(root);
|
|
1032
|
+
|
|
1033
|
+
return enums;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// --- Struct parsing ---
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Parse a clang AST root node and extract struct definitions and struct typedef aliases.
|
|
1040
|
+
*
|
|
1041
|
+
* Structs appear as:
|
|
1042
|
+
* 1. Named RecordDecl with tagUsed:"struct", containing FieldDecl children
|
|
1043
|
+
* e.g., `struct CGPoint { CGFloat x; CGFloat y; }`
|
|
1044
|
+
* 2. Anonymous RecordDecl wrapped by TypedefDecl
|
|
1045
|
+
* e.g., `typedef struct { ... } NSDecimal;`
|
|
1046
|
+
* 3. TypedefDecl aliasing one struct to another
|
|
1047
|
+
* e.g., `typedef CGPoint NSPoint;`
|
|
1048
|
+
*
|
|
1049
|
+
* Returns { structs, aliases } where:
|
|
1050
|
+
* - structs: Map<name, ObjCStruct> — all struct definitions (keyed by public name)
|
|
1051
|
+
* - aliases: ObjCStructAlias[] — typedef aliases between structs
|
|
1052
|
+
*/
|
|
1053
|
+
export function parseStructs(root: ClangASTNode): {
|
|
1054
|
+
structs: Map<string, ObjCStruct>;
|
|
1055
|
+
aliases: ObjCStructAlias[];
|
|
1056
|
+
} {
|
|
1057
|
+
const structs = new Map<string, ObjCStruct>();
|
|
1058
|
+
const aliases: ObjCStructAlias[] = [];
|
|
1059
|
+
|
|
1060
|
+
// Map from RecordDecl id → struct definition (for linking typedefs to anonymous structs)
|
|
1061
|
+
const recordById = new Map<string, { name: string; fields: ObjCStructField[] }>();
|
|
1062
|
+
// Set of all known struct names (for detecting typedef aliases between structs)
|
|
1063
|
+
const knownStructNames = new Set<string>();
|
|
1064
|
+
// Map of internal struct names to their RecordDecl data (for typedef linking)
|
|
1065
|
+
const recordByName = new Map<string, { fields: ObjCStructField[] }>();
|
|
1066
|
+
|
|
1067
|
+
/** Extract fields from a RecordDecl's inner children. */
|
|
1068
|
+
function extractFields(node: ClangASTNode): ObjCStructField[] {
|
|
1069
|
+
const fields: ObjCStructField[] = [];
|
|
1070
|
+
if (!node.inner) return fields;
|
|
1071
|
+
for (const child of node.inner) {
|
|
1072
|
+
if (child.kind === "FieldDecl" && child.name) {
|
|
1073
|
+
fields.push({
|
|
1074
|
+
name: child.name,
|
|
1075
|
+
type: child.type?.qualType ?? "int"
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return fields;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// First pass: collect all RecordDecl nodes (struct definitions)
|
|
1083
|
+
function walkForRecords(node: ClangASTNode): void {
|
|
1084
|
+
if (node.kind === "RecordDecl" && (node as any).tagUsed === "struct") {
|
|
1085
|
+
const fields = extractFields(node);
|
|
1086
|
+
// Only record definitions with fields (skip forward declarations)
|
|
1087
|
+
if (fields.length > 0) {
|
|
1088
|
+
recordById.set(node.id, {
|
|
1089
|
+
name: node.name ?? "",
|
|
1090
|
+
fields
|
|
1091
|
+
});
|
|
1092
|
+
if (node.name && node.name !== "(anonymous)") {
|
|
1093
|
+
knownStructNames.add(node.name);
|
|
1094
|
+
recordByName.set(node.name, { fields });
|
|
1095
|
+
|
|
1096
|
+
// Named structs are directly usable (e.g., struct CGPoint), but
|
|
1097
|
+
// underscore-prefixed names (e.g., _NSRange) are internal C names
|
|
1098
|
+
// that will get a public typedef later — don't add them to structs
|
|
1099
|
+
// directly, only to recordByName for lookup.
|
|
1100
|
+
if (!node.name.startsWith("_")) {
|
|
1101
|
+
structs.set(node.name, {
|
|
1102
|
+
name: node.name,
|
|
1103
|
+
fields
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (node.inner) {
|
|
1111
|
+
for (const child of node.inner) {
|
|
1112
|
+
walkForRecords(child);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Second pass: find TypedefDecl nodes that reference structs
|
|
1118
|
+
function walkForTypedefs(node: ClangASTNode): void {
|
|
1119
|
+
if (node.kind === "TypedefDecl" && node.name) {
|
|
1120
|
+
const qualType = node.type?.qualType ?? "";
|
|
1121
|
+
|
|
1122
|
+
// Case 1: Typedef wrapping an anonymous struct (inline definition)
|
|
1123
|
+
// The RecordDecl appears as a direct child of the TypedefDecl
|
|
1124
|
+
if (node.inner) {
|
|
1125
|
+
for (const child of node.inner) {
|
|
1126
|
+
if (child.kind === "RecordDecl" && (child as any).tagUsed === "struct") {
|
|
1127
|
+
const fields = extractFields(child);
|
|
1128
|
+
if (fields.length > 0) {
|
|
1129
|
+
structs.set(node.name, {
|
|
1130
|
+
name: node.name,
|
|
1131
|
+
fields
|
|
1132
|
+
});
|
|
1133
|
+
knownStructNames.add(node.name);
|
|
1134
|
+
return; // Don't also check typedef alias case
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Case 2: Typedef aliasing a named struct (e.g., typedef struct _NSRange NSRange)
|
|
1141
|
+
// The qualType will be "struct _NSRange" or just "_NSRange"
|
|
1142
|
+
const structMatch = qualType.match(/^(?:struct\s+)?(\w+)$/);
|
|
1143
|
+
if (structMatch) {
|
|
1144
|
+
const targetName = structMatch[1]!;
|
|
1145
|
+
|
|
1146
|
+
// Self-referencing typedef (e.g., typedef struct CGRect CGRect, or
|
|
1147
|
+
// typedef struct NS_SWIFT_SENDABLE { ... } NSOperatingSystemVersion).
|
|
1148
|
+
// For the NS_SWIFT_SENDABLE pattern, the RecordDecl is anonymous but
|
|
1149
|
+
// referenced via ownedTagDecl in the inner ElaboratedType node.
|
|
1150
|
+
if (targetName === node.name) {
|
|
1151
|
+
// Check if there's an ownedTagDecl referencing an anonymous struct
|
|
1152
|
+
if (node.inner) {
|
|
1153
|
+
for (const child of node.inner) {
|
|
1154
|
+
const ownedId = (child as any).ownedTagDecl?.id;
|
|
1155
|
+
if (ownedId) {
|
|
1156
|
+
const record = recordById.get(ownedId);
|
|
1157
|
+
if (record && (!record.name || record.name === "(anonymous)") && record.fields.length > 0) {
|
|
1158
|
+
structs.set(node.name, {
|
|
1159
|
+
name: node.name,
|
|
1160
|
+
fields: record.fields
|
|
1161
|
+
});
|
|
1162
|
+
knownStructNames.add(node.name);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return; // Genuine self-reference, skip
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Check if target is a known record (C struct definition)
|
|
1172
|
+
const targetRecord = recordByName.get(targetName);
|
|
1173
|
+
if (targetRecord) {
|
|
1174
|
+
// If the target is a public struct name (not underscore-prefixed internal),
|
|
1175
|
+
// this typedef creates an alias. E.g., typedef CGRect NSRect means NSRect
|
|
1176
|
+
// is an alias of CGRect. But typedef struct _NSRange NSRange means NSRange
|
|
1177
|
+
// is the public name for the internal _NSRange struct.
|
|
1178
|
+
const isInternalName = targetName.startsWith("_");
|
|
1179
|
+
if (!isInternalName && structs.has(targetName)) {
|
|
1180
|
+
// Target is a public struct name — create an alias
|
|
1181
|
+
aliases.push({ name: node.name, target: targetName });
|
|
1182
|
+
knownStructNames.add(node.name);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Otherwise, this is a typedef for a private internal struct name:
|
|
1187
|
+
// typedef struct _NSRange NSRange
|
|
1188
|
+
structs.set(node.name, {
|
|
1189
|
+
name: node.name,
|
|
1190
|
+
internalName: targetName,
|
|
1191
|
+
fields: targetRecord.fields
|
|
1192
|
+
});
|
|
1193
|
+
knownStructNames.add(node.name);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Check if target is another typedef'd struct name (e.g., NSPoint → CGPoint)
|
|
1198
|
+
if (knownStructNames.has(targetName)) {
|
|
1199
|
+
// Resolve through existing aliases to find the canonical struct name
|
|
1200
|
+
let resolvedTarget = targetName;
|
|
1201
|
+
for (const alias of aliases) {
|
|
1202
|
+
if (alias.name === targetName) {
|
|
1203
|
+
resolvedTarget = alias.target;
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
aliases.push({ name: node.name, target: resolvedTarget });
|
|
1208
|
+
knownStructNames.add(node.name);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (node.inner) {
|
|
1215
|
+
for (const child of node.inner) {
|
|
1216
|
+
walkForTypedefs(child);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
walkForRecords(root);
|
|
1222
|
+
walkForTypedefs(root);
|
|
1223
|
+
|
|
1224
|
+
return { structs, aliases };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// --- General typedef parsing ---
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Parse all TypedefDecl nodes from the AST to build a typedef resolution table.
|
|
1231
|
+
*
|
|
1232
|
+
* This captures arbitrary typedefs like `typedef NSString * NSWindowFrameAutosaveName`
|
|
1233
|
+
* so the type mapper can resolve them instead of falling back to heuristics.
|
|
1234
|
+
* Only captures typedefs whose underlying type is an ObjC pointer type (e.g., `NSString *`)
|
|
1235
|
+
* or a known numeric/boolean type — not struct/enum typedefs (handled elsewhere).
|
|
1236
|
+
*
|
|
1237
|
+
* @returns Map of typedef name → underlying qualType string
|
|
1238
|
+
*/
|
|
1239
|
+
export function parseTypedefs(root: ClangASTNode): Map<string, string> {
|
|
1240
|
+
const typedefs = new Map<string, string>();
|
|
1241
|
+
|
|
1242
|
+
function walk(node: ClangASTNode): void {
|
|
1243
|
+
if (node.kind === "TypedefDecl" && node.name && node.type?.qualType) {
|
|
1244
|
+
const name = node.name;
|
|
1245
|
+
const qualType = node.type.qualType;
|
|
1246
|
+
|
|
1247
|
+
// Skip typedefs that are self-referencing (e.g., typedef struct CGPoint CGPoint)
|
|
1248
|
+
if (name === qualType || qualType === `struct ${name}`) return;
|
|
1249
|
+
|
|
1250
|
+
// Skip typedefs with no useful resolution (e.g., __builtin_va_list)
|
|
1251
|
+
if (qualType.startsWith("__")) return;
|
|
1252
|
+
|
|
1253
|
+
// Store the mapping — the type mapper will use this to resolve
|
|
1254
|
+
// unknown typedef names to their underlying types
|
|
1255
|
+
typedefs.set(name, qualType);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (node.inner) {
|
|
1259
|
+
for (const child of node.inner) {
|
|
1260
|
+
walk(child);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
walk(root);
|
|
1266
|
+
return typedefs;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// --- C function parsing ---
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Parse a clang AST root node and extract C function declarations.
|
|
1273
|
+
*
|
|
1274
|
+
* FunctionDecl nodes contain the full type signature in node.type.qualType
|
|
1275
|
+
* (e.g., "void (NSString * _Nonnull, ...)"), parameters as ParmVarDecl children,
|
|
1276
|
+
* and a variadic flag. We filter to only functions declared in the framework's
|
|
1277
|
+
* own headers (not system/stdlib functions pulled in transitively).
|
|
1278
|
+
*
|
|
1279
|
+
* @param root - The clang AST root node
|
|
1280
|
+
* @param frameworkHeaderPaths - Set of header file paths belonging to this framework.
|
|
1281
|
+
* Only functions declared in these headers are included.
|
|
1282
|
+
* @param frameworkName - The framework name to tag on each function
|
|
1283
|
+
*/
|
|
1284
|
+
export function parseFunctions(
|
|
1285
|
+
root: ClangASTNode,
|
|
1286
|
+
frameworkHeaderPaths: Set<string>,
|
|
1287
|
+
frameworkName: string
|
|
1288
|
+
): Map<string, ObjCFunction> {
|
|
1289
|
+
const functions = new Map<string, ObjCFunction>();
|
|
1290
|
+
|
|
1291
|
+
function isDeprecated(node: ClangASTNode): boolean {
|
|
1292
|
+
if (!node.inner) return false;
|
|
1293
|
+
return node.inner.some((child) => child.kind === "DeprecatedAttr");
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function isUnavailable(node: ClangASTNode): boolean {
|
|
1297
|
+
if (!node.inner) return false;
|
|
1298
|
+
return node.inner.some((child) => child.kind === "UnavailableAttr");
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Running file tracker for batched mode
|
|
1302
|
+
let currentFile: string | undefined;
|
|
1303
|
+
|
|
1304
|
+
function walk(node: ClangASTNode): void {
|
|
1305
|
+
const nodeFile = getLocFile(node);
|
|
1306
|
+
if (nodeFile) currentFile = nodeFile;
|
|
1307
|
+
|
|
1308
|
+
if (node.kind === "FunctionDecl" && node.name) {
|
|
1309
|
+
const name = node.name;
|
|
1310
|
+
|
|
1311
|
+
// Skip private/internal functions (underscore-prefixed)
|
|
1312
|
+
if (name.startsWith("_")) {
|
|
1313
|
+
// Allow through if it's in the inner array of root (walk continues)
|
|
1314
|
+
} else if (!isDeprecated(node) && !isUnavailable(node)) {
|
|
1315
|
+
// Skip inline functions (they have a body/CompoundStmt child, or storageClass)
|
|
1316
|
+
const hasBody = node.inner?.some((child) => child.kind === "CompoundStmt") ?? false;
|
|
1317
|
+
if (!hasBody) {
|
|
1318
|
+
// Skip functions with storageClass "static" (internal linkage)
|
|
1319
|
+
if (node.storageClass !== "static") {
|
|
1320
|
+
// Check that this function is declared in a framework header
|
|
1321
|
+
const file = getLocFile(node) ?? currentFile;
|
|
1322
|
+
const inFramework = file ? frameworkHeaderPaths.has(file) : false;
|
|
1323
|
+
|
|
1324
|
+
if (inFramework && !functions.has(name)) {
|
|
1325
|
+
// Extract return type from the full type signature
|
|
1326
|
+
// node.type.qualType looks like "void (NSString *, ...)" or "NSString *(void)"
|
|
1327
|
+
const fullType = node.type?.qualType ?? "void ()";
|
|
1328
|
+
const parenIdx = fullType.indexOf("(");
|
|
1329
|
+
const returnType = parenIdx >= 0 ? fullType.slice(0, parenIdx).trim() : "void";
|
|
1330
|
+
|
|
1331
|
+
// Extract parameters from ParmVarDecl children
|
|
1332
|
+
const parameters: { name: string; type: string }[] = [];
|
|
1333
|
+
if (node.inner) {
|
|
1334
|
+
for (const child of node.inner) {
|
|
1335
|
+
if (child.kind === "ParmVarDecl") {
|
|
1336
|
+
parameters.push({
|
|
1337
|
+
name: child.name ?? "arg",
|
|
1338
|
+
type: child.type?.qualType ?? "id"
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const isVariadic = node.variadic === true;
|
|
1345
|
+
|
|
1346
|
+
functions.set(name, {
|
|
1347
|
+
name,
|
|
1348
|
+
returnType,
|
|
1349
|
+
parameters,
|
|
1350
|
+
isVariadic,
|
|
1351
|
+
frameworkName
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (node.inner) {
|
|
1360
|
+
for (const child of node.inner) {
|
|
1361
|
+
walk(child);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
walk(root);
|
|
1367
|
+
return functions;
|
|
1368
|
+
}
|