From ea600a97d0e414f901666733dd40009b5d0e21de Mon Sep 17 00:00:00 2001 From: Asger F Date: Mon, 24 Aug 2026 10:46:54 +0200 Subject: [PATCH 01/10] unified: Add test for enums I thought there was a bug here, but it turns out this works as intended. Makes sense to keep the test though. --- .../test/library-tests/static-name-binding/test.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 27453ec34351..64a385ef29b4 100644 --- a/unified/ql/test/library-tests/static-name-binding/test.swift +++ b/unified/ql/test/library-tests/static-name-binding/test.swift @@ -51,3 +51,14 @@ class H { let x2: G.B = nil; // $ access=G access=A.B let x3: G.B.C = nil; // $ access=G access=A.B access=A.B.C } + +enum I { + case one=1, two=2 + case three=3 +} + +func useI() { + I.one // $ access=I access=I.one + I.two // $ access=I access=I.two + I.three // $ access=I access=I.three +} From 3cf499f3d439088c6d42c84653a5ca0ddb2bcd23 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:12:31 +0200 Subject: [PATCH 02/10] unified: Rename TExportedNamespace -> TStaticMemberNamespace Originally this node was intended to cover TopLevels as well, but things evolved differently, and this name works better since it's only for classes. --- .../unified/internal/StaticNameBinding.qll | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index dcb97b4fbae8..38430dce1a01 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -10,7 +10,7 @@ private newtype TNameBindingNode = TIdentifier(Identifier n) or TBulkImport(BulkImportingPattern p) or TLocalName(LocalName local) or - TExportedNamespace(ClassLikeDeclaration cls) or + TStaticMemberNamespace(ClassLikeDeclaration cls) or TLocalNamespace(AstNode n) { n = any(TopLevel t).getBody() or // Imported names come in scope here n instanceof ClassLikeDeclaration @@ -31,8 +31,8 @@ class NameBindingNode extends TNameBindingNode { predicate isLocalName(LocalName local) { this = TLocalName(local) } - /** Holds if this represents the set of static members available in the given namespace. */ - predicate isExportedNamespace(ClassLikeDeclaration cls) { this = TExportedNamespace(cls) } + /** Holds if this represents the set of static members available in the given class. */ + predicate isStaticMemberNamespace(ClassLikeDeclaration cls) { this = TStaticMemberNamespace(cls) } /** Holds if this represents the set of members that can be accessed unqualified within the given scope. */ predicate isLocalNamespace(AstNode n) { this = TLocalNamespace(n) } @@ -56,7 +56,7 @@ class NameBindingNode extends TNameBindingNode { or this.isBulkImport(result) or - this.isExportedNamespace(result) + this.isStaticMemberNamespace(result) or this.isLocalNamespace(result) or @@ -71,7 +71,7 @@ class NameBindingNode extends TNameBindingNode { exists(LocalName local | this.isLocalName(local) and result = "LocalName(" + local + ")") or exists(ClassLikeDeclaration cls | - this.isExportedNamespace(cls) and result = "ExportedNamespace(" + cls + ")" + this.isStaticMemberNamespace(cls) and result = "StaticMemberNamespace(" + cls + ")" ) or exists(AstNode n | this.isLocalNamespace(n) and result = "LocalNamespace(" + n + ")") @@ -92,7 +92,9 @@ class NameBindingNode extends TNameBindingNode { or exists(LocalName local | this.isLocalName(local) and result = local.getLocation()) or - exists(ClassLikeDeclaration cls | this.isExportedNamespace(cls) and result = cls.getLocation()) + exists(ClassLikeDeclaration cls | + this.isStaticMemberNamespace(cls) and result = cls.getLocation() + ) or exists(AstNode n | this.isLocalNamespace(n) and result = n.getLocation()) or @@ -162,7 +164,7 @@ predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { nameDecl.getDeclaration() = member and node1.isIdentifier(nameDecl) and name = nameDecl.getName() and - node2.isExportedNamespace(cls) + node2.isStaticMemberNamespace(cls) ) or exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl | @@ -195,12 +197,12 @@ predicate valueStep(NameBindingNode node1, NameBindingNode node2) { ) or exists(ClassLikeDeclaration cls | - node1.isExportedNamespace(cls) and + node1.isStaticMemberNamespace(cls) and node2.isIdentifier(cls.getName()) ) or exists(ClassLikeDeclaration cls | - node1.isExportedNamespace(cls) and + node1.isStaticMemberNamespace(cls) and node2.isLocalNamespace(cls) ) or @@ -249,7 +251,7 @@ predicate inheritanceStep(NameBindingNode supertype, NameBindingNode subtype) { exists(ClassLikeDeclaration cls, BaseType base | base = cls.getABaseType() and supertype = getNodeFromRef(base.getType()) and - subtype.isExportedNamespace(cls) + subtype.isStaticMemberNamespace(cls) ) } From cd0afb9380d84da5f36658460e7de552474b8764 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:13:33 +0200 Subject: [PATCH 03/10] unified: Add TInstanceMemberNamespace --- .../codeql/unified/internal/StaticNameBinding.qll | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 38430dce1a01..e5664ab53866 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -11,6 +11,7 @@ private newtype TNameBindingNode = TBulkImport(BulkImportingPattern p) or TLocalName(LocalName local) or TStaticMemberNamespace(ClassLikeDeclaration cls) or + TInstanceMemberNamespace(ClassLikeDeclaration cls) or TLocalNamespace(AstNode n) { n = any(TopLevel t).getBody() or // Imported names come in scope here n instanceof ClassLikeDeclaration @@ -34,6 +35,11 @@ class NameBindingNode extends TNameBindingNode { /** Holds if this represents the set of static members available in the given class. */ predicate isStaticMemberNamespace(ClassLikeDeclaration cls) { this = TStaticMemberNamespace(cls) } + /** Holds if this represents the set of instance members available in the given class. */ + predicate isInstanceMemberNamespace(ClassLikeDeclaration cls) { + this = TInstanceMemberNamespace(cls) + } + /** Holds if this represents the set of members that can be accessed unqualified within the given scope. */ predicate isLocalNamespace(AstNode n) { this = TLocalNamespace(n) } @@ -74,6 +80,10 @@ class NameBindingNode extends TNameBindingNode { this.isStaticMemberNamespace(cls) and result = "StaticMemberNamespace(" + cls + ")" ) or + exists(ClassLikeDeclaration cls | + this.isInstanceMemberNamespace(cls) and result = "InstanceMemberNamespace(" + cls + ")" + ) + or exists(AstNode n | this.isLocalNamespace(n) and result = "LocalNamespace(" + n + ")") or exists(ModuleScopeRepr repr | @@ -96,6 +106,10 @@ class NameBindingNode extends TNameBindingNode { this.isStaticMemberNamespace(cls) and result = cls.getLocation() ) or + exists(ClassLikeDeclaration cls | + this.isInstanceMemberNamespace(cls) and result = cls.getLocation() + ) + or exists(AstNode n | this.isLocalNamespace(n) and result = n.getLocation()) or exists(ModuleScopeRepr repr | this.isModuleScopeNode(repr) and result = repr.getLocation()) From 40a029191400cd61203686f98b39e5a1ad43b261 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:13:52 +0200 Subject: [PATCH 04/10] unified: Store instance members in the new node --- .../ql/lib/codeql/unified/internal/StaticNameBinding.qll | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index e5664ab53866..12f463c89b35 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -173,12 +173,15 @@ predicate readStep(NameBindingNode node1, string name, NameBindingNode node2) { predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { exists(ClassLikeDeclaration cls, Member member, NameDeclaration nameDecl | member = cls.getAMember() and - not isInstanceMember(member) and not isPrivateToLocalScope(nameDecl) and nameDecl.getDeclaration() = member and node1.isIdentifier(nameDecl) and name = nameDecl.getName() and - node2.isStaticMemberNamespace(cls) + ( + if isInstanceMember(member) + then node2.isInstanceMemberNamespace(cls) + else node2.isStaticMemberNamespace(cls) + ) ) or exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl | From 2bc839d6d5f65df6be396c41f9c107a7c1e8c48a Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:18:09 +0200 Subject: [PATCH 05/10] unified: Add inheritance between instance namespaces --- .../unified/internal/StaticNameBinding.qll | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 12f463c89b35..84451f23e179 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -327,8 +327,27 @@ class NamespaceNode extends NameBindingNode { /** Holds if this namespace has an own-member of the given name */ predicate hasOwnMember(string name) { exists(this.getOwnMember(name)) } + /** If this is the static namespace for a class, gets the corresponding instance namespace. */ + NamespaceNode toInstanceNamespace() { + exists(ClassLikeDeclaration cls | + this.isStaticMemberNamespace(cls) and + result.isInstanceMemberNamespace(cls) + ) + } + + /** If this is the instance namespace for a class, gets the corresponding static namespace. */ + NamespaceNode toStaticNamespace() { result.toInstanceNamespace() = this } + + private NamespaceNode getAnInheritanceParent1() { inheritanceStep(result.ref(), this) } + /** Gets a namespace from which this namespace inherits directly. */ - NamespaceNode getAnInheritanceParent() { inheritanceStep(result.ref(), this) } + NamespaceNode getAnInheritanceParent() { + result = this.getAnInheritanceParent1() + or + // `inheritanceStep` connects the static namespaces of classes. + // Add the corresponding inheritance relation between the instance namespaces. + result = this.toStaticNamespace().getAnInheritanceParent1().toInstanceNamespace() + } /** Gets a namespace that directly inherits from this one. */ NamespaceNode getAnInheritanceChild() { result.getAnInheritanceParent() = this } From 5b31814c8e787ffc8c98e772332ca8be224bf55b Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:20:16 +0200 Subject: [PATCH 06/10] unified: Ensure member-namespaces are always NamespaceNodes Avoids surprising edges cases when a class has no instance/static members --- .../ql/lib/codeql/unified/internal/StaticNameBinding.qll | 7 ++++++- 1 file 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 84451f23e179..f1253fbb92a3 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -316,7 +316,12 @@ private predicate derivedStoreReadStep(NameBindingNode node1, NameBindingNode no /** A name-binding node that has members. */ class NamespaceNode extends NameBindingNode { - NamespaceNode() { storeStep(_, _, this) or inheritanceStep(_, this) } + NamespaceNode() { + storeStep(_, _, this) or + inheritanceStep(_, this) or + this.isInstanceMemberNamespace(_) or + this.isStaticMemberNamespace(_) + } /** Gets a name-binding node that may refer to this namespace. */ NameBindingNode ref() { result = TrackNamespace::track(this) } From 981fc4d9bd4be8886d070b3f868b3b4e80c827e6 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:21:27 +0200 Subject: [PATCH 07/10] unified: Add language hook for inheritance restriction --- .../unified/internal/NameBindingPlugin.qll | 27 +++++++++++++++++++ .../internal/NameBindingPluginSwift.qll | 6 +++++ .../unified/internal/StaticNameBinding.qll | 12 ++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/NameBindingPlugin.qll b/unified/ql/lib/codeql/unified/internal/NameBindingPlugin.qll index a0a7028dfe92..a3b4ce6f7ff8 100644 --- a/unified/ql/lib/codeql/unified/internal/NameBindingPlugin.qll +++ b/unified/ql/lib/codeql/unified/internal/NameBindingPlugin.qll @@ -25,6 +25,15 @@ class NameBindingPlugin extends Unit { */ bindingset[member, binding] predicate isPrivateToLocalScope(Stmt member, AstNode binding) { none() } + + /** + * Holds if `member` can be inherited by subclasses of `cls`. + * + * The caller has already restricted `member` to be a member of `cls`, and + * ensured that `member` is a `VariableDeclaration` or `FunctionDeclaration`. + */ + bindingset[cls, member] + predicate isInheritableMember(ClassLikeDeclaration cls, Member member) { none() } } /** Holds if `member` is an instance member. */ @@ -35,6 +44,24 @@ predicate isInstanceMember(Member member) { ) } +/** + * Holds if `member` is a non-instance member declared in the context of a class or top-level. + */ +predicate isStaticMember(Member member) { + exists(ClassLikeDeclaration cls | cls.getAMember() = member | + not any(NameBindingPlugin p).isInstanceMember(cls, member) + ) + or + member = any(TopLevel t).getBody().getAStmt() +} + +/** Holds if `member` is an inheritable member. */ +predicate isInheritableMember(Member member) { + exists(ClassLikeDeclaration cls | cls.getAMember() = member | + any(NameBindingPlugin p).isInheritableMember(cls, member) + ) +} + /** Holds if `binding` is only visible in its local scope. */ pragma[nomagic] predicate isPrivateToLocalScope(AstNode binding) { diff --git a/unified/ql/lib/codeql/unified/internal/NameBindingPluginSwift.qll b/unified/ql/lib/codeql/unified/internal/NameBindingPluginSwift.qll index eaa8f46c65f4..329246f4474d 100644 --- a/unified/ql/lib/codeql/unified/internal/NameBindingPluginSwift.qll +++ b/unified/ql/lib/codeql/unified/internal/NameBindingPluginSwift.qll @@ -29,6 +29,12 @@ class NameBindingPluginSwift extends NameBindingPlugin { // Note: Private class members can be seen within type-extensions in the same file, // so we can't declare those private to their local scope. } + + bindingset[cls, member] + override predicate isInheritableMember(ClassLikeDeclaration cls, Member member) { + exists(cls) and + not member.hasModifier("private") + } } private predicate predefinedSourceFolders(string folder, int ordering) { diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index f1253fbb92a3..b4ad8a02f8f4 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -314,6 +314,15 @@ private predicate derivedStoreReadStep(NameBindingNode node1, NameBindingNode no ) } +/** Holds if the member represented by `node` cannot be inherited. */ +pragma[nomagic] +private predicate isInheritableMemberNode(NameBindingNode node) { + exists(NameDeclaration decl | + node.isIdentifier(decl) and + isInheritableMember(decl.getDeclaration()) + ) +} + /** A name-binding node that has members. */ class NamespaceNode extends NameBindingNode { NamespaceNode() { @@ -363,7 +372,8 @@ class NamespaceNode extends NameBindingNode { result = this.getOwnMember(name) or not this.hasOwnMember(name) and - result = this.getAnInheritanceParent().getMember(name) + result = this.getAnInheritanceParent().getMember(name) and + isInheritableMemberNode(result) } } From 2b28f759d04375e5035477565dfcdc555c43f88c Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:26:01 +0200 Subject: [PATCH 08/10] unified: Expose handling of unqualified member accesses UnqualifiedMemberAccess might go into Public one day --- .../lib/codeql/unified/internal/FacadeAst.qll | 7 ++ .../unified/internal/StaticNameBinding.qll | 81 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index cb00bae7eeca..44adef76e425 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -30,6 +30,13 @@ module Unified { not this instanceof ClassLikeDeclaration and result = this.getParent().getEnclosingClass() } + + /** Gets the depth of this node in the AST. The root node has a depth of 0. */ + int getDepth() { + not exists(this.getParent()) and result = 0 + or + result = this.getParent().getDepth() + 1 + } } /** An expression */ diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index b4ad8a02f8f4..ea331b1e8a35 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -559,3 +559,84 @@ private module FolderHeuristic { ) } } + +/** + * Holds if `access` may resolve to `target` through the enclosing `accessingClass`. + * + * `instanceAccess` indicates if this this member should be accessed as an instance of `accessingClass` + * or as a static member. + */ +private predicate unqualifiedMemberAccessCand( + PotentialLocalNameAccess access, boolean instanceAccess, NameDeclaration target, + ClassLikeDeclaration accessingClass +) { + not access instanceof NameDeclaration and + ( + // Resolved by local scoping + exists(LocalName local | + target.getLocalName() = local and + access.getLocalName() = local and + target.getDeclaration() = accessingClass.getAMember() + | + instanceAccess = true and + isInstanceMember(target.getDeclaration()) + or + instanceAccess = false and + isStaticMember(target.getDeclaration()) + ) + or + // Resolved in an uncertain scope + exists(NamespaceNode namespace, string name | + name = access.getName() and + accessingClass = LocalNameBindingOutput::getAnUncertainScope(access, name) + | + instanceAccess = true and + namespace.isInstanceMemberNamespace(accessingClass) and + namespace.getMember(name).isIdentifier(target) + or + instanceAccess = false and + namespace.isStaticMemberNamespace(accessingClass) and + namespace.getMember(name).isIdentifier(target) + ) + ) +} + +private int unqualifiedMemberAccessDepth(PotentialLocalNameAccess access) { + result = max(AstNode scope | unqualifiedMemberAccessCand(access, _, _, scope) | scope.getDepth()) +} + +/** + * Holds if `access` is an unqualified access to `target`. + * + * `accessingClass` is the enclosing class in which the member was found, and + * `instanceAccess` indicates if it is an instance member or static member. + */ +predicate unqualifiedMemberAccess( + PotentialLocalNameAccess access, boolean instanceAccess, NameDeclaration target, + ClassLikeDeclaration accessingClass +) { + unqualifiedMemberAccessCand(access, instanceAccess, target, accessingClass) and + accessingClass.getDepth() = unqualifiedMemberAccessDepth(access) +} + +/** + * An identifier appearing in a unqualified position, referring to a member of an enclosing class. + */ +class UnqualifiedMemberAccess extends Identifier { + private boolean instanceAccess; + private NameDeclaration target; + private ClassLikeDeclaration accessingClass; + + UnqualifiedMemberAccess() { + unqualifiedMemberAccess(this, instanceAccess, target, accessingClass) + } + + /** Gets the name declaration of the member being accessed. */ + NameDeclaration getTarget() { result = target } + + /** Gets the enclosing class whose (possibly inherited) member is being accessed. */ + ClassLikeDeclaration getAccessingClass() { result = accessingClass } + + /** Holds if this is an instance access on the accessing class. */ + predicate isInstanceAccess() { instanceAccess = true } +} From b1ca110785038713cc394a750596baa260efd34c Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:26:37 +0200 Subject: [PATCH 09/10] unified: Add getStaticBindingTarget --- unified/ql/lib/codeql/Definitions.qll | 2 +- .../codeql/unified/internal/StaticNameBinding.qll | 10 ++++++++++ .../implicit-instance-field-access.swift | 13 +++++++++++++ .../test/library-tests/static-name-binding/test.ql | 2 +- .../static-name-binding/unqualified-access.swift | 2 +- 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 unified/ql/test/library-tests/static-name-binding/implicit-instance-field-access.swift diff --git a/unified/ql/lib/codeql/Definitions.qll b/unified/ql/lib/codeql/Definitions.qll index 927119530401..3eb6ddcef102 100644 --- a/unified/ql/lib/codeql/Definitions.qll +++ b/unified/ql/lib/codeql/Definitions.qll @@ -10,7 +10,7 @@ private import codeql.unified.internal.StaticNameBinding */ cached predicate definitionOf(Identifier reference, NameDeclaration definition, string kind) { - reference = trackNameDeclaration(definition).asIdentifier() and + definition = getStaticBindingTarget(reference) and not reference instanceof NameDeclaration and kind = "name" } diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index ea331b1e8a35..b38d405b165d 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -640,3 +640,13 @@ class UnqualifiedMemberAccess extends Identifier { /** Holds if this is an instance access on the accessing class. */ predicate isInstanceAccess() { instanceAccess = true } } + +/** Gets the declaration being accessed by `access`, as determined by static name binding. */ +NameDeclaration getStaticBindingTarget(Identifier access) { + // For unqualified accesses, use the shadowing-aware lookup + result = access.(UnqualifiedMemberAccess).getTarget() + or + // For others, just follow the name binding graph + not access instanceof UnqualifiedMemberAccess and + trackNameDeclaration(result).asIdentifier() = access +} diff --git a/unified/ql/test/library-tests/static-name-binding/implicit-instance-field-access.swift b/unified/ql/test/library-tests/static-name-binding/implicit-instance-field-access.swift new file mode 100644 index 000000000000..6bdf2c0669aa --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/implicit-instance-field-access.swift @@ -0,0 +1,13 @@ +private class A { + let x = 123 // name=A.instance.x + + func getX() { + return x // $ access=A.instance.x + } +} + +private class B : A { // $ access=A + func getX2() { + return x // $ access=A.instance.x + } +} diff --git a/unified/ql/test/library-tests/static-name-binding/test.ql b/unified/ql/test/library-tests/static-name-binding/test.ql index a1e49ce0609c..e6dd7e64f6b3 100644 --- a/unified/ql/test/library-tests/static-name-binding/test.ql +++ b/unified/ql/test/library-tests/static-name-binding/test.ql @@ -8,7 +8,7 @@ module StaticDeclAccess implements TestSig { predicate hasActualResult(Location location, string element, string tag, string value) { exists(NameDeclaration decl, Identifier access | - access = trackNameDeclaration(decl).asIdentifier() and + decl = getStaticBindingTarget(access) and not access instanceof NameDeclaration and location = access.getLocation() and element = access.toString() and diff --git a/unified/ql/test/library-tests/static-name-binding/unqualified-access.swift b/unified/ql/test/library-tests/static-name-binding/unqualified-access.swift index 86cadc0b7a1f..434dd0dc8ac1 100644 --- a/unified/ql/test/library-tests/static-name-binding/unqualified-access.swift +++ b/unified/ql/test/library-tests/static-name-binding/unqualified-access.swift @@ -10,7 +10,7 @@ class ASub : A { // $ access=A class BSub : B { // $ access=A.B let x3: B = nil; // $ access=A.B - let x4: C = nil; // $ access=A.B.C SPURIOUS: access=Target3.C // spurious result from folder-based heuristic + let x4: C = nil; // $ access=A.B.C // spurious result from folder-based heuristic } class BSub2 : B { // $ access=A.B From fa36a114ae33aa01114a40f34094912ea2c2c7d1 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 25 Aug 2026 09:43:33 +0200 Subject: [PATCH 10/10] unified: Sharpen set of candidates when measuring static name binding --- .../unified/internal/AnalysisQuality.qll | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll index a10257ed8326..f2a7acabf41f 100644 --- a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll +++ b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll @@ -6,12 +6,45 @@ private import codeql.unified.internal.NameBindingPlugin /** Stats about identifiers that static name binding could resolve. */ module StaticNameResolutionStats implements EntityStatsSig { + /** + * Holds if `name` has been positively identified as something that refer to a value, and static name binding + * is thus not expected to resolve its members. + */ + private predicate resolvesToValue(Identifier name) { + exists(AstNode decl | + decl = getStaticBindingTarget(name).getDeclaration() and + not decl instanceof ClassLikeDeclaration and + not decl instanceof TypeAliasDeclaration and + not decl instanceof TypeParameter and + not decl instanceof AssociatedTypeDeclaration + ) + } + + /** + * Holds if name-binding for `expr` depends on type inference, and is thus not subject to static name binding. + * + * Usually this holds for qualified instance member accesses (`foo().x`) and leading-dot expressions (`.x`). + */ + private predicate memberAccessDependsOnTypeInference(MemberAccessExpr expr) { + exists(Expr base | base = expr.getBase() | + // Base expression resolves to a value, e.g. a field, variable, or function (for languages where functions are values). + resolvesToValue(getIdentifierFromRef(base)) + or + // Base expression is of a kind that is not subject to static name resolution, e.g. `foo().x` + not exists(getIdentifierFromRef(base)) + or + // Base expression is a confirmed to depend on type inference + memberAccessDependsOnTypeInference(base) + ) + } + class Candidate extends Identifier { Candidate() { - this = getIdentifierFromRef(_) and + exists(AstNode ref | + this = getIdentifierFromRef(ref) and + not memberAccessDependsOnTypeInference(ref) + ) and not this instanceof NameDeclaration - // TODO: exclude names we know are not static references, e.g. unqualified instance-field access, - // currently blocked on getting static name binding to report this information. } NameBindingNode getTarget() {