Skip to content

Commit 5a69ef0

Browse files
authored
Merge pull request #22370 from asgerf/unified/folder-fallback
Unified: Add folder-based fallback for static name resolution
2 parents a892579 + 4e7613a commit 5a69ef0

19 files changed

Lines changed: 323 additions & 1 deletion

File tree

shared/util/codeql/util/ReportStats.qll

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,41 @@ module ReportStats<StatsSig Stats> {
2929
value = Stats::getNumberOfOk() * 100.0 / (Stats::getNumberOfOk() + Stats::getNumberOfNotOk()) and
3030
key = "Percentage of " + Stats::getOkText()
3131
}
32+
33+
predicate keyValuePair(string key, float value) {
34+
numberOfOk(key, value) or
35+
numberOfNotOk(key, value) or
36+
percentageOfOk(key, value)
37+
}
38+
}
39+
40+
/**
41+
* Stats where each Ok/NotOk occurrence has an associated entity.
42+
*/
43+
signature module EntityStatsSig {
44+
class Candidate {
45+
predicate isOk();
46+
}
47+
48+
string getOkText();
49+
50+
string getNotOkText();
51+
}
52+
53+
module EntityReportStats<EntityStatsSig Input> {
54+
private import Input
55+
56+
private module StatsInput implements StatsSig {
57+
int getNumberOfOk() { result = count(Candidate c | c.isOk()) }
58+
59+
int getNumberOfNotOk() { result = count(Candidate c | not c.isOk()) }
60+
61+
import Input
62+
}
63+
64+
import StatsInput
65+
66+
private module ScalarReport = ReportStats<StatsInput>;
67+
68+
import ScalarReport
3269
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
private import unified
2+
private import codeql.util.ReportStats
3+
private import codeql.unified.internal.StaticNameBinding
4+
private import codeql.unified.internal.LocalNameBinding
5+
private import codeql.unified.internal.NameBindingPlugin
6+
7+
/** Stats about identifiers that static name binding could resolve. */
8+
module StaticNameResolutionStats implements EntityStatsSig {
9+
class Candidate extends Identifier {
10+
Candidate() {
11+
this = getIdentifierFromRef(_) and
12+
not this instanceof NameDeclaration
13+
// TODO: exclude names we know are not static references, e.g. unqualified instance-field access,
14+
// currently blocked on getting static name binding to report this information.
15+
}
16+
17+
NameBindingNode getTarget() {
18+
(
19+
exists(NameDeclaration decl |
20+
result.isIdentifier(decl) and
21+
trackNameDeclaration(decl).isIdentifier(this)
22+
)
23+
or
24+
result.isModuleScopeNode(_) and
25+
result.(NamespaceNode).ref().isIdentifier(this)
26+
) and
27+
// Do not consider a type extension to be a valid target
28+
// TODO: Fix in the AST mapping: type extensions should reference their type, not declare it
29+
not exists(ClassLikeDeclaration cls |
30+
cls.hasModifier("extension") and
31+
result.isIdentifier(cls.getName())
32+
)
33+
}
34+
35+
predicate isOk() { exists(this.getTarget()) }
36+
}
37+
38+
string getOkText() { result = "statically resolvable names" }
39+
40+
string getNotOkText() { result = "statically unresolvable names" }
41+
}
42+
43+
module StaticNameResolutionStatsReport = EntityReportStats<StaticNameResolutionStats>;
44+
45+
/** Stats about which files are covered by a module manifest. */
46+
module FilesCoveredByModuleManifestStats implements EntityStatsSig {
47+
class Candidate extends File {
48+
Candidate() { this.getExtension() = "swift" }
49+
50+
ModuleScopeRepr getAModule() { result.getAnIncludedFile() = this }
51+
52+
predicate isOk() { exists(this.getAModule()) }
53+
}
54+
55+
string getOkText() { result = "files covered by a module manifest" }
56+
57+
string getNotOkText() { result = "files not covered by any module manifest" }
58+
}
59+
60+
module FilesCoveredByModuleManifestStatsReport =
61+
EntityReportStats<FilesCoveredByModuleManifestStats>;

unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private newtype TNameBindingNode =
1616
n instanceof ClassLikeDeclaration
1717
} or
1818
TModuleScope(ModuleScopeRepr repr) or
19+
TFolderScope(Folder folder) or
1920
TModuleRoot()
2021

