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/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll index 907cc9f14010..dcb97b4fbae8 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 for 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 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) { + // 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))) + } + + /** + * 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 + 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) + ) + } +} 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..6ac4cec641bd --- /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..501c39126e54 --- /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() 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..4b41a6324afb --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift @@ -0,0 +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 new file mode 100644 index 000000000000..894e6257dbb1 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift @@ -0,0 +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/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..75d9e81affe5 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift @@ -0,0 +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 new file mode 100644 index 000000000000..930301ab9015 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift @@ -0,0 +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 +} 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 +} 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..3e5ce0ba952f --- /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=Subfolder1.DeclaredTwiceInSubFolder + +class OnlyInSubFolder2 {} 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..ac4305b455fc --- /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; // $ access=Subfolder1.DeclaredTwiceInSubFolder + let x5: OnlyInSubFolder1; // $ access=OnlyInSubFolder1 + let x6: OnlyInSubFolder2; // $ access=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..b82b472f39f6 --- /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=Subfolder2.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 +} 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