diff --git a/gitnexus/src/core/ingestion/languages/swift/query.ts b/gitnexus/src/core/ingestion/languages/swift/query.ts index 78c1efa41..1d50be2df 100644 --- a/gitnexus/src/core/ingestion/languages/swift/query.ts +++ b/gitnexus/src/core/ingestion/languages/swift/query.ts @@ -107,6 +107,23 @@ const SWIFT_SCOPE_QUERY = ` (type_annotation (user_type (type_identifier) @type-binding.type))) @type-binding.annotation +;; Optional property annotations: \`var owner: Owner?\` (#2807). The pattern +;; above requires the \`user_type\` to be a DIRECT child of the annotation; +;; an optional inserts an \`optional_type\` level between them, so an optional +;; field was never typed at all and \`self.owner!.method()\` could not resolve +;; its receiver. Declaring a field optional and assigning it later is the +;; idiomatic Swift way to express a field that has no value at init time, so +;; this is the common shape, not an edge case. +;; +;; The INNER \`type_identifier\` is captured, so the binding is \`Owner\` with no +;; reliance on \`stripOptional\` reducing a \`Owner?\` spelling. +(property_declaration + name: (pattern + bound_identifier: (simple_identifier) @type-binding.name) + (type_annotation + (optional_type + (user_type (type_identifier) @type-binding.type)))) @type-binding.annotation + ;; ── Type bindings — stored / local-var constructor inference: ;; \`let p = Product(...)\` (constructor) and \`let u = getUser()\` ;; (free-call result; chain-follow resolves getUser → its return type). diff --git a/gitnexus/src/core/ingestion/utils/call-analysis.ts b/gitnexus/src/core/ingestion/utils/call-analysis.ts index 86be8b8cd..6281a1fbf 100644 --- a/gitnexus/src/core/ingestion/utils/call-analysis.ts +++ b/gitnexus/src/core/ingestion/utils/call-analysis.ts @@ -561,6 +561,32 @@ const TRANSPARENT_RECEIVER_WRAPPERS = new Set([ 'parenthesized_expression', // `(svc)` ]); +/** + * Wrappers that are transparent only for SOME operators, keyed by the operator + * text that makes them so. + * + * Swift force-unwrap (`self.a!`) is the exact semantic of TypeScript's + * `non_null_expression` above — it yields the wrapped type — but Swift parses it + * as the general `postfix_expression`, which ALSO carries user-defined postfix + * operators. Those can return anything, so peeling the node type unconditionally + * would type the receiver as the operand and could produce a confidently wrong + * owner. Reading the operator keeps the peel to the case that is provably + * type-preserving. + */ +const OPERATOR_GATED_RECEIVER_WRAPPERS = new Map([ + ['postfix_expression', '!'], // Swift `self.a!` +]); + +/** Is `node` a wrapper that denotes exactly what its operand denotes? */ +function isTransparentReceiverWrapper(node: SyntaxNode): boolean { + if (TRANSPARENT_RECEIVER_WRAPPERS.has(node.type)) return true; + const operator = OPERATOR_GATED_RECEIVER_WRAPPERS.get(node.type); + if (operator === undefined) return false; + // The operator is an anonymous token, so it is not in `namedChildren`; the + // node's own text is the reliable place to read it. + return node.text.trimEnd().endsWith(operator); +} + /** * Iteration bound for the wrapper peel. Its OWN constant, not `MAX_CHAIN_DEPTH`. * @@ -575,11 +601,7 @@ const MAX_TRANSPARENT_WRAPPER_DEPTH = 3; /** Peel transparent wrappers off a base receiver node. */ function unwrapTransparentReceiver(node: SyntaxNode): SyntaxNode { let current = node; - for ( - let i = 0; - i < MAX_TRANSPARENT_WRAPPER_DEPTH && TRANSPARENT_RECEIVER_WRAPPERS.has(current.type); - i++ - ) { + for (let i = 0; i < MAX_TRANSPARENT_WRAPPER_DEPTH && isTransparentReceiverWrapper(current); i++) { const inner = current.namedChildren?.find((c) => c !== null); if (inner === undefined || inner === null) break; current = inner;