2122
/**
@@ -39,6 +40,9 @@ class NameBindingNode extends TNameBindingNode {
3940
/** Holds if this represents the given module scope. */
4041
predicate isModuleScopeNode(ModuleScopeRepr repr) { this = TModuleScope(repr) }
4142

43+
/** Holds if this represents the set of members that can be accessed unqualified within the given folder and subfolders. */
44+
predicate isFolderScope(Folder folder) { this = TFolderScope(folder) }
45+
4246
/** Holds if this represents the root namespace in which all named modules are members. */
4347
predicate isModuleRoot() { this = TModuleRoot() }
4448

@@ -76,6 +80,8 @@ class NameBindingNode extends TNameBindingNode {
7680
this.isModuleScopeNode(repr) and result = "ModuleScope(" + repr + ")"
7781
)
7882
or
83+
exists(Folder folder | this.isFolderScope(folder) and result = "FolderScope(" + folder + ")")
84+
or
7985
this.isModuleRoot() and result = "ModuleRoot"
8086
}
8187

@@ -173,6 +179,8 @@ predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) {
173179
mod.hasImportableName(name) and
174180
node2.isModuleRoot()
175181
)
182+
or
183+
FolderHeuristic::storeStep(node1, name, node2)
176184
}
177185

178186
predicate valueStep(NameBindingNode node1, NameBindingNode node2) {
@@ -224,6 +232,8 @@ predicate valueStep(NameBindingNode node1, NameBindingNode node2) {
224232
node1 = getNodeFromRef(p) and
225233
node2 = getNodeFromRef(p.getSubPattern())
226234
)
235+
or
236+
FolderHeuristic::valueStep(node1, node2)
227237
}
228238

