mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(swift): type an optional field and read through its force-unwrap
Swift cannot declare a stored property with neither a type nor an initializer,
so its "declare now, assign in init" idiom is an OPTIONAL field read back
through a force-unwrap. That shape resolved nothing, and it was broken in two
independent places — each alone leaves it broken:
1. `var a: Outer?` parses as `type_annotation(optional_type(user_type(…)))`,
but the property-annotation pattern required the `user_type` to be a DIRECT
child, so an optional field was never typed at all. The pattern added here
captures the INNER `type_identifier`, so the binding is `Outer` without
relying on `stripOptional` reducing an `Outer?` spelling.
2. `self.a!` is a `postfix_expression`, which the receiver walk did not peel,
so even a typed field could not be read through the unwrap.
For (2), `postfix_expression` is NOT added to `TRANSPARENT_RECEIVER_WRAPPERS`
outright: unlike TypeScript's `non_null_expression` — which is only ever `!` —
Swift's node also carries user-defined postfix operators, which can return
anything. Peeling those would type the receiver as the operand and mint a
confidently WRONG owner, the failure mode compound-receiver.ts calls strictly
worse than no edge. So the peel is operator-gated: transparent only when the
node's text ends in `!`, which is provably type-preserving.
Verified: force-unwrap `self.a!.inner()`, optional chain `self.b?.inner()`, and
the plain annotated field all resolve; previously only the plain one did.
The gate keeps this off every other language — `postfix_expression` is not a
node type the other grammars produce here — and the full resolver + CFG suite is
green at 3166 passed / 0 failed, against a 3165 baseline.
Refs #2807
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
90c72e49f8
commit
d5d878033b
2 changed files with 44 additions and 5 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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<string, string>([
|
||||
['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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue