From 8e7a6d274e62b63930e2360e224b9c511e5255d9 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Thu, 5 Nov 2026 09:59:38 +0100 Subject: [PATCH 1/2] Unified: add field data flow tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../test/library-tests/dataflow/fields.swift | 97 ++++++++++++ .../test/library-tests/dataflow/test.expected | 139 ++++++++++++++++++ .../test/library-tests/dataflow/test.ext.yml | 1 + 3 files changed, 237 insertions(+) create mode 100644 unified/ql/test/library-tests/dataflow/fields.swift diff --git a/unified/ql/test/library-tests/dataflow/fields.swift b/unified/ql/test/library-tests/dataflow/fields.swift new file mode 100644 index 000000000000..79231f66b7ff --- /dev/null +++ b/unified/ql/test/library-tests/dataflow/fields.swift @@ -0,0 +1,97 @@ +func source(_ s: String) -> String { return s } + +@discardableResult +func sink(_ s: Any) -> String { return "" } + +class FieldBox { + var field: String = "" +} + +class OtherFieldBox { + var field: String = "" +} + +class InheritedFieldBox: FieldBox {} + +func resolvedFields() { + let box = FieldBox() + box.field = source("fields.resolved") + sink(box.field) // $ hasValueFlow=fields.resolved + sink((box as! OtherFieldBox).field) // $ SPURIOUS: hasValueFlow=fields.resolved +} + +func inheritedFields() { + let box = InheritedFieldBox() + box.field = source("fields.inherited") + sink((box as! FieldBox).field) // $ hasValueFlow=fields.inherited +} + +func unresolvedFieldRead() { + let box = FieldBox() + box.field = source("fields.unresolved-read") + sink((box as! UnknownFieldBox).field) // $ hasValueFlow=fields.unresolved-read +} + +func unresolvedFieldStore() { + let box = FieldBox() + (box as! UnknownFieldBox).field = source("fields.unresolved-store") + sink(box.field) // $ hasValueFlow=fields.unresolved-store +} + +func summarizedFieldRead() { + let box = FieldBox() + box.field = source("fields.summary") + sink(TestType.readField(box)) // $ hasValueFlow=fields.summary +} + +class BaseBox { + var field: String = "" +} + +class OverridingBox: BaseBox { + override var field: String { + get { super.field } + set { super.field = newValue } + } +} + +func overriddenFieldRead() { + let base: BaseBox = OverridingBox() + base.field = source("fields.override-read") + let sub = base as! OverridingBox + sink(sub.field) // $ hasValueFlow=fields.override-read +} + +protocol FieldProtocol { + var field: String { get set } +} + +class ProtocolBox: FieldProtocol { + var field: String = "" +} + +class ProtocolBox2: FieldProtocol { + var field: String = "" +} + +func protocolFieldRead() { + let box = ProtocolBox() + box.field = source("fields.protocol") + sink(box.field) // $ hasValueFlow=fields.protocol + let p = box as FieldProtocol + sink(p.field) // $ hasValueFlow=fields.protocol + let box2 = box as! ProtocolBox2 + sink(box2.field) // $ SPURIOUS: hasValueFlow=fields.protocol + let box1or2 = 2 > 3 ? box as FieldProtocol : box2 + sink(box1or2.field) // $ hasValueFlow=fields.protocol +} + +func protocolFieldWrite() { + let box = ProtocolBox() + var p = box as FieldProtocol + p.field = source("fields.protocol") + sink(p.field) // $ hasValueFlow=fields.protocol + sink(box.field) // $ MISSING: hasValueFlow=fields.protocol (aliasing) + let box2 = p as! OtherFieldBox + sink(box2.field) // $ SPURIOUS: hasValueFlow=fields.protocol +} diff --git a/unified/ql/test/library-tests/dataflow/test.expected b/unified/ql/test/library-tests/dataflow/test.expected index a479255f6d23..e3cdba402049 100644 --- a/unified/ql/test/library-tests/dataflow/test.expected +++ b/unified/ql/test/library-tests/dataflow/test.expected @@ -251,6 +251,65 @@ edges | enums.swift:82:12:82:12 | e [foo.0] | enums.swift:83:10:83:23 | ... .foo(...) [foo.0] | provenance | | | enums.swift:83:10:83:23 | ... .foo(...) [foo.0] | enums.swift:83:22:83:22 | x | provenance | | | enums.swift:83:22:83:22 | x | enums.swift:84:14:84:14 | x | provenance | | +| fields.swift:18:5:18:7 | [post] box [field] | fields.swift:19:10:19:12 | box [field] | provenance | | +| fields.swift:18:5:18:7 | [post] box [field] | fields.swift:20:11:20:13 | box [field] | provenance | | +| fields.swift:18:5:18:13 | ... .field | fields.swift:18:5:18:7 | [post] box [field] | provenance | | +| fields.swift:18:17:18:41 | source(...) | fields.swift:18:5:18:13 | ... .field | provenance | | +| fields.swift:19:10:19:12 | box [field] | fields.swift:19:10:19:18 | ... .field | provenance | | +| fields.swift:20:11:20:13 | box [field] | fields.swift:20:11:20:31 | TypeCastExpr [field] | provenance | | +| fields.swift:20:11:20:31 | TypeCastExpr [field] | fields.swift:20:10:20:38 | ... .field | provenance | | +| fields.swift:25:5:25:7 | [post] box [field] | fields.swift:26:11:26:13 | box [field] | provenance | | +| fields.swift:25:5:25:13 | ... .field | fields.swift:25:5:25:7 | [post] box [field] | provenance | | +| fields.swift:25:17:25:42 | source(...) | fields.swift:25:5:25:13 | ... .field | provenance | | +| fields.swift:26:11:26:13 | box [field] | fields.swift:26:11:26:26 | TypeCastExpr [field] | provenance | | +| fields.swift:26:11:26:26 | TypeCastExpr [field] | fields.swift:26:10:26:33 | ... .field | provenance | | +| fields.swift:31:5:31:7 | [post] box [field] | fields.swift:32:11:32:13 | box [field] | provenance | | +| fields.swift:31:5:31:13 | ... .field | fields.swift:31:5:31:7 | [post] box [field] | provenance | | +| fields.swift:31:17:31:48 | source(...) | fields.swift:31:5:31:13 | ... .field | provenance | | +| fields.swift:32:11:32:13 | box [field] | fields.swift:32:11:32:33 | TypeCastExpr [field] | provenance | | +| fields.swift:32:11:32:33 | TypeCastExpr [field] | fields.swift:32:10:32:40 | ... .field | provenance | | +| fields.swift:37:5:37:35 | ... .field | fields.swift:37:6:37:28 | [post] TypeCastExpr [field] | provenance | | +| fields.swift:37:6:37:28 | [post] TypeCastExpr [field] | fields.swift:38:10:38:12 | box [field] | provenance | | +| fields.swift:37:39:37:71 | source(...) | fields.swift:37:5:37:35 | ... .field | provenance | | +| fields.swift:38:10:38:12 | box [field] | fields.swift:38:10:38:18 | ... .field | provenance | | +| fields.swift:43:5:43:7 | [post] box [field] | fields.swift:44:29:44:31 | box [field] | provenance | | +| fields.swift:43:5:43:13 | ... .field | fields.swift:43:5:43:7 | [post] box [field] | provenance | | +| fields.swift:43:17:43:40 | source(...) | fields.swift:43:5:43:13 | ... .field | provenance | | +| fields.swift:44:29:44:31 | box [field] | fields.swift:44:10:44:32 | ... .readField(...) | provenance | | +| fields.swift:53:15:53:19 | super [field] | fields.swift:53:15:53:25 | ... .field | provenance | | +| fields.swift:60:5:60:8 | [post] base [field] | fields.swift:61:15:61:18 | base [field] | provenance | | +| fields.swift:60:5:60:14 | ... .field | fields.swift:60:5:60:8 | [post] base [field] | provenance | | +| fields.swift:60:18:60:47 | source(...) | fields.swift:60:5:60:14 | ... .field | provenance | | +| fields.swift:61:9:61:11 | sub [field] | fields.swift:62:10:62:12 | sub [field] | provenance | | +| fields.swift:61:15:61:18 | base [field] | fields.swift:61:9:61:11 | sub [field] | provenance | | +| fields.swift:62:10:62:12 | sub [field] | fields.swift:53:15:53:19 | super [field] | provenance | | +| fields.swift:62:10:62:12 | sub [field] | fields.swift:62:10:62:18 | ... .field | provenance | | +| fields.swift:79:5:79:7 | [post] box [field] | fields.swift:80:10:80:12 | box [field] | provenance | | +| fields.swift:79:5:79:7 | [post] box [field] | fields.swift:81:13:81:15 | box [field] | provenance | | +| fields.swift:79:5:79:7 | [post] box [field] | fields.swift:83:16:83:18 | box [field] | provenance | | +| fields.swift:79:5:79:7 | [post] box [field] | fields.swift:85:27:85:29 | box [field] | provenance | | +| fields.swift:79:5:79:13 | ... .field | fields.swift:79:5:79:7 | [post] box [field] | provenance | | +| fields.swift:79:17:79:41 | source(...) | fields.swift:79:5:79:13 | ... .field | provenance | | +| fields.swift:80:10:80:12 | box [field] | fields.swift:80:10:80:18 | ... .field | provenance | | +| fields.swift:81:9:81:9 | p [field] | fields.swift:82:10:82:10 | p [field] | provenance | | +| fields.swift:81:13:81:15 | box [field] | fields.swift:81:9:81:9 | p [field] | provenance | | +| fields.swift:82:10:82:10 | p [field] | fields.swift:82:10:82:16 | ... .field | provenance | | +| fields.swift:83:9:83:12 | box2 [field] | fields.swift:84:10:84:13 | box2 [field] | provenance | | +| fields.swift:83:9:83:12 | box2 [field] | fields.swift:85:50:85:53 | box2 [field] | provenance | | +| fields.swift:83:16:83:18 | box [field] | fields.swift:83:9:83:12 | box2 [field] | provenance | | +| fields.swift:84:10:84:13 | box2 [field] | fields.swift:84:10:84:19 | ... .field | provenance | | +| fields.swift:85:9:85:15 | box1or2 [field] | fields.swift:86:10:86:16 | box1or2 [field] | provenance | | +| fields.swift:85:27:85:29 | box [field] | fields.swift:85:9:85:15 | box1or2 [field] | provenance | | +| fields.swift:85:50:85:53 | box2 [field] | fields.swift:85:9:85:15 | box1or2 [field] | provenance | | +| fields.swift:86:10:86:16 | box1or2 [field] | fields.swift:86:10:86:22 | ... .field | provenance | | +| fields.swift:92:5:92:5 | [post] p [field] | fields.swift:93:10:93:10 | p [field] | provenance | | +| fields.swift:92:5:92:5 | [post] p [field] | fields.swift:95:16:95:16 | p [field] | provenance | | +| fields.swift:92:5:92:11 | ... .field | fields.swift:92:5:92:5 | [post] p [field] | provenance | | +| fields.swift:92:15:92:39 | source(...) | fields.swift:92:5:92:11 | ... .field | provenance | | +| fields.swift:93:10:93:10 | p [field] | fields.swift:93:10:93:16 | ... .field | provenance | | +| fields.swift:95:9:95:12 | box2 [field] | fields.swift:96:10:96:13 | box2 [field] | provenance | | +| fields.swift:95:16:95:16 | p [field] | fields.swift:95:9:95:12 | box2 [field] | provenance | | +| fields.swift:96:10:96:13 | box2 [field] | fields.swift:96:10:96:19 | ... .field | provenance | | | flow-summaries.swift:2:9:2:9 | x | flow-summaries.swift:3:35:3:35 | x | provenance | | | flow-summaries.swift:2:13:2:26 | source(...) | flow-summaries.swift:2:9:2:9 | x | provenance | | | flow-summaries.swift:3:9:3:9 | y | flow-summaries.swift:4:10:4:10 | y | provenance | | @@ -849,6 +908,72 @@ nodes | enums.swift:83:10:83:23 | ... .foo(...) [foo.0] | semmle.label | ... .foo(...) [foo.0] | | enums.swift:83:22:83:22 | x | semmle.label | x | | enums.swift:84:14:84:14 | x | semmle.label | x | +| fields.swift:18:5:18:7 | [post] box [field] | semmle.label | [post] box [field] | +| fields.swift:18:5:18:13 | ... .field | semmle.label | ... .field | +| fields.swift:18:17:18:41 | source(...) | semmle.label | source(...) | +| fields.swift:19:10:19:12 | box [field] | semmle.label | box [field] | +| fields.swift:19:10:19:18 | ... .field | semmle.label | ... .field | +| fields.swift:20:10:20:38 | ... .field | semmle.label | ... .field | +| fields.swift:20:11:20:13 | box [field] | semmle.label | box [field] | +| fields.swift:20:11:20:31 | TypeCastExpr [field] | semmle.label | TypeCastExpr [field] | +| fields.swift:25:5:25:7 | [post] box [field] | semmle.label | [post] box [field] | +| fields.swift:25:5:25:13 | ... .field | semmle.label | ... .field | +| fields.swift:25:17:25:42 | source(...) | semmle.label | source(...) | +| fields.swift:26:10:26:33 | ... .field | semmle.label | ... .field | +| fields.swift:26:11:26:13 | box [field] | semmle.label | box [field] | +| fields.swift:26:11:26:26 | TypeCastExpr [field] | semmle.label | TypeCastExpr [field] | +| fields.swift:31:5:31:7 | [post] box [field] | semmle.label | [post] box [field] | +| fields.swift:31:5:31:13 | ... .field | semmle.label | ... .field | +| fields.swift:31:17:31:48 | source(...) | semmle.label | source(...) | +| fields.swift:32:10:32:40 | ... .field | semmle.label | ... .field | +| fields.swift:32:11:32:13 | box [field] | semmle.label | box [field] | +| fields.swift:32:11:32:33 | TypeCastExpr [field] | semmle.label | TypeCastExpr [field] | +| fields.swift:37:5:37:35 | ... .field | semmle.label | ... .field | +| fields.swift:37:6:37:28 | [post] TypeCastExpr [field] | semmle.label | [post] TypeCastExpr [field] | +| fields.swift:37:39:37:71 | source(...) | semmle.label | source(...) | +| fields.swift:38:10:38:12 | box [field] | semmle.label | box [field] | +| fields.swift:38:10:38:18 | ... .field | semmle.label | ... .field | +| fields.swift:43:5:43:7 | [post] box [field] | semmle.label | [post] box [field] | +| fields.swift:43:5:43:13 | ... .field | semmle.label | ... .field | +| fields.swift:43:17:43:40 | source(...) | semmle.label | source(...) | +| fields.swift:44:10:44:32 | ... .readField(...) | semmle.label | ... .readField(...) | +| fields.swift:44:29:44:31 | box [field] | semmle.label | box [field] | +| fields.swift:53:15:53:19 | super [field] | semmle.label | super [field] | +| fields.swift:53:15:53:25 | ... .field | semmle.label | ... .field | +| fields.swift:60:5:60:8 | [post] base [field] | semmle.label | [post] base [field] | +| fields.swift:60:5:60:14 | ... .field | semmle.label | ... .field | +| fields.swift:60:18:60:47 | source(...) | semmle.label | source(...) | +| fields.swift:61:9:61:11 | sub [field] | semmle.label | sub [field] | +| fields.swift:61:15:61:18 | base [field] | semmle.label | base [field] | +| fields.swift:62:10:62:12 | sub [field] | semmle.label | sub [field] | +| fields.swift:62:10:62:18 | ... .field | semmle.label | ... .field | +| fields.swift:79:5:79:7 | [post] box [field] | semmle.label | [post] box [field] | +| fields.swift:79:5:79:13 | ... .field | semmle.label | ... .field | +| fields.swift:79:17:79:41 | source(...) | semmle.label | source(...) | +| fields.swift:80:10:80:12 | box [field] | semmle.label | box [field] | +| fields.swift:80:10:80:18 | ... .field | semmle.label | ... .field | +| fields.swift:81:9:81:9 | p [field] | semmle.label | p [field] | +| fields.swift:81:13:81:15 | box [field] | semmle.label | box [field] | +| fields.swift:82:10:82:10 | p [field] | semmle.label | p [field] | +| fields.swift:82:10:82:16 | ... .field | semmle.label | ... .field | +| fields.swift:83:9:83:12 | box2 [field] | semmle.label | box2 [field] | +| fields.swift:83:16:83:18 | box [field] | semmle.label | box [field] | +| fields.swift:84:10:84:13 | box2 [field] | semmle.label | box2 [field] | +| fields.swift:84:10:84:19 | ... .field | semmle.label | ... .field | +| fields.swift:85:9:85:15 | box1or2 [field] | semmle.label | box1or2 [field] | +| fields.swift:85:27:85:29 | box [field] | semmle.label | box [field] | +| fields.swift:85:50:85:53 | box2 [field] | semmle.label | box2 [field] | +| fields.swift:86:10:86:16 | box1or2 [field] | semmle.label | box1or2 [field] | +| fields.swift:86:10:86:22 | ... .field | semmle.label | ... .field | +| fields.swift:92:5:92:5 | [post] p [field] | semmle.label | [post] p [field] | +| fields.swift:92:5:92:11 | ... .field | semmle.label | ... .field | +| fields.swift:92:15:92:39 | source(...) | semmle.label | source(...) | +| fields.swift:93:10:93:10 | p [field] | semmle.label | p [field] | +| fields.swift:93:10:93:16 | ... .field | semmle.label | ... .field | +| fields.swift:95:9:95:12 | box2 [field] | semmle.label | box2 [field] | +| fields.swift:95:16:95:16 | p [field] | semmle.label | p [field] | +| fields.swift:96:10:96:13 | box2 [field] | semmle.label | box2 [field] | +| fields.swift:96:10:96:19 | ... .field | semmle.label | ... .field | | flow-summaries.swift:2:9:2:9 | x | semmle.label | x | | flow-summaries.swift:2:13:2:26 | source(...) | semmle.label | source(...) | | flow-summaries.swift:3:9:3:9 | y | semmle.label | y | @@ -1211,6 +1336,7 @@ subpaths | capture.swift:49:13:49:13 | x | capture.swift:45:22:45:24 | arg | capture.swift:46:9:46:9 | y | capture.swift:50:10:50:10 | y | | capture.swift:60:13:60:13 | x | capture.swift:56:22:56:24 | arg | capture.swift:57:9:57:9 | [post] y [0] | capture.swift:61:10:61:10 | y [0] | | 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(...) | +| fields.swift:62:10:62:12 | sub [field] | fields.swift:53:15:53:19 | super [field] | fields.swift:53:15:53:25 | ... .field | fields.swift:62:10:62:18 | ... .field | | 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(...) | | 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(...) | @@ -1308,6 +1434,19 @@ testFailures | enums.swift:65:10:65:14 | opt_x | enums.swift:63:31:63:44 | source(...) | enums.swift:65:10:65:14 | opt_x | $@ | enums.swift:63:31:63:44 | source(...) | source(...) | | enums.swift:76:14:76:14 | x | enums.swift:73:34:73:47 | source(...) | enums.swift:76:14:76:14 | x | $@ | enums.swift:73:34:73:47 | source(...) | source(...) | | enums.swift:84:14:84:14 | x | enums.swift:73:34:73:47 | source(...) | enums.swift:84:14:84:14 | x | $@ | enums.swift:73:34:73:47 | source(...) | source(...) | +| fields.swift:19:10:19:18 | ... .field | fields.swift:18:17:18:41 | source(...) | fields.swift:19:10:19:18 | ... .field | $@ | fields.swift:18:17:18:41 | source(...) | source(...) | +| fields.swift:20:10:20:38 | ... .field | fields.swift:18:17:18:41 | source(...) | fields.swift:20:10:20:38 | ... .field | $@ | fields.swift:18:17:18:41 | source(...) | source(...) | +| fields.swift:26:10:26:33 | ... .field | fields.swift:25:17:25:42 | source(...) | fields.swift:26:10:26:33 | ... .field | $@ | fields.swift:25:17:25:42 | source(...) | source(...) | +| fields.swift:32:10:32:40 | ... .field | fields.swift:31:17:31:48 | source(...) | fields.swift:32:10:32:40 | ... .field | $@ | fields.swift:31:17:31:48 | source(...) | source(...) | +| fields.swift:38:10:38:18 | ... .field | fields.swift:37:39:37:71 | source(...) | fields.swift:38:10:38:18 | ... .field | $@ | fields.swift:37:39:37:71 | source(...) | source(...) | +| fields.swift:44:10:44:32 | ... .readField(...) | fields.swift:43:17:43:40 | source(...) | fields.swift:44:10:44:32 | ... .readField(...) | $@ | fields.swift:43:17:43:40 | source(...) | source(...) | +| fields.swift:62:10:62:18 | ... .field | fields.swift:60:18:60:47 | source(...) | fields.swift:62:10:62:18 | ... .field | $@ | fields.swift:60:18:60:47 | source(...) | source(...) | +| fields.swift:80:10:80:18 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:80:10:80:18 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | +| fields.swift:82:10:82:16 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:82:10:82:16 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | +| fields.swift:84:10:84:19 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:84:10:84:19 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | +| fields.swift:86:10:86:22 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:86:10:86:22 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | +| fields.swift:93:10:93:16 | ... .field | fields.swift:92:15:92:39 | source(...) | fields.swift:93:10:93:16 | ... .field | $@ | fields.swift:92:15:92:39 | source(...) | source(...) | +| fields.swift:96:10:96:19 | ... .field | fields.swift:92:15:92:39 | source(...) | fields.swift:96:10:96:19 | ... .field | $@ | fields.swift:92:15:92:39 | source(...) | source(...) | | flow-summaries.swift:4:10:4:10 | y | flow-summaries.swift:2:13:2:26 | source(...) | flow-summaries.swift:4:10:4:10 | y | $@ | flow-summaries.swift:2:13:2:26 | source(...) | source(...) | | flow-summaries.swift:11:14:11:14 | y | flow-summaries.swift:8:13:8:26 | source(...) | flow-summaries.swift:11:14:11:14 | y | $@ | flow-summaries.swift:8:13:8:26 | source(...) | source(...) | | flow-summaries.swift:18:10:18:10 | y | flow-summaries.swift:16:18:16:31 | source(...) | flow-summaries.swift:18:10:18:10 | y | $@ | flow-summaries.swift:16:18:16:31 | 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..bd9f79dc0e63 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, "readField(_:)", "", "", "Argument[0].Member[field]", "ReturnValue", "value", "manual"] From 43332c6301b9d9f5f2e841be11b057513d26def1 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Thu, 5 Nov 2026 09:59:51 +0100 Subject: [PATCH 2/2] Unified: use type-inferred field resolution in data flow Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../unified/internal/dataflow/Content.qll | 61 ++++++++++++++++++- .../internal/dataflow/DataFlowGraph.qll | 8 +-- .../dataflow/DataFlowInstantiation.qll | 8 ++- .../test/library-tests/dataflow/fields.swift | 4 +- .../test/library-tests/dataflow/test.expected | 14 +---- 5 files changed, 73 insertions(+), 22 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/Content.qll b/unified/ql/lib/codeql/unified/internal/dataflow/Content.qll index ecb69457783d..12243bf1df2c 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/Content.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/Content.qll @@ -1,5 +1,7 @@ private import unified private import AllDataFlow +private import codeql.unified.internal.typeinference.TypeInference as T +private import codeql.unified.internal.typeinference.TypeInferencePlugin as TP /** * Holds if a MaD access path token of form `name[arg]` exists. @@ -8,8 +10,20 @@ predicate isAccessPathTokenPresent(string name, string arg) { arg = any(FlowSummaryOutput::Private::AccessPathToken tok).getAnArgument(name) } +private class ResolvedField extends VariableDeclaration { + ResolvedField() { this = T::resolveFieldAccess(_) } + + string getName() { + result = super.getName() and + not this = any(TP::TupleType tt).getField(_) + or + this = any(TP::TupleType tt).getField(result) + } +} + private newtype TContent = TArrayElement() or + TFieldContent(ResolvedField field) or TNamedMember(string name) { name = any(Identifier id).getValue() or @@ -24,6 +38,8 @@ private newtype TContent = TCapturedVariable(CaptureSsaInput::CapturedVariable v) class Content extends TContent { + ResolvedField asField() { this = TFieldContent(result) } + string asNamedMember() { this = TNamedMember(result) } predicate isArrayElement() { this = TArrayElement() } @@ -31,6 +47,8 @@ class Content extends TContent { string toString() { result = this.asNamedMember() or + result = this.asField().getName() + or this.isArrayElement() and result = "ArrayElement" or result = this.asCapturedVariable().toString() @@ -38,9 +56,17 @@ class Content extends TContent { LocalVariable asCapturedVariable() { this = TCapturedVariable(result) } - Location getLocation() { result = this.asCapturedVariable().getLocation() } + Location getLocation() { + result = this.asCapturedVariable().getLocation() + or + result = this.asField().getLocation() + } } +private class TContentApprox = TArrayElement or TNamedMember or TCapturedVariable; + +class ContentApprox extends Content, TContentApprox { } + private newtype TContentSet = TSingleton(Content content) class ContentSet extends TContentSet { @@ -52,10 +78,41 @@ class ContentSet extends TContentSet { Content getAStoreContent() { result = this.asSingleton() } - Content getAReadContent() { result = this.asSingleton() } + Content getAReadContent() { + result = this.asSingleton() + or + result.asNamedMember() = this.asSingleton().asField().getName() + or + result.asField().getName() = this.asSingleton().asNamedMember() + } } module ContentSet { + /** + * Gets the member name to use for `access`, if any. + * + * Only returns a member name if it cannot be resolved uniquely to a field. + */ + private string getMemberName(Expr access) { + access = + any(MemberAccessExpr mae | + result = mae.getMemberName() and + if exists(T::inferTypeUnique(mae.getBase())) + then not exists(T::resolveFieldAccess(mae)) + else any() + ) + or + result = access.(UnqualifiedMemberAccess).getMemberName() and + not exists(T::resolveFieldAccess(access)) + } + + ContentSet member(Expr access) { + result.asSingleton().asField() = T::resolveFieldAccess(access) and + not exists(getMemberName(access)) + or + result = namedMember(getMemberName(access)) + } + ContentSet namedMember(string name) { result.asSingleton().asNamedMember() = name } ContentSet arrayElement() { result.asSingleton().isArrayElement() } diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll index d804e790b7a7..2f355d790823 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowGraph.qll @@ -92,11 +92,11 @@ predicate step(BuilderNode node1, Step step, BuilderNode node2) { or exists(UnqualifiedMemberAccess access | access.isInstanceAccess() | node1.isLocalVariableRead(access, access.getImplicitQualifierVariable()) and - step.readName(access.getMemberName()) and + step.read(ContentSet::member(access)) and node2.isResultValue(access) or (node1.isIncomingValue(access) or node1.isPostUpdate(access)) and - step.storeName(access.getMemberName()) and + step.store(ContentSet::member(access)) and node2.isLocalVariablePostUpdate(access, access.getImplicitQualifierVariable()) or // Pass the receiver along to the getter/setter invoked by this expression @@ -129,11 +129,11 @@ predicate step(BuilderNode node1, Step step, BuilderNode node2) { or exists(MemberAccessExpr expr | node1.isResultValue(expr.getBase()) and - step.readName(expr.getMemberName()) and + step.read(ContentSet::member(expr)) and node2.isResultValue(expr) or (node1.isIncomingValue(expr) or node1.isPostUpdate(expr)) and - step.storeName(expr.getMemberName()) and + step.store(ContentSet::member(expr)) and node2.isPostUpdate(expr.getBase()) or // Pass the receiver along to the getter/setter invoked by this expression diff --git a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll index 1c18b907c57c..8bce64150db6 100644 --- a/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll +++ b/unified/ql/lib/codeql/unified/internal/dataflow/DataFlowInstantiation.qll @@ -15,9 +15,13 @@ module DataFlowInput implements InputSig { class ContentSet = D::ContentSet; - class ContentApprox = Content; // TODO + class ContentApprox = D::ContentApprox; - ContentApprox getContentApprox(Content c) { result = c } // TODO + ContentApprox getContentApprox(Content c) { + result = c + or + result.asNamedMember() = c.asField().getName() + } // // Parameter, argument, return, and out nodes and their positions/kinds diff --git a/unified/ql/test/library-tests/dataflow/fields.swift b/unified/ql/test/library-tests/dataflow/fields.swift index 79231f66b7ff..34c86f348345 100644 --- a/unified/ql/test/library-tests/dataflow/fields.swift +++ b/unified/ql/test/library-tests/dataflow/fields.swift @@ -17,7 +17,7 @@ func resolvedFields() { let box = FieldBox() box.field = source("fields.resolved") sink(box.field) // $ hasValueFlow=fields.resolved - sink((box as! OtherFieldBox).field) // $ SPURIOUS: hasValueFlow=fields.resolved + sink((box as! OtherFieldBox).field) } func inheritedFields() { @@ -81,7 +81,7 @@ func protocolFieldRead() { let p = box as FieldProtocol sink(p.field) // $ hasValueFlow=fields.protocol let box2 = box as! ProtocolBox2 - sink(box2.field) // $ SPURIOUS: hasValueFlow=fields.protocol + sink(box2.field) let box1or2 = 2 > 3 ? box as FieldProtocol : box2 sink(box1or2.field) // $ hasValueFlow=fields.protocol } diff --git a/unified/ql/test/library-tests/dataflow/test.expected b/unified/ql/test/library-tests/dataflow/test.expected index e3cdba402049..d1e0ee86455b 100644 --- a/unified/ql/test/library-tests/dataflow/test.expected +++ b/unified/ql/test/library-tests/dataflow/test.expected @@ -32,6 +32,7 @@ edges | accessors_instance.swift:119:13:119:17 | field | accessors_instance.swift:125:18:125:22 | field | provenance | | | accessors_instance.swift:119:21:119:35 | source(...) | accessors_instance.swift:119:13:119:17 | field | provenance | | | accessors_instance.swift:134:24:134:31 | newValue [0] | accessors_instance.swift:134:24:134:33 | ... .0 | provenance | | +| accessors_instance.swift:134:24:134:31 | newValue [0] | accessors_instance.swift:134:24:134:33 | ... .0 | provenance | | | accessors_instance.swift:143:5:143:7 | [post] ... .p [0] | accessors_instance.swift:134:24:134:31 | newValue [0] | provenance | | | accessors_instance.swift:143:5:143:9 | ... .0 | accessors_instance.swift:143:5:143:7 | [post] ... .p [0] | provenance | | | accessors_instance.swift:143:13:143:27 | source(...) | accessors_instance.swift:143:5:143:9 | ... .0 | provenance | | @@ -252,12 +253,9 @@ edges | enums.swift:83:10:83:23 | ... .foo(...) [foo.0] | enums.swift:83:22:83:22 | x | provenance | | | enums.swift:83:22:83:22 | x | enums.swift:84:14:84:14 | x | provenance | | | fields.swift:18:5:18:7 | [post] box [field] | fields.swift:19:10:19:12 | box [field] | provenance | | -| fields.swift:18:5:18:7 | [post] box [field] | fields.swift:20:11:20:13 | box [field] | provenance | | | fields.swift:18:5:18:13 | ... .field | fields.swift:18:5:18:7 | [post] box [field] | provenance | | | fields.swift:18:17:18:41 | source(...) | fields.swift:18:5:18:13 | ... .field | provenance | | | fields.swift:19:10:19:12 | box [field] | fields.swift:19:10:19:18 | ... .field | provenance | | -| fields.swift:20:11:20:13 | box [field] | fields.swift:20:11:20:31 | TypeCastExpr [field] | provenance | | -| fields.swift:20:11:20:31 | TypeCastExpr [field] | fields.swift:20:10:20:38 | ... .field | provenance | | | fields.swift:25:5:25:7 | [post] box [field] | fields.swift:26:11:26:13 | box [field] | provenance | | | fields.swift:25:5:25:13 | ... .field | fields.swift:25:5:25:7 | [post] box [field] | provenance | | | fields.swift:25:17:25:42 | source(...) | fields.swift:25:5:25:13 | ... .field | provenance | | @@ -294,10 +292,8 @@ edges | fields.swift:81:9:81:9 | p [field] | fields.swift:82:10:82:10 | p [field] | provenance | | | fields.swift:81:13:81:15 | box [field] | fields.swift:81:9:81:9 | p [field] | provenance | | | fields.swift:82:10:82:10 | p [field] | fields.swift:82:10:82:16 | ... .field | provenance | | -| fields.swift:83:9:83:12 | box2 [field] | fields.swift:84:10:84:13 | box2 [field] | provenance | | | fields.swift:83:9:83:12 | box2 [field] | fields.swift:85:50:85:53 | box2 [field] | provenance | | | fields.swift:83:16:83:18 | box [field] | fields.swift:83:9:83:12 | box2 [field] | provenance | | -| fields.swift:84:10:84:13 | box2 [field] | fields.swift:84:10:84:19 | ... .field | provenance | | | fields.swift:85:9:85:15 | box1or2 [field] | fields.swift:86:10:86:16 | box1or2 [field] | provenance | | | fields.swift:85:27:85:29 | box [field] | fields.swift:85:9:85:15 | box1or2 [field] | provenance | | | fields.swift:85:50:85:53 | box2 [field] | fields.swift:85:9:85:15 | box1or2 [field] | provenance | | @@ -635,6 +631,7 @@ nodes | accessors_instance.swift:119:21:119:35 | source(...) | semmle.label | source(...) | | accessors_instance.swift:125:18:125:22 | field | semmle.label | field | | accessors_instance.swift:134:24:134:31 | newValue [0] | semmle.label | newValue [0] | +| accessors_instance.swift:134:24:134:31 | newValue [0] | semmle.label | newValue [0] | | accessors_instance.swift:134:24:134:33 | ... .0 | semmle.label | ... .0 | | accessors_instance.swift:143:5:143:7 | [post] ... .p [0] | semmle.label | [post] ... .p [0] | | accessors_instance.swift:143:5:143:9 | ... .0 | semmle.label | ... .0 | @@ -913,9 +910,6 @@ nodes | fields.swift:18:17:18:41 | source(...) | semmle.label | source(...) | | fields.swift:19:10:19:12 | box [field] | semmle.label | box [field] | | fields.swift:19:10:19:18 | ... .field | semmle.label | ... .field | -| fields.swift:20:10:20:38 | ... .field | semmle.label | ... .field | -| fields.swift:20:11:20:13 | box [field] | semmle.label | box [field] | -| fields.swift:20:11:20:31 | TypeCastExpr [field] | semmle.label | TypeCastExpr [field] | | fields.swift:25:5:25:7 | [post] box [field] | semmle.label | [post] box [field] | | fields.swift:25:5:25:13 | ... .field | semmle.label | ... .field | | fields.swift:25:17:25:42 | source(...) | semmle.label | source(...) | @@ -958,8 +952,6 @@ nodes | fields.swift:82:10:82:16 | ... .field | semmle.label | ... .field | | fields.swift:83:9:83:12 | box2 [field] | semmle.label | box2 [field] | | fields.swift:83:16:83:18 | box [field] | semmle.label | box [field] | -| fields.swift:84:10:84:13 | box2 [field] | semmle.label | box2 [field] | -| fields.swift:84:10:84:19 | ... .field | semmle.label | ... .field | | fields.swift:85:9:85:15 | box1or2 [field] | semmle.label | box1or2 [field] | | fields.swift:85:27:85:29 | box [field] | semmle.label | box [field] | | fields.swift:85:50:85:53 | box2 [field] | semmle.label | box2 [field] | @@ -1435,7 +1427,6 @@ testFailures | enums.swift:76:14:76:14 | x | enums.swift:73:34:73:47 | source(...) | enums.swift:76:14:76:14 | x | $@ | enums.swift:73:34:73:47 | source(...) | source(...) | | enums.swift:84:14:84:14 | x | enums.swift:73:34:73:47 | source(...) | enums.swift:84:14:84:14 | x | $@ | enums.swift:73:34:73:47 | source(...) | source(...) | | fields.swift:19:10:19:18 | ... .field | fields.swift:18:17:18:41 | source(...) | fields.swift:19:10:19:18 | ... .field | $@ | fields.swift:18:17:18:41 | source(...) | source(...) | -| fields.swift:20:10:20:38 | ... .field | fields.swift:18:17:18:41 | source(...) | fields.swift:20:10:20:38 | ... .field | $@ | fields.swift:18:17:18:41 | source(...) | source(...) | | fields.swift:26:10:26:33 | ... .field | fields.swift:25:17:25:42 | source(...) | fields.swift:26:10:26:33 | ... .field | $@ | fields.swift:25:17:25:42 | source(...) | source(...) | | fields.swift:32:10:32:40 | ... .field | fields.swift:31:17:31:48 | source(...) | fields.swift:32:10:32:40 | ... .field | $@ | fields.swift:31:17:31:48 | source(...) | source(...) | | fields.swift:38:10:38:18 | ... .field | fields.swift:37:39:37:71 | source(...) | fields.swift:38:10:38:18 | ... .field | $@ | fields.swift:37:39:37:71 | source(...) | source(...) | @@ -1443,7 +1434,6 @@ testFailures | fields.swift:62:10:62:18 | ... .field | fields.swift:60:18:60:47 | source(...) | fields.swift:62:10:62:18 | ... .field | $@ | fields.swift:60:18:60:47 | source(...) | source(...) | | fields.swift:80:10:80:18 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:80:10:80:18 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | | fields.swift:82:10:82:16 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:82:10:82:16 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | -| fields.swift:84:10:84:19 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:84:10:84:19 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | | fields.swift:86:10:86:22 | ... .field | fields.swift:79:17:79:41 | source(...) | fields.swift:86:10:86:22 | ... .field | $@ | fields.swift:79:17:79:41 | source(...) | source(...) | | fields.swift:93:10:93:16 | ... .field | fields.swift:92:15:92:39 | source(...) | fields.swift:93:10:93:16 | ... .field | $@ | fields.swift:92:15:92:39 | source(...) | source(...) | | fields.swift:96:10:96:19 | ... .field | fields.swift:92:15:92:39 | source(...) | fields.swift:96:10:96:19 | ... .field | $@ | fields.swift:92:15:92:39 | source(...) | source(...) |