229239
private predicate isImportPrefix(Expr e) {
@@ -403,3 +413,96 @@ module DebugGraph<relevantNodeSig/1 relevantNode> {
403413
)
404414
}
405415
}
416+
417+
/**
418+
* Implements a folder-based heuristic for linking up top-level names
419+
* between files that are not included in any module scope.
420+
*/
421+
private module FolderHeuristic {
422+
private predicate topLevelNameDef(File file, string name, NameBindingNode node) {
423+
exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl |
424+
top.getFile() = file and
425+
stmt = top.getBody().getAStmt() and
426+
not stmt.(ClassLikeDeclaration).hasModifier("extension") and // TODO: target of type extensions should not be seen as a NameDeclaration
427+
not isPrivateToLocalScope(nameDecl) and
428+
nameDecl.getDeclaration() = stmt and
429+
name = nameDecl.getName() and
430+
node.isIdentifier(nameDecl)
431+
)
432+
}
433+
434+
private predicate uniqueTopLevelName(File file, string name) {
435+
file = unique(File f | topLevelNameDef(f, name, _))
436+
}
437+
438+
/**
439+
* Holds if `file` has one of the definitions of the given ambiguous name.
440+
*
441+
* A name is considered "ambiguous" if there is more than one file exporting it.
442+
*/
443+
private predicate ambiguousTopLevelName(File file, string name) {
444+
topLevelNameDef(file, name, _) and
445+
not uniqueTopLevelName(file, name)
446+
}
447+
448+
/** Holds if `folder` contains one or more definitions of the given ambiguous name */
449+
private predicate containsDef(Folder folder, string name) {
450+
exists(File f |
451+
ambiguousTopLevelName(f, name) and
452+
folder = f.getParentContainer+()
453+
)
454+
}
455+
456+
/**
457+
* Holds if `folder` has two or more subfolders containing a definition of `name`.
458+
*/
459+
private predicate hasConflictingDefs(Folder folder, string name) {
460+
// Check for "two or more" using `exists(X) and not exists(unique(X))`
461+
containsDef(folder.getAFolder(), name) and
462+
not exists(unique(Folder child | child = folder.getAFolder() and containsDef(child, name)))
463+
}
464+
465+
/**
466+
* Holds if `folder` is an outermost folder containing exactly one definition of `name`.
467+
*
468+
* This means `folder` should act as the scope of that definition.
469+
*/
470+
private predicate isOutermostNonConflictingScope(Folder folder, string name) {
471+
containsDef(folder, name) and
472+
hasConflictingDefs(folder.getParentContainer(), name) and
473+
not hasConflictingDefs(folder, name)
474+
}
475+
476+
/**
477+
* Gets the scope into which a definition of `name` appearing in `folder` should target.
478+
*/
479+
private Folder getOutermostNonConflictingScope(Folder folder, string name) {
480+
isOutermostNonConflictingScope(folder, name) and
481+
result = folder
482+
or
483+
result = getOutermostNonConflictingScope(folder.getParentContainer(), name) and
484+
containsDef(folder, name) // Prune to the subfolder actually containing the definition
485+
}
486+
487+
predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) {
488+
exists(File file | topLevelNameDef(file, name, node1) |
489+
node2.isFolderScope(getOutermostNonConflictingScope(file.getParentContainer(), name))
490+
or
491+
uniqueTopLevelName(file, name) and
492+
node2.isFolderScope(any(Folder f | f.getRelativePath() = ""))
493+
)
494+
}
495+
496+
predicate valueStep(NameBindingNode node1, NameBindingNode node2) {
497+
exists(TopLevel top |
498+
node1.isFolderScope(top.getFile().getParentContainer()) and
499+
node2.isLocalNamespace(top.getBody()) and
500+
not top.getFile() = any(ModuleScopeRepr r).getAnIncludedFile()
501+
)
502+
or
503+
exists(Folder folder |
504+
node1.isFolderScope(folder.getParentContainer()) and
505+
node2.isFolderScope(folder)
506+
)
507+
}
508+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @name Unified extractor/analysis information
3+
* @description Information about the extraction and analysis for a database
4+
* @kind metric
5+
* @tags summary telemetry
6+
* @id unified/telemetry/extraction-information
7+
*/
8+
9+
private import unified
10+
private import codeql.unified.internal.AnalysisQuality
11+
12+
from string key, float value
13+
where
14+
(
15+
StaticNameResolutionStatsReport::keyValuePair(key, value) or
16+
FilesCoveredByModuleManifestStatsReport::keyValuePair(key, value)
17+
) and
18+
/* Infinity */
19+
value != 1.0 / 0.0 and
20+
/* -Infinity */
21+
value != -1.0 / 0.0 and
22+
/* NaN */
23+
value != 0.0 / 0.0
24+
select key, value
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @name Files covered by module manifest
3+
* @description Files that are included from a module manifest
4+
* @kind problem
5+
* @problem.severity recommendation
6+
* @id unified/diagnostic/files-covered-by-module-manifest
7+
* @tags meta
8+
* @precision very-low
9+
*/
10+
11+
import unified
12+
import codeql.unified.internal.StaticNameBinding
13+
import codeql.unified.internal.NameBindingPlugin
14+
import codeql.unified.internal.AnalysisQuality
15+
16+
from FilesCoveredByModuleManifestStats::Candidate c, ModuleScopeRepr mod
17+
where c.isOk() and mod = c.getAModule()
18+
select c, "File included in $@.", mod, mod.toString()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @name Static name resolution
3+
* @description Static name references that could be resolved to a target
4+
* @kind problem
5+
* @problem.severity recommendation
6+
* @id unified/diagnostic/static-name-resolution
7+
* @tags meta
8+
* @precision very-low
9+
*/
10+
11+
import unified
12+
import codeql.unified.internal.StaticNameBinding
13+
import codeql.unified.internal.AnalysisQuality
14+
15+
from StaticNameResolutionStats::Candidate c, NameBindingNode target
16+
where target = c.getTarget()
17+
select c, "Resolved to $@.", target, target.toString()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class Driver { // name=Main.Driver
2+
class Nested {} // name=Main.Driver.Nested
3+
}
4+
5+
class UniqueToMain {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
func main() {
2+
Driver(); // $ access=Main.Driver
3+
Driver.Nested(); // $ access=Main.Driver access=Main.Driver.Nested
4+
UniqueToMain(); // $ access=UniqueToMain
5+
UniqueToMock(); // $ access=UniqueToMock
6+
}
7+
8+
class MyDriver: Driver { // $ access=Main.Driver
9+
class B: Nested {} // $ access=Main.Driver.Nested
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
func getDriver() -> Driver { // $ access=Main.Driver
2+
return Driver() // $ access=Main.Driver
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class Driver { // name=Mock.Driver
2+
class Nested {} // name=Mock.Driver.Nested
3+
}
4+
5+
class UniqueToMock {}

0 commit comments

Comments
 (0)