Skip to content

Commit f072c91

Browse files
committed
unified: Add folder-based fallback heuristic
1 parent 90413a8 commit f072c91

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

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 to 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 a 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+
containsDef(folder, name) and
461+
not exists(unique(Folder child | child = folder.getAFolder() and containsDef(child, name)))
462+
}
463+
464+
/**
465+
* Holds if `folder` is an outermost folder containing exactly one definition of `name`.
466+
*
467+
* This means `folder` should act as the scope of that definition.
468+
*/
469+
private predicate isOutermostNonConflictingScope(Folder folder, string name) {
470+
containsDef(folder, name) and
471+
hasConflictingDefs(folder.getParentContainer(), name) and
472+
not hasConflictingDefs(folder, name)
473+
}
474+
475+
/**
476+
* Gets the scope into which a definition of `name` appearing in `folder` should target.
477+
*/
478+
private Folder getOutermostNonConflictingScope(Folder folder, string name) {
479+
isOutermostNonConflictingScope(folder, name) and
480+
result = folder
481+
or
482+
result = getOutermostNonConflictingScope(folder.getParentContainer(), name) and
483+
not isOutermostNonConflictingScope(folder, 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+
}

0 commit comments

Comments
 (0)