From 5267dda1227a01244e45a1b678bdba0781e57fd8 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 8 Sep 2026 09:54:30 +0200 Subject: [PATCH 01/23] unified: Add AstNode.getEnclosingCallable --- .../lib/codeql/unified/internal/ControlFlowGraph.qll | 9 +-------- unified/ql/lib/codeql/unified/internal/FacadeAst.qll | 10 ++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll index d545c589185f..2c60994c4cd5 100644 --- a/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll +++ b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll @@ -39,14 +39,7 @@ private module Ast implements AstSig { not skipControlFlow(result) } - Callable getEnclosingCallable(AstNode node) { - exists(AstNode parent | parent = node.getParent() | - result = parent - or - not parent instanceof Callable and - result = getEnclosingCallable(parent) - ) - } + Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() } class Callable = U::Callable; diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index dcec57f0a4ae..db32fadc8a5d 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -31,6 +31,16 @@ module Unified { result = this.getParent().getEnclosingClass() } + /** Gets the nearest callable containing this AST node. */ + Callable getEnclosingCallable() { + exists(AstNode parent | parent = this.getParent() | + result = parent + or + not parent instanceof Callable and + result = parent.getEnclosingCallable() + ) + } + /** 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 From 896a8a599dbdf21f302e5bcec76f9de32f28dafd Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 8 Sep 2026 12:56:06 +0200 Subject: [PATCH 02/23] unified: Update getEnclosingClass to be strict, for consistency --- unified/ql/lib/codeql/unified/internal/FacadeAst.qll | 12 +++++++----- unified/ql/lib/utils/test/TestUtils.qll | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index db32fadc8a5d..1cc6d6106e93 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -23,12 +23,14 @@ module Unified { ) } - /** Gets the nearest enclosing class declaration, possibly this node itself. */ + /** Gets the nearest enclosing class declaration. */ ClassLikeDeclaration getEnclosingClass() { - result = this - or - not this instanceof ClassLikeDeclaration and - result = this.getParent().getEnclosingClass() + exists(AstNode parent | parent = this.getParent() | + result = parent + or + not parent instanceof ClassLikeDeclaration and + result = parent.getEnclosingClass() + ) } /** Gets the nearest callable containing this AST node. */ diff --git a/unified/ql/lib/utils/test/TestUtils.qll b/unified/ql/lib/utils/test/TestUtils.qll index 6a7fbae3d592..15155c44758a 100644 --- a/unified/ql/lib/utils/test/TestUtils.qll +++ b/unified/ql/lib/utils/test/TestUtils.qll @@ -3,10 +3,10 @@ private import CommentUtil private import codeql.unified.internal.StaticNameBinding private string deriveClassName(ClassLikeDeclaration cls) { - not exists(cls.getParent().getEnclosingClass()) and + not exists(cls.getEnclosingClass()) and result = cls.getName() or - result = deriveClassName(cls.getParent().getEnclosingClass()) + "." + cls.getName() + result = deriveClassName(cls.getEnclosingClass()) + "." + cls.getName() } private string defaultName(NameDeclaration decl) { From 87ba367fb9ae5263fa3c23e9322078aa25a6c35e Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 8 Sep 2026 09:59:07 +0200 Subject: [PATCH 03/23] unified: Add LocalVariable --- .../unified/internal/LocalNameBinding.qll | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 6fe8ace83fcf..497b800b81a9 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -6,6 +6,7 @@ private import unified private import unified as U private import codeql.namebinding.LocalNameBinding private import codeql.unified.internal.NameBindingPlugin +private import codeql.unified.internal.StaticNameBinding private module LocalNameBindingInput implements LocalNameBindingInputSig { class AstNode = U::AstNode; @@ -325,6 +326,7 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig Date: Fri, 11 Sep 2026 08:46:39 +0200 Subject: [PATCH 04/23] unified: Refine definition of LocalVariableDeclaration --- .../ql/lib/codeql/unified/internal/AstExtra.qll | 15 +++------------ .../codeql/unified/internal/LocalNameBinding.qll | 2 -- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/AstExtra.qll b/unified/ql/lib/codeql/unified/internal/AstExtra.qll index fea1a2490363..5e8e54faa61a 100644 --- a/unified/ql/lib/codeql/unified/internal/AstExtra.qll +++ b/unified/ql/lib/codeql/unified/internal/AstExtra.qll @@ -3,6 +3,7 @@ */ private import unified +private import codeql.unified.internal.NameBindingPlugin module Public { /** A short-circuiting logical AND expression. */ @@ -29,24 +30,14 @@ module Public { * Declaration of a local or top-level variable. */ class LocalVariableDeclaration extends VariableDeclaration { - private Block block; - - LocalVariableDeclaration() { this = block.getStmt(_) } - - /** Gets the block in which this variable is declared. */ - Block getDeclaringBlock() { result = block } + LocalVariableDeclaration() { not isStaticMember(this) and not isInstanceMember(this) } } /** * Declaration of a local or top-level function. */ class LocalFunctionDeclaration extends FunctionDeclaration { - private Block block; - - LocalFunctionDeclaration() { this = block.getStmt(_) } - - /** Gets the block in which this function is declared. */ - Block getDeclaringBlock() { result = block } + LocalFunctionDeclaration() { not isStaticMember(this) and not isInstanceMember(this) } } /** diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 497b800b81a9..2349fb0e1e7c 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -168,8 +168,6 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig Date: Wed, 9 Sep 2026 21:34:21 +0200 Subject: [PATCH 05/23] unified: Rename NameDeclaration -> NameBinding We used the term declaration both for the identifier and the surrounding node declaring containing one or more such identifiers (possibly nested in a pattern). It led to code like `getADeclaration().getDeclaration()`. We now use the term "binding" for identifiers in binding position, and declarations as contexts that establish the binding position. --- unified/ql/lib/codeql/Definitions.qll | 4 +- .../unified/internal/AnalysisQuality.qll | 2 +- .../unified/internal/LocalNameBinding.qll | 16 +++---- .../unified/internal/StaticNameBinding.qll | 42 +++++++++---------- .../lib/ide-contextual-queries/definitions.ql | 2 +- unified/ql/lib/utils/test/TestUtils.qll | 6 +-- .../ql/test/library-tests/definitions/test.ql | 4 +- .../library-tests/local-name-binding/test.ql | 3 +- .../library-tests/static-name-binding/test.ql | 6 +-- 9 files changed, 42 insertions(+), 43 deletions(-) diff --git a/unified/ql/lib/codeql/Definitions.qll b/unified/ql/lib/codeql/Definitions.qll index 3eb6ddcef102..941d00b425cc 100644 --- a/unified/ql/lib/codeql/Definitions.qll +++ b/unified/ql/lib/codeql/Definitions.qll @@ -9,8 +9,8 @@ private import codeql.unified.internal.StaticNameBinding * Holds if `reference` refers to `definition`. */ cached -predicate definitionOf(Identifier reference, NameDeclaration definition, string kind) { +predicate definitionOf(Identifier reference, NameBinding definition, string kind) { definition = getStaticBindingTarget(reference) and - not reference instanceof NameDeclaration and + not reference instanceof NameBinding and kind = "name" } diff --git a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll index d1568edfff80..c2c6bf66f6d5 100644 --- a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll +++ b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll @@ -44,7 +44,7 @@ module StaticNameResolutionStats implements EntityStatsSig { this = getIdentifierFromRef(ref) and not memberAccessDependsOnTypeInference(ref) ) and - not this instanceof NameDeclaration + not this instanceof NameBinding } NameBindingNode getTarget() { diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 2349fb0e1e7c..6ea58b5cb878 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -360,13 +360,13 @@ module Public { LocalNameAccess getAnAccess() { result.getLocalName() = this } - /** Gets an identiier that declares this local name. */ - NameDeclaration getADeclaration() { result.getLocalName() = this } + /** Gets a name binding that declares this local name. */ + NameBinding getABinding() { result.getLocalName() = this } } - /** A name node that appears as the declaration site of a name, such as the `x` in `let x = 123`. */ - class NameDeclaration extends Identifier { - NameDeclaration() { LocalNameBindingInput::bindingContext(this, _, _) } + /** An identifier appearing in a name-binding position, such as the `x` in `let x = 123`. */ + class NameBinding extends Identifier { + NameBinding() { LocalNameBindingInput::bindingContext(this, _, _) } /** Gets the statement-like node declaring this name, such as a `VariableDeclaration` or `CatchClause`. */ AstNode getDeclaration() { LocalNameBindingInput::bindingContext(this, _, result) } @@ -382,7 +382,7 @@ module Public { class LocalVariable extends LocalName { LocalVariable() { exists(AstNode decl | - decl = this.getADeclaration().getDeclaration() and + decl = this.getABinding().getDeclaration() and not isInstanceMember(decl) and not isStaticMember(decl) | @@ -429,6 +429,6 @@ class PotentialLocalNameAccess extends IdentifierExpr { string getName() { result = this.getValue() } - /** Holds if this is one of the declaration sites for a name, such as the `x` in `let x = 123`. */ - predicate isDeclarationSite() { this instanceof NameDeclaration } + /** Holds if this is one of the binding sites for a name, such as the `x` in `let x = 123`. */ + predicate isBindingSite() { this instanceof NameBinding } } diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 9055e7331e7c..d1a6e96d32e8 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -139,7 +139,7 @@ private predicate isPrivateToLocalScope(AstNode binding) { member = any(ClassLikeDeclaration cls).getAMember() or member = any(TopLevel t).getBody().getAStmt() ) and - (binding instanceof NameDeclaration or binding instanceof BulkImportingPattern) and + (binding instanceof NameBinding or binding instanceof BulkImportingPattern) and any(NameBindingPlugin p).isPrivateToLocalScope(member, binding) ) } @@ -168,7 +168,7 @@ predicate readStep(NameBindingNode node1, string name, NameBindingNode node2) { ) or exists(PotentialLocalNameAccess access | - not access.isDeclarationSite() and + not access.isBindingSite() and name = access.getName() and node1 = getNodeFromUncertainScope(LocalNameBindingOutput::getAnUncertainScope(access, name)) and node2.isIdentifier(access) @@ -183,7 +183,7 @@ predicate readStep(NameBindingNode node1, string name, NameBindingNode node2) { } predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { - exists(ClassLikeDeclaration cls, Member member, NameDeclaration nameDecl | + exists(ClassLikeDeclaration cls, Member member, NameBinding nameDecl | member = cls.getAMember() and not isPrivateToLocalScope(nameDecl) and nameDecl.getDeclaration() = member and @@ -194,7 +194,7 @@ predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { else node2.isStaticMemberNamespace(cls) ) or - exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl | + exists(TopLevel top, Stmt stmt, NameBinding nameDecl | stmt = top.getBody().getAStmt() and not isPrivateToLocalScope(nameDecl) and nameDecl.getDeclaration() = stmt and @@ -214,11 +214,11 @@ predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { predicate valueStep(NameBindingNode node1, NameBindingNode node2) { exists(PotentialLocalNameAccess access | - access.isDeclarationSite() and + access.isBindingSite() and node1.isIdentifier(access) and node2.isLocalName(access.getLocalName()) or - not access.isDeclarationSite() and + not access.isBindingSite() and node1.isLocalName(access.getLocalName()) and node2.isIdentifier(access) ) @@ -327,7 +327,7 @@ private predicate derivedStoreReadStep(NameBindingNode node1, NameBindingNode no /** Holds if the member represented by `node` can be inherited. */ pragma[nomagic] private predicate isInheritableMemberNode(NameBindingNode node) { - exists(NameDeclaration decl | + exists(NameBinding decl | node.isIdentifier(decl) and isInheritableMember(decl.getDeclaration()) ) @@ -414,29 +414,29 @@ private predicate sameName(Identifier node1, Identifier node2) { * will be passed through. */ pragma[nomagic] -predicate isTrivialNameAlias(NameDeclaration decl) { +predicate isTrivialNameAlias(NameBinding decl) { exists(ImportDeclaration imprt | decl = getImportBindingIdentifier(imprt) and sameName(decl, getIdentifierFromRef(imprt.getImportedExpr())) ) } -private module TrackNameDeclarationInput implements TrackInputSig { +private module TrackNameBindingInput implements TrackInputSig { predicate shouldTrack(NameBindingNode node) { - exists(NameDeclaration decl | + exists(NameBinding decl | node.isIdentifier(decl) and not isTrivialNameAlias(decl) ) } } -private module TrackNameDeclaration = Track; +private module TrackNameBinding = Track; /** Gets a name-binding node that may refer to the given declaration. */ -NameBindingNode trackNameDeclaration(NameDeclaration decl) { +NameBindingNode trackNameBinding(NameBinding decl) { exists(NameBindingNode start | start.isIdentifier(decl) and - result = TrackNameDeclaration::track(start) + result = TrackNameBinding::track(start) ) } @@ -490,7 +490,7 @@ module DebugGraph { */ private module FolderHeuristic { private predicate topLevelNameDef(File file, string name, NameBindingNode node) { - exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl | + exists(TopLevel top, Stmt stmt, NameBinding nameDecl | top.getFile() = file and stmt = top.getBody().getAStmt() and not isPrivateToLocalScope(nameDecl) and @@ -583,10 +583,10 @@ private module FolderHeuristic { * or as a static member. */ private predicate unqualifiedMemberAccessCand( - PotentialLocalNameAccess access, boolean instanceAccess, NameDeclaration target, + PotentialLocalNameAccess access, boolean instanceAccess, NameBinding target, ClassLikeDeclaration accessingClass ) { - not access instanceof NameDeclaration and + not access instanceof NameBinding and ( // Resolved by local scoping exists(LocalName local | @@ -628,7 +628,7 @@ private int unqualifiedMemberAccessDepth(PotentialLocalNameAccess access) { * `instanceAccess` indicates if it is an instance member or static member. */ predicate unqualifiedMemberAccess( - PotentialLocalNameAccess access, boolean instanceAccess, NameDeclaration target, + PotentialLocalNameAccess access, boolean instanceAccess, NameBinding target, ClassLikeDeclaration accessingClass ) { unqualifiedMemberAccessCand(access, instanceAccess, target, accessingClass) and @@ -640,7 +640,7 @@ predicate unqualifiedMemberAccess( */ class UnqualifiedMemberAccess extends Identifier { private boolean instanceAccess; - private NameDeclaration target; + private NameBinding target; private ClassLikeDeclaration accessingClass; UnqualifiedMemberAccess() { @@ -648,7 +648,7 @@ class UnqualifiedMemberAccess extends Identifier { } /** Gets the name declaration of the member being accessed. */ - NameDeclaration getTarget() { result = target } + NameBinding getTarget() { result = target } /** Gets the enclosing class whose (possibly inherited) member is being accessed. */ ClassLikeDeclaration getAccessingClass() { result = accessingClass } @@ -658,11 +658,11 @@ class UnqualifiedMemberAccess extends Identifier { } /** Gets the declaration being accessed by `access`, as determined by static name binding. */ -NameDeclaration getStaticBindingTarget(Identifier access) { +NameBinding 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 + trackNameBinding(result).asIdentifier() = access } diff --git a/unified/ql/lib/ide-contextual-queries/definitions.ql b/unified/ql/lib/ide-contextual-queries/definitions.ql index f72c91bcd9dd..0d2477a83b57 100644 --- a/unified/ql/lib/ide-contextual-queries/definitions.ql +++ b/unified/ql/lib/ide-contextual-queries/definitions.ql @@ -13,7 +13,7 @@ import unified external string selectedSourceFile(); -from Identifier reference, NameDeclaration definition, string kind +from Identifier reference, NameBinding definition, string kind where definitionOf(reference, definition, kind) and reference.getLocation().getFile() = getFileBySourceArchiveName(selectedSourceFile()) diff --git a/unified/ql/lib/utils/test/TestUtils.qll b/unified/ql/lib/utils/test/TestUtils.qll index 15155c44758a..9665121e79be 100644 --- a/unified/ql/lib/utils/test/TestUtils.qll +++ b/unified/ql/lib/utils/test/TestUtils.qll @@ -9,7 +9,7 @@ private string deriveClassName(ClassLikeDeclaration cls) { result = deriveClassName(cls.getEnclosingClass()) + "." + cls.getName() } -private string defaultName(NameDeclaration decl) { +private string defaultName(NameBinding decl) { exists(ClassLikeDeclaration cls | decl.getDeclaration() = cls.getAMember() and result = deriveClassName(cls) + "." + decl.getName() @@ -19,11 +19,11 @@ private string defaultName(NameDeclaration decl) { result = decl.getName() } -private predicate declAt(NameDeclaration v, string filepath, int line) { +private predicate declAt(NameBinding v, string filepath, int line) { v.getLocation().hasLocationInfo(filepath, line, _, _, _) } -predicate nameDeclaration(NameDeclaration v, string alias) { +predicate nameBinding(NameBinding v, string alias) { exists(string filepath, int line | declAt(v, filepath, line) | keyValueCommentAt(filepath, line, "name", alias) or diff --git a/unified/ql/test/library-tests/definitions/test.ql b/unified/ql/test/library-tests/definitions/test.ql index 361a9fd6a03c..9479e756bc94 100644 --- a/unified/ql/test/library-tests/definitions/test.ql +++ b/unified/ql/test/library-tests/definitions/test.ql @@ -7,11 +7,11 @@ module DefinitionsTest implements TestSig { string getARelevantTag() { result = "definition" } predicate hasActualResult(Location location, string element, string tag, string value) { - exists(Identifier reference, NameDeclaration definition | + exists(Identifier reference, NameBinding definition | definitionOf(reference, definition, "name") and location = reference.getLocation() and element = reference.toString() and - nameDeclaration(definition, value) and + nameBinding(definition, value) and tag = "definition" ) } diff --git a/unified/ql/test/library-tests/local-name-binding/test.ql b/unified/ql/test/library-tests/local-name-binding/test.ql index e430f16ba48d..d042daacb2e5 100644 --- a/unified/ql/test/library-tests/local-name-binding/test.ql +++ b/unified/ql/test/library-tests/local-name-binding/test.ql @@ -20,8 +20,7 @@ module VariableAccessTest implements TestSig { } private PotentialLocalNameAccess getUniqueDeclarationSite(LocalName name) { - result = - unique(PotentialLocalNameAccess ac | ac.isDeclarationSite() and ac.getLocalName() = name) + result = unique(PotentialLocalNameAccess ac | ac.isBindingSite() and ac.getLocalName() = name) } predicate hasActualResult(Location location, string element, string tag, string value) { 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 e6dd7e64f6b3..87bcec48f4f5 100644 --- a/unified/ql/test/library-tests/static-name-binding/test.ql +++ b/unified/ql/test/library-tests/static-name-binding/test.ql @@ -7,12 +7,12 @@ module StaticDeclAccess implements TestSig { string getARelevantTag() { result = "access" } predicate hasActualResult(Location location, string element, string tag, string value) { - exists(NameDeclaration decl, Identifier access | + exists(NameBinding decl, Identifier access | decl = getStaticBindingTarget(access) and - not access instanceof NameDeclaration and + not access instanceof NameBinding and location = access.getLocation() and element = access.toString() and - nameDeclaration(decl, value) and + nameBinding(decl, value) and tag = "access" ) } From 0c8cf6db89a67bb2ce9d01c2b42ad15396798072 Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 9 Sep 2026 22:49:35 +0200 Subject: [PATCH 06/23] unified: Expose UnqualifiedMemberAccess --- .../unified/internal/StaticNameBinding.qll | 34 ++++++++++--------- unified/ql/lib/unified.qll | 1 + 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index d1a6e96d32e8..fade9ba6241c 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -635,26 +635,28 @@ predicate unqualifiedMemberAccess( accessingClass.getDepth() = unqualifiedMemberAccessDepth(access) } -/** - * A name node appearing in an unqualified position, referring to a member of an enclosing class. - */ -class UnqualifiedMemberAccess extends Identifier { - private boolean instanceAccess; - private NameBinding target; - private ClassLikeDeclaration accessingClass; +module Public { + /** + * A name node appearing in an unqualified position, referring to a member of an enclosing class. + */ + class UnqualifiedMemberAccess extends Identifier { + private boolean instanceAccess; + private NameBinding target; + private ClassLikeDeclaration accessingClass; - UnqualifiedMemberAccess() { - unqualifiedMemberAccess(this, instanceAccess, target, accessingClass) - } + UnqualifiedMemberAccess() { + unqualifiedMemberAccess(this, instanceAccess, target, accessingClass) + } - /** Gets the name declaration of the member being accessed. */ - NameBinding getTarget() { result = target } + /** Gets the name declaration of the member being accessed. */ + NameBinding getTarget() { result = target } - /** Gets the enclosing class whose (possibly inherited) member is being accessed. */ - ClassLikeDeclaration getAccessingClass() { result = accessingClass } + /** 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 } + /** 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. */ diff --git a/unified/ql/lib/unified.qll b/unified/ql/lib/unified.qll index 033d00aa6def..8c15fee5adbb 100644 --- a/unified/ql/lib/unified.qll +++ b/unified/ql/lib/unified.qll @@ -8,3 +8,4 @@ import codeql.unified.internal.Ast::UnifiedFinal import codeql.unified.internal.AstExtra::Public import codeql.unified.internal.ControlFlowGraph import codeql.unified.internal.LocalNameBinding::Public +import codeql.unified.internal.StaticNameBinding::Public From 51f771f3c5375f32e35e364c1e6c791d62c9d132 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 10 Sep 2026 01:27:01 +0200 Subject: [PATCH 07/23] unified: Add some more predicates in LocalVariable --- .../ql/lib/codeql/unified/internal/LocalNameBinding.qll | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 6ea58b5cb878..308b76bb8790 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -395,6 +395,14 @@ module Public { decl instanceof SwitchCase ) } + + /** Gets the callable containing the declaration of this local variable. */ + Callable getDeclaringCallable() { result = this.getABinding().getEnclosingCallable() } + + /** Holds if this local variable is captured, that is, it is accessed from another callable than the one declaring it. */ + predicate isCaptured() { + this.getAnAccess().getEnclosingCallable() != this.getDeclaringCallable() + } } /** An access to a locally-declared name. */ From b10e121ca89ff4d3e1b14c992cfb3c538613ca31 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 10 Sep 2026 09:01:38 +0200 Subject: [PATCH 08/23] unified: Declare implicit 'self' --- .../unified/internal/LocalNameBinding.qll | 30 +++++++++++++++---- .../unified/internal/NameBindingPlugin.qll | 3 ++ .../internal/NameBindingPluginSwift.qll | 6 ++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 308b76bb8790..e881adfe7d62 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -321,12 +321,16 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig Date: Thu, 10 Sep 2026 09:34:25 +0200 Subject: [PATCH 09/23] unified: Wrap access-lookup in a reusable module This moves a handful of predicates into a module, parameterised by the 'accessCand' predicate. There are no other changes to predicates, they are just moved. This is to allow further variable-lookups at a later stage. --- .../codeql/namebinding/LocalNameBinding.qll | 126 ++++++++++-------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll index 4a9c5b61db92..a652d16f25a1 100644 --- a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll +++ b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll @@ -337,54 +337,89 @@ module LocalNameBinding ) } - private predicate accessCandInLookupScope(AstNode n, string name, Scope lookup) { - accessCand(n, name) and - ( - lookupStartsAt(n, lookup) - or - not lookupStartsAt(n, _) and - lookup = getEnclosingScope(n) - ) - } - - pragma[nomagic] - private predicate lookupInScope(string name, Scope lookup, Scope scope) { - accessCandInLookupScope(_, name, lookup) and - scope = lookup - or - exists(Scope mid | - lookupInScope(name, lookup, mid) and - not declInScope(name, mid) and - not isTopScope(mid) and - scope = getEnclosingScope(mid) - ) - } - private predicate declInScope(string name, AstNode scope) { declInScope(_, name, scope) or implicitDeclInScope(name, scope) } + signature predicate accessCandSig(AstNode n, string name); + /** - * Holds if `name`, when resolved from `lookup`, may resolve to one of the uncertain members of `scope`. + * Allows resolution of access candidates. + * + * This is instantiated once by the local name binding library itself in order to populate `LocalAccces`. + * It can be instantiated further by the client, to resolve additional lookups at a later evaluation stage. */ - pragma[nomagic] - private predicate lookupInUncertainScope(string name, Scope lookup, Scope scope) { - lookupInScope(name, lookup, scope) and - uncertainScope(scope) and - not declInScope(name, scope) + module ResolveAccesses { + private predicate accessCandInLookupScope(AstNode n, string name, Scope lookup) { + accessCandInput(n, name) and + ( + lookupStartsAt(n, lookup) + or + not lookupStartsAt(n, _) and + lookup = getEnclosingScope(n) + ) + } + + pragma[nomagic] + private predicate lookupInScope(string name, Scope lookup, Scope scope) { + accessCandInLookupScope(_, name, lookup) and + scope = lookup + or + exists(Scope mid | + lookupInScope(name, lookup, mid) and + not declInScope(name, mid) and + not isTopScope(mid) and + scope = getEnclosingScope(mid) + ) + } + + pragma[nomagic] + private predicate resolveInScope(string name, Scope lookup, Local l) { + exists(Scope scope | lookupInScope(name, lookup, scope) | + l = TExplicitLocal(_, name, scope) or + l = TImplicitLocal(name, scope) + ) + } + + predicate access(AstNode access, Local l) { + exists(Scope lookup, string name | + accessCandInLookupScope(access, name, lookup) and + resolveInScope(name, lookup, l) + ) + } + + /** + * Holds if `name`, when resolved from `lookup`, may resolve to one of the uncertain members of `scope`. + */ + pragma[nomagic] + private predicate lookupInUncertainScope(string name, Scope lookup, Scope scope) { + lookupInScope(name, lookup, scope) and + uncertainScope(scope) and + not declInScope(name, scope) + } + + /** + * Gets an uncertain scope in which the `accessCand` pair may resolve. + */ + AstNode getAnUncertainScope(AstNode access, string name) { + exists(Scope lookup | + accessCandInLookupScope(access, name, lookup) and + lookupInUncertainScope(name, lookup, result) + ) + } } - /** - * Gets an uncertain scope in which the `accessCand` pair may resolve. - */ - AstNode getAnUncertainScope(AstNode access, string name) { - exists(Scope lookup | - accessCandInLookupScope(access, name, lookup) and - lookupInUncertainScope(name, lookup, result) - ) + private module DefaultAccesses = ResolveAccesses; + + cached + predicate access(AstNode access, Local l) { + CachedStage::ref() and + DefaultAccesses::access(access, l) } + predicate getAnUncertainScope = DefaultAccesses::getAnUncertainScope/2; + cached private newtype TLocal = TExplicitLocal(AstNode definingNode, string name, AstNode scope) { @@ -449,23 +484,6 @@ module LocalNameBinding override Location getLocation() { result = scope.getLocation() } } - pragma[nomagic] - private predicate resolveInScope(string name, Scope lookup, Local l) { - exists(Scope scope | lookupInScope(name, lookup, scope) | - l = TExplicitLocal(_, name, scope) or - l = TImplicitLocal(name, scope) - ) - } - - cached - private predicate access(AstNode access, Local l) { - CachedStage::ref() and - exists(Scope lookup, string name | - accessCandInLookupScope(access, name, lookup) and - resolveInScope(name, lookup, l) - ) - } - /** A local access. */ final class LocalAccess extends AstNodeFinal { private Local l; From fcbf8169696bb897aa952579c59ee0333e764d14 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 10 Sep 2026 09:47:16 +0200 Subject: [PATCH 10/23] unified: Resolve the implicit 'self' reference to a LocalVariable --- .../unified/internal/StaticNameBinding.qll | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index fade9ba6241c..044c5d421d60 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -656,6 +656,11 @@ module Public { /** Holds if this is an instance access on the accessing class. */ predicate isInstanceAccess() { instanceAccess = true } + + /** Gets the local variable implicitly referenced as the base of this access. */ + LocalVariable getImplicitQualifierVariable() { + ResolveImplicitReceiverAccess::access(this, result) + } } } @@ -668,3 +673,24 @@ NameBinding getStaticBindingTarget(Identifier access) { not access instanceof UnqualifiedMemberAccess and trackNameBinding(result).asIdentifier() = access } + +/** + * Gets the name of the implicit receiver parameter in scope at `callable` (possibly declared by an outer callable). + * + * Note that we only propagate the name, not the LocalVariable, since capture-declarations and Swift's `guard let self` statements + * may re-introduce a new binding for `self`, which becomes the one referenced by subsequent unqualified member accesses. + */ +private string getEnclosingReceiverParameterName(Callable callable) { + result = any(NameBindingPlugin p).getImplicitReceiverParameterName(callable) + or + not exists(any(NameBindingPlugin p).getImplicitReceiverParameterName(callable)) and + result = getEnclosingReceiverParameterName(callable.getEnclosingCallable()) +} + +/** Holds if `access` contains a reference to the implicit receiver parameter `name`. */ +private predicate implicitReceiverAccess(AstNode access, string name) { + name = getEnclosingReceiverParameterName(access.(UnqualifiedMemberAccess).getEnclosingCallable()) +} + +private module ResolveImplicitReceiverAccess = + LocalNameBindingOutput::ResolveAccesses; From aca4c931176cdac99bff876972e6767f7615efab Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 09:05:22 +0200 Subject: [PATCH 11/23] unified: Add local name binding test --- unified/ql/lib/utils/test/CommentUtil.qll | 2 +- .../local-name-binding/class_scope.swift | 8 +++--- .../library-tests/local-name-binding/test.ql | 27 ++++++++++++++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/unified/ql/lib/utils/test/CommentUtil.qll b/unified/ql/lib/utils/test/CommentUtil.qll index bd6f887a4010..932be8671a86 100644 --- a/unified/ql/lib/utils/test/CommentUtil.qll +++ b/unified/ql/lib/utils/test/CommentUtil.qll @@ -12,7 +12,7 @@ predicate plainCommentAt(string filepath, int line, string text) { predicate keyValueCommentAt(string filepath, int line, string key, string value) { exists(string text, string regexp, string match | plainCommentAt(filepath, line, text) and - regexp = "(\\w+)=([\\w.0-9]+)" and + regexp = "([\\w.-]+)=([\\w.0-9]+)" and match = text.regexpFind(regexp, _, _) and key = match.regexpCapture(regexp, 1) and value = match.regexpCapture(regexp, 2) diff --git a/unified/ql/test/library-tests/local-name-binding/class_scope.swift b/unified/ql/test/library-tests/local-name-binding/class_scope.swift index 5e251b8e6a0d..5c20bc19cbc1 100644 --- a/unified/ql/test/library-tests/local-name-binding/class_scope.swift +++ b/unified/ql/test/library-tests/local-name-binding/class_scope.swift @@ -5,8 +5,8 @@ class A { let b: B = nil // $ access=A.B let c: C = nil // $ access=A.C } - func instance_before() { - print(instanceVar) // $ access=instanceVar + func instance_before() { // implicit-self=instance_before.self + print(instanceVar) // $ access=instanceVar implicit-qualifier=instance_before.self B(); // $ access=A.B let b: B = nil // $ access=A.B let c: C = nil // $ access=A.C @@ -25,8 +25,8 @@ class A { let c: C = nil // $ access=A.C } - func instance_after() { - print(instanceVar) // $ access=instanceVar + func instance_after() { // implicit-self=instance_after.self + print(instanceVar) // $ access=instanceVar implicit-qualifier=instance_after.self B(); // $ access=A.B let b: B = nil // $ access=A.B let c: C = nil // $ access=A.C diff --git a/unified/ql/test/library-tests/local-name-binding/test.ql b/unified/ql/test/library-tests/local-name-binding/test.ql index d042daacb2e5..122aadee6168 100644 --- a/unified/ql/test/library-tests/local-name-binding/test.ql +++ b/unified/ql/test/library-tests/local-name-binding/test.ql @@ -4,17 +4,27 @@ import utils.test.CommentUtil import codeql.unified.internal.LocalNameBinding module VariableAccessTest implements TestSig { - string getARelevantTag() { result = "access" } + string getARelevantTag() { result = ["access", "implicit-qualifier"] } additional predicate declAt(LocalName v, string filepath, int line) { v.getLocation().hasLocationInfo(filepath, line, _, _, _) } private predicate decl(LocalName v, string alias) { - exists(string filepath, int line | declAt(v, filepath, line) | - keyValueCommentAt(filepath, line, "name", alias) + exists(string filepath, int line, string tag | + declAt(v, filepath, line) and + if exists(v.getABinding()) + then + // explicit declarations must be annotated with 'name' + tag = "name" + else ( + // implicit declarations have their own tags + v.getName() = "self" and tag = "implicit-self" + ) + | + keyValueCommentAt(filepath, line, tag, alias) or - not keyValueCommentAt(filepath, line, "name", _) and + not keyValueCommentAt(filepath, line, tag, _) and alias = v.getName() ) } @@ -32,6 +42,15 @@ module VariableAccessTest implements TestSig { decl(v, value) and tag = "access" ) + or + exists(UnqualifiedMemberAccess access, LocalName v | + v = access.getImplicitQualifierVariable() and + location = access.getLocation() and + element = access.toString() and + decl(v, value) and + access.isInstanceAccess() and // For now, don't annotate receiver access in static methods. It technically exists, it's just not important yet. + tag = "implicit-qualifier" + ) } } From 7ded08c61450741c62bcb7cf7962b539102742a9 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 09:22:14 +0200 Subject: [PATCH 12/23] unified: Add tests and handle capture-declaration scoping --- .../unified/internal/LocalNameBinding.qll | 5 +++ .../local-name-binding/self_access.swift | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 unified/ql/test/library-tests/local-name-binding/self_access.swift diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index e881adfe7d62..82f9f876584e 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -168,6 +168,11 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig referring to .some() if it + // has not been GC'ed yet. Swift does not allow unqualified self access here. + + print(self) // $ access=weak.self + + // Unwrap the 'self' optional to get a strong reference. + guard let self else { return } // $ access=weak.self // name=guarded.self + + print(self) // $ access=guarded.self + print(instanceField) // $ access=instanceField implicit-qualifier=guarded.self + } + } +} From c4c4b976338fc5edd3cda40c1d480b903060fcf2 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 09:27:16 +0200 Subject: [PATCH 13/23] unified: Fix enclosing callable of capture declarations --- .../ql/lib/codeql/unified/internal/FacadeAst.qll | 16 +++++++++++++++- .../local-name-binding/self_access.swift | 4 ++-- .../library-tests/local-name-binding/test.ql | 10 +++++++++- .../library-tests/local-name-binding/test.swift | 4 ++-- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index 1cc6d6106e93..ad66ed6864b9 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -33,9 +33,23 @@ module Unified { ) } + private AstNode overrideEnclosingCallableParent() { + exists(FunctionExpr func | + // Capture declarations are evaluated as part of the outer context, and + // considered to be captured by the function expression. + this = func.getACaptureDeclaration() and + result = func.getParent() + ) + } + /** Gets the nearest callable containing this AST node. */ Callable getEnclosingCallable() { - exists(AstNode parent | parent = this.getParent() | + exists(AstNode parent | + parent = this.overrideEnclosingCallableParent() + or + not exists(this.overrideEnclosingCallableParent()) and + parent = this.getParent() + | result = parent or not parent instanceof Callable and diff --git a/unified/ql/test/library-tests/local-name-binding/self_access.swift b/unified/ql/test/library-tests/local-name-binding/self_access.swift index 7eed767e2385..795d7b8bac7a 100644 --- a/unified/ql/test/library-tests/local-name-binding/self_access.swift +++ b/unified/ql/test/library-tests/local-name-binding/self_access.swift @@ -10,14 +10,14 @@ class C { } func t3() { // implicit-self=t3.self - foo(123) { [self] in // name=closure.self + foo(123) { [self] in // $ captured=closure.self // name=closure.self print(self) // $ access=closure.self print(instanceField) // $ access=instanceField implicit-qualifier=closure.self } } func t4() { // implicit-self=t4.self - foo(123) { [weak self] in // name=weak.self + foo(123) { [weak self] in // $ captured=weak.self // name=weak.self // Here, 'self' is an Option referring to .some() if it // has not been GC'ed yet. Swift does not allow unqualified self access here. diff --git a/unified/ql/test/library-tests/local-name-binding/test.ql b/unified/ql/test/library-tests/local-name-binding/test.ql index 122aadee6168..3fc1ce7b5155 100644 --- a/unified/ql/test/library-tests/local-name-binding/test.ql +++ b/unified/ql/test/library-tests/local-name-binding/test.ql @@ -4,7 +4,7 @@ import utils.test.CommentUtil import codeql.unified.internal.LocalNameBinding module VariableAccessTest implements TestSig { - string getARelevantTag() { result = ["access", "implicit-qualifier"] } + string getARelevantTag() { result = ["access", "implicit-qualifier", "captured"] } additional predicate declAt(LocalName v, string filepath, int line) { v.getLocation().hasLocationInfo(filepath, line, _, _, _) @@ -51,6 +51,14 @@ module VariableAccessTest implements TestSig { access.isInstanceAccess() and // For now, don't annotate receiver access in static methods. It technically exists, it's just not important yet. tag = "implicit-qualifier" ) + or + exists(LocalVariable v | + v.isCaptured() and + location = v.getLocation() and + element = v.toString() and + decl(v, value) and + tag = "captured" + ) } } diff --git a/unified/ql/test/library-tests/local-name-binding/test.swift b/unified/ql/test/library-tests/local-name-binding/test.swift index 23d71d8f6dcd..02558d808d4e 100644 --- a/unified/ql/test/library-tests/local-name-binding/test.swift +++ b/unified/ql/test/library-tests/local-name-binding/test.swift @@ -135,7 +135,7 @@ func t16() throws { // Closure captures func t17() { - let x = 1 // name=x1 + let x = 1 // $ captured=x1 // name=x1 let closure = { // name=closure1 print(x) // $ access=x1 } @@ -181,7 +181,7 @@ func t21() { // Nested functions func t22() { let x = 1 // name=x1 - func inner() { // name=inner1 + func inner() { // $ captured=inner1 // name=inner1 let x = 2 // name=x2 print(x) // $ access=x2 } From 4f47b891478d5fff82b842d322bb440f4840cff8 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 09:52:35 +0200 Subject: [PATCH 14/23] unified: Add some missing qldoc --- shared/namebinding/codeql/namebinding/LocalNameBinding.qll | 2 ++ unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll | 3 +++ unified/ql/lib/utils/test/TestUtils.qll | 1 + 3 files changed, 6 insertions(+) diff --git a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll index a652d16f25a1..2eaace736b1a 100644 --- a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll +++ b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll @@ -382,6 +382,7 @@ module LocalNameBinding ) } + /** Holds if `access` resolves to `l`. */ predicate access(AstNode access, Local l) { exists(Scope lookup, string name | accessCandInLookupScope(access, name, lookup) and @@ -412,6 +413,7 @@ module LocalNameBinding private module DefaultAccesses = ResolveAccesses; + /** Holds if `access` resolves to `l`. */ cached predicate access(AstNode access, Local l) { CachedStage::ref() and diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 82f9f876584e..7f146d6121d9 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -367,6 +367,7 @@ module Public { /** Gets the name of this local, as a string. */ string getName() { result = super.getName() } + /** Gets an access to this entity, through its lexically scoped name. */ LocalNameAccess getAnAccess() { result.getLocalName() = this } /** Gets a name binding that declares this local name. */ @@ -458,8 +459,10 @@ module Public { * ``` */ class PotentialLocalNameAccess extends IdentifierExpr { + /** Gets the representative for the local name being accessed. */ LocalName getLocalName() { result = this.(LocalNameBindingOutput::LocalAccess).getLocal() } + /** Gets the name being accessed. */ string getName() { result = this.getValue() } /** Holds if this is one of the binding sites for a name, such as the `x` in `let x = 123`. */ diff --git a/unified/ql/lib/utils/test/TestUtils.qll b/unified/ql/lib/utils/test/TestUtils.qll index 9665121e79be..36583b9a8a34 100644 --- a/unified/ql/lib/utils/test/TestUtils.qll +++ b/unified/ql/lib/utils/test/TestUtils.qll @@ -23,6 +23,7 @@ private predicate declAt(NameBinding v, string filepath, int line) { v.getLocation().hasLocationInfo(filepath, line, _, _, _) } +/** Holds if the name-binding `v` has been assigned the given `alias` by a comment in the test code. */ predicate nameBinding(NameBinding v, string alias) { exists(string filepath, int line | declAt(v, filepath, line) | keyValueCommentAt(filepath, line, "name", alias) From 92df6d26b781a9d38a3b3e18649381296b26894c Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 10:12:04 +0200 Subject: [PATCH 15/23] unified: Make access() private again This caused name clashes in other languages. --- shared/namebinding/codeql/namebinding/LocalNameBinding.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll index 2eaace736b1a..27d2dfb3bf10 100644 --- a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll +++ b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll @@ -415,7 +415,7 @@ module LocalNameBinding /** Holds if `access` resolves to `l`. */ cached - predicate access(AstNode access, Local l) { + private predicate access(AstNode access, Local l) { CachedStage::ref() and DefaultAccesses::access(access, l) } From cb264da0ffbcc52936c3b08b64e8f0ddb2bfed1b Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 10:16:51 +0200 Subject: [PATCH 16/23] unified: Add NameBinding.qll Now that StaticNameBinding also has a module called 'Public' we get conflicts when importing both. Adding a facade for importing both. --- .../codeql/unified/internal/AnalysisQuality.qll | 4 +--- .../lib/codeql/unified/internal/NameBinding.qll | 15 +++++++++++++++ unified/ql/lib/unified.qll | 3 +-- 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 unified/ql/lib/codeql/unified/internal/NameBinding.qll diff --git a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll index c2c6bf66f6d5..986238a60594 100644 --- a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll +++ b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll @@ -1,8 +1,6 @@ private import unified private import codeql.util.ReportStats -private import codeql.unified.internal.StaticNameBinding -private import codeql.unified.internal.LocalNameBinding -private import codeql.unified.internal.NameBindingPlugin +private import codeql.unified.internal.NameBinding /** Stats about name nodes that static name binding could resolve. */ module StaticNameResolutionStats implements EntityStatsSig { diff --git a/unified/ql/lib/codeql/unified/internal/NameBinding.qll b/unified/ql/lib/codeql/unified/internal/NameBinding.qll new file mode 100644 index 000000000000..a07142de1d5f --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/NameBinding.qll @@ -0,0 +1,15 @@ +/** + * Re-exports everything from the name-binding passes, while avoiding conflicts on 'Public'. + * + * TODO: move name binding passes into a subfolder. + */ + +import StaticNameBinding +import LocalNameBinding +import NameBindingPlugin + +module Public { + // re-declare here to avoid conflicting import + import StaticNameBinding::Public + import LocalNameBinding::Public +} diff --git a/unified/ql/lib/unified.qll b/unified/ql/lib/unified.qll index 8c15fee5adbb..500de80b64f3 100644 --- a/unified/ql/lib/unified.qll +++ b/unified/ql/lib/unified.qll @@ -7,5 +7,4 @@ import codeql.files.FileSystem import codeql.unified.internal.Ast::UnifiedFinal import codeql.unified.internal.AstExtra::Public import codeql.unified.internal.ControlFlowGraph -import codeql.unified.internal.LocalNameBinding::Public -import codeql.unified.internal.StaticNameBinding::Public +import codeql.unified.internal.NameBinding::Public From 57c071f4a2fd7a85762226aa86252915ff853e28 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 10:19:30 +0200 Subject: [PATCH 17/23] unified: Prefer importing the NameBinding.qll facade --- unified/ql/lib/codeql/Definitions.qll | 2 +- .../codeql/unified/internal/dev/debugLocalNameBindingGraph.ql | 2 +- .../codeql/unified/internal/dev/debugStaticNameBindingGraph.ql | 2 +- unified/ql/lib/utils/test/TestUtils.qll | 2 +- unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql | 2 +- unified/ql/src/diagnostic/StaticNameResolution.ql | 2 +- unified/ql/test/library-tests/local-name-binding/test.ql | 2 +- unified/ql/test/library-tests/static-name-binding/test.ql | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/unified/ql/lib/codeql/Definitions.qll b/unified/ql/lib/codeql/Definitions.qll index 941d00b425cc..1f6a1bbc9e81 100644 --- a/unified/ql/lib/codeql/Definitions.qll +++ b/unified/ql/lib/codeql/Definitions.qll @@ -3,7 +3,7 @@ */ private import unified -private import codeql.unified.internal.StaticNameBinding +private import codeql.unified.internal.NameBinding /** * Holds if `reference` refers to `definition`. diff --git a/unified/ql/lib/codeql/unified/internal/dev/debugLocalNameBindingGraph.ql b/unified/ql/lib/codeql/unified/internal/dev/debugLocalNameBindingGraph.ql index 71887f476f16..4c0db3a049df 100644 --- a/unified/ql/lib/codeql/unified/internal/dev/debugLocalNameBindingGraph.ql +++ b/unified/ql/lib/codeql/unified/internal/dev/debugLocalNameBindingGraph.ql @@ -6,7 +6,7 @@ */ private import unified -private import codeql.unified.internal.LocalNameBinding +private import codeql.unified.internal.NameBinding /** * Holds if `node` should be shown in the graph. diff --git a/unified/ql/lib/codeql/unified/internal/dev/debugStaticNameBindingGraph.ql b/unified/ql/lib/codeql/unified/internal/dev/debugStaticNameBindingGraph.ql index f365d3915f89..e8c97ce41852 100644 --- a/unified/ql/lib/codeql/unified/internal/dev/debugStaticNameBindingGraph.ql +++ b/unified/ql/lib/codeql/unified/internal/dev/debugStaticNameBindingGraph.ql @@ -6,7 +6,7 @@ */ private import unified -private import codeql.unified.internal.StaticNameBinding +private import codeql.unified.internal.NameBinding /** * Holds if `node` should be shown in the graph. diff --git a/unified/ql/lib/utils/test/TestUtils.qll b/unified/ql/lib/utils/test/TestUtils.qll index 36583b9a8a34..4a232e984e78 100644 --- a/unified/ql/lib/utils/test/TestUtils.qll +++ b/unified/ql/lib/utils/test/TestUtils.qll @@ -1,6 +1,6 @@ private import unified private import CommentUtil -private import codeql.unified.internal.StaticNameBinding +private import codeql.unified.internal.NameBinding private string deriveClassName(ClassLikeDeclaration cls) { not exists(cls.getEnclosingClass()) and diff --git a/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql b/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql index 6ac4cec641bd..ca226bedc005 100644 --- a/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql +++ b/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql @@ -9,7 +9,7 @@ */ import unified -import codeql.unified.internal.StaticNameBinding +import codeql.unified.internal.NameBinding import codeql.unified.internal.NameBindingPlugin import codeql.unified.internal.AnalysisQuality diff --git a/unified/ql/src/diagnostic/StaticNameResolution.ql b/unified/ql/src/diagnostic/StaticNameResolution.ql index 501c39126e54..b15516a3b7b8 100644 --- a/unified/ql/src/diagnostic/StaticNameResolution.ql +++ b/unified/ql/src/diagnostic/StaticNameResolution.ql @@ -9,7 +9,7 @@ */ import unified -import codeql.unified.internal.StaticNameBinding +import codeql.unified.internal.NameBinding import codeql.unified.internal.AnalysisQuality from StaticNameResolutionStats::Candidate c, NameBindingNode target diff --git a/unified/ql/test/library-tests/local-name-binding/test.ql b/unified/ql/test/library-tests/local-name-binding/test.ql index 3fc1ce7b5155..974322851944 100644 --- a/unified/ql/test/library-tests/local-name-binding/test.ql +++ b/unified/ql/test/library-tests/local-name-binding/test.ql @@ -1,7 +1,7 @@ import unified import utils.test.InlineExpectationsTest import utils.test.CommentUtil -import codeql.unified.internal.LocalNameBinding +import codeql.unified.internal.NameBinding module VariableAccessTest implements TestSig { string getARelevantTag() { result = ["access", "implicit-qualifier", "captured"] } 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 87bcec48f4f5..c39202fe5a41 100644 --- a/unified/ql/test/library-tests/static-name-binding/test.ql +++ b/unified/ql/test/library-tests/static-name-binding/test.ql @@ -1,7 +1,7 @@ import unified import utils.test.InlineExpectationsTest import utils.test.TestUtils -import codeql.unified.internal.StaticNameBinding +import codeql.unified.internal.NameBinding module StaticDeclAccess implements TestSig { string getARelevantTag() { result = "access" } From 1229b226a534e1b20a333b90faa02ac6930ceb4b Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 10:55:04 +0200 Subject: [PATCH 18/23] Fix typo in documentation comment for ResolveAccesses Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- shared/namebinding/codeql/namebinding/LocalNameBinding.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll index 27d2dfb3bf10..c0c33fe1aa7c 100644 --- a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll +++ b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll @@ -347,7 +347,7 @@ module LocalNameBinding /** * Allows resolution of access candidates. * - * This is instantiated once by the local name binding library itself in order to populate `LocalAccces`. + * This is instantiated once by the local name binding library itself in order to populate `LocalAccess`. * It can be instantiated further by the client, to resolve additional lookups at a later evaluation stage. */ module ResolveAccesses { From 7e365ed58b4104c39bae3bb4be0bcad34b41d450 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 11:42:00 +0200 Subject: [PATCH 19/23] unified: Elaborate on getEnclosingCallable --- unified/ql/lib/codeql/unified/internal/FacadeAst.qll | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index ad66ed6864b9..1968133e76bd 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -42,7 +42,16 @@ module Unified { ) } - /** Gets the nearest callable containing this AST node. */ + /** + * Gets the nearest callable containing this AST node. + * + * If this node is itself a callable, this gets the outer callable, not the node itself. + * + * Note that the `TopLevel` is callable, so all nodes other than the `TopLevel` itself has an enclosing callable. + * + * In some cases this predicate skips overs the syntactically-enclosing callable in order to get the callable in which + * the AST is actually evaluated (such as for capture declarations in a function expression). + */ Callable getEnclosingCallable() { exists(AstNode parent | parent = this.overrideEnclosingCallableParent() From e7772a22a60cac3a573c6910811be90d237f554d Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 11:45:33 +0200 Subject: [PATCH 20/23] unified: More typo fixes --- unified/ql/lib/codeql/unified/internal/FacadeAst.qll | 2 +- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index 1968133e76bd..f5ad7e6b29a5 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -23,7 +23,7 @@ module Unified { ) } - /** Gets the nearest enclosing class declaration. */ + /** Gets the nearest enclosing class declaration, if any. */ ClassLikeDeclaration getEnclosingClass() { exists(AstNode parent | parent = this.getParent() | result = parent diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 044c5d421d60..230608b682f8 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -648,7 +648,7 @@ module Public { unqualifiedMemberAccess(this, instanceAccess, target, accessingClass) } - /** Gets the name declaration of the member being accessed. */ + /** Gets the name binding of the member being accessed. */ NameBinding getTarget() { result = target } /** Gets the enclosing class whose (possibly inherited) member is being accessed. */ From f730e9fa0f0dc4b36a8e7c8161cb483d85584681 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 12:54:18 +0200 Subject: [PATCH 21/23] unified: Join on name and scope simultaneously --- shared/namebinding/codeql/namebinding/LocalNameBinding.qll | 4 ++++ unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll index c0c33fe1aa7c..2dd12bbc8abc 100644 --- a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll +++ b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll @@ -484,6 +484,10 @@ module LocalNameBinding override string getName() { result = name } override Location getLocation() { result = scope.getLocation() } + + /** Holds if this variable has the given name and scope. */ + pragma[nomagic] + predicate hasNameAndScope(string name_, AstNode scope_) { name = name_ and scope = scope_ } } /** A local access. */ diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 7f146d6121d9..a944db724c5a 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -326,6 +326,7 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig Date: Fri, 11 Sep 2026 13:13:10 +0200 Subject: [PATCH 22/23] unified: Handle implicit locals in AnalysisQuality.qll --- .../ql/lib/codeql/unified/internal/AnalysisQuality.qll | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll index 986238a60594..0b0dea71ee7e 100644 --- a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll +++ b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll @@ -51,6 +51,14 @@ module StaticNameResolutionStats implements EntityStatsSig { or result.isModuleScopeNode(_) and result.(NamespaceNode).ref().isIdentifier(this) + or + // Resolving to an implicitly-declared local such as "self" should count as + // as a successfully resolved name + exists(LocalName implicitLocal | + implicitLocal = this.(LocalNameAccess).getLocalName() and + not exists(implicitLocal.getABinding()) and + result.isLocalName(implicitLocal) + ) ) } From d2070cf1cf87156f749bfc6f05415be7d1a93213 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 11 Sep 2026 13:13:37 +0200 Subject: [PATCH 23/23] unified: Remove superfluous parens --- .../unified/internal/AnalysisQuality.qll | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll index 0b0dea71ee7e..d5eb5071176e 100644 --- a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll +++ b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll @@ -46,19 +46,17 @@ module StaticNameResolutionStats implements EntityStatsSig { } NameBindingNode getTarget() { - ( - result.asIdentifier() = getStaticBindingTarget(this) - or - result.isModuleScopeNode(_) and - result.(NamespaceNode).ref().isIdentifier(this) - or - // Resolving to an implicitly-declared local such as "self" should count as - // as a successfully resolved name - exists(LocalName implicitLocal | - implicitLocal = this.(LocalNameAccess).getLocalName() and - not exists(implicitLocal.getABinding()) and - result.isLocalName(implicitLocal) - ) + result.asIdentifier() = getStaticBindingTarget(this) + or + result.isModuleScopeNode(_) and + result.(NamespaceNode).ref().isIdentifier(this) + or + // Resolving to an implicitly-declared local such as "self" should count as + // as a successfully resolved name + exists(LocalName implicitLocal | + implicitLocal = this.(LocalNameAccess).getLocalName() and + not exists(implicitLocal.getABinding()) and + result.isLocalName(implicitLocal) ) }