@@ -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
178186predicate 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
229239private 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