From 360aec3a0249d8c2260db934b14cfec6d4154220 Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 11 Aug 2026 10:59:33 +0200 Subject: [PATCH 01/13] unified: Add meta and diagmetric queries for static name binding --- shared/util/codeql/util/ReportStats.qll | 37 +++++++++++ .../unified/internal/AnalysisQuality.qll | 61 +++++++++++++++++++ .../ql/src/diagnostic/ExtractorInformation.ql | 24 ++++++++ .../FilesCoveredByModuleManifest.ql | 18 ++++++ .../ql/src/diagnostic/StaticNameResolution.ql | 17 ++++++ 5 files changed, 157 insertions(+) create mode 100644 unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll create mode 100644 unified/ql/src/diagnostic/ExtractorInformation.ql create mode 100644 unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql create mode 100644 unified/ql/src/diagnostic/StaticNameResolution.ql diff --git a/shared/util/codeql/util/ReportStats.qll b/shared/util/codeql/util/ReportStats.qll index 947eff548e75..10157d95c006 100644 --- a/shared/util/codeql/util/ReportStats.qll +++ b/shared/util/codeql/util/ReportStats.qll @@ -29,4 +29,41 @@ module ReportStats { value = Stats::getNumberOfOk() * 100.0 / (Stats::getNumberOfOk() + Stats::getNumberOfNotOk()) and key = "Percentage of " + Stats::getOkText() } + + predicate keyValuePair(string key, float value) { + numberOfOk(key, value) or + numberOfNotOk(key, value) or + percentageOfOk(key, value) + } +} + +/** + * Stats where each Ok/NotOk occurrence has an associated entity. + */ +signature module EntityStatsSig { + class Candidate { + predicate isOk(); + } + + string getOkText(); + + string getNotOkText(); +} + +module EntityReportStats { + private import Input + + private module StatsInput implements StatsSig { + int getNumberOfOk() { result = count(Candidate c | c.isOk()) } + + int getNumberOfNotOk() { result = count(Candidate c | not c.isOk()) } + + import Input + } + + import StatsInput + + private module ScalarReport = ReportStats; + + import ScalarReport } diff --git a/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll new file mode 100644 index 000000000000..a10257ed8326 --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll @@ -0,0 +1,61 @@ +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 + +/** Stats about identifiers that static name binding could resolve. */ +module StaticNameResolutionStats implements EntityStatsSig { + class Candidate extends Identifier { + Candidate() { + this = getIdentifierFromRef(_) 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() { + ( + exists(NameDeclaration decl | + result.isIdentifier(decl) and + trackNameDeclaration(decl).isIdentifier(this) + ) + or + result.isModuleScopeNode(_) and + result.(NamespaceNode).ref().isIdentifier(this) + ) and + // Do not consider a type extension to be a valid target + // TODO: Fix in the AST mapping: type extensions should reference their type, not declare it + not exists(ClassLikeDeclaration cls | + cls.hasModifier("extension") and + result.isIdentifier(cls.getName()) + ) + } + + predicate isOk() { exists(this.getTarget()) } + } + + string getOkText() { result = "statically resolvable names" } + + string getNotOkText() { result = "statically unresolvable names" } +} + +module StaticNameResolutionStatsReport = EntityReportStats; + +/** Stats about which files are covered by a module manifest. */ +module FilesCoveredByModuleManifestStats implements EntityStatsSig { + class Candidate extends File { + Candidate() { this.getExtension() = "swift" } + + ModuleScopeRepr getAModule() { result.getAnIncludedFile() = this } + + predicate isOk() { exists(this.getAModule()) } + } + + string getOkText() { result = "files covered by a module manifest" } + + string getNotOkText() { result = "files not covered by any module manifest" } +} + +module FilesCoveredByModuleManifestStatsReport = + EntityReportStats; diff --git a/unified/ql/src/diagnostic/ExtractorInformation.ql b/unified/ql/src/diagnostic/ExtractorInformation.ql new file mode 100644 index 000000000000..37469504b617 --- /dev/null +++ b/unified/ql/src/diagnostic/ExtractorInformation.ql @@ -0,0 +1,24 @@ +/** + * @name Unified extractor/analysis information + * @description Information about the extraction and analysis for a database + * @kind metric + * @tags summary telemetry + * @id unified/telemetry/extraction-information + */ + +private import unified +private import codeql.unified.internal.AnalysisQuality + +from string key, float value +where + ( + StaticNameResolutionStatsReport::keyValuePair(key, value) or + FilesCoveredByModuleManifestStatsReport::keyValuePair(key, value) + ) and + /* Infinity */ + value != 1.0 / 0.0 and + /* -Infinity */ + value != -1.0 / 0.0 and + /* NaN */ + value != 0.0 / 0.0 +select key, value diff --git a/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql b/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql new file mode 100644 index 000000000000..b426b6c06786 --- /dev/null +++ b/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql @@ -0,0 +1,18 @@ +/** + * @name Files covered by module manifest + * @description Files that are included from a module manifest + * @kind problem + * @problem.severity recommendation + * @id unified/diagnostic/files-covered-by-module-manifest + * @tags meta + * @precision very-low + */ + +import unified +import codeql.unified.internal.StaticNameBinding +import codeql.unified.internal.NameBindingPlugin +import codeql.unified.internal.AnalysisQuality + +from FilesCoveredByModuleManifestStats::Candidate c, ModuleScopeRepr mod +where c.isOk() and mod = c.getAModule() +select c, "File included in $@", mod, mod.toString() diff --git a/unified/ql/src/diagnostic/StaticNameResolution.ql b/unified/ql/src/diagnostic/StaticNameResolution.ql new file mode 100644 index 000000000000..a49ba94791b6 --- /dev/null +++ b/unified/ql/src/diagnostic/StaticNameResolution.ql @@ -0,0 +1,17 @@ +/** + * @name Static name resolution + * @description Static name references that could be resolved to a target + * @kind problem + * @problem.severity recommendation + * @id unified/diagnostic/static-name-resolution + * @tags meta + * @precision very-low + */ + +import unified +import codeql.unified.internal.StaticNameBinding +import codeql.unified.internal.AnalysisQuality + +from StaticNameResolutionStats::Candidate c, NameBindingNode target +where target = c.getTarget() +select c, "Resolved to $@", target, target.toString() From 0a2b80a15fdfdb9f28a09ec0f974c3e2f51a329c Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 12 Aug 2026 11:45:03 +0200 Subject: [PATCH 02/13] unified: Add folder-based fallback heuristic --- .../unified/internal/StaticNameBinding.qll | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 907cc9f14010..ca9cca2fc61a 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -16,6 +16,7 @@ private newtype TNameBindingNode = n instanceof ClassLikeDeclaration } or TModuleScope(ModuleScopeRepr repr) or + TFolderScope(Folder folder) or TModuleRoot() /** @@ -39,6 +40,9 @@ class NameBindingNode extends TNameBindingNode { /** Holds if this represents the given module scope. */ predicate isModuleScopeNode(ModuleScopeRepr repr) { this = TModuleScope(repr) } + /** Holds if this represents the set of members that can be accessed unqualified within the given folder and subfolders. */ + predicate isFolderScope(Folder folder) { this = TFolderScope(folder) } + /** Holds if this represents the root namespace in which all named modules are members. */ predicate isModuleRoot() { this = TModuleRoot() } @@ -76,6 +80,8 @@ class NameBindingNode extends TNameBindingNode { this.isModuleScopeNode(repr) and result = "ModuleScope(" + repr + ")" ) or + exists(Folder folder | this.isFolderScope(folder) and result = "FolderScope(" + folder + ")") + or this.isModuleRoot() and result = "ModuleRoot" } @@ -173,6 +179,8 @@ predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { mod.hasImportableName(name) and node2.isModuleRoot() ) + or + FolderHeuristic::storeStep(node1, name, node2) } predicate valueStep(NameBindingNode node1, NameBindingNode node2) { @@ -224,6 +232,8 @@ predicate valueStep(NameBindingNode node1, NameBindingNode node2) { node1 = getNodeFromRef(p) and node2 = getNodeFromRef(p.getSubPattern()) ) + or + FolderHeuristic::valueStep(node1, node2) } private predicate isImportPrefix(Expr e) { @@ -403,3 +413,96 @@ module DebugGraph { ) } } + +/** + * Implements a folder-based heuristic to linking up top-level names + * between files that are not included in any module scope. + */ +private module FolderHeuristic { + private predicate topLevelNameDef(File file, string name, NameBindingNode node) { + exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl | + top.getFile() = file and + stmt = top.getBody().getAStmt() and + not stmt.(ClassLikeDeclaration).hasModifier("extension") and // TODO: target of type extensions should not be seen as a NameDeclaration + not isPrivateToLocalScope(nameDecl) and + nameDecl.getDeclaration() = stmt and + name = nameDecl.getName() and + node.isIdentifier(nameDecl) + ) + } + + private predicate uniqueTopLevelName(File file, string name) { + file = unique(File f | topLevelNameDef(f, name, _)) + } + + /** + * Holds if `file` has a one of the definitions of the given ambiguous name. + * + * A name is considered "ambiguous" if there is more than one file exporting it. + */ + private predicate ambiguousTopLevelName(File file, string name) { + topLevelNameDef(file, name, _) and + not uniqueTopLevelName(file, name) + } + + /** Holds if `folder` contains one or more definitions of the given ambiguous name */ + private predicate containsDef(Folder folder, string name) { + exists(File f | + ambiguousTopLevelName(f, name) and + folder = f.getParentContainer+() + ) + } + + /** + * Holds if `folder` has two or more subfolders containing a definition of `name`. + */ + private predicate hasConflictingDefs(Folder folder, string name) { + containsDef(folder, name) and + not exists(unique(Folder child | child = folder.getAFolder() and containsDef(child, name))) + } + + /** + * Holds if `folder` is an outermost folder containing exactly one definition of `name`. + * + * This means `folder` should act as the scope of that definition. + */ + private predicate isOutermostNonConflictingScope(Folder folder, string name) { + containsDef(folder, name) and + hasConflictingDefs(folder.getParentContainer(), name) and + not hasConflictingDefs(folder, name) + } + + /** + * Gets the scope into which a definition of `name` appearing in `folder` should target. + */ + private Folder getOutermostNonConflictingScope(Folder folder, string name) { + isOutermostNonConflictingScope(folder, name) and + result = folder + or + result = getOutermostNonConflictingScope(folder.getParentContainer(), name) and + not isOutermostNonConflictingScope(folder, name) and + containsDef(folder, name) // Prune to the subfolder actually containing the definition + } + + predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { + exists(File file | topLevelNameDef(file, name, node1) | + node2.isFolderScope(getOutermostNonConflictingScope(file.getParentContainer(), name)) + or + uniqueTopLevelName(file, name) and + node2.isFolderScope(any(Folder f | f.getRelativePath() = "")) + ) + } + + predicate valueStep(NameBindingNode node1, NameBindingNode node2) { + exists(TopLevel top | + node1.isFolderScope(top.getFile().getParentContainer()) and + node2.isLocalNamespace(top.getBody()) and + not top.getFile() = any(ModuleScopeRepr r).getAnIncludedFile() + ) + or + exists(Folder folder | + node1.isFolderScope(folder.getParentContainer()) and + node2.isFolderScope(folder) + ) + } +} From 468d392fd74b9304590f1f5e87dca2621e8b9272 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 13 Aug 2026 13:41:25 +0200 Subject: [PATCH 03/13] unified: Add test exercising folder-based heuristic --- .../not-a-package/Main/Drivers/Driver.swift | 4 ++++ .../static-name-binding/not-a-package/Main/Runner.swift | 5 +++++ .../static-name-binding/not-a-package/Main/Util/Util.swift | 3 +++ .../not-a-package/Mock/Drivers/Driver.swift | 4 ++++ .../static-name-binding/not-a-package/Mock/Runner.swift | 5 +++++ .../static-name-binding/not-a-package/Mock/Util/Util.swift | 3 +++ 6 files changed, 24 insertions(+) create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Util/Util.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Util/Util.swift diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift new file mode 100644 index 000000000000..0b9dea4e773e --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift @@ -0,0 +1,4 @@ +class Driver { // name=Main.Driver +} + +class UniqueToMain {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift new file mode 100644 index 000000000000..19589ab35cba --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift @@ -0,0 +1,5 @@ +func main() { + Driver(); // $ access=Main.Driver + UniqueToMain(); // $ access=UniqueToMain + UniqueToMock(); // $ access=UniqueToMock +} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Util/Util.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Util/Util.swift new file mode 100644 index 000000000000..adc7a3004127 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Util/Util.swift @@ -0,0 +1,3 @@ +func getDriver() -> Driver { // $ access=Main.Driver + return Driver() // $ access=Main.Driver +} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift new file mode 100644 index 000000000000..e3b5d0a789cf --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift @@ -0,0 +1,4 @@ +class Driver { // name=Mock.Driver +} + +class UniqueToMock {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift new file mode 100644 index 000000000000..06712f12b2b7 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift @@ -0,0 +1,5 @@ +func main() { + Driver(); // $ access=Mock.Driver + UniqueToMain(); // $ access=UniqueToMain + UniqueToMock(); // $ access=UniqueToMock +} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Util/Util.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Util/Util.swift new file mode 100644 index 000000000000..1d83c82c4f9d --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Util/Util.swift @@ -0,0 +1,3 @@ +func getDriver() -> Driver { // $ access=Mock.Driver + return Driver() // $ access=Mock.Driver +} From a143d2c03e20ed380851af1ef8b3d323c0499691 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 13 Aug 2026 13:42:09 +0200 Subject: [PATCH 04/13] unified: Record spurious result from lack of shadowing The spurious result is offered by the folder-based heuristic, but would have been blocked by proper shadowing support. --- .../library-tests/static-name-binding/unqualified-access.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 732e65563b3b..86cadc0b7a1f 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 + let x4: C = nil; // $ access=A.B.C SPURIOUS: access=Target3.C // spurious result from folder-based heuristic } class BSub2 : B { // $ access=A.B From eb9d80b49fb34fd188aa1502e8258c11142c741d Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 13 Aug 2026 13:51:40 +0200 Subject: [PATCH 05/13] unified: Add test with inheritance+nested classes Inheritance and access to nested classes is one of the reasons not to apply the heuristic as a "fixup" after the recursive layer, because when a base class is resolved through the folder heuristic, access to nested members still need to be resolved through the standard logic. --- .../not-a-package/Main/Drivers/Driver.swift | 1 + .../static-name-binding/not-a-package/Main/Runner.swift | 5 +++++ .../not-a-package/Mock/Drivers/Driver.swift | 1 + .../static-name-binding/not-a-package/Mock/Runner.swift | 5 +++++ 4 files changed, 12 insertions(+) diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift index 0b9dea4e773e..4b41a6324afb 100644 --- a/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift @@ -1,4 +1,5 @@ class Driver { // name=Main.Driver + class Nested {} // name=Main.Driver.Nested } class UniqueToMain {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift index 19589ab35cba..894e6257dbb1 100644 --- a/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift @@ -1,5 +1,10 @@ func main() { Driver(); // $ access=Main.Driver + Driver.Nested(); // $ access=Main.Driver access=Main.Driver.Nested UniqueToMain(); // $ access=UniqueToMain UniqueToMock(); // $ access=UniqueToMock } + +class MyDriver: Driver { // $ access=Main.Driver + class B: Nested {} // $ access=Main.Driver.Nested +} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift index e3b5d0a789cf..75d9e81affe5 100644 --- a/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift @@ -1,4 +1,5 @@ class Driver { // name=Mock.Driver + class Nested {} // name=Mock.Driver.Nested } class UniqueToMock {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift index 06712f12b2b7..930301ab9015 100644 --- a/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift @@ -1,5 +1,10 @@ func main() { Driver(); // $ access=Mock.Driver + Driver.Nested(); // $ access=Mock.Driver access=Mock.Driver.Nested UniqueToMain(); // $ access=UniqueToMain UniqueToMock(); // $ access=UniqueToMock } + +class MyDriver: Driver { // $ access=Mock.Driver + class B: Nested {} // $ access=Mock.Driver.Nested +} From b6741aad142080ccab6849fcec24a46c7498a9e9 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 14 Aug 2026 09:54:56 +0200 Subject: [PATCH 06/13] unified: Add some same-folder tests --- .../not-a-package/SiblingFiles/Def1.swift | 3 +++ .../not-a-package/SiblingFiles/Def2.swift | 3 +++ .../not-a-package/SiblingFiles/SubFolder1/Def.swift | 3 +++ .../not-a-package/SiblingFiles/SubFolder2/Def.swift | 3 +++ .../not-a-package/SiblingFiles/Use.swift | 9 +++++++++ 5 files changed, 21 insertions(+) create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def1.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def2.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Use.swift diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def1.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def1.swift new file mode 100644 index 000000000000..6edb39c550d7 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def1.swift @@ -0,0 +1,3 @@ +class DeclaredTwiceInSameFolder {} // name=Def1.DeclaredTwiceInSameFolder + +class OnlyInDef1 {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def2.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def2.swift new file mode 100644 index 000000000000..f41e33bc34bb --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def2.swift @@ -0,0 +1,3 @@ +class DeclaredTwiceInSameFolder {} // name=Def2.DeclaredTwiceInSameFolder + +class OnlyInDef2 {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift new file mode 100644 index 000000000000..139c0d2ab378 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift @@ -0,0 +1,3 @@ +class DeclaredTwiceInSubFolder {} // name=Subfolder2.DeclaredTwiceInSubFolder + +class OnlyInSubFolder2 {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift new file mode 100644 index 000000000000..07e2510e5cd1 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift @@ -0,0 +1,3 @@ +class DeclaredTwiceInSubFolder {} // name=Subfolder1.DeclaredTwiceInSubFolder + +class OnlyInSubFolder1 {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Use.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Use.swift new file mode 100644 index 000000000000..897386e9e93e --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Use.swift @@ -0,0 +1,9 @@ +private protocol P { + let x1: DeclaredTwiceInSameFolder; // unresolved; ambiguous reference + let x2: OnlyInDef1; // $ access=OnlyInDef1 + let x3: OnlyInDef2; // $ access=OnlyInDef2 + + let x4: DeclaredTwiceInSubFolder; // unresolved; ambiguous reference + let x5: OnlyInSubFolder1; // $ access=OnlyInSubFolder1 + let x6: OnlyInSubFolder2; // $ access=OnlyInSubFolder2 +} From b52ffdc78ab2ab69dcfddec7dc5045b728ffc7b7 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 20 Aug 2026 14:35:28 +0200 Subject: [PATCH 07/13] unified: Satisfy ql4ql warning --- unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql | 2 +- unified/ql/src/diagnostic/StaticNameResolution.ql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql b/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql index b426b6c06786..6ac4cec641bd 100644 --- a/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql +++ b/unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql @@ -15,4 +15,4 @@ import codeql.unified.internal.AnalysisQuality from FilesCoveredByModuleManifestStats::Candidate c, ModuleScopeRepr mod where c.isOk() and mod = c.getAModule() -select c, "File included in $@", mod, mod.toString() +select c, "File included in $@.", mod, mod.toString() diff --git a/unified/ql/src/diagnostic/StaticNameResolution.ql b/unified/ql/src/diagnostic/StaticNameResolution.ql index a49ba94791b6..501c39126e54 100644 --- a/unified/ql/src/diagnostic/StaticNameResolution.ql +++ b/unified/ql/src/diagnostic/StaticNameResolution.ql @@ -14,4 +14,4 @@ import codeql.unified.internal.AnalysisQuality from StaticNameResolutionStats::Candidate c, NameBindingNode target where target = c.getTarget() -select c, "Resolved to $@", target, target.toString() +select c, "Resolved to $@.", target, target.toString() From 420769740a8dea8acbad2ed34bc749f84d0e01a0 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 20 Aug 2026 14:44:53 +0200 Subject: [PATCH 08/13] unified: Fix accidentally swapped def names --- .../not-a-package/SiblingFiles/SubFolder1/Def.swift | 2 +- .../not-a-package/SiblingFiles/SubFolder2/Def.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift index 139c0d2ab378..3e5ce0ba952f 100644 --- a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift @@ -1,3 +1,3 @@ -class DeclaredTwiceInSubFolder {} // name=Subfolder2.DeclaredTwiceInSubFolder +class DeclaredTwiceInSubFolder {} // name=Subfolder1.DeclaredTwiceInSubFolder class OnlyInSubFolder2 {} diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift index 07e2510e5cd1..b82b472f39f6 100644 --- a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift @@ -1,3 +1,3 @@ -class DeclaredTwiceInSubFolder {} // name=Subfolder1.DeclaredTwiceInSubFolder +class DeclaredTwiceInSubFolder {} // name=Subfolder2.DeclaredTwiceInSubFolder class OnlyInSubFolder1 {} From 771130a57d6bb25708a7caadd9843c44b1b30340 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 20 Aug 2026 14:46:18 +0200 Subject: [PATCH 09/13] unified: Add a test showing FN for same-folder ref --- .../not-a-package/SiblingFiles/SubFolder1/Use.swift | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift new file mode 100644 index 000000000000..cada63b4ca4e --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift @@ -0,0 +1,5 @@ +private protocol P { + let x4: DeclaredTwiceInSubFolder; // $ MISSING: access=Subfolder1.DeclaredTwiceInSubFolder + let x5: OnlyInSubFolder1; // $ access=OnlyInSubFolder1 + let x6: OnlyInSubFolder2; // $ access=OnlyInSubFolder2 +} From e49a71242ddc0ae689026b34bb5c64628206cc31 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 20 Aug 2026 14:51:58 +0200 Subject: [PATCH 10/13] unified: Fix same-folder bug --- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 2 ++ .../not-a-package/SiblingFiles/SubFolder1/Use.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index ca9cca2fc61a..463c7a13f818 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -458,6 +458,8 @@ private module FolderHeuristic { */ private predicate hasConflictingDefs(Folder folder, string name) { containsDef(folder, name) and + // Check for "two or more" using `exists(X) and not exists(unique(X))` + containsDef(folder.getAFolder(), name) and not exists(unique(Folder child | child = folder.getAFolder() and containsDef(child, name))) } diff --git a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift index cada63b4ca4e..ac4305b455fc 100644 --- a/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Use.swift @@ -1,5 +1,5 @@ private protocol P { - let x4: DeclaredTwiceInSubFolder; // $ MISSING: access=Subfolder1.DeclaredTwiceInSubFolder + let x4: DeclaredTwiceInSubFolder; // $ access=Subfolder1.DeclaredTwiceInSubFolder let x5: OnlyInSubFolder1; // $ access=OnlyInSubFolder1 let x6: OnlyInSubFolder2; // $ access=OnlyInSubFolder2 } From 1a89fe4802bff44046d873596820e69c709f0443 Mon Sep 17 00:00:00 2001 From: Asger F Date: Thu, 20 Aug 2026 14:58:08 +0200 Subject: [PATCH 11/13] unified: fix typo --- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 463c7a13f818..a1334ff0cd1f 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -415,7 +415,7 @@ module DebugGraph { } /** - * Implements a folder-based heuristic to linking up top-level names + * Implements a folder-based heuristic for linking up top-level names * between files that are not included in any module scope. */ private module FolderHeuristic { From fd39a427d87b2726e0a0fe2174b602c63774fc3c Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 21 Aug 2026 11:34:28 +0200 Subject: [PATCH 12/13] unified: Fix grammar --- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index a1334ff0cd1f..33c158537077 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -436,7 +436,7 @@ private module FolderHeuristic { } /** - * Holds if `file` has a one of the definitions of the given ambiguous name. + * Holds if `file` has one of the definitions of the given ambiguous name. * * A name is considered "ambiguous" if there is more than one file exporting it. */ From 4e7613a30a2064eadf7570f5886a34f50d447856 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 21 Aug 2026 11:48:42 +0200 Subject: [PATCH 13/13] unified: Remove some unnecessary checks --- unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll | 2 -- 1 file changed, 2 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 33c158537077..dcb97b4fbae8 100644 --- a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -457,7 +457,6 @@ private module FolderHeuristic { * Holds if `folder` has two or more subfolders containing a definition of `name`. */ private predicate hasConflictingDefs(Folder folder, string name) { - containsDef(folder, name) and // Check for "two or more" using `exists(X) and not exists(unique(X))` containsDef(folder.getAFolder(), name) and not exists(unique(Folder child | child = folder.getAFolder() and containsDef(child, name))) @@ -482,7 +481,6 @@ private module FolderHeuristic { result = folder or result = getOutermostNonConflictingScope(folder.getParentContainer(), name) and - not isOutermostNonConflictingScope(folder, name) and containsDef(folder, name) // Prune to the subfolder actually containing the definition }