Skip to content
Open
37 changes: 37 additions & 0 deletions shared/util/codeql/util/ReportStats.qll
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,41 @@
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<EntityStatsSig Input> {
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<StatsInput>;

import ScalarReport
}
61 changes: 61 additions & 0 deletions unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll
Original file line number Diff line number Diff line change
@@ -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<StaticNameResolutionStats>;

/** 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<FilesCoveredByModuleManifestStats>;
105 changes: 105 additions & 0 deletions unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ private newtype TNameBindingNode =
n instanceof ClassLikeDeclaration
} or
TModuleScope(ModuleScopeRepr repr) or
TFolderScope(Folder folder) or
TModuleRoot()

/**
Expand All @@ -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() }

Expand Down Expand Up @@ -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"
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -403,3 +413,98 @@ module DebugGraph<relevantNodeSig/1 relevantNode> {
)
}
}

/**
* 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 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
// 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)))
}
Comment thread
asgerf marked this conversation as resolved.

/**
* 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)
)
}
}
24 changes: 24 additions & 0 deletions unified/ql/src/diagnostic/ExtractorInformation.ql
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql
Original file line number Diff line number Diff line change
@@ -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()
17 changes: 17 additions & 0 deletions unified/ql/src/diagnostic/StaticNameResolution.ql
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Driver { // name=Main.Driver
class Nested {} // name=Main.Driver.Nested
}

class UniqueToMain {}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
func getDriver() -> Driver { // $ access=Main.Driver
return Driver() // $ access=Main.Driver
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Driver { // name=Mock.Driver
class Nested {} // name=Mock.Driver.Nested
}

class UniqueToMock {}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
func getDriver() -> Driver { // $ access=Mock.Driver
return Driver() // $ access=Mock.Driver
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class DeclaredTwiceInSameFolder {} // name=Def1.DeclaredTwiceInSameFolder

class OnlyInDef1 {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class DeclaredTwiceInSameFolder {} // name=Def2.DeclaredTwiceInSameFolder

class OnlyInDef2 {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class DeclaredTwiceInSubFolder {} // name=Subfolder1.DeclaredTwiceInSubFolder

class OnlyInSubFolder2 {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
private protocol P {
let x4: DeclaredTwiceInSubFolder; // $ access=Subfolder1.DeclaredTwiceInSubFolder
let x5: OnlyInSubFolder1; // $ access=OnlyInSubFolder1
let x6: OnlyInSubFolder2; // $ access=OnlyInSubFolder2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class DeclaredTwiceInSubFolder {} // name=Subfolder2.DeclaredTwiceInSubFolder

class OnlyInSubFolder1 {}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading