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.
Files changed (194) hide show
  1. package/bin/objcjs-types.ts +31 -0
  2. package/dist/AVFoundation/functions.d.ts +21 -0
  3. package/dist/AVFoundation/functions.js +32 -0
  4. package/dist/AVFoundation/index.d.ts +1 -0
  5. package/dist/AVFoundation/index.js +1 -0
  6. package/dist/Accessibility/functions.d.ts +16 -0
  7. package/dist/Accessibility/functions.js +35 -0
  8. package/dist/Accessibility/index.d.ts +1 -0
  9. package/dist/Accessibility/index.js +1 -0
  10. package/dist/AddressBook/functions.d.ts +98 -0
  11. package/dist/AddressBook/functions.js +290 -0
  12. package/dist/AddressBook/index.d.ts +1 -0
  13. package/dist/AddressBook/index.js +1 -0
  14. package/dist/AppKit/functions.d.ts +112 -0
  15. package/dist/AppKit/functions.js +272 -0
  16. package/dist/AppKit/index.d.ts +1 -0
  17. package/dist/AppKit/index.js +1 -0
  18. package/dist/AudioToolbox/functions.d.ts +377 -0
  19. package/dist/AudioToolbox/functions.js +1124 -0
  20. package/dist/AudioToolbox/index.d.ts +1 -0
  21. package/dist/AudioToolbox/index.js +1 -0
  22. package/dist/AuthenticationServices/functions.d.ts +2 -0
  23. package/dist/AuthenticationServices/functions.js +5 -0
  24. package/dist/AuthenticationServices/index.d.ts +1 -0
  25. package/dist/AuthenticationServices/index.js +1 -0
  26. package/dist/BrowserEngineCore/functions.d.ts +3 -0
  27. package/dist/BrowserEngineCore/functions.js +11 -0
  28. package/dist/BrowserEngineCore/index.d.ts +1 -0
  29. package/dist/BrowserEngineCore/index.js +1 -0
  30. package/dist/CoreAudio/functions.d.ts +60 -0
  31. package/dist/CoreAudio/functions.js +173 -0
  32. package/dist/CoreAudio/index.d.ts +1 -0
  33. package/dist/CoreAudio/index.js +1 -0
  34. package/dist/CoreMIDI/functions.d.ts +96 -0
  35. package/dist/CoreMIDI/functions.js +287 -0
  36. package/dist/CoreMIDI/index.d.ts +1 -0
  37. package/dist/CoreMIDI/index.js +1 -0
  38. package/dist/CoreML/functions.d.ts +2 -0
  39. package/dist/CoreML/functions.js +5 -0
  40. package/dist/CoreML/index.d.ts +1 -0
  41. package/dist/CoreML/index.js +1 -0
  42. package/dist/CoreMediaIO/functions.d.ts +38 -0
  43. package/dist/CoreMediaIO/functions.js +107 -0
  44. package/dist/CoreMediaIO/index.d.ts +1 -0
  45. package/dist/CoreMediaIO/index.js +1 -0
  46. package/dist/CoreText/functions.d.ts +209 -0
  47. package/dist/CoreText/functions.js +611 -0
  48. package/dist/CoreText/index.d.ts +1 -0
  49. package/dist/CoreText/index.js +1 -0
  50. package/dist/CoreWLAN/functions.d.ts +23 -0
  51. package/dist/CoreWLAN/functions.js +56 -0
  52. package/dist/CoreWLAN/index.d.ts +1 -0
  53. package/dist/CoreWLAN/index.js +1 -0
  54. package/dist/DeviceDiscoveryExtension/functions.d.ts +11 -0
  55. package/dist/DeviceDiscoveryExtension/functions.js +17 -0
  56. package/dist/DeviceDiscoveryExtension/index.d.ts +1 -0
  57. package/dist/DeviceDiscoveryExtension/index.js +1 -0
  58. package/dist/DiscRecording/functions.d.ts +97 -0
  59. package/dist/DiscRecording/functions.js +290 -0
  60. package/dist/DiscRecording/index.d.ts +1 -0
  61. package/dist/DiscRecording/index.js +1 -0
  62. package/dist/DiscRecordingUI/functions.d.ts +13 -0
  63. package/dist/DiscRecordingUI/functions.js +38 -0
  64. package/dist/DiscRecordingUI/index.d.ts +1 -0
  65. package/dist/DiscRecordingUI/index.js +1 -0
  66. package/dist/ExceptionHandling/functions.d.ts +1 -0
  67. package/dist/ExceptionHandling/functions.js +5 -0
  68. package/dist/ExceptionHandling/index.d.ts +1 -0
  69. package/dist/ExceptionHandling/index.js +1 -0
  70. package/dist/FSKit/functions.d.ts +4 -0
  71. package/dist/FSKit/functions.js +11 -0
  72. package/dist/FSKit/index.d.ts +1 -0
  73. package/dist/FSKit/index.js +1 -0
  74. package/dist/Foundation/functions.d.ts +145 -0
  75. package/dist/Foundation/functions.js +386 -0
  76. package/dist/Foundation/index.d.ts +1 -0
  77. package/dist/Foundation/index.js +1 -0
  78. package/dist/GLKit/functions.d.ts +51 -0
  79. package/dist/GLKit/functions.js +146 -0
  80. package/dist/GLKit/index.d.ts +1 -0
  81. package/dist/GLKit/index.js +1 -0
  82. package/dist/GameController/functions.d.ts +18 -0
  83. package/dist/GameController/functions.js +44 -0
  84. package/dist/GameController/index.d.ts +1 -0
  85. package/dist/GameController/index.js +1 -0
  86. package/dist/HealthKit/functions.d.ts +19 -0
  87. package/dist/HealthKit/functions.js +35 -0
  88. package/dist/HealthKit/index.d.ts +1 -0
  89. package/dist/HealthKit/index.js +1 -0
  90. package/dist/IOSurface/functions.d.ts +53 -0
  91. package/dist/IOSurface/functions.js +155 -0
  92. package/dist/IOSurface/index.d.ts +1 -0
  93. package/dist/IOSurface/index.js +1 -0
  94. package/dist/IOUSBHost/functions.d.ts +44 -0
  95. package/dist/IOUSBHost/functions.js +131 -0
  96. package/dist/IOUSBHost/index.d.ts +1 -0
  97. package/dist/IOUSBHost/index.js +1 -0
  98. package/dist/InstantMessage/functions.d.ts +1 -0
  99. package/dist/InstantMessage/functions.js +5 -0
  100. package/dist/InstantMessage/index.d.ts +1 -0
  101. package/dist/InstantMessage/index.js +1 -0
  102. package/dist/JavaRuntimeSupport/functions.d.ts +40 -0
  103. package/dist/JavaRuntimeSupport/functions.js +113 -0
  104. package/dist/JavaRuntimeSupport/index.d.ts +1 -0
  105. package/dist/JavaRuntimeSupport/index.js +1 -0
  106. package/dist/JavaScriptCore/functions.d.ts +120 -0
  107. package/dist/JavaScriptCore/functions.js +359 -0
  108. package/dist/JavaScriptCore/index.d.ts +1 -0
  109. package/dist/JavaScriptCore/index.js +1 -0
  110. package/dist/MLCompute/functions.d.ts +27 -0
  111. package/dist/MLCompute/functions.js +41 -0
  112. package/dist/MLCompute/index.d.ts +1 -0
  113. package/dist/MLCompute/index.js +1 -0
  114. package/dist/MapKit/functions.d.ts +23 -0
  115. package/dist/MapKit/functions.js +56 -0
  116. package/dist/MapKit/index.d.ts +1 -0
  117. package/dist/MapKit/index.js +1 -0
  118. package/dist/Matter/functions.d.ts +17 -0
  119. package/dist/Matter/functions.js +26 -0
  120. package/dist/Matter/index.d.ts +1 -0
  121. package/dist/Matter/index.js +1 -0
  122. package/dist/MediaAccessibility/functions.d.ts +28 -0
  123. package/dist/MediaAccessibility/functions.js +83 -0
  124. package/dist/MediaAccessibility/index.d.ts +1 -0
  125. package/dist/MediaAccessibility/index.js +1 -0
  126. package/dist/MediaPlayer/functions.d.ts +3 -0
  127. package/dist/MediaPlayer/functions.js +11 -0
  128. package/dist/MediaPlayer/index.d.ts +1 -0
  129. package/dist/MediaPlayer/index.js +1 -0
  130. package/dist/Metal/functions.d.ts +14 -0
  131. package/dist/Metal/functions.js +26 -0
  132. package/dist/Metal/index.d.ts +1 -0
  133. package/dist/Metal/index.js +1 -0
  134. package/dist/MetalKit/functions.d.ts +11 -0
  135. package/dist/MetalKit/functions.js +20 -0
  136. package/dist/MetalKit/index.d.ts +1 -0
  137. package/dist/MetalKit/index.js +1 -0
  138. package/dist/MetalPerformanceShaders/functions.d.ts +7 -0
  139. package/dist/MetalPerformanceShaders/functions.js +14 -0
  140. package/dist/MetalPerformanceShaders/index.d.ts +1 -0
  141. package/dist/MetalPerformanceShaders/index.js +1 -0
  142. package/dist/NearbyInteraction/functions.d.ts +3 -0
  143. package/dist/NearbyInteraction/functions.js +5 -0
  144. package/dist/NearbyInteraction/index.d.ts +1 -0
  145. package/dist/NearbyInteraction/index.js +1 -0
  146. package/dist/ParavirtualizedGraphics/functions.d.ts +7 -0
  147. package/dist/ParavirtualizedGraphics/functions.js +14 -0
  148. package/dist/ParavirtualizedGraphics/index.d.ts +1 -0
  149. package/dist/ParavirtualizedGraphics/index.js +1 -0
  150. package/dist/QuartzCore/functions.d.ts +19 -0
  151. package/dist/QuartzCore/functions.js +50 -0
  152. package/dist/QuartzCore/index.d.ts +1 -0
  153. package/dist/QuartzCore/index.js +1 -0
  154. package/dist/SceneKit/functions.d.ts +17 -0
  155. package/dist/SceneKit/functions.js +38 -0
  156. package/dist/SceneKit/index.d.ts +1 -0
  157. package/dist/SceneKit/index.js +1 -0
  158. package/dist/SensorKit/functions.d.ts +4 -0
  159. package/dist/SensorKit/functions.js +14 -0
  160. package/dist/SensorKit/index.d.ts +1 -0
  161. package/dist/SensorKit/index.js +1 -0
  162. package/dist/ServiceManagement/functions.d.ts +7 -0
  163. package/dist/ServiceManagement/functions.js +20 -0
  164. package/dist/ServiceManagement/index.d.ts +1 -0
  165. package/dist/ServiceManagement/index.js +1 -0
  166. package/dist/StoreKit/functions.d.ts +1 -0
  167. package/dist/StoreKit/functions.js +5 -0
  168. package/dist/StoreKit/index.d.ts +1 -0
  169. package/dist/StoreKit/index.js +1 -0
  170. package/dist/VideoToolbox/functions.d.ts +81 -0
  171. package/dist/VideoToolbox/functions.js +236 -0
  172. package/dist/VideoToolbox/index.d.ts +1 -0
  173. package/dist/VideoToolbox/index.js +1 -0
  174. package/dist/Vision/functions.d.ts +16 -0
  175. package/dist/Vision/functions.js +38 -0
  176. package/dist/Vision/index.d.ts +1 -0
  177. package/dist/Vision/index.js +1 -0
  178. package/generator/ast-parser.ts +1368 -0
  179. package/generator/clang.ts +167 -0
  180. package/generator/custom.ts +936 -0
  181. package/generator/discover.ts +111 -0
  182. package/generator/emitter.ts +2026 -0
  183. package/generator/frameworks.ts +135 -0
  184. package/generator/index.ts +1334 -0
  185. package/generator/parse-worker.ts +263 -0
  186. package/generator/resolve-strings.ts +121 -0
  187. package/generator/struct-fields.ts +46 -0
  188. package/generator/templates/bind.ts +100 -0
  189. package/generator/templates/helpers.ts +70 -0
  190. package/generator/templates/nsdata.ts +97 -0
  191. package/generator/templates/osversion.ts +91 -0
  192. package/generator/type-mapper.ts +615 -0
  193. package/generator/worker-pool.ts +309 -0
  194. 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
+ }