Skip to content

Unified: Add data flow support for getters and setters - #22896

Merged
asgerf merged 38 commits into
github:mainfrom
asgerf:unified/accessors
Oct 29, 2026
Merged

asgerf merged 38 commits into
github:mainfrom
asgerf:unified/accessors

Conversation

@asgerf

@asgerf asgerf commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Adds support calls to getters and setters, called "computed properties" in Swift.

Some complications for Swift:

  • Getters and setters can be declared locally. A local getter/setter can capture variables.
  • Setters have an implicit parameter called newValue. We insert this parameter in the AST mapping.
  • Writebacks: If p refers to a value-typed computed property p.x = y results in a call to p's setter.
  • Writebacks also occur in foo(&p) where p is passed as an inout argument. Full support for inout parameters is out of scope for this PR, however, so this is only half-supported right now.
  • Writebacks also occur for calls like p.mutate() where mutate refers to a mutating func. This is effectively the same as seeing the receiver as an inout argument.

Also does some clean up in CaptureSsa

Commit-by-commit review recommended.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved accessor resolution, state propagation, and capture-ordering gaps can cause valid data flows to be missed.

5 open issues
What changed in this PR

Adds getter and setter modeling to Unified’s data-flow analysis, with Swift extraction support and regression fixtures.

Changes:

  • Extracts accessor return types, parameters, and property modifiers.
  • Models accessor calls, receiver flow, and captured-variable synchronization.
  • Adds local, instance, and static accessor tests and updates expected results.
File Description
unified/​ql/​test/​library-tests/​dataflow/​test.expected Updates accessor and closure-flow expectations.
unified/​ql/​test/​library-tests/​dataflow/​accessors_static.swift Adds static accessor cases.
unified/​ql/​test/​library-tests/​dataflow/​accessors_local.swift Adds local accessor and capture cases.
unified/​ql/​test/​library-tests/​dataflow/​accessors_instance.swift Adds instance accessor and receiver-flow cases.
unified/​ql/​lib/​unified.dbscheme Renames the accessor return-type relation.
unified/​ql/​lib/​codeql/​unified/​internal/​typeinference/​Type.qll Exposes the class represented by a type.
unified/​ql/​lib/​codeql/​unified/​internal/​StaticNameBinding.qll Unifies qualified and unqualified member access.
unified/​ql/​lib/​codeql/​unified/​internal/​NameBindingPlugin.qll Recognizes instance accessors.
unified/​ql/​lib/​codeql/​unified/​internal/​LocalNameBinding.qll Identifies computed local variables.
unified/​ql/​lib/​codeql/​unified/​internal/​ExprPositions.qll Uses the renamed accessor return-type API.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​Step.qll Adds call-graph-only flow steps.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​LocalSsa.qll Excludes computed locals from stored-variable tracking.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​DataFlowNode.qll Extends receiver APIs and accessor control-flow positions.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​DataFlowInstantiation.qll Connects getter results and setter arguments.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​DataFlowGraph.qll Adds accessor receiver and callable edges.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​DataFlowCall.qll Represents implicit getter and setter calls.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​CaptureSsa.qll Revises closure alias and escape tracking.
unified/​ql/​lib/​codeql/​unified/​internal/​dataflow/​CallGraph.qll Resolves accessor call targets.
unified/​ql/​lib/​codeql/​unified/​internal/​Ast.qll Updates generated accessor return-type APIs.
unified/​ql/​consistency-queries/​DataFlowConsistency.ql Exempts setter inputs from post-update checks.
unified/​extractor/​tests/​corpus/​swift/​variables/​property-with-willset-and-didset-observers.output Records synthesized observer parameters.
unified/​extractor/​tests/​corpus/​swift/​types/​protocol-with-read-only-and-read-write-property-requirements.output Updates protocol accessor expectations.
unified/​extractor/​tests/​corpus/​swift/​types/​property-with-getter-and-setter.output Records getter return types and setter parameters.
unified/​extractor/​tests/​corpus/​swift/​types/​computed-property.output Updates computed-property return-type output.
unified/​extractor/​tests/​corpus/​swift/​types/​binding-modifier-does-not-leak-into-accessor-body.output Updates the accessor return-type field.
unified/​extractor/​src/​languages/​swift/​swift.rs Extracts accessor parameters, return types, and modifiers.
unified/​extractor/​ast_types.yml Defines the accessor return-type field.

Review effort: Balanced

asgerf and others added 26 commits October 27, 2026 14:15
Tests are split into three categories: local, static, and instance. They follow mostly the same structure.
Accessors previously had a 'type' field. Now, it has a return_type instead, which is only set for getters. For set, didSet, and willSet, we insert a synthetic newValue/oldValue parameter, which also carries the type previously stored in accessor_declaration.type.
Modifiers like 'static' did not get preserved when extracting accessors, making most of the tests fail for static getters/setters. All those tests now pass.
This does the minimal change needed to get things working, but it's not
pretty. A later commit will refactor this to have less ad-hoc logic here.
Now that all data flow nodes are in Expr, we get more noise if we don't restrict this
This was already handled in the AST mapping, but no QL test was added initially
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
If `p` is a getter/setter pair for a value type, then `p.f = x` ends with a call to p's setter with the updated value (called "writebacks" in Swift).
Writebacks are a language-specific thing so let's add a hook for it

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isMutatedByCallOrAssign was a synonym for hasPostUpdateAfterCallOrAssign
@asgerf
asgerf force-pushed the unified/accessors branch from 29a2bc5 to 405e322 Compare October 28, 2026 10:21
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@asgerf asgerf added the no-change-note-required This PR does not need a change note label Oct 28, 2026
UnqualifiedMemberAccess is only populated when static name binding
positively identifies the resolution target, so either the first case
catches it, and if unresolved, the second case will miss it anyway
because it won't be a MemberAccess instance.
@asgerf
asgerf marked this pull request as ready for review October 28, 2026 13:04
@asgerf
asgerf requested review from a team as code owners October 28, 2026 13:04

@hvitved hvitved left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks plausible to me (lots of moving parts).

hvitved
hvitved previously approved these changes Oct 29, 2026
Comment on lines +40 to +69
isInWriteBackPosition(expr)
// TODO: Check that 'expr' has a value type. Reference types don't cause writebacks.
}
}

/**
* Holds if `expr` is target of an assignment or 'inout' argument,
* or the base of such an expression, e.g. `x` in `x.f1.f2 = foo()`
*/
private predicate isInWriteBackPosition(Expr expr) {
isInAssignmentContext(expr, _)
or
// Arguments to an 'inout' parameter must be prefixed with '&'
exists(UnaryExpr unary |
unary.getOperator().(PrefixOperator).getValue() = "&" and
unary.getOperand() = expr
// TODO: support custom operators with an 'inout' parameter, which lack the '&' indicator
)
or
// The receiver of a call to a 'mutating' function counts as an 'inout' argument
exists(CallExpr call, Callable target |
target = T::resolveCallTarget(call) and
target.hasModifier("mutating") and
expr = call.getCallee().(MemberAccessExpr).getBase()
)
or
exists(MemberAccessExpr member |
isInWriteBackPosition(member) and
expr = member.getBase()
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this addition meant for the Swift plugin?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it was. Will move it 👍

@aschackmull aschackmull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One clarifying question, otherwise LGTM.

This was accidentally put outside the plugin
@asgerf
asgerf merged commit 824bba2 into github:main Oct 29, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-change-note-required This PR does not need a change note Unified

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants