From 2a4d9b3a9866b072c82b2eba23313b4a98d80e03 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 11 Nov 2026 11:58:23 +0100 Subject: [PATCH 1/4] Unified: Swift subscript calls and declarations Extract subscript accesses as calls to a 'subscript' member, and subscript declarations as getter/setter 'subscript' methods marked with get/set modifiers. The setter clones the subscript parameters and takes the new value as an extra trailing positional parameter. Calls in assignment or in-place update positions now invoke setter methods, generalizing the existing property setter call logic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- unified/extractor/ast_types.yml | 8 + .../extractor/src/languages/swift/swift.rs | 228 +++++++++++- .../collections/dictionary-subscript.output | 7 +- .../swift/collections/subscript-access.output | 7 +- .../swift/expressions/subscript-call.output | 14 +- .../subscripts/subscript-assignment.output | 154 ++++++++ .../subscripts/subscript-assignment.swift | 3 + .../swift/subscripts/subscript-get-set.output | 333 ++++++++++++++++++ .../swift/subscripts/subscript-get-set.swift | 11 + .../subscripts/subscript-getter-only.output | 95 +++++ .../subscripts/subscript-getter-only.swift | 5 + .../subscript-protocol-requirement.output | 93 +++++ .../subscript-protocol-requirement.swift | 3 + .../DataFlowConsistency.ql | 6 + .../lib/codeql/unified/internal/FacadeAst.qll | 34 ++ .../unified/internal/dataflow/CallGraph.qll | 5 +- .../internal/dataflow/DataFlowCall.qll | 27 +- .../internal/dataflow/DataFlowGraph.qll | 16 +- .../dataflow/DataFlowInstantiation.qll | 8 +- .../internal/dataflow/DataFlowPluginSwift.qll | 8 + .../codeql/unified/internal/mad/LegacyMaD.qll | 1 + .../library-tests/BasicTest/test.expected | 4 + .../controlflow/basicblock-slices.expected | 56 +-- .../library-tests/dataflow/subscripts.swift | 152 ++++++++ .../test/library-tests/dataflow/test.expected | 150 ++++++++ .../test/library-tests/dataflow/test.ext.yml | 1 + .../type-inference/type-inference.expected | 2 +- 27 files changed, 1371 insertions(+), 60 deletions(-) create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.output create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.swift create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.swift create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.swift create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output create mode 100644 unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.swift create mode 100644 unified/ql/test/library-tests/dataflow/subscripts.swift diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index fb9d88333087..59d0611683f2 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -335,6 +335,14 @@ named: type: expr # A top-level function declaration. + # + # A function marked with a `get` or `set` modifier is a getter or setter + # invoked via call syntax, such as a Swift subscript accessor. A call to it + # in a read position invokes the getter, while a call in an assignment target + # or otherwise updated in place invokes the setter. The setter takes the new + # value as an extra trailing positional parameter. A getter/setter pair is + # emitted as two adjacent declarations, the second one tagged with a + # `chained_declaration` modifier. function_declaration: modifier*: modifier name_node: identifier diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 4d467539337e..b13cce9335a6 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -38,6 +38,25 @@ struct SwiftContext { /// True while translating a pattern. Optional chaining in this context is /// represented as an `Optional.some` call rather than being unwrapped. in_pattern: bool, + /// The enclosing `subscriptDecl`, set while translating its accessor + /// block so the inner `accessorDecl` rule emits `subscript` methods + /// rather than property accessors. + subscript: Option, + /// True while translating the parameters of a subscript. Unlike function + /// parameters, a subscript parameter with a single name has no argument + /// label. + in_subscript_parameters: bool, +} + +/// The parts of a `subscriptDecl` shared by its getter and setter. These are +/// kept untranslated so that each emitted method gets its own copy. +#[derive(Clone)] +struct SubscriptContext { + keyword: yeast::Id, + modifiers: Vec, + type_params: Vec, + params: Vec, + return_type: yeast::Id, } impl SwiftContext { @@ -141,6 +160,79 @@ fn member_chain( ) } +/// The accessor-specific parts of a subscript getter or setter. +struct SubscriptAccessor { + /// The `get`/`set` modifier marking the kind of accessor. + kind: yeast::OutputId, + /// Modifiers written on the accessor itself, such as `nonmutating`. + modifiers: Vec, + /// For a setter, the explicit name of its new-value parameter (`set(v)`), + /// if any, and the token at which to locate an implicit `newValue`. + setter: Option<(Option, yeast::Id)>, + /// The raw accessor body, if any. + body: Option, +} + +/// Emit a `function_declaration` named `subscript` for one accessor of a +/// subscript. The shared parts of the subscript (modifiers, generic and value +/// parameters and the element type) are translated afresh for each accessor, +/// so a getter/setter pair each get their own copy. A setter takes the new +/// value as an extra trailing positional parameter and has no return type. +fn subscript_method( + ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, + sub: SubscriptContext, + accessor: SubscriptAccessor, +) -> yeast::OutputId { + let is_chained = ctx.is_chained; + let (mods, type_params, params, element_type, body) = ctx.scoped(|ctx| { + ctx.reset(); + let mods = ctx.translate(sub.modifiers); + let type_params = ctx.translate(sub.type_params); + let element_type = ctx.translate(sub.return_type).into_iter().next(); + let body = ctx.translate(accessor.body).into_iter().next(); + ctx.in_subscript_parameters = true; + let params = ctx.translate(sub.params); + (mods, type_params, params, element_type, body) + }); + let body = body.unwrap_or_else(|| tree!((block))); + let kw = sub.keyword; + match accessor.setter { + Some((param_name, spec)) => { + let implicit_param_name = param_name.is_none().then_some("newValue"); + tree!((function_declaration + modifier: {mods} + modifier: {accessor.kind} + modifier: {accessor.modifiers} + modifier: [(modifier "chained_declaration") if { is_chained }] + name_node: (identifier #{kw}) + type_parameter: {type_params} + parameter: {params} + parameter: [ + (parameter + pattern: (identifier #{param_name}) + type: {element_type} + location: {param_name}) + (parameter + modifier: (modifier "generated") + pattern: (identifier #{implicit_param_name} location: {spec}) + type: {element_type} + location: {spec}) + ] + body: {body})) + } + None => tree!((function_declaration + modifier: {mods} + modifier: {accessor.kind} + modifier: {accessor.modifiers} + modifier: [(modifier "chained_declaration") if { is_chained }] + name_node: (identifier #{kw}) + type_parameter: {type_params} + parameter: {params} + return_type: {element_type} + body: {body})), + } +} + fn translation_rules() -> Vec> { yeast::rules! { input: "swift_node_types.yml", @@ -402,6 +494,95 @@ fn translation_rules() -> Vec> { result } ), + // ---- Subscripts ---- + // A subscript declaration becomes one `function_declaration` named + // `subscript` per accessor, tagged with a `get`/`set` modifier. A + // subscript with an explicit accessor block publishes its shared parts + // in `ctx` and lets the `accessorDecl` rule below emit one method per + // accessor; each one after the first is tagged `chained_declaration`. + rule!( + (subscriptDecl + modifiers: _* @@mods + subscriptKeyword: @@kw + genericParameterClause: (genericParameterClause parameters: _* @@type_params)? + parameterClause: (functionParameterClause parameters: _* @@params) + returnClause: (returnClause type: @@ret) + accessorBlock: (accessorBlock accessors: (accessorDecl)+ @@accessors)) + => + member* { + ctx.subscript = Some(SubscriptContext { + keyword: kw, + modifiers: mods, + type_params, + params, + return_type: ret, + }); + let mut result = Vec::new(); + for acc in accessors.into_iter() { + ctx.is_chained = !result.is_empty(); + result.extend(ctx.translate(acc)); + } + result + } + ), + // A subscript with a getter-only body (`subscript(i: Int) -> T { … }`). + rule!( + (subscriptDecl + modifiers: _* @@mods + subscriptKeyword: @@kw + genericParameterClause: (genericParameterClause parameters: _* @@type_params)? + parameterClause: (functionParameterClause parameters: _* @@params) + returnClause: (returnClause type: @@ret) + accessorBlock: _? @@body) + => + function_declaration { + let kind = match body { + Some(body) => ctx.literal_at_start_of("modifier", "get", body), + None => ctx.literal_at_start_of("modifier", "get", kw), + }; + let sub = SubscriptContext { + keyword: kw, + modifiers: mods, + type_params, + params, + return_type: ret, + }; + ctx.is_chained = false; + subscript_method(&mut ctx, sub, SubscriptAccessor { + kind, + modifiers: Vec::new(), + setter: None, + body, + }) + } + ), + // An accessor of a subscript (see above). Accessor kinds other than + // `get` and `set` (such as `_modify`) are not supported. + rule!( + (accessorDecl + modifiers: _* @acc_mods + accessorSpecifier: @@spec + parameters: (accessorParameters name: @@param_name)? + body: _? @@body) + where ctx.subscript.is_some() + => + member { + let sub = ctx.subscript.clone().expect("checked by guard"); + let spec_text = ctx.source_text(spec); + if spec_text == "get" || spec_text == "set" { + let setter = (spec_text == "set").then_some((param_name, spec)); + let kind = tree!((modifier #{spec})); + subscript_method(&mut ctx, sub, SubscriptAccessor { + kind, + modifiers: acc_mods, + setter, + body, + }) + } else { + tree!((unsupported_node "")) + } + } + ), // Each `accessorDecl` becomes an `accessor_declaration`, reading the // property name/type and the modifiers from `ctx`. The // accessor kind comes straight from the specifier keyword @@ -668,6 +849,27 @@ fn translation_rules() -> Vec> { return_type: {ret} body: (block)) ), + // A subscript parameter with a single name has no argument label + // (`subscript(i: Int)` is called as `x[0]`). It is translated in a reset + // context so its type and default value are not affected. + rule!( + (functionParameter + firstName: @@first + secondName: _? @@second + type: @@ty + defaultValue: (initializerClause value: @@val)?) + where ctx.in_subscript_parameters && second.is_none() + => + parameter { + ctx.reset(); + let ty = ctx.translate(ty).into_iter().next(); + let val = ctx.translate(val).into_iter().next(); + tree!((parameter + pattern: {parameter_pattern(&mut ctx, first)} + type: {ty} + default: {val})) + } + ), // A function parameter. With two names (`firstName`+`secondName`) the // first is the external argument label and the second the internal name; // with one name it is both the external and internal name. @@ -1054,22 +1256,29 @@ fn translation_rules() -> Vec> { => (key_value_pair key: {key} value: {value}) ), - // A subscript access (`xs[0]`) is modelled as a call. swift-syntax does - // report a distinct `subscriptCallExpr`, so giving - // subscripts their own shape needs only a `subscript_expr` node in - // ast_types.yml and a remap here. + // A subscript access (`xs[0]`) is modelled as a call to a member named + // `subscript` on the subscripted value, i.e. `xs.subscript(0)`. This + // matches the `subscript` methods emitted for `subscriptDecl`s. rule!( (subscriptCallExpr calledExpression: @callee + leftSquare: @@left_square arguments: _* @args trailingClosure: _? @trailing additionalTrailingClosures: _* @additional) => - (call_expr - callee: {callee} - argument: {args} - argument: {trailing.map(|trailing| tree!((argument value: {trailing})))} - argument: {additional}) + call_expr { + let member = tree!((member_access_expr + base: {callee} + member_name_node: (identifier "subscript" location: {left_square}) + location: {callee} + location: {left_square})); + tree!((call_expr + callee: {member} + argument: {args} + argument: {trailing.map(|trailing| tree!((argument value: {trailing})))} + argument: {additional})) + } ), // ---- Optionals and errors ---- // Optional chaining — unwrap the marker @@ -1569,7 +1778,6 @@ fn translation_rules() -> Vec> { rule!((poundSourceLocation) => (unsupported_node "")), rule!((precedenceGroupDecl) => (unsupported_node "")), rule!((someOrAnyType) => (unsupported_node "")), - rule!((subscriptDecl) => (unsupported_node "")), rule!((suppressedType) => (unsupported_node "")), rule!((typeExpr) => (unsupported_node "")), rule!((unsafeExpr) => (unsupported_node "")), diff --git a/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output b/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output index b7719365bbc6..08711dcbfc8d 100644 --- a/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output +++ b/unified/extractor/tests/corpus/swift/collections/dictionary-subscript.output @@ -49,8 +49,11 @@ top_level source="⟨body⟩" modifier: modifier "let" source="let" pattern: identifier "v" source="v" value: - call_expr source="⟨callee⟩[⟨argument⟩]" - callee: identifier "d" source="d" + call_expr source="⟨callee⟩⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "d" source="d" + member_name_node: identifier "subscript" source="[" argument: argument source="⟨value⟩" value: string_literal "\"key\"" source="\"key\"" diff --git a/unified/extractor/tests/corpus/swift/collections/subscript-access.output b/unified/extractor/tests/corpus/swift/collections/subscript-access.output index 771004ff717f..2681054a0972 100644 --- a/unified/extractor/tests/corpus/swift/collections/subscript-access.output +++ b/unified/extractor/tests/corpus/swift/collections/subscript-access.output @@ -46,8 +46,11 @@ top_level source="⟨body⟩" modifier: modifier "let" source="let" pattern: identifier "first" source="first" value: - call_expr source="⟨callee⟩[⟨argument⟩]" - callee: identifier "xs" source="xs" + call_expr source="⟨callee⟩⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "xs" source="xs" + member_name_node: identifier "subscript" source="[" argument: argument source="⟨value⟩" value: int_literal "0" source="0" diff --git a/unified/extractor/tests/corpus/swift/expressions/subscript-call.output b/unified/extractor/tests/corpus/swift/expressions/subscript-call.output index 138caa29eb62..351cc6441714 100644 --- a/unified/extractor/tests/corpus/swift/expressions/subscript-call.output +++ b/unified/extractor/tests/corpus/swift/expressions/subscript-call.output @@ -99,13 +99,19 @@ top_level source="⟨body⟩" body: block source="⟨stmt⟩\n\n⟨stmt⟩" stmt: - call_expr source="⟨callee⟩[⟨argument⟩]" - callee: identifier "handlers" source="handlers" + call_expr source="⟨callee⟩⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "handlers" source="handlers" + member_name_node: identifier "subscript" source="[" argument: argument source="⟨value⟩" value: int_literal "0" source="0" - call_expr source="⟨callee⟩[⟨argument⟩] ⟨argument⟩ ⟨argument⟩ ⟨argument⟩" - callee: identifier "handlers" source="handlers" + call_expr source="⟨callee⟩⟨argument⟩] ⟨argument⟩ ⟨argument⟩ ⟨argument⟩" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "handlers" source="handlers" + member_name_node: identifier "subscript" source="[" argument: argument source="⟨value⟩" value: int_literal "0" source="0" diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.output new file mode 100644 index 000000000000..d84fd8cebd41 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.output @@ -0,0 +1,154 @@ +xs[0] = 1 +xs[at: 1, 2] += 3 +m["a"]["b"] = c + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + infixOperatorExpr + operator: + assignmentExpr + equal: = + leftOperand: + subscriptCallExpr + leftSquare: [ + rightSquare: ] + arguments: + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "0" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "xs" + rightOperand: + integerLiteralExpr + literal: integerLiteral "1" + codeBlockItem + item: + infixOperatorExpr + operator: + binaryOperatorExpr + operator: binaryOperator "+=" + leftOperand: + subscriptCallExpr + leftSquare: [ + rightSquare: ] + arguments: + labeledExpr + colon: : + label: identifier "at" + expression: + integerLiteralExpr + literal: integerLiteral "1" + trailingComma: , + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "2" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "xs" + rightOperand: + integerLiteralExpr + literal: integerLiteral "3" + codeBlockItem + item: + infixOperatorExpr + operator: + assignmentExpr + equal: = + leftOperand: + subscriptCallExpr + leftSquare: [ + rightSquare: ] + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "b" + additionalTrailingClosures: + calledExpression: + subscriptCallExpr + leftSquare: [ + rightSquare: ] + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "a" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "m" + rightOperand: + declReferenceExpr + baseName: identifier "c" + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩\n⟨stmt⟩\n⟨stmt⟩" + stmt: + binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩" + left: + call_expr source="⟨callee⟩⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "xs" source="xs" + member_name_node: identifier "subscript" source="[" + argument: + argument source="⟨value⟩" + value: int_literal "0" source="0" + operator: infix_operator "=" source="=" + right: int_literal "1" source="1" + binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩" + left: + call_expr source="⟨callee⟩⟨argument⟩, ⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "xs" source="xs" + member_name_node: identifier "subscript" source="[" + argument: + argument source="⟨name_node⟩: ⟨value⟩" + name_node: identifier "at" source="at" + value: int_literal "1" source="1" + argument source="⟨value⟩" + value: int_literal "2" source="2" + operator: infix_operator "+=" source="+=" + right: int_literal "3" source="3" + binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩" + left: + call_expr source="⟨callee⟩⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: + call_expr source="⟨callee⟩⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "m" source="m" + member_name_node: identifier "subscript" source="[" + argument: + argument source="⟨value⟩" + value: string_literal "\"a\"" source="\"a\"" + member_name_node: identifier "subscript" source="[" + argument: + argument source="⟨value⟩" + value: string_literal "\"b\"" source="\"b\"" + operator: infix_operator "=" source="=" + right: identifier "c" source="c" diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.swift b/unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.swift new file mode 100644 index 000000000000..a34d687f4565 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-assignment.swift @@ -0,0 +1,3 @@ +xs[0] = 1 +xs[at: 1, 2] += 3 +m["a"]["b"] = c diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output new file mode 100644 index 000000000000..8af39886d37c --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output @@ -0,0 +1,333 @@ +struct S { + var data: [Int] + subscript(at index: Int, _ other: Int) -> Int { + get { data[index] } + nonmutating set(v) { print(v) } + } + static subscript(key: T) -> T { + set { print(newValue) } + get { key } + } +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + structDecl + attributes: + modifiers: + name: identifier "S" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + memberBlockItem + decl: + variableDecl + attributes: + modifiers: + bindingSpecifier: var + bindings: + patternBinding + pattern: + identifierPattern + identifier: identifier "data" + typeAnnotation: + typeAnnotation + colon: : + type: + arrayType + leftSquare: [ + rightSquare: ] + element: + identifierType + name: identifier "Int" + memberBlockItem + decl: + subscriptDecl + attributes: + modifiers: + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + functionParameter + colon: : + attributes: + modifiers: + trailingComma: , + type: + identifierType + name: identifier "Int" + firstName: identifier "at" + secondName: identifier "index" + functionParameter + colon: : + attributes: + modifiers: + type: + identifierType + name: identifier "Int" + firstName: _ + secondName: identifier "other" + returnClause: + returnClause + arrow: -> + type: + identifierType + name: identifier "Int" + accessorBlock: + accessorBlock + accessors: + accessorDecl + accessorSpecifier: get + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + subscriptCallExpr + leftSquare: [ + rightSquare: ] + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "index" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "data" + modifiers: + accessorDecl + accessorSpecifier: set + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "v" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + modifiers: + declModifier + name: nonmutating + parameters: + accessorParameters + leftParen: ( + rightParen: ) + name: identifier "v" + leftBrace: { + rightBrace: } + subscriptKeyword: subscript + memberBlockItem + decl: + subscriptDecl + attributes: + modifiers: + declModifier + name: static + genericParameterClause: + genericParameterClause + parameters: + genericParameter + attributes: + name: identifier "T" + leftAngle: < + rightAngle: > + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + functionParameter + colon: : + attributes: + modifiers: + type: + identifierType + name: identifier "T" + firstName: identifier "key" + returnClause: + returnClause + arrow: -> + type: + identifierType + name: identifier "T" + accessorBlock: + accessorBlock + accessors: + accessorDecl + accessorSpecifier: set + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "newValue" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + modifiers: + accessorDecl + accessorSpecifier: get + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + declReferenceExpr + baseName: identifier "key" + modifiers: + leftBrace: { + rightBrace: } + subscriptKeyword: subscript + structKeyword: struct + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + class_like_declaration source="⟨modifier⟩ ⟨name_node⟩ {\n ⟨member⟩\n ⟨member⟩⟨member⟩\n }\n ⟨member⟩⟨member⟩\n }\n}" + modifier: modifier "struct" source="struct" + name_node: identifier "S" source="S" + member: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩: ⟨type⟩" + modifier: modifier "var" source="var" + pattern: identifier "data" source="data" + type: + generic_type_expr source="[⟨type_argument⟩]" + base: identifier "Array" source="" + type_argument: identifier "Int" source="Int" + function_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> ⟨return_type⟩ {\n ⟨modifier⟩ ⟨body⟩" + modifier: modifier "get" source="get" + name_node: identifier "subscript" source="subscript" + parameter: + parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "at" source="at" + type: identifier "Int" source="Int" + pattern: identifier "index" source="index" + parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "_" source="_" + type: identifier "Int" source="Int" + pattern: identifier "other" source="other" + return_type: identifier "Int" source="Int" + body: + block source="{ ⟨stmt⟩ }" + stmt: + call_expr source="⟨callee⟩⟨argument⟩]" + callee: + member_access_expr source="⟨base⟩⟨member_name_node⟩" + base: identifier "data" source="data" + member_name_node: identifier "subscript" source="[" + argument: + argument source="⟨value⟩" + value: identifier "index" source="index" + function_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> Int {\n get { data[index] }\n ⟨modifier⟩ ⟨modifier⟩(⟨parameter⟩) ⟨body⟩" + modifier: + modifier "set" source="set" + modifier "nonmutating" source="nonmutating" + modifier "chained_declaration" source="" + name_node: identifier "subscript" source="subscript" + parameter: + parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "at" source="at" + type: identifier "Int" source="Int" + pattern: identifier "index" source="index" + parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "_" source="_" + type: identifier "Int" source="Int" + pattern: identifier "other" source="other" + parameter source="⟨pattern⟩" + type: identifier "Int" source="Int" (external) + pattern: identifier "v" source="v" + body: + block source="{ ⟨stmt⟩ }" + stmt: + call_expr source="⟨callee⟩(⟨argument⟩)" + callee: identifier "print" source="print" + argument: + argument source="⟨value⟩" + value: identifier "v" source="v" + function_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> T {\n ⟨modifier⟩⟨parameter⟩ ⟨body⟩" + modifier: + modifier "static" source="static" + modifier "set" source="set" + name_node: identifier "subscript" source="subscript" + type_parameter: + type_parameter source="⟨name_node⟩" + name_node: identifier "T" source="T" + parameter: + parameter source="⟨pattern⟩: ⟨type⟩" + type: identifier "T" source="T" + pattern: identifier "key" source="key" + parameter source="⟨pattern⟩" + modifier: modifier "generated" source="" + type: identifier "T" source="T" (external) + pattern: identifier "newValue" source="set" + body: + block source="{ ⟨stmt⟩ }" + stmt: + call_expr source="⟨callee⟩(⟨argument⟩)" + callee: identifier "print" source="print" + argument: + argument source="⟨value⟩" + value: identifier "newValue" source="newValue" + function_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> ⟨return_type⟩ {\n set { print(newValue) }\n ⟨modifier⟩ ⟨body⟩" + modifier: + modifier "static" source="static" + modifier "get" source="get" + modifier "chained_declaration" source="" + name_node: identifier "subscript" source="subscript" + type_parameter: + type_parameter source="⟨name_node⟩" + name_node: identifier "T" source="T" + parameter: + parameter source="⟨pattern⟩: ⟨type⟩" + type: identifier "T" source="T" + pattern: identifier "key" source="key" + return_type: identifier "T" source="T" + body: + block source="{ ⟨stmt⟩ }" + stmt: identifier "key" source="key" + constructor_declaration source="" + modifier: modifier "generated" source="" + name_node: identifier "init" source="" + body: block source="" diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.swift b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.swift new file mode 100644 index 000000000000..f97ac3907068 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.swift @@ -0,0 +1,11 @@ +struct S { + var data: [Int] + subscript(at index: Int, _ other: Int) -> Int { + get { data[index] } + nonmutating set(v) { print(v) } + } + static subscript(key: T) -> T { + set { print(newValue) } + get { key } + } +} diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output new file mode 100644 index 000000000000..f1fe54827799 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output @@ -0,0 +1,95 @@ +struct S { + subscript(i: Int) -> Int { + i * 2 + } +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + structDecl + attributes: + modifiers: + name: identifier "S" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + memberBlockItem + decl: + subscriptDecl + attributes: + modifiers: + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + functionParameter + colon: : + attributes: + modifiers: + type: + identifierType + name: identifier "Int" + firstName: identifier "i" + returnClause: + returnClause + arrow: -> + type: + identifierType + name: identifier "Int" + accessorBlock: + accessorBlock + accessors: + codeBlockItem + item: + infixOperatorExpr + operator: + binaryOperatorExpr + operator: binaryOperator "*" + leftOperand: + declReferenceExpr + baseName: identifier "i" + rightOperand: + integerLiteralExpr + literal: integerLiteral "2" + leftBrace: { + rightBrace: } + subscriptKeyword: subscript + structKeyword: struct + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + class_like_declaration source="⟨modifier⟩ ⟨name_node⟩ {\n ⟨member⟩\n}" + modifier: modifier "struct" source="struct" + name_node: identifier "S" source="S" + member: + function_declaration source="⟨name_node⟩(⟨parameter⟩) -> ⟨return_type⟩ ⟨body⟩" + modifier: modifier "get" source="" + name_node: identifier "subscript" source="subscript" + parameter: + parameter source="⟨pattern⟩: ⟨type⟩" + type: identifier "Int" source="Int" + pattern: identifier "i" source="i" + return_type: identifier "Int" source="Int" + body: + block source="{\n ⟨stmt⟩\n }" + stmt: + binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩" + left: identifier "i" source="i" + operator: infix_operator "*" source="*" + right: int_literal "2" source="2" + constructor_declaration source="" + modifier: modifier "generated" source="" + name_node: identifier "init" source="" + body: block source="" diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.swift b/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.swift new file mode 100644 index 000000000000..8d0e072f85df --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.swift @@ -0,0 +1,5 @@ +struct S { + subscript(i: Int) -> Int { + i * 2 + } +} diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output new file mode 100644 index 000000000000..a0597c194b68 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output @@ -0,0 +1,93 @@ +protocol P { + subscript(i: Int) -> String { get set } +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + protocolDecl + attributes: + modifiers: + name: identifier "P" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + memberBlockItem + decl: + subscriptDecl + attributes: + modifiers: + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + functionParameter + colon: : + attributes: + modifiers: + type: + identifierType + name: identifier "Int" + firstName: identifier "i" + returnClause: + returnClause + arrow: -> + type: + identifierType + name: identifier "String" + accessorBlock: + accessorBlock + accessors: + accessorDecl + accessorSpecifier: get + attributes: + modifiers: + accessorDecl + accessorSpecifier: set + attributes: + modifiers: + leftBrace: { + rightBrace: } + subscriptKeyword: subscript + protocolKeyword: protocol + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + class_like_declaration source="⟨modifier⟩ ⟨name_node⟩ {\n ⟨member⟩⟨member⟩ }\n}" + modifier: modifier "protocol" source="protocol" + name_node: identifier "P" source="P" + member: + function_declaration source="⟨name_node⟩(⟨parameter⟩) -> ⟨return_type⟩ { ⟨modifier⟩" + modifier: modifier "get" source="get" + name_node: identifier "subscript" source="subscript" + parameter: + parameter source="⟨pattern⟩: ⟨type⟩" + type: identifier "Int" source="Int" + pattern: identifier "i" source="i" + return_type: identifier "String" source="String" + body: block source="" + function_declaration source="⟨name_node⟩(⟨parameter⟩) -> String { get ⟨modifier⟩⟨parameter⟩" + modifier: + modifier "set" source="set" + modifier "chained_declaration" source="" + name_node: identifier "subscript" source="subscript" + parameter: + parameter source="⟨pattern⟩: ⟨type⟩" + type: identifier "Int" source="Int" + pattern: identifier "i" source="i" + parameter source="⟨pattern⟩" + modifier: modifier "generated" source="" + type: identifier "String" source="String" (external) + pattern: identifier "newValue" source="set" + body: block source="" diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.swift b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.swift new file mode 100644 index 000000000000..a11dd62de0de --- /dev/null +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.swift @@ -0,0 +1,3 @@ +protocol P { + subscript(i: Int) -> String { get set } +} diff --git a/unified/ql/consistency-queries/DataFlowConsistency.ql b/unified/ql/consistency-queries/DataFlowConsistency.ql index 71cd02531030..a57ab33e5743 100644 --- a/unified/ql/consistency-queries/DataFlowConsistency.ql +++ b/unified/ql/consistency-queries/DataFlowConsistency.ql @@ -21,6 +21,12 @@ module ConsistencyInput implements InputSig { // In summaries, 'inout' parameters result in flow directly into the post-update node for an argument n instanceof TSummaryNode } + + predicate multipleArgumentCallExclude(DataFlowInput::ArgumentNode arg, DataFlowCall call) { + // When a call expression is updated in place, such as `x[i]` in `x[i] += y`, it invokes both + // a getter and a setter, which share the explicit arguments. + exists(CallExpr e | e = call.asSetterCall() | arg.asExpr() = e.getAnArgument().getValue()) + } } module ConsistencyOutput = diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index d17e832d4202..c632259cd84c 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -475,6 +475,40 @@ module Unified { SetterDeclaration() { this.getAccessorKind().getValue() = "set" } } + /** + * A function declaration marked with a `get` modifier, representing a getter that is invoked + * via call syntax when the call appears in a position where its value is read. + * + * It is typically paired with a `SetterMethodDeclaration` of the same name. For example, in Swift + * the getter of a subscript declaration: + * ```swift + * subscript(i: Int) -> String { + * get { "42" } // getter method declaration + * set { print(newValue) } + * } + * ``` + */ + final class GetterMethodDeclaration extends FunctionDeclaration { + GetterMethodDeclaration() { this.hasModifier("get") } + } + + /** + * A function declaration marked with a `set` modifier, representing a setter that is invoked + * via call syntax when the call appears in an assignment target or is otherwise updated in place. + * + * The new value is passed as an additional trailing positional parameter. For example, in Swift + * the setter of a subscript declaration: + * ```swift + * subscript(i: Int) -> String { + * get { "42" } + * set { print(newValue) } // setter method declaration, with parameters `i` and `newValue` + * } + * ``` + */ + final class SetterMethodDeclaration extends FunctionDeclaration { + SetterMethodDeclaration() { this.hasModifier("set") } + } + /** An `if` statement or conditional expression. */ class IfExpr extends G::IfExpr { /** Gets then `then` or `else` branch. */ diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll b/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll index b035a03c817a..8275dcf0afd8 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll @@ -5,11 +5,14 @@ private import codeql.unified.internal.NameBinding as N private import codeql.unified.internal.ExprPositions DataFlowCallable viableCallable(DataFlowCall c) { - result.asSourceCallable() = T::resolveCallTarget(c.asExplicitCall()) + result.asSourceCallable() = T::resolveCallTarget(c.asExplicitCall()) and + not result.asSourceCallable() instanceof SetterMethodDeclaration or result.asSourceCallable() = T::resolveAccessorAccess(c.asGetterCall()).(GetterDeclaration) or result.asSourceCallable() = T::resolveAccessorAccess(c.asSetterCall()).(SetterDeclaration) or + result.asSourceCallable() = T::resolveCallTarget(c.asSetterCall()).(SetterMethodDeclaration) + or result.asSummarizedCallable().matchesCall(c.asExplicitCall()) } diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll index 3d4b32e191fc..290461771026 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll @@ -6,7 +6,8 @@ private import codeql.unified.internal.typeinference.TypeInference as T private newtype TDataFlowCall = TExplicitCall(CallExpr call) { - not isInBindingContext(call, _) // ignore constructor patterns + not isInBindingContext(call, _) and // ignore constructor patterns + not isInAssignmentContext(call, any(AssignExpr a)) // only invokes a setter } or TGetterCall(Expr expr) { hasResultValue(expr) and @@ -20,7 +21,11 @@ private newtype TDataFlowCall = hasPostUpdateAfterCallOrAssign(expr, _) and mayWriteBackViaSetterCall(expr) ) and - T::resolveAccessorAccess(expr) instanceof SetterDeclaration + ( + T::resolveAccessorAccess(expr) instanceof SetterDeclaration + or + T::resolveCallTarget(expr) instanceof SetterMethodDeclaration + ) } or TSummaryCall(SummarizedCallable callable, SummaryNode callee) { FlowSummaryOutput::summaryCallbackRange(callable, callee) @@ -39,9 +44,25 @@ class DataFlowCall extends TDataFlowCall { /** Gets the member/variable access for which this represents the call to a getter. */ Expr asGetterCall() { this = TGetterCall(result) } - /** Gets the member/variable access for which this represents the call to a setter. */ + /** + * Gets the member/variable access or call expression for which this represents the call to a setter. + * + * A call expression invokes a setter when it is the target of an assignment or is otherwise updated in place, + * such as `x[i]` in `x[i] = y` or `x[i].f = y` in Swift. + */ Expr asSetterCall() { this = TSetterCall(result) } + /** + * Gets the call expression that syntactically represents this call, if any. + * + * This is either an explicit call or a call expression that invokes a setter. + */ + CallExpr getCallExpr() { + result = this.asExplicitCall() + or + result = this.asSetterCall() + } + /** Holds if this is a synthesized call to `callee` in `callable`. */ predicate isSummaryCall(SummarizedCallable callable, SummaryNode callee) { this = TSummaryCall(callable, callee) diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll index d804e790b7a7..6c8f577f5d7d 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll @@ -24,14 +24,14 @@ predicate step(BuilderNode node1, Step step, BuilderNode node2) { node2.isLocalVariableWrite(callable, getImplicitReceiverVariable(callable)) ) or - exists(CallExpr call, Expr receiverExpr | - receiverExpr = call.getCallee().(MemberAccessExpr).getBase() + exists(DataFlowCall call, Expr receiverExpr | + receiverExpr = call.getCallExpr().getCallee().(MemberAccessExpr).getBase() | node1.isResultValue(receiverExpr) and step.value() and - node2.isReceiverArgument(call) + node2.isReceiverArgumentEx(call) or - node1.isReceiverPostUpdate(call) and + node1.isReceiverPostUpdateEx(call) and step.value() and node2.isPostUpdate(receiverExpr) ) @@ -46,12 +46,14 @@ predicate step(BuilderNode node1, Step step, BuilderNode node2) { node2.isPostUpdate(call.getCallee()) ) or - exists(CallExpr call, UnqualifiedMemberAccess callee | callee = call.getCallee() | + exists(DataFlowCall call, UnqualifiedMemberAccess callee | + callee = call.getCallExpr().getCallee() + | node1.isLocalVariableRead(callee, callee.getImplicitQualifierVariable()) and step.value() and - node2.isReceiverArgument(call) + node2.isReceiverArgumentEx(call) or - node1.isReceiverPostUpdate(call) and + node1.isReceiverPostUpdateEx(call) and step.value() and node2.isLocalVariablePostUpdate(callee, callee.getImplicitQualifierVariable()) ) diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll index 1c18b907c57c..c5785a2b0b16 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll @@ -98,7 +98,7 @@ module DataFlowInput implements InputSig { private predicate isArgumentNodeImpl(Node n, DataFlowCall call, ArgumentPosition pos) { exists(Argument arg | n.asExpr() = arg.getValue() and - call.asExplicitCall().getAnArgument() = arg + call.getCallExpr().getAnArgument() = arg | pos.asPositional() = arg.getPositionalIndex() or @@ -108,7 +108,11 @@ module DataFlowInput implements InputSig { n.(BuilderNode).isImplicitArgument(call, pos, false) or n.(BuilderNode).isSetterValueArgument(call) and - pos.asPositional() = 0 + // The new value is passed after the positional arguments, if any + pos.asPositional() = + count(Argument arg | + arg = call.asSetterCall().(CallExpr).getAnArgument() and arg.isPositional() + ) or exists(FlowSummaryOutput::SummaryNode callee | FlowSummaryOutput::summaryArgumentNode(callee, getSummaryNode(n), pos) and diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll index f845a5f20f3c..8c880548f65e 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll @@ -131,4 +131,12 @@ private predicate isInWriteBackPosition(Expr expr, boolean direct) { expr = member.getBase() and direct = false ) + or + // A call in write-back position, such as `x.p[i]` in `x.p[i] = y`, is a call to a setter, + // which mutates its receiver. Propagate to the callee so the receiver `x.p` is also written back. + exists(CallExpr call | + isInWriteBackPosition(call, _) and + expr = call.getCallee() and + direct = false + ) } diff --git a/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll b/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll index 09da505b7d0b..139b7307cecf 100644 --- a/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll +++ b/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll @@ -116,6 +116,7 @@ private predicate callableSelector( FunctionDeclaration callable, string type, string name, string argLabels ) { name = callable.getName() and + not callable instanceof SetterMethodDeclaration and // models refer to the getter, e.g. of a subscript argLabels = getArgLabelsFromCallable(callable) and exists(ClassLikeDeclaration cls | callable = cls.getAMemberIncludingExtensions() and diff --git a/unified/ql/test/library-tests/BasicTest/test.expected b/unified/ql/test/library-tests/BasicTest/test.expected index 95d54935f8ee..a512eea0164b 100644 --- a/unified/ql/test/library-tests/BasicTest/test.expected +++ b/unified/ql/test/library-tests/BasicTest/test.expected @@ -90,6 +90,7 @@ identifier | test.swift:48:37:48:40 | data | data | | test.swift:48:42:48:46 | count | count | | test.swift:49:16:49:19 | data | data | +| test.swift:49:20:49:20 | subscript | subscript | | test.swift:49:21:49:25 | index | index | | test.swift:52:10:52:12 | add | add | | test.swift:52:14:52:14 | _ | _ | @@ -131,8 +132,10 @@ identifier | test.swift:75:13:75:13 | i | i | | test.swift:75:23:75:27 | count | count | | test.swift:76:16:76:19 | self | self | +| test.swift:76:20:76:20 | subscript | subscript | | test.swift:76:21:76:21 | i | i | | test.swift:76:26:76:29 | self | self | +| test.swift:76:30:76:30 | subscript | subscript | | test.swift:76:31:76:31 | i | i | | test.swift:85:6:85:12 | combine | combine | | test.swift:85:14:85:14 | T | T | @@ -153,6 +156,7 @@ identifier | test.swift:87:19:87:27 | dropFirst | dropFirst | | test.swift:87:31:87:36 | reduce | reduce | | test.swift:87:38:87:43 | values | values | +| test.swift:87:44:87:44 | subscript | subscript | | test.swift:87:49:87:57 | transform | transform | | test.swift:90:5:90:9 | tuple | tuple | | test.swift:91:5:91:10 | unary1 | unary1 | diff --git a/unified/ql/test/library-tests/controlflow/basicblock-slices.expected b/unified/ql/test/library-tests/controlflow/basicblock-slices.expected index de0738748fb9..c5cdbc7a71e6 100644 --- a/unified/ql/test/library-tests/controlflow/basicblock-slices.expected +++ b/unified/ql/test/library-tests/controlflow/basicblock-slices.expected @@ -245,32 +245,32 @@ | 288 | cfg.swift:288:1:319:1 | func testSubscriptExpr | 'func testSubscriptExpr -V testSubscriptExpr' | | 288 | cfg.swift:288:55:319:1 | Block | 'Block' | | 289 | cfg.swift:289:3:289:44 | var a | 'var a -V a -> 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -^ ArrayLiteral' | -| 290 | cfg.swift:290:3:290:3 | a | 'a -> Argument -V 0 -^ a(...) -> 0 -^ ... = ...' | -| 291 | cfg.swift:291:3:291:3 | a | 'a -> Argument -V 1 -^ a(...) -> 1 -^ ... += ...' | -| 292 | cfg.swift:292:3:292:3 | a | 'a -> Argument -V 2 -^ a(...) -> 1 -^ ... -= ...' | -| 293 | cfg.swift:293:3:293:3 | a | 'a -> Argument -V 3 -^ a(...) -> 1 -^ ... *= ...' | -| 294 | cfg.swift:294:3:294:3 | a | 'a -> Argument -V 4 -^ a(...) -> 1 -^ ... /= ...' | -| 295 | cfg.swift:295:3:295:3 | a | 'a -> Argument -V 5 -^ a(...) -> 1 -^ ... %= ...' | -| 296 | cfg.swift:296:3:296:3 | a | 'a -> Argument -V 6 -^ a(...) -> 1 -^ ... &= ...' | -| 297 | cfg.swift:297:3:297:3 | a | 'a -> Argument -V 7 -^ a(...) -> 1 -^ ... \|= ...' | -| 298 | cfg.swift:298:3:298:3 | a | 'a -> Argument -V 8 -^ a(...) -> 1 -^ ... ^= ...' | -| 299 | cfg.swift:299:3:299:3 | a | 'a -> Argument -V 9 -^ a(...) -> 1 -^ ... <<= ...' | -| 300 | cfg.swift:300:3:300:3 | a | 'a -> Argument -V 10 -^ a(...) -> 1 -^ ... >>= ...' | -| 302 | cfg.swift:302:3:302:49 | var tupleWithA | 'var tupleWithA -V tupleWithA -> Argument -V a -> Argument -V 0 -^ a(...) -> Argument -V a -> Argument -V 1 -^ a(...) -> Argument -V a -> Argument -V 2 -^ a(...) -> Argument -V a -> Argument -V 3 -^ a(...) -> Argument -V a -> Argument -V 4 -^ a(...) -^ TupleExpr' | +| 290 | cfg.swift:290:3:290:3 | a | 'a -^ ... .subscript -> Argument -V 0 -^ ... .subscript(...) -> 0 -^ ... = ...' | +| 291 | cfg.swift:291:3:291:3 | a | 'a -^ ... .subscript -> Argument -V 1 -^ ... .subscript(...) -> 1 -^ ... += ...' | +| 292 | cfg.swift:292:3:292:3 | a | 'a -^ ... .subscript -> Argument -V 2 -^ ... .subscript(...) -> 1 -^ ... -= ...' | +| 293 | cfg.swift:293:3:293:3 | a | 'a -^ ... .subscript -> Argument -V 3 -^ ... .subscript(...) -> 1 -^ ... *= ...' | +| 294 | cfg.swift:294:3:294:3 | a | 'a -^ ... .subscript -> Argument -V 4 -^ ... .subscript(...) -> 1 -^ ... /= ...' | +| 295 | cfg.swift:295:3:295:3 | a | 'a -^ ... .subscript -> Argument -V 5 -^ ... .subscript(...) -> 1 -^ ... %= ...' | +| 296 | cfg.swift:296:3:296:3 | a | 'a -^ ... .subscript -> Argument -V 6 -^ ... .subscript(...) -> 1 -^ ... &= ...' | +| 297 | cfg.swift:297:3:297:3 | a | 'a -^ ... .subscript -> Argument -V 7 -^ ... .subscript(...) -> 1 -^ ... \|= ...' | +| 298 | cfg.swift:298:3:298:3 | a | 'a -^ ... .subscript -> Argument -V 8 -^ ... .subscript(...) -> 1 -^ ... ^= ...' | +| 299 | cfg.swift:299:3:299:3 | a | 'a -^ ... .subscript -> Argument -V 9 -^ ... .subscript(...) -> 1 -^ ... <<= ...' | +| 300 | cfg.swift:300:3:300:3 | a | 'a -^ ... .subscript -> Argument -V 10 -^ ... .subscript(...) -> 1 -^ ... >>= ...' | +| 302 | cfg.swift:302:3:302:49 | var tupleWithA | 'var tupleWithA -V tupleWithA -> Argument -V a -^ ... .subscript -> Argument -V 0 -^ ... .subscript(...) -> Argument -V a -^ ... .subscript -> Argument -V 1 -^ ... .subscript(...) -> Argument -V a -^ ... .subscript -> Argument -V 2 -^ ... .subscript(...) -> Argument -V a -^ ... .subscript -> Argument -V 3 -^ ... .subscript(...) -> Argument -V a -^ ... .subscript -> Argument -V 4 -^ ... .subscript(...) -^ TupleExpr' | | 304 | cfg.swift:304:3:304:48 | var b | 'var b -V b -> 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -^ ArrayLiteral' | -| 305 | cfg.swift:305:3:305:3 | b | 'b -> Argument -V 0 -^ b(...) -> a -> Argument -V 10 -^ a(...) -^ ... = ...' | -| 306 | cfg.swift:306:3:306:3 | b | 'b -> Argument -V 1 -^ b(...) -> b -> Argument -V 0 -^ b(...) -> 1 -^ ... + ... -^ ... = ...' | -| 307 | cfg.swift:307:3:307:3 | b | 'b -> Argument -V 2 -^ b(...) -> b -> Argument -V 1 -^ b(...) -> 1 -^ ... - ... -^ ... = ...' | -| 308 | cfg.swift:308:3:308:3 | b | 'b -> Argument -V 3 -^ b(...) -> b -> Argument -V 2 -^ b(...) -> 1 -^ ... * ... -^ ... = ...' | -| 309 | cfg.swift:309:3:309:3 | b | 'b -> Argument -V 4 -^ b(...) -> b -> Argument -V 3 -^ b(...) -> 1 -^ ... / ... -^ ... = ...' | -| 310 | cfg.swift:310:3:310:3 | b | 'b -> Argument -V 5 -^ b(...) -> b -> Argument -V 4 -^ b(...) -> 1 -^ ... % ... -^ ... = ...' | -| 311 | cfg.swift:311:3:311:3 | b | 'b -> Argument -V 6 -^ b(...) -> b -> Argument -V 5 -^ b(...) -> 1 -^ ... & ... -^ ... = ...' | -| 312 | cfg.swift:312:3:312:3 | b | 'b -> Argument -V 7 -^ b(...) -> b -> Argument -V 6 -^ b(...) -> 1 -^ ... \| ... -^ ... = ...' | -| 313 | cfg.swift:313:3:313:3 | b | 'b -> Argument -V 8 -^ b(...) -> b -> Argument -V 7 -^ b(...) -> 1 -^ ... ^ ... -^ ... = ...' | -| 314 | cfg.swift:314:3:314:3 | b | 'b -> Argument -V 9 -^ b(...) -> b -> Argument -V 8 -^ b(...) -> 1 -^ ... << ... -^ ... = ...' | -| 315 | cfg.swift:315:3:315:3 | b | 'b -> Argument -V 10 -^ b(...) -> b -> Argument -V 9 -^ b(...) -> 1 -^ ... >> ... -^ ... = ...' | +| 305 | cfg.swift:305:3:305:3 | b | 'b -^ ... .subscript -> Argument -V 0 -^ ... .subscript(...) -> a -^ ... .subscript -> Argument -V 10 -^ ... .subscript(...) -^ ... = ...' | +| 306 | cfg.swift:306:3:306:3 | b | 'b -^ ... .subscript -> Argument -V 1 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 0 -^ ... .subscript(...) -> 1 -^ ... + ... -^ ... = ...' | +| 307 | cfg.swift:307:3:307:3 | b | 'b -^ ... .subscript -> Argument -V 2 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 1 -^ ... .subscript(...) -> 1 -^ ... - ... -^ ... = ...' | +| 308 | cfg.swift:308:3:308:3 | b | 'b -^ ... .subscript -> Argument -V 3 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 2 -^ ... .subscript(...) -> 1 -^ ... * ... -^ ... = ...' | +| 309 | cfg.swift:309:3:309:3 | b | 'b -^ ... .subscript -> Argument -V 4 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 3 -^ ... .subscript(...) -> 1 -^ ... / ... -^ ... = ...' | +| 310 | cfg.swift:310:3:310:3 | b | 'b -^ ... .subscript -> Argument -V 5 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 4 -^ ... .subscript(...) -> 1 -^ ... % ... -^ ... = ...' | +| 311 | cfg.swift:311:3:311:3 | b | 'b -^ ... .subscript -> Argument -V 6 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 5 -^ ... .subscript(...) -> 1 -^ ... & ... -^ ... = ...' | +| 312 | cfg.swift:312:3:312:3 | b | 'b -^ ... .subscript -> Argument -V 7 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 6 -^ ... .subscript(...) -> 1 -^ ... \| ... -^ ... = ...' | +| 313 | cfg.swift:313:3:313:3 | b | 'b -^ ... .subscript -> Argument -V 8 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 7 -^ ... .subscript(...) -> 1 -^ ... ^ ... -^ ... = ...' | +| 314 | cfg.swift:314:3:314:3 | b | 'b -^ ... .subscript -> Argument -V 9 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 8 -^ ... .subscript(...) -> 1 -^ ... << ... -^ ... = ...' | +| 315 | cfg.swift:315:3:315:3 | b | 'b -^ ... .subscript -> Argument -V 10 -^ ... .subscript(...) -> b -^ ... .subscript -> Argument -V 9 -^ ... .subscript(...) -> 1 -^ ... >> ... -^ ... = ...' | | 317 | cfg.swift:317:3:317:39 | let (pattern assignment) | 'let (pattern assignment) -V Argument -V a1 -> Argument -V a2 -> Argument -V a3 -> Argument -V a4 -> Argument -V a5 -^ TupleExpr -> tupleWithA' | -| 318 | cfg.swift:318:11:318:19 | Argument | 'Argument -V a1 -> b -> Argument -V 0 -^ b(...) -^ ... + ... -> Argument -V a2 -> b -> Argument -V 1 -^ b(...) -^ ... + ... -> Argument -V a3 -> b -> Argument -V 2 -^ b(...) -^ ... + ... -> Argument -V a4 -> b -> Argument -V 3 -^ b(...) -^ ... + ... -> Argument -V a5 -> b -> Argument -V 4 -^ b(...) -^ ... + ... -^ TupleExpr -^ ReturnExpr' | +| 318 | cfg.swift:318:11:318:19 | Argument | 'Argument -V a1 -> b -^ ... .subscript -> Argument -V 0 -^ ... .subscript(...) -^ ... + ... -> Argument -V a2 -> b -^ ... .subscript -> Argument -V 1 -^ ... .subscript(...) -^ ... + ... -> Argument -V a3 -> b -^ ... .subscript -> Argument -V 2 -^ ... .subscript(...) -^ ... + ... -> Argument -V a4 -> b -^ ... .subscript -> Argument -V 3 -^ ... .subscript(...) -^ ... + ... -> Argument -V a5 -> b -^ ... .subscript -> Argument -V 4 -^ ... .subscript(...) -^ ... + ... -^ TupleExpr -^ ReturnExpr' | | 321 | cfg.swift:321:1:326:1 | func loop1 | 'func loop1 -V loop1' | | 321 | cfg.swift:321:12:321:12 | x | 'x -> Block' | | 322 | cfg.swift:322:3:325:3 | WhileStmt | 'WhileStmt' | @@ -396,10 +396,10 @@ | 467 | cfg.swift:467:3:467:31 | var kpGet_bs_0_x | 'var kpGet_bs_0_x -V kpGet_bs_0_x -> ' | | 468 | cfg.swift:468:3:468:37 | var kpGet_mayB_force_x | 'var kpGet_mayB_force_x -V kpGet_mayB_force_x -> ' | | 469 | cfg.swift:469:3:469:31 | var kpGet_mayB_x | 'var kpGet_mayB_x -V kpGet_mayB_x -> ' | -| 471 | cfg.swift:471:3:471:45 | var apply_kpGet_b_x | 'var apply_kpGet_b_x -V apply_kpGet_b_x -> a -> Argument -V kpGet_b_x -^ a(...)' | -| 472 | cfg.swift:472:3:472:51 | var apply_kpGet_bs_0_x | 'var apply_kpGet_bs_0_x -V apply_kpGet_bs_0_x -> a -> Argument -V kpGet_bs_0_x -^ a(...)' | -| 473 | cfg.swift:473:3:473:63 | var apply_kpGet_mayB_force_x | 'var apply_kpGet_mayB_force_x -V apply_kpGet_mayB_force_x -> a -> Argument -V kpGet_mayB_force_x -^ a(...)' | -| 474 | cfg.swift:474:3:474:51 | var apply_kpGet_mayB_x | 'var apply_kpGet_mayB_x -V apply_kpGet_mayB_x -> a -> Argument -V kpGet_mayB_x -^ a(...)' | +| 471 | cfg.swift:471:3:471:45 | var apply_kpGet_b_x | 'var apply_kpGet_b_x -V apply_kpGet_b_x -> a -^ ... .subscript -> Argument -V kpGet_b_x -^ ... .subscript(...)' | +| 472 | cfg.swift:472:3:472:51 | var apply_kpGet_bs_0_x | 'var apply_kpGet_bs_0_x -V apply_kpGet_bs_0_x -> a -^ ... .subscript -> Argument -V kpGet_bs_0_x -^ ... .subscript(...)' | +| 473 | cfg.swift:473:3:473:63 | var apply_kpGet_mayB_force_x | 'var apply_kpGet_mayB_force_x -V apply_kpGet_mayB_force_x -> a -^ ... .subscript -> Argument -V kpGet_mayB_force_x -^ ... .subscript(...)' | +| 474 | cfg.swift:474:3:474:51 | var apply_kpGet_mayB_x | 'var apply_kpGet_mayB_x -V apply_kpGet_mayB_x -> a -^ ... .subscript -> Argument -V kpGet_mayB_x -^ ... .subscript(...)' | | 477 | cfg.swift:477:1:504:1 | func testIfConfig | 'func testIfConfig -V testIfConfig' | | 477 | cfg.swift:477:21:504:1 | Block | 'Block' | | 478 | cfg.swift:478:1:484:6 | | '' | diff --git a/unified/ql/test/library-tests/dataflow/subscripts.swift b/unified/ql/test/library-tests/dataflow/subscripts.swift new file mode 100644 index 000000000000..42a16f9def9a --- /dev/null +++ b/unified/ql/test/library-tests/dataflow/subscripts.swift @@ -0,0 +1,152 @@ +func source(_ s: String) -> String { return s } + +@discardableResult +func sink(_ s: String) -> String { return "" } + +func s1() { + class C { + subscript(i: Int) -> String { source("s1.1") } + } + sink(C()[0]) // $ hasValueFlow=s1.1 +} + +func s2() { + class C { + subscript(key: String) -> String { + get { key } + set { sink(newValue) } // $ hasValueFlow=s2.2 + } + } + let c = C() + sink(c[source("s2.1")]) // $ hasValueFlow=s2.1 + c["safe"] = source("s2.2") +} + +func s3() { + class C { + subscript(key: String) -> String { + get { "safe" } + set(v) { sink(key); sink(v) } // $ hasValueFlow=s3.1 hasValueFlow=s3.2 + } + } + let c = C() + c[source("s3.1")] = source("s3.2") +} + +func s4() { + class C { + var field: String = "safe" + subscript(at i: Int, _ j: Int) -> String { + get { field } + set { field = newValue } + } + } + let c = C() + c[at: 0, 1] = source("s4.1") + sink(c[at: 1, 2]) // $ hasValueFlow=s4.1 + sink(c.field) // $ hasValueFlow=s4.1 +} + +func s5() { + class C { + subscript(i: Int) -> String { + get { source("s5.1") } + set { sink(newValue) } // $ hasTaintFlow=s5.1 + } + } + let c = C() + c[0] += "x" +} + +func s6() { + struct S { + var field: String = "safe" + } + class C { + subscript(i: Int) -> S { + get { S() } + set { sink(newValue.field) } // $ hasValueFlow=s6.1 + } + } + let c = C() + // `S` has value type, so this calls the subscript getter followed by the setter + c[0].field = source("s6.1") +} + +func s7() { + class C { + var field: String = "safe" + subscript(i: Int) -> String { + get { field } + set { field = newValue } + } + + func method() { + self[0] = source("s7.1") + sink(self[1]) // $ hasValueFlow=s7.1 + } + } +} + +func s8() { + struct S { + var field: String = "safe" + subscript(i: Int) -> String { + get { field } + set { field = newValue } + } + } + class C { + var s: S { + get { S() } + set { sink(newValue.field) } // $ hasValueFlow=s8.1 + } + } + let c = C() + // `S` has value type, so this writes back to `s` via its setter + c.s[0] = source("s8.1") +} + +func s9() { + struct S { + var field: String = "safe" + subscript(i: Int) -> String { + get { field } + set { field = newValue } + } + } + var s = S() + s[0] = source("s9.1") + sink(s[0]) // $ hasValueFlow=s9.1 +} + +func s10() { + class C { + static subscript(i: Int) -> String { + get { source("s10.1") } + set { sink(newValue) } // $ hasValueFlow=s10.2 + } + } + C[0] = source("s10.2") + sink(C[0]) // $ hasValueFlow=s10.1 +} + +func s11(t: TestType) { + sink(t[testKey: source("s11.1")]) // $ hasValueFlow=s11.1 + t[testKey: source("s11.2")] = "safe" +} + +func s12() { + struct S { + var field: String = "safe" + } + class C { + subscript(i: Int) -> S { + get { S() } + set { sink(newValue.field) } // $ hasValueFlow=s12.1 + } + } + func update(_ s: inout S) { s.field = source("s12.1") } + let c = C() + update(&c[0]) +} diff --git a/unified/ql/test/library-tests/dataflow/test.expected b/unified/ql/test/library-tests/dataflow/test.expected index a479255f6d23..fe155a489ef6 100644 --- a/unified/ql/test/library-tests/dataflow/test.expected +++ b/unified/ql/test/library-tests/dataflow/test.expected @@ -365,6 +365,61 @@ edges | implicit-self.swift:85:29:85:43 | source(...) | implicit-self.swift:85:9:85:14 | [incoming] ... .x | provenance | | | implicit-self.swift:87:14:87:17 | self [x] | implicit-self.swift:87:14:87:19 | ... .x | provenance | | | short-return.swift:7:33:7:46 | source(...) | short-return.swift:8:10:8:19 | getValue(...) | provenance | | +| subscripts.swift:8:39:8:52 | source(...) | subscripts.swift:10:10:10:15 | ... .subscript(...) | provenance | | +| subscripts.swift:15:19:15:21 | key | subscripts.swift:16:19:16:21 | key | provenance | | +| subscripts.swift:21:12:21:25 | source(...) | subscripts.swift:15:19:15:21 | key | provenance | | +| subscripts.swift:21:12:21:25 | source(...) | subscripts.swift:21:10:21:26 | ... .subscript(...) | provenance | | +| subscripts.swift:22:5:22:13 | ... .subscript(...) | subscripts.swift:17:24:17:31 | newValue | provenance | | +| subscripts.swift:22:17:22:30 | source(...) | subscripts.swift:22:5:22:13 | ... .subscript(...) | provenance | | +| subscripts.swift:27:19:27:21 | key | subscripts.swift:29:27:29:29 | key | provenance | | +| subscripts.swift:29:17:29:17 | v | subscripts.swift:29:38:29:38 | v | provenance | | +| subscripts.swift:33:5:33:21 | ... .subscript(...) | subscripts.swift:29:17:29:17 | v | provenance | | +| subscripts.swift:33:7:33:20 | source(...) | subscripts.swift:27:19:27:21 | key | provenance | | +| subscripts.swift:33:25:33:38 | source(...) | subscripts.swift:33:5:33:21 | ... .subscript(...) | provenance | | +| subscripts.swift:41:27:41:34 | newValue | subscripts.swift:41:19:41:23 | field | provenance | | +| subscripts.swift:45:5:45:15 | ... .subscript(...) | subscripts.swift:41:27:41:34 | newValue | provenance | | +| subscripts.swift:45:5:45:15 | ... .subscript(...) | subscripts.swift:46:10:46:10 | c [field] | provenance | | +| subscripts.swift:45:5:45:15 | ... .subscript(...) | subscripts.swift:47:10:47:10 | c [field] | provenance | | +| subscripts.swift:45:19:45:32 | source(...) | subscripts.swift:45:5:45:15 | ... .subscript(...) | provenance | | +| subscripts.swift:46:10:46:10 | c [field] | subscripts.swift:40:19:40:23 | field | provenance | | +| subscripts.swift:46:10:46:10 | c [field] | subscripts.swift:46:10:46:20 | ... .subscript(...) | provenance | | +| subscripts.swift:47:10:47:10 | c [field] | subscripts.swift:47:10:47:16 | ... .field | provenance | | +| subscripts.swift:53:19:53:32 | source(...) | subscripts.swift:58:5:58:8 | ... .subscript(...) | provenance | | +| subscripts.swift:58:5:58:8 | ... .subscript(...) | subscripts.swift:58:5:58:8 | [incoming] ... .subscript(...) | provenance | | +| subscripts.swift:58:5:58:8 | [incoming] ... .subscript(...) | subscripts.swift:54:24:54:31 | newValue | provenance | | +| subscripts.swift:68:24:68:31 | newValue [field] | subscripts.swift:68:24:68:37 | ... .field | provenance | | +| subscripts.swift:73:5:73:8 | [post] ... .subscript(...) [field] | subscripts.swift:68:24:68:31 | newValue [field] | provenance | | +| subscripts.swift:73:5:73:14 | ... .field | subscripts.swift:73:5:73:8 | [post] ... .subscript(...) [field] | provenance | | +| subscripts.swift:73:18:73:31 | source(...) | subscripts.swift:73:5:73:14 | ... .field | provenance | | +| subscripts.swift:81:27:81:34 | newValue | subscripts.swift:81:19:81:23 | field | provenance | | +| subscripts.swift:85:13:85:19 | ... .subscript(...) | subscripts.swift:81:27:81:34 | newValue | provenance | | +| subscripts.swift:85:13:85:19 | ... .subscript(...) | subscripts.swift:86:18:86:21 | self [field] | provenance | | +| subscripts.swift:85:23:85:36 | source(...) | subscripts.swift:85:13:85:19 | ... .subscript(...) | provenance | | +| subscripts.swift:86:18:86:21 | self [field] | subscripts.swift:80:19:80:23 | field | provenance | | +| subscripts.swift:86:18:86:21 | self [field] | subscripts.swift:86:18:86:24 | ... .subscript(...) | provenance | | +| subscripts.swift:96:27:96:34 | newValue | subscripts.swift:96:19:96:23 | field | provenance | | +| subscripts.swift:102:24:102:31 | newValue [field] | subscripts.swift:102:24:102:37 | ... .field | provenance | | +| subscripts.swift:107:5:107:7 | [post] ... .s [field] | subscripts.swift:102:24:102:31 | newValue [field] | provenance | | +| subscripts.swift:107:5:107:10 | ... .subscript(...) | subscripts.swift:96:27:96:34 | newValue | provenance | | +| subscripts.swift:107:5:107:10 | ... .subscript(...) | subscripts.swift:107:5:107:7 | [post] ... .s [field] | provenance | | +| subscripts.swift:107:14:107:27 | source(...) | subscripts.swift:107:5:107:10 | ... .subscript(...) | provenance | | +| subscripts.swift:115:27:115:34 | newValue | subscripts.swift:115:19:115:23 | field | provenance | | +| subscripts.swift:119:5:119:8 | ... .subscript(...) | subscripts.swift:115:27:115:34 | newValue | provenance | | +| subscripts.swift:119:5:119:8 | ... .subscript(...) | subscripts.swift:120:10:120:10 | s [field] | provenance | | +| subscripts.swift:119:12:119:25 | source(...) | subscripts.swift:119:5:119:8 | ... .subscript(...) | provenance | | +| subscripts.swift:120:10:120:10 | s [field] | subscripts.swift:114:19:114:23 | field | provenance | | +| subscripts.swift:120:10:120:10 | s [field] | subscripts.swift:120:10:120:13 | ... .subscript(...) | provenance | | +| subscripts.swift:126:19:126:33 | source(...) | subscripts.swift:131:10:131:13 | ... .subscript(...) | provenance | | +| subscripts.swift:130:5:130:8 | ... .subscript(...) | subscripts.swift:127:24:127:31 | newValue | provenance | | +| subscripts.swift:130:12:130:26 | source(...) | subscripts.swift:130:5:130:8 | ... .subscript(...) | provenance | | +| subscripts.swift:135:21:135:35 | source(...) | subscripts.swift:135:10:135:36 | ... .subscript(...) | provenance | | +| subscripts.swift:146:24:146:31 | newValue [field] | subscripts.swift:146:24:146:37 | ... .field | provenance | | +| subscripts.swift:149:19:149:19 | s [Return] [field] | subscripts.swift:151:12:151:16 | [post] & ... [field] | provenance | | +| subscripts.swift:149:33:149:33 | [post] s [field] | subscripts.swift:149:19:149:19 | s [Return] [field] | provenance | | +| subscripts.swift:149:33:149:39 | ... .field | subscripts.swift:149:33:149:33 | [post] s [field] | provenance | | +| subscripts.swift:149:43:149:57 | source(...) | subscripts.swift:149:33:149:39 | ... .field | provenance | | +| subscripts.swift:151:12:151:16 | [post] & ... [field] | subscripts.swift:151:13:151:16 | [post] ... .subscript(...) [field] | provenance | | +| subscripts.swift:151:13:151:16 | [post] ... .subscript(...) [field] | subscripts.swift:146:24:146:31 | newValue [field] | provenance | | | test.swift:17:10:17:23 | source(...) | test.swift:17:10:17:32 | ... + ... | provenance | | | test.swift:18:19:18:32 | source(...) | test.swift:18:10:18:32 | ... + ... | provenance | | | test.swift:20:13:20:26 | source(...) | test.swift:20:10:20:33 | StringInterpolationExpr | provenance | | @@ -987,6 +1042,76 @@ nodes | implicit-self.swift:87:14:87:19 | ... .x | semmle.label | ... .x | | short-return.swift:7:33:7:46 | source(...) | semmle.label | source(...) | | short-return.swift:8:10:8:19 | getValue(...) | semmle.label | getValue(...) | +| subscripts.swift:8:39:8:52 | source(...) | semmle.label | source(...) | +| subscripts.swift:10:10:10:15 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:15:19:15:21 | key | semmle.label | key | +| subscripts.swift:16:19:16:21 | key | semmle.label | key | +| subscripts.swift:17:24:17:31 | newValue | semmle.label | newValue | +| subscripts.swift:21:10:21:26 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:21:12:21:25 | source(...) | semmle.label | source(...) | +| subscripts.swift:22:5:22:13 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:22:17:22:30 | source(...) | semmle.label | source(...) | +| subscripts.swift:27:19:27:21 | key | semmle.label | key | +| subscripts.swift:29:17:29:17 | v | semmle.label | v | +| subscripts.swift:29:27:29:29 | key | semmle.label | key | +| subscripts.swift:29:38:29:38 | v | semmle.label | v | +| subscripts.swift:33:5:33:21 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:33:7:33:20 | source(...) | semmle.label | source(...) | +| subscripts.swift:33:25:33:38 | source(...) | semmle.label | source(...) | +| subscripts.swift:40:19:40:23 | field | semmle.label | field | +| subscripts.swift:41:19:41:23 | field | semmle.label | field | +| subscripts.swift:41:27:41:34 | newValue | semmle.label | newValue | +| subscripts.swift:45:5:45:15 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:45:19:45:32 | source(...) | semmle.label | source(...) | +| subscripts.swift:46:10:46:10 | c [field] | semmle.label | c [field] | +| subscripts.swift:46:10:46:20 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:47:10:47:10 | c [field] | semmle.label | c [field] | +| subscripts.swift:47:10:47:16 | ... .field | semmle.label | ... .field | +| subscripts.swift:53:19:53:32 | source(...) | semmle.label | source(...) | +| subscripts.swift:54:24:54:31 | newValue | semmle.label | newValue | +| subscripts.swift:58:5:58:8 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:58:5:58:8 | [incoming] ... .subscript(...) | semmle.label | [incoming] ... .subscript(...) | +| subscripts.swift:68:24:68:31 | newValue [field] | semmle.label | newValue [field] | +| subscripts.swift:68:24:68:37 | ... .field | semmle.label | ... .field | +| subscripts.swift:73:5:73:8 | [post] ... .subscript(...) [field] | semmle.label | [post] ... .subscript(...) [field] | +| subscripts.swift:73:5:73:14 | ... .field | semmle.label | ... .field | +| subscripts.swift:73:18:73:31 | source(...) | semmle.label | source(...) | +| subscripts.swift:80:19:80:23 | field | semmle.label | field | +| subscripts.swift:81:19:81:23 | field | semmle.label | field | +| subscripts.swift:81:27:81:34 | newValue | semmle.label | newValue | +| subscripts.swift:85:13:85:19 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:85:23:85:36 | source(...) | semmle.label | source(...) | +| subscripts.swift:86:18:86:21 | self [field] | semmle.label | self [field] | +| subscripts.swift:86:18:86:24 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:96:19:96:23 | field | semmle.label | field | +| subscripts.swift:96:27:96:34 | newValue | semmle.label | newValue | +| subscripts.swift:102:24:102:31 | newValue [field] | semmle.label | newValue [field] | +| subscripts.swift:102:24:102:37 | ... .field | semmle.label | ... .field | +| subscripts.swift:107:5:107:7 | [post] ... .s [field] | semmle.label | [post] ... .s [field] | +| subscripts.swift:107:5:107:10 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:107:14:107:27 | source(...) | semmle.label | source(...) | +| subscripts.swift:114:19:114:23 | field | semmle.label | field | +| subscripts.swift:115:19:115:23 | field | semmle.label | field | +| subscripts.swift:115:27:115:34 | newValue | semmle.label | newValue | +| subscripts.swift:119:5:119:8 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:119:12:119:25 | source(...) | semmle.label | source(...) | +| subscripts.swift:120:10:120:10 | s [field] | semmle.label | s [field] | +| subscripts.swift:120:10:120:13 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:126:19:126:33 | source(...) | semmle.label | source(...) | +| subscripts.swift:127:24:127:31 | newValue | semmle.label | newValue | +| subscripts.swift:130:5:130:8 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:130:12:130:26 | source(...) | semmle.label | source(...) | +| subscripts.swift:131:10:131:13 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:135:10:135:36 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:135:21:135:35 | source(...) | semmle.label | source(...) | +| subscripts.swift:146:24:146:31 | newValue [field] | semmle.label | newValue [field] | +| subscripts.swift:146:24:146:37 | ... .field | semmle.label | ... .field | +| subscripts.swift:149:19:149:19 | s [Return] [field] | semmle.label | s [Return] [field] | +| subscripts.swift:149:33:149:33 | [post] s [field] | semmle.label | [post] s [field] | +| subscripts.swift:149:33:149:39 | ... .field | semmle.label | ... .field | +| subscripts.swift:149:43:149:57 | source(...) | semmle.label | source(...) | +| subscripts.swift:151:12:151:16 | [post] & ... [field] | semmle.label | [post] & ... [field] | +| subscripts.swift:151:13:151:16 | [post] ... .subscript(...) [field] | semmle.label | [post] ... .subscript(...) [field] | | test.swift:9:10:9:21 | source(...) | semmle.label | source(...) | | test.swift:17:10:17:23 | source(...) | semmle.label | source(...) | | test.swift:17:10:17:32 | ... + ... | semmle.label | ... + ... | @@ -1213,6 +1338,15 @@ subpaths | capture.swift:93:9:93:9 | x | capture.swift:99:16:99:16 | x | capture.swift:99:16:99:16 | x | capture.swift:94:13:94:19 | local(...) | | flow-summaries.swift:54:35:54:48 | source(...) | flow-summaries.swift:54:53:54:53 | y | flow-summaries.swift:54:65:54:65 | y | flow-summaries.swift:54:10:54:67 | ... .throughCallback(...) | | flow-summaries.swift:55:35:55:48 | source(...) | flow-summaries.swift:55:53:55:53 | y | flow-summaries.swift:55:65:55:71 | ... + ... | flow-summaries.swift:55:10:55:73 | ... .throughCallback(...) | +| subscripts.swift:21:12:21:25 | source(...) | subscripts.swift:15:19:15:21 | key | subscripts.swift:16:19:16:21 | key | subscripts.swift:21:10:21:26 | ... .subscript(...) | +| subscripts.swift:45:5:45:15 | ... .subscript(...) | subscripts.swift:41:27:41:34 | newValue | subscripts.swift:41:19:41:23 | field | subscripts.swift:46:10:46:10 | c [field] | +| subscripts.swift:45:5:45:15 | ... .subscript(...) | subscripts.swift:41:27:41:34 | newValue | subscripts.swift:41:19:41:23 | field | subscripts.swift:47:10:47:10 | c [field] | +| subscripts.swift:46:10:46:10 | c [field] | subscripts.swift:40:19:40:23 | field | subscripts.swift:40:19:40:23 | field | subscripts.swift:46:10:46:20 | ... .subscript(...) | +| subscripts.swift:85:13:85:19 | ... .subscript(...) | subscripts.swift:81:27:81:34 | newValue | subscripts.swift:81:19:81:23 | field | subscripts.swift:86:18:86:21 | self [field] | +| subscripts.swift:86:18:86:21 | self [field] | subscripts.swift:80:19:80:23 | field | subscripts.swift:80:19:80:23 | field | subscripts.swift:86:18:86:24 | ... .subscript(...) | +| subscripts.swift:107:5:107:10 | ... .subscript(...) | subscripts.swift:96:27:96:34 | newValue | subscripts.swift:96:19:96:23 | field | subscripts.swift:107:5:107:7 | [post] ... .s [field] | +| subscripts.swift:119:5:119:8 | ... .subscript(...) | subscripts.swift:115:27:115:34 | newValue | subscripts.swift:115:19:115:23 | field | subscripts.swift:120:10:120:10 | s [field] | +| subscripts.swift:120:10:120:10 | s [field] | subscripts.swift:114:19:114:23 | field | subscripts.swift:114:19:114:23 | field | subscripts.swift:120:10:120:13 | ... .subscript(...) | | test.swift:196:30:196:44 | source(...) | test.swift:189:22:189:22 | s | test.swift:189:58:189:58 | s | test.swift:196:16:196:45 | asyncIdentity(...) | | test.swift:200:31:200:45 | source(...) | test.swift:191:25:191:25 | s | test.swift:191:62:191:62 | s | test.swift:200:14:200:46 | throwingIdentity(...) | | test.swift:201:37:201:51 | source(...) | test.swift:191:25:191:25 | s | test.swift:191:62:191:62 | s | test.swift:201:20:201:52 | throwingIdentity(...) | @@ -1343,6 +1477,22 @@ testFailures | implicit-self.swift:86:14:86:14 | x | implicit-self.swift:85:29:85:43 | source(...) | implicit-self.swift:86:14:86:14 | x | $@ | implicit-self.swift:85:29:85:43 | source(...) | source(...) | | implicit-self.swift:87:14:87:19 | ... .x | implicit-self.swift:85:29:85:43 | source(...) | implicit-self.swift:87:14:87:19 | ... .x | $@ | implicit-self.swift:85:29:85:43 | source(...) | source(...) | | short-return.swift:8:10:8:19 | getValue(...) | short-return.swift:7:33:7:46 | source(...) | short-return.swift:8:10:8:19 | getValue(...) | $@ | short-return.swift:7:33:7:46 | source(...) | source(...) | +| subscripts.swift:10:10:10:15 | ... .subscript(...) | subscripts.swift:8:39:8:52 | source(...) | subscripts.swift:10:10:10:15 | ... .subscript(...) | $@ | subscripts.swift:8:39:8:52 | source(...) | source(...) | +| subscripts.swift:17:24:17:31 | newValue | subscripts.swift:22:17:22:30 | source(...) | subscripts.swift:17:24:17:31 | newValue | $@ | subscripts.swift:22:17:22:30 | source(...) | source(...) | +| subscripts.swift:21:10:21:26 | ... .subscript(...) | subscripts.swift:21:12:21:25 | source(...) | subscripts.swift:21:10:21:26 | ... .subscript(...) | $@ | subscripts.swift:21:12:21:25 | source(...) | source(...) | +| subscripts.swift:29:27:29:29 | key | subscripts.swift:33:7:33:20 | source(...) | subscripts.swift:29:27:29:29 | key | $@ | subscripts.swift:33:7:33:20 | source(...) | source(...) | +| subscripts.swift:29:38:29:38 | v | subscripts.swift:33:25:33:38 | source(...) | subscripts.swift:29:38:29:38 | v | $@ | subscripts.swift:33:25:33:38 | source(...) | source(...) | +| subscripts.swift:46:10:46:20 | ... .subscript(...) | subscripts.swift:45:19:45:32 | source(...) | subscripts.swift:46:10:46:20 | ... .subscript(...) | $@ | subscripts.swift:45:19:45:32 | source(...) | source(...) | +| subscripts.swift:47:10:47:16 | ... .field | subscripts.swift:45:19:45:32 | source(...) | subscripts.swift:47:10:47:16 | ... .field | $@ | subscripts.swift:45:19:45:32 | source(...) | source(...) | +| subscripts.swift:54:24:54:31 | newValue | subscripts.swift:53:19:53:32 | source(...) | subscripts.swift:54:24:54:31 | newValue | $@ | subscripts.swift:53:19:53:32 | source(...) | source(...) | +| subscripts.swift:68:24:68:37 | ... .field | subscripts.swift:73:18:73:31 | source(...) | subscripts.swift:68:24:68:37 | ... .field | $@ | subscripts.swift:73:18:73:31 | source(...) | source(...) | +| subscripts.swift:86:18:86:24 | ... .subscript(...) | subscripts.swift:85:23:85:36 | source(...) | subscripts.swift:86:18:86:24 | ... .subscript(...) | $@ | subscripts.swift:85:23:85:36 | source(...) | source(...) | +| subscripts.swift:102:24:102:37 | ... .field | subscripts.swift:107:14:107:27 | source(...) | subscripts.swift:102:24:102:37 | ... .field | $@ | subscripts.swift:107:14:107:27 | source(...) | source(...) | +| subscripts.swift:120:10:120:13 | ... .subscript(...) | subscripts.swift:119:12:119:25 | source(...) | subscripts.swift:120:10:120:13 | ... .subscript(...) | $@ | subscripts.swift:119:12:119:25 | source(...) | source(...) | +| subscripts.swift:127:24:127:31 | newValue | subscripts.swift:130:12:130:26 | source(...) | subscripts.swift:127:24:127:31 | newValue | $@ | subscripts.swift:130:12:130:26 | source(...) | source(...) | +| subscripts.swift:131:10:131:13 | ... .subscript(...) | subscripts.swift:126:19:126:33 | source(...) | subscripts.swift:131:10:131:13 | ... .subscript(...) | $@ | subscripts.swift:126:19:126:33 | source(...) | source(...) | +| subscripts.swift:135:10:135:36 | ... .subscript(...) | subscripts.swift:135:21:135:35 | source(...) | subscripts.swift:135:10:135:36 | ... .subscript(...) | $@ | subscripts.swift:135:21:135:35 | source(...) | source(...) | +| subscripts.swift:146:24:146:37 | ... .field | subscripts.swift:149:43:149:57 | source(...) | subscripts.swift:146:24:146:37 | ... .field | $@ | subscripts.swift:149:43:149:57 | source(...) | source(...) | | test.swift:9:10:9:21 | source(...) | test.swift:9:10:9:21 | source(...) | test.swift:9:10:9:21 | source(...) | $@ | test.swift:9:10:9:21 | source(...) | source(...) | | test.swift:17:10:17:32 | ... + ... | test.swift:17:10:17:23 | source(...) | test.swift:17:10:17:32 | ... + ... | $@ | test.swift:17:10:17:23 | source(...) | source(...) | | test.swift:18:10:18:32 | ... + ... | test.swift:18:19:18:32 | source(...) | test.swift:18:10:18:32 | ... + ... | $@ | test.swift:18:19:18:32 | source(...) | source(...) | diff --git a/unified/ql/test/library-tests/dataflow/test.ext.yml b/unified/ql/test/library-tests/dataflow/test.ext.yml index 352ac28dad95..796d1ca2d00b 100644 --- a/unified/ql/test/library-tests/dataflow/test.ext.yml +++ b/unified/ql/test/library-tests/dataflow/test.ext.yml @@ -18,3 +18,4 @@ extensions: - ["", "TestType", true, "swap(_:_:)", "", "", "Argument[1]", "Argument[0]", "value", "manual"] - ["", "TestType", true, "getField()", "", "", "Argument[self].Member[field]", "ReturnValue", "value", "manual"] - ["", "TestType", true, "setField(_:)", "", "", "Argument[0]", "Argument[self].Member[field]", "value", "manual"] + - ["", "TestType", true, "subscript(testKey:)", "", "", "Argument[testKey:]", "ReturnValue", "value", "manual"] diff --git a/unified/ql/test/library-tests/type-inference/type-inference.expected b/unified/ql/test/library-tests/type-inference/type-inference.expected index e978a676689b..177dcc387147 100644 --- a/unified/ql/test/library-tests/type-inference/type-inference.expected +++ b/unified/ql/test/library-tests/type-inference/type-inference.expected @@ -2728,7 +2728,7 @@ inferType | key_paths.swift:131:20:131:22 | 1.0 | | {EXTERNAL LOCATION} | Double | | key_paths.swift:131:28:131:30 | 2.0 | | {EXTERNAL LOCATION} | Double | | key_paths.swift:133:3:133:3 | p | | key_paths.swift:3:1:15:1 | Point | -| key_paths.swift:133:3:133:17 | p(...) | | {EXTERNAL LOCATION} | Double | +| key_paths.swift:133:3:133:17 | ... .subscript(...) | | {EXTERNAL LOCATION} | Double | | key_paths.swift:133:3:133:24 | ... = ... | | {EXTERNAL LOCATION} | Double | | key_paths.swift:133:21:133:24 | 10.0 | | {EXTERNAL LOCATION} | Double | | key_paths.swift:134:14:134:14 | p | | key_paths.swift:3:1:15:1 | Point | From ad56757e91a624d10fb695cfe084a12241eadb30 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 11 Nov 2026 13:35:00 +0100 Subject: [PATCH 2/4] Unified: Name Swift subscript setters 'subscript=' Replace the get/set modifiers on subscript methods with a distinct setter name, resolve setter calls by looking up 'name=' on the receiver, and let MaD models match setter calls via the 'name=' method name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- unified/extractor/ast_types.yml | 12 +++--- .../extractor/src/languages/swift/swift.rs | 23 +++------- .../swift/subscripts/subscript-get-set.output | 19 +++----- .../subscripts/subscript-getter-only.output | 1 - .../subscript-protocol-requirement.output | 11 ++--- .../lib/codeql/unified/internal/FacadeAst.qll | 28 +++--------- .../unified/internal/dataflow/CallGraph.qll | 7 ++- .../internal/dataflow/DataFlowCall.qll | 7 ++- .../unified/internal/dataflow/FlowSummary.qll | 2 +- .../codeql/unified/internal/mad/LegacyMaD.qll | 43 ++++++++++++++----- .../internal/typeinference/TypeInference.qll | 24 +++++++++++ .../library-tests/dataflow/subscripts.swift | 2 + .../test/library-tests/dataflow/test.expected | 40 ++++++++++------- .../test/library-tests/dataflow/test.ext.yml | 1 + 14 files changed, 122 insertions(+), 98 deletions(-) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index 59d0611683f2..36e36c594064 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -336,12 +336,12 @@ named: # A top-level function declaration. # - # A function marked with a `get` or `set` modifier is a getter or setter - # invoked via call syntax, such as a Swift subscript accessor. A call to it - # in a read position invokes the getter, while a call in an assignment target - # or otherwise updated in place invokes the setter. The setter takes the new - # value as an extra trailing positional parameter. A getter/setter pair is - # emitted as two adjacent declarations, the second one tagged with a + # A function whose name is an identifier followed by `=` (such as `subscript=`) + # is a setter invoked via call syntax: a call to `foo` (such as Swift's + # `x[i]`, a call to `x.subscript`) in an assignment target or otherwise + # updated in place invokes the setter `foo=`. The setter takes the new value + # as an extra trailing positional parameter. A getter/setter pair is emitted + # as two adjacent declarations, the second one tagged with a # `chained_declaration` modifier. function_declaration: modifier*: modifier diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index b13cce9335a6..385f353e40b2 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -162,8 +162,6 @@ fn member_chain( /// The accessor-specific parts of a subscript getter or setter. struct SubscriptAccessor { - /// The `get`/`set` modifier marking the kind of accessor. - kind: yeast::OutputId, /// Modifiers written on the accessor itself, such as `nonmutating`. modifiers: Vec, /// For a setter, the explicit name of its new-value parameter (`set(v)`), @@ -173,8 +171,8 @@ struct SubscriptAccessor { body: Option, } -/// Emit a `function_declaration` named `subscript` for one accessor of a -/// subscript. The shared parts of the subscript (modifiers, generic and value +/// Emit a `function_declaration` for one accessor of a subscript: the getter +/// is named `subscript` and the setter `subscript=`. The shared parts of the subscript (modifiers, generic and value /// parameters and the element type) are translated afresh for each accessor, /// so a getter/setter pair each get their own copy. A setter takes the new /// value as an extra trailing positional parameter and has no return type. @@ -201,10 +199,9 @@ fn subscript_method( let implicit_param_name = param_name.is_none().then_some("newValue"); tree!((function_declaration modifier: {mods} - modifier: {accessor.kind} modifier: {accessor.modifiers} modifier: [(modifier "chained_declaration") if { is_chained }] - name_node: (identifier #{kw}) + name_node: (identifier "subscript=" location: {kw}) type_parameter: {type_params} parameter: {params} parameter: [ @@ -222,7 +219,6 @@ fn subscript_method( } None => tree!((function_declaration modifier: {mods} - modifier: {accessor.kind} modifier: {accessor.modifiers} modifier: [(modifier "chained_declaration") if { is_chained }] name_node: (identifier #{kw}) @@ -495,9 +491,9 @@ fn translation_rules() -> Vec> { } ), // ---- Subscripts ---- - // A subscript declaration becomes one `function_declaration` named - // `subscript` per accessor, tagged with a `get`/`set` modifier. A - // subscript with an explicit accessor block publishes its shared parts + // A subscript declaration becomes a `function_declaration` named + // `subscript` for its getter and, if it has a setter, one named + // `subscript=` for the setter. A subscript with an explicit accessor block publishes its shared parts // in `ctx` and lets the `accessorDecl` rule below emit one method per // accessor; each one after the first is tagged `chained_declaration`. rule!( @@ -536,10 +532,6 @@ fn translation_rules() -> Vec> { accessorBlock: _? @@body) => function_declaration { - let kind = match body { - Some(body) => ctx.literal_at_start_of("modifier", "get", body), - None => ctx.literal_at_start_of("modifier", "get", kw), - }; let sub = SubscriptContext { keyword: kw, modifiers: mods, @@ -549,7 +541,6 @@ fn translation_rules() -> Vec> { }; ctx.is_chained = false; subscript_method(&mut ctx, sub, SubscriptAccessor { - kind, modifiers: Vec::new(), setter: None, body, @@ -571,9 +562,7 @@ fn translation_rules() -> Vec> { let spec_text = ctx.source_text(spec); if spec_text == "get" || spec_text == "set" { let setter = (spec_text == "set").then_some((param_name, spec)); - let kind = tree!((modifier #{spec})); subscript_method(&mut ctx, sub, SubscriptAccessor { - kind, modifiers: acc_mods, setter, body, diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output index 8af39886d37c..f29ff763eef5 100644 --- a/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output @@ -236,8 +236,7 @@ top_level source="⟨body⟩" generic_type_expr source="[⟨type_argument⟩]" base: identifier "Array" source="" type_argument: identifier "Int" source="Int" - function_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> ⟨return_type⟩ {\n ⟨modifier⟩ ⟨body⟩" - modifier: modifier "get" source="get" + function_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> ⟨return_type⟩ {\n get ⟨body⟩" name_node: identifier "subscript" source="subscript" parameter: parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" @@ -260,12 +259,11 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: identifier "index" source="index" - function_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> Int {\n get { data[index] }\n ⟨modifier⟩ ⟨modifier⟩(⟨parameter⟩) ⟨body⟩" + function_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> Int {\n get { data[index] }\n ⟨modifier⟩ set(⟨parameter⟩) ⟨body⟩" modifier: - modifier "set" source="set" modifier "nonmutating" source="nonmutating" modifier "chained_declaration" source="" - name_node: identifier "subscript" source="subscript" + name_node: identifier "subscript=" source="subscript" parameter: parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" external_name_node: identifier "at" source="at" @@ -286,11 +284,9 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: identifier "v" source="v" - function_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> T {\n ⟨modifier⟩⟨parameter⟩ ⟨body⟩" - modifier: - modifier "static" source="static" - modifier "set" source="set" - name_node: identifier "subscript" source="subscript" + function_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> T {\n ⟨parameter⟩ ⟨body⟩" + modifier: modifier "static" source="static" + name_node: identifier "subscript=" source="subscript" type_parameter: type_parameter source="⟨name_node⟩" name_node: identifier "T" source="T" @@ -310,10 +306,9 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: identifier "newValue" source="newValue" - function_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> ⟨return_type⟩ {\n set { print(newValue) }\n ⟨modifier⟩ ⟨body⟩" + function_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> ⟨return_type⟩ {\n set { print(newValue) }\n get ⟨body⟩" modifier: modifier "static" source="static" - modifier "get" source="get" modifier "chained_declaration" source="" name_node: identifier "subscript" source="subscript" type_parameter: diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output index f1fe54827799..2c25b9fed9b9 100644 --- a/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-getter-only.output @@ -75,7 +75,6 @@ top_level source="⟨body⟩" name_node: identifier "S" source="S" member: function_declaration source="⟨name_node⟩(⟨parameter⟩) -> ⟨return_type⟩ ⟨body⟩" - modifier: modifier "get" source="" name_node: identifier "subscript" source="subscript" parameter: parameter source="⟨pattern⟩: ⟨type⟩" diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output index a0597c194b68..9a7a2da8f920 100644 --- a/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output @@ -68,8 +68,7 @@ top_level source="⟨body⟩" modifier: modifier "protocol" source="protocol" name_node: identifier "P" source="P" member: - function_declaration source="⟨name_node⟩(⟨parameter⟩) -> ⟨return_type⟩ { ⟨modifier⟩" - modifier: modifier "get" source="get" + function_declaration source="⟨name_node⟩(⟨parameter⟩) -> ⟨return_type⟩ { get" name_node: identifier "subscript" source="subscript" parameter: parameter source="⟨pattern⟩: ⟨type⟩" @@ -77,11 +76,9 @@ top_level source="⟨body⟩" pattern: identifier "i" source="i" return_type: identifier "String" source="String" body: block source="" - function_declaration source="⟨name_node⟩(⟨parameter⟩) -> String { get ⟨modifier⟩⟨parameter⟩" - modifier: - modifier "set" source="set" - modifier "chained_declaration" source="" - name_node: identifier "subscript" source="subscript" + function_declaration source="⟨name_node⟩(⟨parameter⟩) -> String { get ⟨parameter⟩" + modifier: modifier "chained_declaration" source="" + name_node: identifier "subscript=" source="subscript" parameter: parameter source="⟨pattern⟩: ⟨type⟩" type: identifier "Int" source="Int" diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index c632259cd84c..7e7072845432 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -476,28 +476,12 @@ module Unified { } /** - * A function declaration marked with a `get` modifier, representing a getter that is invoked - * via call syntax when the call appears in a position where its value is read. + * A function declaration whose name is an identifier followed by `=`, representing a setter that + * is invoked via call syntax when a call appears as an assignment target or is otherwise updated + * in place. A call to `foo` in such a position invokes the setter `foo=`. * - * It is typically paired with a `SetterMethodDeclaration` of the same name. For example, in Swift - * the getter of a subscript declaration: - * ```swift - * subscript(i: Int) -> String { - * get { "42" } // getter method declaration - * set { print(newValue) } - * } - * ``` - */ - final class GetterMethodDeclaration extends FunctionDeclaration { - GetterMethodDeclaration() { this.hasModifier("get") } - } - - /** - * A function declaration marked with a `set` modifier, representing a setter that is invoked - * via call syntax when the call appears in an assignment target or is otherwise updated in place. - * - * The new value is passed as an additional trailing positional parameter. For example, in Swift - * the setter of a subscript declaration: + * The new value is passed as an additional trailing positional parameter. For example, the + * setter of a Swift subscript declaration is named `subscript=`: * ```swift * subscript(i: Int) -> String { * get { "42" } @@ -506,7 +490,7 @@ module Unified { * ``` */ final class SetterMethodDeclaration extends FunctionDeclaration { - SetterMethodDeclaration() { this.hasModifier("set") } + SetterMethodDeclaration() { this.getName().regexpMatch("[\\p{L}_][\\p{L}\\p{N}_]*=") } } /** An `if` statement or conditional expression. */ diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll b/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll index 8275dcf0afd8..64ed6699f1e4 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/CallGraph.qll @@ -5,14 +5,13 @@ private import codeql.unified.internal.NameBinding as N private import codeql.unified.internal.ExprPositions DataFlowCallable viableCallable(DataFlowCall c) { - result.asSourceCallable() = T::resolveCallTarget(c.asExplicitCall()) and - not result.asSourceCallable() instanceof SetterMethodDeclaration + result.asSourceCallable() = T::resolveCallTarget(c.asExplicitCall()) or result.asSourceCallable() = T::resolveAccessorAccess(c.asGetterCall()).(GetterDeclaration) or result.asSourceCallable() = T::resolveAccessorAccess(c.asSetterCall()).(SetterDeclaration) or - result.asSourceCallable() = T::resolveCallTarget(c.asSetterCall()).(SetterMethodDeclaration) + result.asSourceCallable() = T::resolveSetterCallTarget(c.asSetterCall()) or - result.asSummarizedCallable().matchesCall(c.asExplicitCall()) + result.asSummarizedCallable().matchesCall(c) } diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll index 290461771026..c0dea00e66ac 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowCall.qll @@ -24,8 +24,13 @@ private newtype TDataFlowCall = ( T::resolveAccessorAccess(expr) instanceof SetterDeclaration or - T::resolveCallTarget(expr) instanceof SetterMethodDeclaration + exists(T::resolveSetterCallTarget(expr)) ) + or + // Assigning to a method call can only invoke a setter. Include such calls even if the setter + // can't be resolved, so library models can target them. + isInAssignmentContext(expr, _) and + expr.(CallExpr).getCallee() instanceof MemberAccessExpr } or TSummaryCall(SummarizedCallable callable, SummaryNode callee) { FlowSummaryOutput::summaryCallbackRange(callable, callee) diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/FlowSummary.qll b/unified/ql/lib/codeql/unified/internal/dataflow/FlowSummary.qll index c4676aa46ea2..5e4bdde541e4 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/FlowSummary.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/FlowSummary.qll @@ -111,7 +111,7 @@ private predicate getSummaryNodeAlias = getSummaryNode/1; private module StepsInput implements StepsInputSig { predicate getSummaryNode = getSummaryNodeAlias/1; - DataFlowCall getACall(SummarizedCallable sc) { sc.matchesCall(result.asExplicitCall()) } + DataFlowCall getACall(SummarizedCallable sc) { sc.matchesCall(result) } } private import StepsInput diff --git a/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll b/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll index 139b7307cecf..a473bdfdf90e 100644 --- a/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll +++ b/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll @@ -4,6 +4,8 @@ private import unified private import codeql.dataflow.internal.AccessPathSyntax +private import codeql.unified.internal.dataflow.DataFlowCall +private import codeql.unified.internal.dataflow.DataFlowNode extensible predicate legacySourceModel( string namespace, string type, boolean subtypes, string name, string signature, string ext, @@ -75,6 +77,17 @@ private predicate methodCallSelector(CallExpr call, string name, string argLabel argLabels = getArgLabelsFromCall(call) } +/** + * Holds if `call` invokes a setter named `name`, such as `subscript=` for `x[i] = y`. + * + * The new value is passed as an extra positional argument after the explicit ones. + */ +pragma[nomagic] +private predicate setterCallSelector(CallExpr call, string name, string argLabels) { + name = call.getCallee().(MemberAccessExpr).getMemberName() + "=" and + argLabels = "_:" + getArgLabelsFromCall(call) +} + pragma[nomagic] private predicate constructorCallSelector(CallExpr call, string name, string argLabels) { ( @@ -116,7 +129,6 @@ private predicate callableSelector( FunctionDeclaration callable, string type, string name, string argLabels ) { name = callable.getName() and - not callable instanceof SetterMethodDeclaration and // models refer to the getter, e.g. of a subscript argLabels = getArgLabelsFromCallable(callable) and exists(ClassLikeDeclaration cls | callable = cls.getAMemberIncludingExtensions() and @@ -174,13 +186,16 @@ class Selector extends TSelector { this = MkSelector(name, _, "init", argLabels, _) } - predicate matchesCall(CallExpr call) { + predicate matchesCall(DataFlowCall call) { exists(string name, string argLabels | this.matchesMethodCallSelector(name, argLabels) and - methodCallSelector(call, name, argLabels) + methodCallSelector(call.asExplicitCall(), name, argLabels) + or + this.matchesMethodCallSelector(name, argLabels) and + setterCallSelector(call.asSetterCall(), name, argLabels) or this.matchesConstructorCallSelector(name, argLabels) and - constructorCallSelector(call, name, argLabels) + constructorCallSelector(call.asExplicitCall(), name, argLabels) ) } @@ -214,10 +229,10 @@ private import AccessPathOutput * Gets the input to `call` specified by `token`. Only singleton access paths are supported. */ bindingset[call, token] -private DataFlow::Node getSinkFromCall(CallExpr call, AccessPathToken token) { +private DataFlow::Node getSinkFromCall(DataFlowCall call, AccessPathToken token) { token.getName() = "Argument" and exists(Argument arg | - arg = call.getAnArgument() and + arg = call.getCallExpr().getAnArgument() and result.asExpr() = arg.getValue() | arg.getPositionalIndex() = parseIntUnbounded(token.getAnArgument()) @@ -225,18 +240,24 @@ private DataFlow::Node getSinkFromCall(CallExpr call, AccessPathToken token) { arg.getName() + ":" = token.getAnArgument() ) or + // The new value passed to a setter comes after the positional arguments + token.getName() = "Argument" and + result.(BuilderNode).isSetterValueArgument(call) and + parseIntUnbounded(token.getAnArgument()) = + count(Argument arg | arg = call.getCallExpr().getAnArgument() and arg.isPositional()) + or token.getName() = "Argument" and token.getAnArgument() = "self" and - result.isReceiverArgument(call) + result.(BuilderNode).isReceiverArgumentEx(call) } /** * Gets the output from `call` specified by `token`. Only singleton access paths are supported. */ bindingset[call, token] -private DataFlow::Node getSourceFromCall(CallExpr call, AccessPathToken token) { +private DataFlow::Node getSourceFromCall(DataFlowCall call, AccessPathToken token) { token = "ReturnValue" and - result.asExpr() = call + result.asExpr() = call.asExplicitCall() or result = getSinkFromCall(call, token).getPostUpdateNode() } @@ -313,7 +334,7 @@ DataFlow::Node getASource(string kind, string provenance) { exists(Selector selector, AccessPath output | normalizedSourceModel(selector, output, kind, provenance) | - exists(CallExpr call | + exists(DataFlowCall call | selector.matchesCall(call) and result = getSourceFromCall(call, output) ) @@ -332,7 +353,7 @@ DataFlow::Node getASink(string kind, string provenance) { exists(Selector selector, AccessPath input | normalizedSinkModel(selector, input, kind, provenance) | - exists(CallExpr call | + exists(DataFlowCall call | selector.matchesCall(call) and result = getSinkFromCall(call, input) ) diff --git a/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll b/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll index f7a773c1dc6d..c661f296dec5 100644 --- a/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll +++ b/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll @@ -131,6 +131,8 @@ module Input3 implements InputSig3 { (exists(resolveAccessorAccess(_)) implies any()) or (exists(resolveCallTarget(_)) implies any()) + or + (exists(resolveSetterCallTarget(_)) implies any()) } predicate inferTypeForDefaults = M3::inferType/2; @@ -929,6 +931,28 @@ Callable resolveCallTarget(CallExpr ce) { result = ce.(Input3::Invocation).getTargetImpl(_).asCallable() } +/** + * Gets the setter invoked when the given call expression appears as an assignment target, + * or is otherwise updated in place. + * + * For a call `x.foo(...)` this is the member `foo=` of the receiver type, such as + * the `subscript=` setter for a Swift subscript call `x[i]`. + */ +cached +SetterMethodDeclaration resolveSetterCallTarget(CallExpr ce) { + CachedStage::ref() and + exists(MemberAccessExpr callee, ClassLikeDeclaration cls, NamespaceNode ns | + callee = ce.getCallee() and + ns.getMember(callee.getMemberName() + "=").isIdentifier(result.getNameNode()) + | + cls = inferType(callee.getBase()).asClass() and + ns.isInstanceMemberNamespace(cls) + or + cls = callee.getBase().(TypeMention).getType().asClass() and + ns.isStaticMemberNamespace(cls) + ) +} + private predicate typeTestAstNodeRepr(AstNode n, string repr) { repr = [n.toString(), n.(Identifier).getValue(), "." + n.(MemberAccessExpr).getMemberName()] or diff --git a/unified/ql/test/library-tests/dataflow/subscripts.swift b/unified/ql/test/library-tests/dataflow/subscripts.swift index 42a16f9def9a..e656026ab1d8 100644 --- a/unified/ql/test/library-tests/dataflow/subscripts.swift +++ b/unified/ql/test/library-tests/dataflow/subscripts.swift @@ -134,6 +134,8 @@ func s10() { func s11(t: TestType) { sink(t[testKey: source("s11.1")]) // $ hasValueFlow=s11.1 t[testKey: source("s11.2")] = "safe" + t[testKey: "safe"] = source("s11.3") + sink(t.getField()) // $ hasValueFlow=s11.3 } func s12() { diff --git a/unified/ql/test/library-tests/dataflow/test.expected b/unified/ql/test/library-tests/dataflow/test.expected index fe155a489ef6..119bfa22a5a6 100644 --- a/unified/ql/test/library-tests/dataflow/test.expected +++ b/unified/ql/test/library-tests/dataflow/test.expected @@ -413,13 +413,16 @@ edges | subscripts.swift:130:5:130:8 | ... .subscript(...) | subscripts.swift:127:24:127:31 | newValue | provenance | | | subscripts.swift:130:12:130:26 | source(...) | subscripts.swift:130:5:130:8 | ... .subscript(...) | provenance | | | subscripts.swift:135:21:135:35 | source(...) | subscripts.swift:135:10:135:36 | ... .subscript(...) | provenance | | -| subscripts.swift:146:24:146:31 | newValue [field] | subscripts.swift:146:24:146:37 | ... .field | provenance | | -| subscripts.swift:149:19:149:19 | s [Return] [field] | subscripts.swift:151:12:151:16 | [post] & ... [field] | provenance | | -| subscripts.swift:149:33:149:33 | [post] s [field] | subscripts.swift:149:19:149:19 | s [Return] [field] | provenance | | -| subscripts.swift:149:33:149:39 | ... .field | subscripts.swift:149:33:149:33 | [post] s [field] | provenance | | -| subscripts.swift:149:43:149:57 | source(...) | subscripts.swift:149:33:149:39 | ... .field | provenance | | -| subscripts.swift:151:12:151:16 | [post] & ... [field] | subscripts.swift:151:13:151:16 | [post] ... .subscript(...) [field] | provenance | | -| subscripts.swift:151:13:151:16 | [post] ... .subscript(...) [field] | subscripts.swift:146:24:146:31 | newValue [field] | provenance | | +| subscripts.swift:137:5:137:22 | ... .subscript(...) | subscripts.swift:138:10:138:10 | t [field] | provenance | | +| subscripts.swift:137:26:137:40 | source(...) | subscripts.swift:137:5:137:22 | ... .subscript(...) | provenance | | +| subscripts.swift:138:10:138:10 | t [field] | subscripts.swift:138:10:138:21 | ... .getField(...) | provenance | | +| subscripts.swift:148:24:148:31 | newValue [field] | subscripts.swift:148:24:148:37 | ... .field | provenance | | +| subscripts.swift:151:19:151:19 | s [Return] [field] | subscripts.swift:153:12:153:16 | [post] & ... [field] | provenance | | +| subscripts.swift:151:33:151:33 | [post] s [field] | subscripts.swift:151:19:151:19 | s [Return] [field] | provenance | | +| subscripts.swift:151:33:151:39 | ... .field | subscripts.swift:151:33:151:33 | [post] s [field] | provenance | | +| subscripts.swift:151:43:151:57 | source(...) | subscripts.swift:151:33:151:39 | ... .field | provenance | | +| subscripts.swift:153:12:153:16 | [post] & ... [field] | subscripts.swift:153:13:153:16 | [post] ... .subscript(...) [field] | provenance | | +| subscripts.swift:153:13:153:16 | [post] ... .subscript(...) [field] | subscripts.swift:148:24:148:31 | newValue [field] | provenance | | | test.swift:17:10:17:23 | source(...) | test.swift:17:10:17:32 | ... + ... | provenance | | | test.swift:18:19:18:32 | source(...) | test.swift:18:10:18:32 | ... + ... | provenance | | | test.swift:20:13:20:26 | source(...) | test.swift:20:10:20:33 | StringInterpolationExpr | provenance | | @@ -1104,14 +1107,18 @@ nodes | subscripts.swift:131:10:131:13 | ... .subscript(...) | semmle.label | ... .subscript(...) | | subscripts.swift:135:10:135:36 | ... .subscript(...) | semmle.label | ... .subscript(...) | | subscripts.swift:135:21:135:35 | source(...) | semmle.label | source(...) | -| subscripts.swift:146:24:146:31 | newValue [field] | semmle.label | newValue [field] | -| subscripts.swift:146:24:146:37 | ... .field | semmle.label | ... .field | -| subscripts.swift:149:19:149:19 | s [Return] [field] | semmle.label | s [Return] [field] | -| subscripts.swift:149:33:149:33 | [post] s [field] | semmle.label | [post] s [field] | -| subscripts.swift:149:33:149:39 | ... .field | semmle.label | ... .field | -| subscripts.swift:149:43:149:57 | source(...) | semmle.label | source(...) | -| subscripts.swift:151:12:151:16 | [post] & ... [field] | semmle.label | [post] & ... [field] | -| subscripts.swift:151:13:151:16 | [post] ... .subscript(...) [field] | semmle.label | [post] ... .subscript(...) [field] | +| subscripts.swift:137:5:137:22 | ... .subscript(...) | semmle.label | ... .subscript(...) | +| subscripts.swift:137:26:137:40 | source(...) | semmle.label | source(...) | +| subscripts.swift:138:10:138:10 | t [field] | semmle.label | t [field] | +| subscripts.swift:138:10:138:21 | ... .getField(...) | semmle.label | ... .getField(...) | +| subscripts.swift:148:24:148:31 | newValue [field] | semmle.label | newValue [field] | +| subscripts.swift:148:24:148:37 | ... .field | semmle.label | ... .field | +| subscripts.swift:151:19:151:19 | s [Return] [field] | semmle.label | s [Return] [field] | +| subscripts.swift:151:33:151:33 | [post] s [field] | semmle.label | [post] s [field] | +| subscripts.swift:151:33:151:39 | ... .field | semmle.label | ... .field | +| subscripts.swift:151:43:151:57 | source(...) | semmle.label | source(...) | +| subscripts.swift:153:12:153:16 | [post] & ... [field] | semmle.label | [post] & ... [field] | +| subscripts.swift:153:13:153:16 | [post] ... .subscript(...) [field] | semmle.label | [post] ... .subscript(...) [field] | | test.swift:9:10:9:21 | source(...) | semmle.label | source(...) | | test.swift:17:10:17:23 | source(...) | semmle.label | source(...) | | test.swift:17:10:17:32 | ... + ... | semmle.label | ... + ... | @@ -1492,7 +1499,8 @@ testFailures | subscripts.swift:127:24:127:31 | newValue | subscripts.swift:130:12:130:26 | source(...) | subscripts.swift:127:24:127:31 | newValue | $@ | subscripts.swift:130:12:130:26 | source(...) | source(...) | | subscripts.swift:131:10:131:13 | ... .subscript(...) | subscripts.swift:126:19:126:33 | source(...) | subscripts.swift:131:10:131:13 | ... .subscript(...) | $@ | subscripts.swift:126:19:126:33 | source(...) | source(...) | | subscripts.swift:135:10:135:36 | ... .subscript(...) | subscripts.swift:135:21:135:35 | source(...) | subscripts.swift:135:10:135:36 | ... .subscript(...) | $@ | subscripts.swift:135:21:135:35 | source(...) | source(...) | -| subscripts.swift:146:24:146:37 | ... .field | subscripts.swift:149:43:149:57 | source(...) | subscripts.swift:146:24:146:37 | ... .field | $@ | subscripts.swift:149:43:149:57 | source(...) | source(...) | +| subscripts.swift:138:10:138:21 | ... .getField(...) | subscripts.swift:137:26:137:40 | source(...) | subscripts.swift:138:10:138:21 | ... .getField(...) | $@ | subscripts.swift:137:26:137:40 | source(...) | source(...) | +| subscripts.swift:148:24:148:37 | ... .field | subscripts.swift:151:43:151:57 | source(...) | subscripts.swift:148:24:148:37 | ... .field | $@ | subscripts.swift:151:43:151:57 | source(...) | source(...) | | test.swift:9:10:9:21 | source(...) | test.swift:9:10:9:21 | source(...) | test.swift:9:10:9:21 | source(...) | $@ | test.swift:9:10:9:21 | source(...) | source(...) | | test.swift:17:10:17:32 | ... + ... | test.swift:17:10:17:23 | source(...) | test.swift:17:10:17:32 | ... + ... | $@ | test.swift:17:10:17:23 | source(...) | source(...) | | test.swift:18:10:18:32 | ... + ... | test.swift:18:19:18:32 | source(...) | test.swift:18:10:18:32 | ... + ... | $@ | test.swift:18:19:18:32 | source(...) | source(...) | diff --git a/unified/ql/test/library-tests/dataflow/test.ext.yml b/unified/ql/test/library-tests/dataflow/test.ext.yml index 796d1ca2d00b..a751df21c849 100644 --- a/unified/ql/test/library-tests/dataflow/test.ext.yml +++ b/unified/ql/test/library-tests/dataflow/test.ext.yml @@ -19,3 +19,4 @@ extensions: - ["", "TestType", true, "getField()", "", "", "Argument[self].Member[field]", "ReturnValue", "value", "manual"] - ["", "TestType", true, "setField(_:)", "", "", "Argument[0]", "Argument[self].Member[field]", "value", "manual"] - ["", "TestType", true, "subscript(testKey:)", "", "", "Argument[testKey:]", "ReturnValue", "value", "manual"] + - ["", "TestType", true, "subscript=(_:testKey:)", "", "", "Argument[0]", "Argument[self].Member[field]", "value", "manual"] From f83db13bf1da03446719592bd2ea2374310e4705 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 11 Nov 2026 21:48:12 +0100 Subject: [PATCH 3/4] Unified: Extract Swift subscript setters as setter_method accessors The subscript getter is a plain 'subscript' function declaration, and the setter an accessor_declaration of kind 'setter_method' with the same name. Accessor declarations gain a type_parameter field for generic subscripts. Setter calls resolve to the setter_method accessor with the callee's name; MaD models still refer to setter calls as 'name='. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- unified/extractor/ast_types.yml | 18 +++++++++--------- unified/extractor/src/languages/swift/swift.rs | 15 +++++++++------ .../swift/subscripts/subscript-get-set.output | 10 ++++++---- .../subscript-protocol-requirement.output | 5 +++-- unified/ql/lib/codeql/unified/internal/Ast.qll | 13 ++++++++++++- .../lib/codeql/unified/internal/FacadeAst.qll | 12 ++++++------ .../codeql/unified/internal/mad/LegacyMaD.qll | 3 ++- .../unified/internal/typeinference/Type.qll | 2 ++ .../internal/typeinference/TypeInference.qll | 13 +++++++++---- unified/ql/lib/unified.dbscheme | 7 +++++++ 10 files changed, 65 insertions(+), 33 deletions(-) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index 36e36c594064..28bab7c72ffd 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -335,14 +335,6 @@ named: type: expr # A top-level function declaration. - # - # A function whose name is an identifier followed by `=` (such as `subscript=`) - # is a setter invoked via call syntax: a call to `foo` (such as Swift's - # `x[i]`, a call to `x.subscript`) in an assignment target or otherwise - # updated in place invokes the setter `foo=`. The setter takes the new value - # as an extra trailing positional parameter. A getter/setter pair is emitted - # as two adjacent declarations, the second one tagged with a - # `chained_declaration` modifier. function_declaration: modifier*: modifier name_node: identifier @@ -537,15 +529,23 @@ named: # downstream. Stored properties with observers are emitted as a # variable_declaration followed by one accessor_declaration per observer # (each observer also tagged with `chained_declaration`). + # + # An accessor of kind `setter_method` is a setter invoked via call syntax, + # such as the setter of a Swift subscript. A call `x.foo(...)` that is the + # target of an assignment, or is otherwise updated in place, invokes the + # `setter_method` accessor named `foo` (whereas a call in a read position + # invokes the ordinary method `foo`). The setter takes the new value as an + # extra trailing positional parameter. accessor_declaration: modifier*: modifier name_node: identifier accessor_kind: accessor_kind + type_parameter*: type_parameter parameter*: parameter return_type?: expr body?: block - # "get", "set", or a language-specific kind like "didSet" + # "get", "set", "setter_method", or a language-specific kind like "didSet" accessor_kind: # Static or instance initializer block. That is, code that runs at initialization time of either the class or an instance. diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 385f353e40b2..dd65bf8d2bc5 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -171,8 +171,9 @@ struct SubscriptAccessor { body: Option, } -/// Emit a `function_declaration` for one accessor of a subscript: the getter -/// is named `subscript` and the setter `subscript=`. The shared parts of the subscript (modifiers, generic and value +/// Emit the declaration for one accessor of a subscript: the getter is a +/// `function_declaration` named `subscript`, and the setter an +/// `accessor_declaration` of kind `setter_method`. The shared parts of the subscript (modifiers, generic and value /// parameters and the element type) are translated afresh for each accessor, /// so a getter/setter pair each get their own copy. A setter takes the new /// value as an extra trailing positional parameter and has no return type. @@ -197,11 +198,12 @@ fn subscript_method( match accessor.setter { Some((param_name, spec)) => { let implicit_param_name = param_name.is_none().then_some("newValue"); - tree!((function_declaration + tree!((accessor_declaration modifier: {mods} modifier: {accessor.modifiers} modifier: [(modifier "chained_declaration") if { is_chained }] - name_node: (identifier "subscript=" location: {kw}) + name_node: (identifier #{kw}) + accessor_kind: (accessor_kind "setter_method" location: {spec}) type_parameter: {type_params} parameter: {params} parameter: [ @@ -492,8 +494,9 @@ fn translation_rules() -> Vec> { ), // ---- Subscripts ---- // A subscript declaration becomes a `function_declaration` named - // `subscript` for its getter and, if it has a setter, one named - // `subscript=` for the setter. A subscript with an explicit accessor block publishes its shared parts + // `subscript` for its getter and, if it has a setter, an + // `accessor_declaration` of kind `setter_method` named `subscript`. + // A subscript with an explicit accessor block publishes its shared parts // in `ctx` and lets the `accessorDecl` rule below emit one method per // accessor; each one after the first is tagged `chained_declaration`. rule!( diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output index f29ff763eef5..95adcbba760f 100644 --- a/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-get-set.output @@ -259,11 +259,12 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: identifier "index" source="index" - function_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> Int {\n get { data[index] }\n ⟨modifier⟩ set(⟨parameter⟩) ⟨body⟩" + accessor_declaration source="⟨name_node⟩(⟨parameter⟩, ⟨parameter⟩) -> Int {\n get { data[index] }\n ⟨modifier⟩ ⟨accessor_kind⟩(⟨parameter⟩) ⟨body⟩" modifier: modifier "nonmutating" source="nonmutating" modifier "chained_declaration" source="" - name_node: identifier "subscript=" source="subscript" + name_node: identifier "subscript" source="subscript" + accessor_kind: accessor_kind "setter_method" source="set" parameter: parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" external_name_node: identifier "at" source="at" @@ -284,9 +285,10 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: identifier "v" source="v" - function_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> T {\n ⟨parameter⟩ ⟨body⟩" + accessor_declaration source="⟨modifier⟩ ⟨name_node⟩<⟨type_parameter⟩>(⟨parameter⟩) -> T {\n ⟨accessor_kind⟩⟨parameter⟩ ⟨body⟩" modifier: modifier "static" source="static" - name_node: identifier "subscript=" source="subscript" + name_node: identifier "subscript" source="subscript" + accessor_kind: accessor_kind "setter_method" source="set" type_parameter: type_parameter source="⟨name_node⟩" name_node: identifier "T" source="T" diff --git a/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output index 9a7a2da8f920..4a5833d5c58a 100644 --- a/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output +++ b/unified/extractor/tests/corpus/swift/subscripts/subscript-protocol-requirement.output @@ -76,9 +76,10 @@ top_level source="⟨body⟩" pattern: identifier "i" source="i" return_type: identifier "String" source="String" body: block source="" - function_declaration source="⟨name_node⟩(⟨parameter⟩) -> String { get ⟨parameter⟩" + accessor_declaration source="⟨name_node⟩(⟨parameter⟩) -> String { get ⟨accessor_kind⟩⟨parameter⟩" modifier: modifier "chained_declaration" source="" - name_node: identifier "subscript=" source="subscript" + name_node: identifier "subscript" source="subscript" + accessor_kind: accessor_kind "setter_method" source="set" parameter: parameter source="⟨pattern⟩: ⟨type⟩" type: identifier "Int" source="Int" diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 5f410460536e..91aba1b60327 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -128,6 +128,14 @@ module Unified { unified_accessor_declaration_return_type(this, result) } + /** Gets the node corresponding to the field `type_parameter`. */ + final F::TypeParameter getTypeParameter(int i) { + unified_accessor_declaration_type_parameter(this, i, result) + } + + /** Gets the node corresponding to the field `type_parameter`. */ + final F::TypeParameter getATypeParameter() { result = this.getTypeParameter(_) } + /** Gets a field or child node of this node. */ final override F::AstNode getAFieldOrChild() { unified_accessor_declaration_def(this, result, _) or @@ -135,7 +143,8 @@ module Unified { unified_accessor_declaration_modifier(this, _, result) or unified_accessor_declaration_def(this, _, result) or unified_accessor_declaration_parameter(this, _, result) or - unified_accessor_declaration_return_type(this, result) + unified_accessor_declaration_return_type(this, result) or + unified_accessor_declaration_type_parameter(this, _, result) } } @@ -1628,6 +1637,8 @@ module Unified { or result = node.(AccessorDeclaration).getReturnType() and i = -1 and name = "getReturnType" or + result = node.(AccessorDeclaration).getTypeParameter(i) and name = "getTypeParameter" + or result = node.(Argument).getModifier(i) and name = "getModifier" or result = node.(Argument).getNameNode() and i = -1 and name = "getNameNode" diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index 7e7072845432..d602ae9e71cb 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -476,12 +476,12 @@ module Unified { } /** - * A function declaration whose name is an identifier followed by `=`, representing a setter that - * is invoked via call syntax when a call appears as an assignment target or is otherwise updated - * in place. A call to `foo` in such a position invokes the setter `foo=`. + * An accessor declaration of kind `setter_method`, representing a setter that is invoked via call + * syntax when a call appears as an assignment target or is otherwise updated in place. Such a call + * to `foo` invokes the `setter_method` accessor named `foo`, rather than the ordinary method `foo`. * * The new value is passed as an additional trailing positional parameter. For example, the - * setter of a Swift subscript declaration is named `subscript=`: + * setter of a Swift subscript declaration: * ```swift * subscript(i: Int) -> String { * get { "42" } @@ -489,8 +489,8 @@ module Unified { * } * ``` */ - final class SetterMethodDeclaration extends FunctionDeclaration { - SetterMethodDeclaration() { this.getName().regexpMatch("[\\p{L}_][\\p{L}\\p{N}_]*=") } + final class SetterMethodDeclaration extends AccessorDeclaration { + SetterMethodDeclaration() { this.getAccessorKind().getValue() = "setter_method" } } /** An `if` statement or conditional expression. */ diff --git a/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll b/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll index a473bdfdf90e..74ce318868a6 100644 --- a/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll +++ b/unified/ql/lib/codeql/unified/internal/mad/LegacyMaD.qll @@ -78,7 +78,8 @@ private predicate methodCallSelector(CallExpr call, string name, string argLabel } /** - * Holds if `call` invokes a setter named `name`, such as `subscript=` for `x[i] = y`. + * Holds if `call` invokes a setter that models refer to as `name`. By convention, the setter + * invoked by a call to `foo` is named `foo=` in models, such as `subscript=` for `x[i] = y`. * * The new value is passed as an extra positional argument after the explicit ones. */ diff --git a/unified/ql/lib/codeql/unified/internal/typeinference/Type.qll b/unified/ql/lib/codeql/unified/internal/typeinference/Type.qll index 0ffe9fccdd7a..73aff31e9d9c 100644 --- a/unified/ql/lib/codeql/unified/internal/typeinference/Type.qll +++ b/unified/ql/lib/codeql/unified/internal/typeinference/Type.qll @@ -239,6 +239,8 @@ class TypeParameterType extends TypeParameter, TTypeParameterType { typeParam = result.(Unified::TypeAliasDeclaration).getATypeParameter() or typeParam = result.(Unified::FunctionDeclaration).getATypeParameter() + or + typeParam = result.(Unified::AccessorDeclaration).getATypeParameter() } override Identifier getNameNode() { result = typeParam.getNameNode() } diff --git a/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll b/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll index c661f296dec5..742cce0f6124 100644 --- a/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll +++ b/unified/ql/lib/codeql/unified/internal/typeinference/TypeInference.qll @@ -284,7 +284,10 @@ module Input3 implements InputSig3 { private class AccessorField extends FieldImpl, Callable { private AccessorDeclaration accessor; - AccessorField() { accessor = this.getCallable() } + AccessorField() { + accessor = this.getCallable() and + not accessor instanceof SetterMethodDeclaration // invoked via call syntax, not a field + } override TypeMention getType() { result = accessor.getReturnType() } @@ -457,6 +460,8 @@ module Input3 implements InputSig3 { TypeParameter getTypeParameter(int i) { result = TTypeParameterType(c.(FunctionDeclaration).getTypeParameter(i)) + or + result = TTypeParameterType(c.(AccessorDeclaration).getTypeParameter(i)) } TypeMention getAdditionalTypeParameterConstraint(TypeParameter tp) { @@ -935,15 +940,15 @@ Callable resolveCallTarget(CallExpr ce) { * Gets the setter invoked when the given call expression appears as an assignment target, * or is otherwise updated in place. * - * For a call `x.foo(...)` this is the member `foo=` of the receiver type, such as - * the `subscript=` setter for a Swift subscript call `x[i]`. + * For a call `x.foo(...)` this is the `setter_method` accessor named `foo` of the receiver type, + * such as the setter of a subscript for a Swift subscript call `x[i]`. */ cached SetterMethodDeclaration resolveSetterCallTarget(CallExpr ce) { CachedStage::ref() and exists(MemberAccessExpr callee, ClassLikeDeclaration cls, NamespaceNode ns | callee = ce.getCallee() and - ns.getMember(callee.getMemberName() + "=").isIdentifier(result.getNameNode()) + ns.getMember(callee.getMemberName()).isIdentifier(result.getNameNode()) | cls = inferType(callee.getBase()).asClass() and ns.isInstanceMemberNamespace(cls) diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index 54ede4d9d45c..870c546c85f8 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -160,6 +160,13 @@ unified_accessor_declaration_return_type( unique int return_type: @unified_expr ref ); +#keyset[unified_accessor_declaration, index] +unified_accessor_declaration_type_parameter( + int unified_accessor_declaration: @unified_accessor_declaration ref, + int index: int ref, + unique int type_parameter: @unified_type_parameter ref +); + unified_accessor_declaration_def( unique int id: @unified_accessor_declaration, int accessor_kind: @unified_token_accessor_kind ref, From 82d058d3da662fe12055b35627f491478b118c3e Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 12 Nov 2026 20:38:20 +0100 Subject: [PATCH 4/4] unified: Simple flow in and out of subscript calls --- .../internal/dataflow/DataFlowPluginSwift.qll | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll index 8c880548f65e..91f0ea9b656b 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowPluginSwift.qll @@ -68,6 +68,21 @@ private class SwiftDataFlowPlugin extends DataFlowPlugin { node1.isResultValue(call.getArgument(0).getValue()) and step.value() and node2.isResultValue(call) + or + // Simple flow in and out of subscript operators + call.getCallee().(MemberAccess).getMemberName() = "subscript" and + ( + node1.isReceiverArgument(call) and + step.readArrayElement() and + node2.isResultValue(call) + or + exists(DataFlowCall setter | + setter.asSetterCall() = call and + node1.isSetterValueArgument(setter) and + step.storeArrayElement() and + node2.isReceiverPostUpdateEx(setter) + ) + ) ) or exists(UnaryExpr expr |