Providing cross-references for module systems
Many languages provide top-level mechanisms for organization that make writing large systems easier. These have explicit syntactic forms for expressing which components of a module are to be exported and which components are to be imported from other modules. It may also be possible to refer to the module itself as a first-class value in a program.
In the following sections, we’ll discuss common user expectations for cross-references in programs that use modules. The representation of a module itself is not in this document’s scope.
Simple imports
The second line makes the module named fmt
available in scope as fmt
. The
exact semantics of what the local fmt
is can differ depending on the
language. In some languages, fmt
is simply a local variable that points to
the (first-class) module object; in others, it is part of a special category of
identifiers that cannot be rebound. Users generally prefer to see any use of
fmt
ref the node representing the module’s definition instead of the
definition of the local alias, which may not even have a sensible syntactic
location to anchor to:
import ( //- @"\"fmt\"" ref/imports FmtModule "fmt" ) //- @fmt ref FmtModule fmt.Sprint
With these edges, users who attempt to navigate from the module’s use site will
be brought to the definition of fmt
, not to the definition of the local
alias. In the same way, users investigating cross-references from the module’s
definition will see every use of that module in the database (up to re-exporting
and vexing control-flow), not just the places where it’s been imported. In
addition, other information (like documentation) will be available at every use
site.
Some languages allow particular values to be imported into local scope. These should be handled in the same way as module definitions:
//- @value ref/imports Value //- @"'./module'" ref/imports Module import {value} from './module'; //- @value ref Value value;
Imports with renaming
It’s sometimes possible (or is even required) to choose a different name for the local binding created by an import from the target definition’s. In this case, the local name does have a sensible syntactic location and is semantically relevant to the programmer. A use site of the local name refers to the local alias, which in turn aliases the module (or value) being imported.
//- @"'./module'" ref/imports Module //- @defaultExport defines/binding DefaultExport //- DefaultExport aliases DefaultExportDef //- DefaultExport.subkind import import defaultExport from './module'; //- @"'./module'" ref/imports Module //- @mod_imp defines/binding ModImp //- ModImp aliases Module //- ModImp.subkind import import * as mod_imp from './module'; //- @"'./module'" ref/imports Module //- @value ref/imports Value //- @renamedValue defines/binding RenamedValue //- RenamedValue aliases Value //- RenamedValue.subkind import import {value as renamedValue} from './module'; //- @defaultExport ref DefaultExport defaultExport; //- @mod_imp ref ModImp mod_imp.foo; //- @renamedValue ref RenamedValue renamedValue;
Imports that do not create local bindings
Languages like TypeScript allow modules to be imported strictly for their
side-effects. In this case, only a ref/imports
edge should be placed at
the import site:
//- @"'./module'" ref/imports Module import './module';
//- @"\"lib/math\"" ref/imports MathModule import ( _ "lib/math" )
Re-exporting and rebinding
If a module re-exports a definition (renaming it or not) that it previously imported, that re-export site should be considered to be the definition of that name.
If a program rebinds an imported name, indexers may treat the new name as a different variable:
//- @"'./module'" ref/imports Module //- @module defines/binding DefaultOne //- DefaultOne aliases DefaultOneDef import module from './module'; //- @module ref DefaultOne module.foo(); //- @"'./debug/module'" ref/imports DebugModule //- @module defines/binding DefaultTwo //- DefaultTwo aliases DefaultTwoDef import module from './debug/module'; //- @module ref DefaultTwo //- ! { @module ref DefaultOne } module.foo();
It may not be possible to determine that an import is changing, or to determine the value it’s being changed to. In this case, indexers can treat import names like ordinary variables:
//- @"'./module'" ref/imports Module //- @module defines/binding ModuleImport //- ModuleImport.subkind import //- ModuleImport aliases Module import module from './module'; //- @module ref ModuleImport if (random()) module = otherModule; //- @module ref ModuleImport module.foo();
Caveats
Downstream consumers of the graph should tolerate overlapping references.
Variables defined by imports should have subkind import
. Consumers may
choose to prioritize showing documentation or definitions of non-import
nodes over import
nodes.