-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubmodule-reference.ts
More file actions
187 lines (166 loc) · 6.26 KB
/
submodule-reference.ts
File metadata and controls
187 lines (166 loc) · 6.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import * as ts from 'typescript';
export type SubmoduleReferenceMap = ReadonlyMap<
ts.PropertyAccessExpression | ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier,
SubmoduleReference
>;
export class SubmoduleReference {
public static inSourceFile(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker): SubmoduleReferenceMap {
const importDeclarations = sourceFile.statements
.filter((stmt) => ts.isImportDeclaration(stmt))
.flatMap((stmt) => importedSymbolsFrom(stmt as ts.ImportDeclaration, sourceFile, typeChecker));
return SubmoduleReference.inNode(sourceFile, typeChecker, new Set(importDeclarations));
}
private static inNode(
node: ts.Node,
typeChecker: ts.TypeChecker,
importDeclarations: ReadonlySet<ts.Symbol>,
map = new Map<
ts.PropertyAccessExpression | ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier,
SubmoduleReference
>(),
): Map<
ts.PropertyAccessExpression | ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier,
SubmoduleReference
> {
if (ts.isPropertyAccessExpression(node)) {
const [head, ...tail] = propertyPath(node);
const symbol = typeChecker.getSymbolAtLocation(head.name);
if (symbol && importDeclarations.has(symbol)) {
// This is a reference within an imported namespace, so we need to record that...
const firstNonNamespace = tail.findIndex((item) => !isLikelyNamespace(item.name, typeChecker));
if (firstNonNamespace < 0) {
map.set(node.expression, new SubmoduleReference(symbol, node.expression, []));
} else {
const tailEnd = tail[firstNonNamespace].expression;
const path = tail.slice(0, firstNonNamespace).map((item) => item.name);
map.set(tailEnd, new SubmoduleReference(symbol, tailEnd, path));
}
}
return map;
}
// Faster than ||-ing a bung of if statements to avoid traversing uninteresting nodes...
switch (node.kind) {
case ts.SyntaxKind.ImportDeclaration:
case ts.SyntaxKind.ExportDeclaration:
break;
default:
for (const child of node.getChildren()) {
map = SubmoduleReference.inNode(child, typeChecker, importDeclarations, map);
}
}
return map;
}
private constructor(
public readonly root: ts.Symbol,
public readonly submoduleChain: ts.LeftHandSideExpression | ts.Identifier | ts.PrivateIdentifier,
public readonly path: readonly ts.Node[],
) {}
public get lastNode(): ts.Node {
if (this.path.length === 0) {
const node = this.root.valueDeclaration ?? this.root.declarations[0];
return ts.isNamespaceImport(node) || ts.isImportSpecifier(node) ? node.name : node;
}
return this.path[this.path.length - 1];
}
public toString(): string {
return `${this.constructor.name}<root=${this.root.name}, path=${JSON.stringify(
this.path.map((item) => item.getText(item.getSourceFile())),
)}>`;
}
}
/**
* Determines what symbols are imported by the given TypeScript import
* declaration, in the context of the specified file, using the provided type
* checker.
*
* @param decl an import declaration.
* @param sourceFile the source file that contains the import declaration.
* @param typeChecker a TypeChecker instance valid for the provided source file.
*
* @returns the (possibly empty) list of symbols imported by this declaration.
*/
function importedSymbolsFrom(
decl: ts.ImportDeclaration,
sourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker,
): ts.Symbol[] {
const { importClause } = decl;
if (importClause == null) {
// This is a "for side effects" import, which isn't relevant for our business here...
return [];
}
const { name, namedBindings } = importClause;
const imports = new Array<ts.Symbol>();
if (name != null) {
const symbol = typeChecker.getSymbolAtLocation(name);
if (symbol == null) {
throw new Error(`No symbol was defined for node ${name.getText(sourceFile)}`);
}
imports.push(symbol);
}
if (namedBindings != null) {
if (ts.isNamespaceImport(namedBindings)) {
const { name } = namedBindings;
const symbol = typeChecker.getSymbolAtLocation(name);
if (symbol == null) {
throw new Error(`No symbol was defined for node ${name.getText(sourceFile)}`);
}
imports.push(symbol);
} else {
for (const specifier of namedBindings.elements) {
const { name } = specifier;
const symbol = typeChecker.getSymbolAtLocation(name);
if (symbol == null) {
throw new Error(`No symbol was defined for node ${name.getText(sourceFile)}`);
}
imports.push(symbol);
}
}
}
return imports;
}
interface PathEntry {
readonly name: ts.Identifier | ts.PrivateIdentifier | ts.LeftHandSideExpression;
readonly expression: ts.LeftHandSideExpression;
}
function propertyPath(node: ts.PropertyAccessExpression): readonly PathEntry[] {
const { expression, name } = node;
if (!ts.isPropertyAccessExpression(expression)) {
return [
{ name: expression, expression },
{ name, expression },
];
}
return [...propertyPath(expression), { name, expression }];
}
/**
* A heuristic to determine whether the provided node likely refers to some
* namespace.
*
* @param node the node to be checked.
* @param typeChecker a type checker that can obtain symbols for this node.
*
* @returns true if the node likely refers to a namespace name.
*/
function isLikelyNamespace(node: ts.Node, typeChecker: ts.TypeChecker): boolean {
if (!ts.isIdentifier(node)) {
return false;
}
// If the identifier was bound to a symbol, we can inspect the declarations of
// it to validate they are all module or namespace declarations.
const symbol = typeChecker.getSymbolAtLocation(node);
if (symbol != null) {
return (
symbol.declarations.length > 0 &&
symbol.declarations.every(
(decl) => ts.isModuleDeclaration(decl) || ts.isNamespaceExport(decl) || ts.isNamespaceImport(decl),
)
);
}
// We understand this is likely a namespace if the name does not start with
// upper-case letter.
return !startsWithUpperCase(node.text);
}
function startsWithUpperCase(text: string): boolean {
return text.length > 0 && text[0] === text[0].toUpperCase();
}