From 8d48ea17ac17a4c3742e363c7b861ac7ef6ef96b Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 21:21:27 +0200 Subject: [PATCH 01/18] unified: Add simple extension-method test --- .../static-name-binding/extensions.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 unified/ql/test/library-tests/static-name-binding/extensions.swift diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift new file mode 100644 index 000000000000..891f899883df --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -0,0 +1,23 @@ +class A { + func ownMethod() { + ownMethod() // $ access=A.ownMethod + extensionMethod1() // $ MISSING: access=A.extensionMethod1 + extensionMethod2() // $ MISSING: access=A.extensionMethod2 + } +} + +extension A { // $ access=A + func extensionMethod1() { // name=A.extensionMethod1 + ownMethod() // $ MISSING: access=A.ownMethod + extensionMethod1() // $ MISSING: access=A.extensionMethod1 + extensionMethod2() // $ MISSING: access=A.extensionMethod2 + } +} + +extension A { // $ access=A + func extensionMethod2() { // name=A.extensionMethod2 + ownMethod() // $ MISSING: access=A.ownMethod + extensionMethod1() // $ MISSING: access=A.extensionMethod1 + extensionMethod2() // $ MISSING: access=A.extensionMethod2 + } +} From fcda1626f9ceb7cb0d5c8443d8e8a3255ee2b787 Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 21:26:07 +0200 Subject: [PATCH 02/18] unified: Add extension_target field and use it --- unified/extractor/ast_types.yml | 1 + unified/extractor/src/languages/swift/swift.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index 6f929f115b0e..f1ee78f1c621 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -478,6 +478,7 @@ named: class_like_declaration: modifier*: modifier name_node?: identifier + extension_target?: expr type_parameter*: type_parameter type_constraint*: type_constraint base_type*: base_type diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index c1e49181ea13..ad690fb37a11 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -1274,14 +1274,14 @@ fn translation_rules() -> Vec> { (extensionDecl extensionKeyword: @kind modifiers: _* @mods - extendedType: @@name + extendedType: @extendedType inheritanceClause: (inheritanceClause inheritedTypes: (inheritedType type: @bases)*)? memberBlock: (memberBlock members: _* @members)) => (class_like_declaration modifier: (modifier #{kind}) modifier: {mods} - name_node: (identifier #{name}) + extension_target: {extendedType} base_type: {bases.into_iter().map(|ty| tree!((base_type type: {ty})))} member: {members}) ), From 6ce06bfdc59f4ac0acd233217feea9668b15502d Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 21:26:17 +0200 Subject: [PATCH 03/18] unified: Regenerate QL --- unified/ql/lib/codeql/unified/internal/Ast.qll | 10 ++++++++++ unified/ql/lib/unified.dbscheme | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 3de5e2ff3aad..a04b93291707 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -418,6 +418,11 @@ module Unified { /** Gets the node corresponding to the field `base_type`. */ final F::BaseType getABaseType() { result = this.getBaseType(_) } + /** Gets the node corresponding to the field `extension_target`. */ + final F::Expr getExtensionTarget() { + unified_class_like_declaration_extension_target(this, result) + } + /** Gets the node corresponding to the field `member`. */ final F::Member getMember(int i) { unified_class_like_declaration_member(this, i, result) } @@ -454,6 +459,7 @@ module Unified { /** Gets a field or child node of this node. */ final override F::AstNode getAFieldOrChild() { unified_class_like_declaration_base_type(this, _, result) or + unified_class_like_declaration_extension_target(this, result) or unified_class_like_declaration_member(this, _, result) or unified_class_like_declaration_modifier(this, _, result) or unified_class_like_declaration_name_node(this, result) or @@ -1614,6 +1620,10 @@ module Unified { or result = node.(ClassLikeDeclaration).getBaseType(i) and name = "getBaseType" or + result = node.(ClassLikeDeclaration).getExtensionTarget() and + i = -1 and + name = "getExtensionTarget" + or result = node.(ClassLikeDeclaration).getMember(i) and name = "getMember" or result = node.(ClassLikeDeclaration).getModifier(i) and name = "getModifier" diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index b32deb29d367..cb0facfc1d0a 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -318,6 +318,11 @@ unified_class_like_declaration_base_type( unique int base_type: @unified_base_type ref ); +unified_class_like_declaration_extension_target( + unique int unified_class_like_declaration: @unified_class_like_declaration ref, + unique int extension_target: @unified_expr ref +); + #keyset[unified_class_like_declaration, index] unified_class_like_declaration_member( int unified_class_like_declaration: @unified_class_like_declaration ref, From 4c22372e3db0519a6c3158b6d68a41305f2ce13d Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 21:42:30 +0200 Subject: [PATCH 04/18] unified: Update test after extractor change --- unified/ql/test/library-tests/static-name-binding/test.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unified/ql/test/library-tests/static-name-binding/test.swift b/unified/ql/test/library-tests/static-name-binding/test.swift index 0fc9ae1c42e2..a7432344eb48 100644 --- a/unified/ql/test/library-tests/static-name-binding/test.swift +++ b/unified/ql/test/library-tests/static-name-binding/test.swift @@ -68,5 +68,5 @@ protocol P { } extension H // $ access=H1 : P { } // $ access=P -extension A.B.C // $ MISSING: access=A access=A.B access=A.B.C (`A.B.C` is currently parsed as a single identifier) - : P { } // $ access=P \ No newline at end of file +extension A.B.C // $ access=A access=A.B access=A.B.C + : P { } // $ access=P From 29a33cc39a2afb6a367ebcf1fb5d3fcc77657cd3 Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 21:44:06 +0200 Subject: [PATCH 05/18] unified: Include extensions in namespace of target class --- .../unified/internal/StaticNameBinding.qll | 20 +++++++++++++++++++ .../static-name-binding/extensions.swift | 8 ++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 230608b682f8..64d417153052 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -282,6 +282,13 @@ predicate inheritanceStep(NameBindingNode supertype, NameBindingNode subtype) { ) } +predicate extensionStep(NameBindingNode extension, NameBindingNode targetClass) { + exists(ClassLikeDeclaration cls | + targetClass = getNodeFromRef(cls.getExtensionTarget()) and + extension.isStaticMemberNamespace(cls) + ) +} + signature module TrackInputSig { /** Holds if the forward-flow of `node` should be tracked. */ predicate shouldTrack(NameBindingNode node); @@ -362,6 +369,17 @@ class NamespaceNode extends NameBindingNode { /** If this is the instance namespace for a class, gets the corresponding static namespace. */ NamespaceNode toStaticNamespace() { result.toInstanceNamespace() = this } + private NamespaceNode getAnExtension1() { extensionStep(result, this.ref()) } + + /** Gets a namespace that is an extension (i.e. containing extension methods) of this node. */ + NamespaceNode getAnExtension() { + result = this.getAnExtension1() + or + // `extensionStep` connects the static namespaces of classes. + // Add the corresponding extension relation between the instance namespaces. + result = this.toStaticNamespace().getAnExtension1().toInstanceNamespace() + } + private NamespaceNode getAnInheritanceParent1() { inheritanceStep(result.ref(), this) } /** Gets a namespace from which this namespace inherits directly. */ @@ -384,6 +402,8 @@ class NamespaceNode extends NameBindingNode { not this.hasOwnMember(name) and result = this.getAnInheritanceParent().getMember(name) and isInheritableMemberNode(result) + or + result = this.getAnExtension().getMember(name) } } diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 891f899883df..017dba613a5e 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -1,15 +1,15 @@ class A { func ownMethod() { ownMethod() // $ access=A.ownMethod - extensionMethod1() // $ MISSING: access=A.extensionMethod1 - extensionMethod2() // $ MISSING: access=A.extensionMethod2 + extensionMethod1() // $ access=A.extensionMethod1 + extensionMethod2() // $ access=A.extensionMethod2 } } extension A { // $ access=A func extensionMethod1() { // name=A.extensionMethod1 ownMethod() // $ MISSING: access=A.ownMethod - extensionMethod1() // $ MISSING: access=A.extensionMethod1 + extensionMethod1() // $ access=A.extensionMethod1 extensionMethod2() // $ MISSING: access=A.extensionMethod2 } } @@ -18,6 +18,6 @@ extension A { // $ access=A func extensionMethod2() { // name=A.extensionMethod2 ownMethod() // $ MISSING: access=A.ownMethod extensionMethod1() // $ MISSING: access=A.extensionMethod1 - extensionMethod2() // $ MISSING: access=A.extensionMethod2 + extensionMethod2() // $ access=A.extensionMethod2 } } From b60ac2503435a458b09a4b0ac836b8a58e2affe2 Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 21:54:56 +0200 Subject: [PATCH 06/18] unified: Handle unqualified member access inside extension --- .../codeql/unified/internal/StaticNameBinding.qll | 14 +++++++++++++- .../static-name-binding/extensions.swift | 8 ++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 64d417153052..59038c452949 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -596,6 +596,17 @@ private module FolderHeuristic { } } +private ClassLikeDeclaration resolveExtensionTarget(ClassLikeDeclaration cls) { + trackNameBinding(result.getNameNode()) = getNodeFromRef(cls.getExtensionTarget()) +} + +private ClassLikeDeclaration tryResolveExtensionTarget(ClassLikeDeclaration cls) { + result = resolveExtensionTarget(cls) + or + not exists(resolveExtensionTarget(cls)) and + result = cls +} + /** * Holds if `access` may resolve to `target` through the enclosing `accessingClass`. * @@ -624,7 +635,8 @@ private predicate unqualifiedMemberAccessCand( // Resolved in an uncertain scope exists(NamespaceNode namespace, string name | name = access.getName() and - accessingClass = LocalNameBindingOutput::getAnUncertainScope(access, name) + accessingClass = + tryResolveExtensionTarget(LocalNameBindingOutput::getAnUncertainScope(access, name)) | instanceAccess = true and namespace.isInstanceMemberNamespace(accessingClass) and diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 017dba613a5e..af4edb95f012 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -8,16 +8,16 @@ class A { extension A { // $ access=A func extensionMethod1() { // name=A.extensionMethod1 - ownMethod() // $ MISSING: access=A.ownMethod + ownMethod() // $ access=A.ownMethod extensionMethod1() // $ access=A.extensionMethod1 - extensionMethod2() // $ MISSING: access=A.extensionMethod2 + extensionMethod2() // $ access=A.extensionMethod2 } } extension A { // $ access=A func extensionMethod2() { // name=A.extensionMethod2 - ownMethod() // $ MISSING: access=A.ownMethod - extensionMethod1() // $ MISSING: access=A.extensionMethod1 + ownMethod() // $ access=A.ownMethod + extensionMethod1() // $ access=A.extensionMethod1 extensionMethod2() // $ access=A.extensionMethod2 } } From 6490d2dec92723ee8a31d2b198ce2301bd0f5111 Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 21:59:23 +0200 Subject: [PATCH 07/18] unified: Add test with inheritance resolved through extension --- .../static-name-binding/extensions.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index af4edb95f012..7010b48fe6df 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -21,3 +21,17 @@ extension A { // $ access=A extensionMethod2() // $ access=A.extensionMethod2 } } + +class B { +} + +extension B { // $ access=B + class C { // name=B.C + class D {} // name=B.C.D + } +} +extension B { // $ access=B + class Nested : C { // $ access=B.C + let x : D // $ MISSING: access=B.C.D + } +} From a19545880887c3eb609a3f8130f13fdd974bf2a1 Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 22:01:51 +0200 Subject: [PATCH 08/18] unified: Ensure static namespace is available in graph Inheritance must be resolvable in the main recursive SCC, prior to unqualified member resolution. We now ensure that the static namespace of the extension target is in the local scope. Note that the instance namespace is not needed here; it is handled in unqualified lookup which is the only case where static name binding needs to worry about instance member lookups. --- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 5 +++++ .../test/library-tests/static-name-binding/extensions.swift | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 59038c452949..8d1e3519a409 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -262,6 +262,11 @@ predicate valueStep(NameBindingNode node1, NameBindingNode node2) { node2 = getNodeFromRef(p.getSubPattern()) ) or + exists(ClassLikeDeclaration extension | + node1 = getNodeFromRef(extension.getExtensionTarget()) and + node2.isLocalNamespace(extension) + ) + or FolderHeuristic::valueStep(node1, node2) } diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 7010b48fe6df..e0b6a0ad41c3 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -32,6 +32,6 @@ extension B { // $ access=B } extension B { // $ access=B class Nested : C { // $ access=B.C - let x : D // $ MISSING: access=B.C.D + let x : D // $ access=B.C.D } } From 21b5ebbd1a0016ca0b6be48b69a4f5d157aebab9 Mon Sep 17 00:00:00 2001 From: Asger F Date: Sat, 12 Sep 2026 22:03:13 +0200 Subject: [PATCH 09/18] unified: Add test with inheritance through extension --- .../static-name-binding/extensions.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index e0b6a0ad41c3..3b1a5e603c69 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -35,3 +35,15 @@ extension B { // $ access=B let x : D // $ access=B.C.D } } + +// Inheritance through extension +class Base { + func baseMethod() { + } +} +class X { + func xMethod() { + baseMethod() // $ access=Base.baseMethod + } +} +extension X : Base {} // $ access=X access=Base From 281cd004170944d5734d89e5c226535029f81dfa Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 14 Sep 2026 15:33:45 +0200 Subject: [PATCH 10/18] unified: Update corpus test output --- unified/extractor/tests/corpus/swift/types/extension.output | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified/extractor/tests/corpus/swift/types/extension.output b/unified/extractor/tests/corpus/swift/types/extension.output index 3b0621788175..76dedcdcd909 100644 --- a/unified/extractor/tests/corpus/swift/types/extension.output +++ b/unified/extractor/tests/corpus/swift/types/extension.output @@ -70,7 +70,7 @@ top_level stmt: class_like_declaration modifier: modifier "extension" - name_node: identifier "Int" + extension_target: identifier "Int" member: function_declaration name_node: identifier "squared" From fb1456394566118bc3120a50235c3ba9d874bfc4 Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 14 Sep 2026 15:56:52 +0200 Subject: [PATCH 11/18] unified: Fix comment --- unified/extractor/src/languages/swift/swift.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index ad690fb37a11..4780129ba8d5 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -1266,10 +1266,7 @@ fn translation_rules() -> Vec> { base_type: {bases.into_iter().map(|ty| tree!((base_type type: {ty})))} member: {members}) ), - // An `extension Foo { … }` is likewise a `class_like_declaration`, named - // by the extended type. The extended type is captured opaquely (as its - // source text) so that qualified names (`extension String.Interpolation`, - // a `memberType`) name the declaration just like simple ones. + // An `extension Foo.Bar { … }` is likewise a `class_like_declaration`. rule!( (extensionDecl extensionKeyword: @kind From ffdd2a484fb6f005b4d28d56ccf53ca845afe7c9 Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 14 Sep 2026 16:15:00 +0200 Subject: [PATCH 12/18] unified: Include extension steps in debug view --- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 8d1e3519a409..679b90b8808e 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -505,6 +505,9 @@ module DebugGraph { or inheritanceStep(node1, node2) and value = "inheritedBy" + or + extensionStep(node1, node2) and + value = "extensionOf" ) } } From e5034d974b4810e0184ec157f49c26589f0519b7 Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 14 Sep 2026 16:16:43 +0200 Subject: [PATCH 13/18] unified: Update test case to be valid Swift An extension can add protocols as base, but not classes. Expanding the test a bit to target a protocol-base and also extension the protocol to add a method with a "default" implementation. --- .../static-name-binding/extensions.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 3b1a5e603c69..080357c1bf4a 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -36,14 +36,17 @@ extension B { // $ access=B } } -// Inheritance through extension -class Base { - func baseMethod() { - } +// Protocol conformance through extension +protocol Base { + func baseMethod(); +} +extension Base { // $ access=Base + func baseMethodExt() {} // name=Base.baseMethodExt } class X { func xMethod() { baseMethod() // $ access=Base.baseMethod + baseMethodExt() // $ access=Base.baseMethodExt } } extension X : Base {} // $ access=X access=Base From 5bbae07bd21add2180346a7499243fdfea5c619c Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 14 Sep 2026 17:17:17 +0200 Subject: [PATCH 14/18] unified: Add tests for missing features --- .../test/library-tests/static-name-binding/extensions.swift | 6 ++++++ .../package1/Sources/Target1/File1.swift | 6 ++++++ .../package1/Sources/Target2/File3.swift | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 080357c1bf4a..13564ac753e7 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -50,3 +50,9 @@ class X { } } extension X : Base {} // $ access=X access=Base + +// Type parameters of the extended type should be in scope in the extension. +class GenericExtensionTarget {} +extension GenericExtensionTarget { // $ access=GenericExtensionTarget + func useTypeParameter(_: ExtensionTypeParameter) {} // $ MISSING: access=ExtensionTypeParameter +} diff --git a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File1.swift b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File1.swift index 6e807bcf5dba..c2a289cc989a 100644 --- a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File1.swift +++ b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File1.swift @@ -1,2 +1,8 @@ let x: A; // $ access=Target1.A let y: Target2.A; // not a valid reference + +public class ScopedExtensionTarget { + func useExtensionFromUnimportedModule() { + target2ExtensionMethod() // $ SPURIOUS: access=Target1.ScopedExtensionTarget.target2ExtensionMethod + } +} diff --git a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target2/File3.swift b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target2/File3.swift index 8309118a867b..1247d900be40 100644 --- a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target2/File3.swift +++ b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target2/File3.swift @@ -1,5 +1,11 @@ +import Target1 + public class A {} // name=Target2.A public class B { // name=Target2.B public class C {} // name=Target2.B.C } + +extension ScopedExtensionTarget { // $ access=ScopedExtensionTarget + func target2ExtensionMethod() {} // name=Target1.ScopedExtensionTarget.target2ExtensionMethod +} From f5be54404240a048ff169d79c822696eb9bf46e0 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 15 Sep 2026 13:23:16 +0200 Subject: [PATCH 15/18] unified: Test protocol method with and without implementation --- .../library-tests/static-name-binding/extensions.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 13564ac753e7..0f12add91a07 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -39,17 +39,21 @@ extension B { // $ access=B // Protocol conformance through extension protocol Base { func baseMethod(); + func baseMethodNoImpl(); } extension Base { // $ access=Base func baseMethodExt() {} // name=Base.baseMethodExt } class X { func xMethod() { - baseMethod() // $ access=Base.baseMethod + baseMethod() // $ access=X.baseMethod + baseMethodNoImpl() // $ access=Base.baseMethodNoImpl // with no visible implementation, just resolve to the signature baseMethodExt() // $ access=Base.baseMethodExt } } -extension X : Base {} // $ access=X access=Base +extension X : Base { // $ access=X access=Base + func baseMethod() {} // name=X.baseMethod +} // Type parameters of the extended type should be in scope in the extension. class GenericExtensionTarget {} From 22ae803bc79af4a8281ec778915ba720facb3dfa Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 15 Sep 2026 13:26:19 +0200 Subject: [PATCH 16/18] unified: Add test for "default implementation" pattern --- .../static-name-binding/extensions.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 0f12add91a07..19d666850baa 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -40,21 +40,35 @@ extension B { // $ access=B protocol Base { func baseMethod(); func baseMethodNoImpl(); + func baseMethodDefaultImpl(); } extension Base { // $ access=Base - func baseMethodExt() {} // name=Base.baseMethodExt + func baseMethodExt() {} // name=BaseImpl.baseMethodExt + func baseMethodDefaultImpl() {} // name=BaseImpl.baseMethodDefaultImpl } class X { func xMethod() { baseMethod() // $ access=X.baseMethod baseMethodNoImpl() // $ access=Base.baseMethodNoImpl // with no visible implementation, just resolve to the signature - baseMethodExt() // $ access=Base.baseMethodExt + baseMethodExt() // $ access=BaseImpl.baseMethodExt + baseMethodDefaultImpl() // $ access=Base.baseMethodDefaultImpl access=BaseImpl.baseMethodDefaultImpl } } extension X : Base { // $ access=X access=Base func baseMethod() {} // name=X.baseMethod } +class Y { + func yMethod() { + baseMethod() // $ access=Y.baseMethod + baseMethodDefaultImpl() // $ access=Y.baseMethodDefaultImpl + } +} +extension Y : Base { // $ access=Y access=Base + func baseMethod() {} // name=Y.baseMethod + func baseMethodDefaultImpl() {} // name=Y.baseMethodDefaultImpl +} + // Type parameters of the extended type should be in scope in the extension. class GenericExtensionTarget {} extension GenericExtensionTarget { // $ access=GenericExtensionTarget From 1b72559855dde56e7fb500804741006cafe4dfb8 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 15 Sep 2026 13:35:36 +0200 Subject: [PATCH 17/18] unified: Add comment regarding local namespace in an extension --- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 679b90b8808e..3cab856199c1 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -262,6 +262,8 @@ predicate valueStep(NameBindingNode node1, NameBindingNode node2) { node2 = getNodeFromRef(p.getSubPattern()) ) or + // Extensions have access to the members of the entity they extend. + // TODO: The type parameters of the target type should also be in the local scope (for Swift). exists(ClassLikeDeclaration extension | node1 = getNodeFromRef(extension.getExtensionTarget()) and node2.isLocalNamespace(extension) From 432f8c0775d90edf140ae4d12f02334f73c26919 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 15 Sep 2026 14:16:09 +0200 Subject: [PATCH 18/18] unified: Add comment clarifying case with multiple targets --- .../ql/test/library-tests/static-name-binding/extensions.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unified/ql/test/library-tests/static-name-binding/extensions.swift b/unified/ql/test/library-tests/static-name-binding/extensions.swift index 19d666850baa..35c65dcb9a89 100644 --- a/unified/ql/test/library-tests/static-name-binding/extensions.swift +++ b/unified/ql/test/library-tests/static-name-binding/extensions.swift @@ -51,9 +51,12 @@ class X { baseMethod() // $ access=X.baseMethod baseMethodNoImpl() // $ access=Base.baseMethodNoImpl // with no visible implementation, just resolve to the signature baseMethodExt() // $ access=BaseImpl.baseMethodExt + + // Static name binding may find multiple targets. Type inference should disambiguate. baseMethodDefaultImpl() // $ access=Base.baseMethodDefaultImpl access=BaseImpl.baseMethodDefaultImpl } } + extension X : Base { // $ access=X access=Base func baseMethod() {} // name=X.baseMethod }