archstrict 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/hooks/hooks.json +29 -0
- package/.agents/hooks/post-tool-use.mjs +107 -0
- package/.agents/hooks/pre-tool-use.mjs +182 -0
- package/.agents/mcp/server.mjs +71 -0
- package/.agents/plugin.json +19 -0
- package/AGENTS.md +69 -0
- package/CHANGELOG.md +38 -0
- package/README.ja.md +62 -0
- package/README.md +63 -2
- package/dist/augmentation-cache.js +65 -0
- package/dist/check-options.js +40 -0
- package/dist/classify.js +148 -0
- package/dist/cli.js +239 -0
- package/dist/config-pointer.js +251 -0
- package/dist/config.js +186 -0
- package/dist/edge-cache.js +530 -0
- package/dist/mcp-server.js +111 -0
- package/dist/module-candidates.js +118 -0
- package/dist/module-graph.js +2072 -0
- package/dist/project-path.js +59 -0
- package/dist/report-error.js +13 -0
- package/dist/rules/config-meaning.js +143 -0
- package/dist/rules/constraints.js +417 -0
- package/dist/rules/cycles.js +257 -0
- package/dist/rules/deprecated.js +67 -0
- package/dist/rules/empty-rule.js +101 -0
- package/dist/rules/moves.js +79 -0
- package/dist/rules/must-be-empty.js +52 -0
- package/dist/rules/public-surface.js +100 -0
- package/dist/rules/type-leak.js +562 -0
- package/dist/rules/uncovered.js +75 -0
- package/dist/todo-migration.js +112 -0
- package/dist/todo-store.js +434 -0
- package/dist/type-closure.js +959 -0
- package/dist/verbs/agents.js +116 -0
- package/dist/verbs/check.js +957 -0
- package/dist/verbs/fix.js +170 -0
- package/dist/verbs/hotspots.js +261 -0
- package/dist/verbs/init.js +522 -0
- package/dist/verbs/recommend.js +800 -0
- package/dist/verbs/rules.js +188 -0
- package/dist/verbs/search.js +109 -0
- package/dist/verbs/simulate.js +220 -0
- package/dist/verbs/todo.js +163 -0
- package/dist/warm-graph.js +82 -0
- package/docs/boundary-patterns.md +374 -0
- package/docs/calibrated-rules-design.md +124 -0
- package/docs/init-singleton-modules.md +128 -0
- package/docs/maintenance.md +82 -0
- package/docs/releasing.md +55 -0
- package/docs/rules-edge-cache.md +50 -0
- package/docs/todo-single-file-migration.md +58 -0
- package/llms.txt +19 -0
- package/package.json +57 -4
- package/skills/archstrict/SKILL.md +42 -0
- package/skills/archstrict/references/agents-verb.md +39 -0
- package/skills/archstrict/references/config.md +107 -0
- package/skills/archstrict/references/hook.md +57 -0
- package/skills/archstrict/references/path-rules.md +57 -0
- package/skills/archstrict/references/patterns.md +883 -0
- package/skills/archstrict/references/prove-rules.md +58 -0
- package/skills/archstrict/references/rearchitect.md +35 -0
- package/skills/archstrict/references/recommend.md +80 -0
- package/skills/archstrict/references/rules.md +146 -0
- package/skills/archstrict/references/simulate.md +109 -0
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
// Responsibility: compute the file set rule 6's own checker Program must
|
|
2
|
+
// load to answer every real type-leak question - not every analyzed file,
|
|
3
|
+
// but every file reachable from a module surface by following an export
|
|
4
|
+
// chain, a type position, or an unannotated declaration's own inferred
|
|
5
|
+
// type, plus every file whose own top-level names are ambient (a script,
|
|
6
|
+
// or a `declare global`/`declare module "..."` body) and so bind
|
|
7
|
+
// regardless of who imports it. On a large project this closure is a small
|
|
8
|
+
// fraction of the analyzed set - the whole reason this module exists is
|
|
9
|
+
// that the checker's own bound SourceFile trees, not the edges archstrict
|
|
10
|
+
// otherwise builds, dominate memory on a codebase of tens of thousands of
|
|
11
|
+
// files.
|
|
12
|
+
//
|
|
13
|
+
// Boundary: syntax only. Each file is parsed once, reduced immediately to a small per-declaration
|
|
14
|
+
// summary (which names it exports, which specifiers and identifiers each
|
|
15
|
+
// declaration references), and the AST is dropped. Specifier resolution
|
|
16
|
+
// never happens here - `resolvedSpecifiers` is the graph's own
|
|
17
|
+
// fromFile -> resolutionKey(specifier, mode) -> resolvedFile edges, already built once by
|
|
18
|
+
// module-graph.ts's own per-file walk; asking `ts.resolveModuleName`
|
|
19
|
+
// again here would be a second resolution pass over the same specifiers.
|
|
20
|
+
// No rule logic and no checker: this module only names the files rule 6's
|
|
21
|
+
// own Program needs to see. Its public-name walk also reports every file
|
|
22
|
+
// it visits, because an augmentation matters only when that walk depends
|
|
23
|
+
// on the augmented file. It never decides whether a closure is complete -
|
|
24
|
+
// module-graph.ts's own safety net makes that call, from rule
|
|
25
|
+
// 6's own real resolution failures on the Program this module's output
|
|
26
|
+
// became, not from anything this module reports about itself.
|
|
27
|
+
//
|
|
28
|
+
// An incomplete closure gets rule 6 wrong in two different directions.
|
|
29
|
+
// Within the syntax archstrict analyzes (ESM import/export; `import x =
|
|
30
|
+
// require(...)` is counted as unsupported and never followed), the
|
|
31
|
+
// rules below guarantee the first: a missing file that holds a
|
|
32
|
+
// declaration a reached export or an internal type structurally reaches
|
|
33
|
+
// drops a real finding, since the checker would see no declaration at
|
|
34
|
+
// all where that file's own export should have resolved. module-graph.ts's
|
|
35
|
+
// own safety net guards the second, separately: a missing file that
|
|
36
|
+
// holds a hop INSIDE a re-export chain (or a generic constraint's own
|
|
37
|
+
// reference) turns a surface's own public name into an error type, so
|
|
38
|
+
// rule 6's own "does a consumer already have a name for this" lookup
|
|
39
|
+
// reads it as absent, and an internal declaration that already had a
|
|
40
|
+
// real public name through that chain reads as newly, wrongly leaked -
|
|
41
|
+
// the net recovers this because the chain's own alias resolves to the
|
|
42
|
+
// checker's own unknown symbol, a fact rule 6 reports and module-graph.ts
|
|
43
|
+
// resolves through the edge records. A lost `export *` target is the
|
|
44
|
+
// rules below own responsibility, not the net's: R2 (export chains)
|
|
45
|
+
// already follows every file `export * from` can reach, so a missing
|
|
46
|
+
// target there is this module's own bug, not a case the net exists to
|
|
47
|
+
// paper over.
|
|
48
|
+
import ts from "typescript";
|
|
49
|
+
import { resolutionKey } from "./edge-cache.js";
|
|
50
|
+
// TypeScript hands back a real resolvedFileName for a target the
|
|
51
|
+
// checker can bind (a real .ts/.tsx/.mts/.cts source, or a hand-authored
|
|
52
|
+
// .d.ts/.d.mts/.d.cts) - anything else (a resolved .js with no
|
|
53
|
+
// declaration file, a JSON import, ...) is not something a Program root
|
|
54
|
+
// could type, the same filter module-graph.ts's own resolution loop
|
|
55
|
+
// already keeps in mind when deciding whether a target is real project
|
|
56
|
+
// source.
|
|
57
|
+
function isProgramSource(file) {
|
|
58
|
+
return /\.(?:d\.)?[mc]?tsx?$/.test(file);
|
|
59
|
+
}
|
|
60
|
+
function segmentsOf(node) {
|
|
61
|
+
if (ts.isIdentifier(node))
|
|
62
|
+
return [node.text];
|
|
63
|
+
if (ts.isQualifiedName(node)) {
|
|
64
|
+
const left = segmentsOf(node.left);
|
|
65
|
+
return left === undefined ? undefined : [...left, node.right.text];
|
|
66
|
+
}
|
|
67
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
|
68
|
+
const left = segmentsOf(node.expression);
|
|
69
|
+
return left === undefined ? undefined : [...left, node.name.text];
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function isFunctionLikeWithBody(node) {
|
|
74
|
+
return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) ||
|
|
75
|
+
ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isGetAccessorDeclaration(node);
|
|
76
|
+
}
|
|
77
|
+
function hasExportModifier(node) {
|
|
78
|
+
return (ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined)
|
|
79
|
+
?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
80
|
+
}
|
|
81
|
+
function hasDefaultModifier(node) {
|
|
82
|
+
return (ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined)
|
|
83
|
+
?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
|
|
84
|
+
}
|
|
85
|
+
// Each bound name, paired with the node the checker itself reports as its
|
|
86
|
+
// own declaration - a plain identifier binding (`const x = 1`) resolves to
|
|
87
|
+
// the VariableDeclaration itself; a destructured one (`const { a } = x`,
|
|
88
|
+
// `const [a] = x`) resolves to that ONE binding element, not the
|
|
89
|
+
// declaration as a whole (measured directly against a real checker: two
|
|
90
|
+
// names destructured from the same declarator get two different
|
|
91
|
+
// positions) - `contextNode` starts as the declaration and becomes each
|
|
92
|
+
// binding element in turn as the pattern nests.
|
|
93
|
+
function bindingElements(name, contextNode, out) {
|
|
94
|
+
if (ts.isIdentifier(name)) {
|
|
95
|
+
out.push({ name: name.text, node: contextNode });
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
for (const element of name.elements) {
|
|
99
|
+
if (!ts.isOmittedExpression(element))
|
|
100
|
+
bindingElements(element.name, element, out);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function newInfo() {
|
|
105
|
+
return { refs: [], imports: [] };
|
|
106
|
+
}
|
|
107
|
+
// The same (file, line, column) `declarationKey` (below) turns into one
|
|
108
|
+
// string - kept as a plain position, not a node reference, so a
|
|
109
|
+
// declaration reached through a syntax-only parse (no bound Program, no
|
|
110
|
+
// live symbol) can still be compared against one the checker itself
|
|
111
|
+
// returned for the identical source text.
|
|
112
|
+
function positionOf(node, sf, file) {
|
|
113
|
+
const start = node.getStart(sf);
|
|
114
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(start);
|
|
115
|
+
return { file, line: line + 1, column: character + 1 };
|
|
116
|
+
}
|
|
117
|
+
// A dynamic `import(...)` call's own string-literal argument, or
|
|
118
|
+
// undefined for anything else - the one specifier-bearing node shape
|
|
119
|
+
// that is a plain CallExpression rather than its own dedicated node kind.
|
|
120
|
+
function dynamicImportSpecifier(node) {
|
|
121
|
+
if (!ts.isCallExpression(node) || node.expression.kind !== ts.SyntaxKind.ImportKeyword)
|
|
122
|
+
return undefined;
|
|
123
|
+
const argument = node.arguments[0];
|
|
124
|
+
return argument !== undefined && ts.isStringLiteral(argument) ? argument : undefined;
|
|
125
|
+
}
|
|
126
|
+
// Walks one declaration's own subtree, filling `info`. `inferring` is
|
|
127
|
+
// true once a value position with no type annotation has been entered -
|
|
128
|
+
// only then does a bare identifier/property-access count as a reference
|
|
129
|
+
// at all (an annotated declaration's own body/initializer is never
|
|
130
|
+
// walked: the annotation already says everything the checker needs).
|
|
131
|
+
function collect(node, info, inferring, sf, compilerOptions) {
|
|
132
|
+
const resolvedSpecifier = (literal) => ({
|
|
133
|
+
specifier: literal.text,
|
|
134
|
+
key: resolutionKey({ specifier: literal.text, mode: ts.getModeForUsageLocation(sf, literal, compilerOptions) }),
|
|
135
|
+
});
|
|
136
|
+
const visit = (n, inferHere) => {
|
|
137
|
+
const importTypeSpecifier = ts.isImportTypeNode(n) && ts.isLiteralTypeNode(n.argument) && ts.isStringLiteral(n.argument.literal)
|
|
138
|
+
? n.argument.literal : undefined;
|
|
139
|
+
const dynamicSpecifier = dynamicImportSpecifier(n);
|
|
140
|
+
// A dynamic `import(...)` reached while inferring (an unannotated
|
|
141
|
+
// declaration's own value walk) can resolve to any type its target
|
|
142
|
+
// module exports - the same reasoning the inference rule below uses
|
|
143
|
+
// for a static reference, applied to the one import shape a value
|
|
144
|
+
// expression can hold. Always reaches the target whole: this closure
|
|
145
|
+
// decides only which FILE needs loading, never which member of it a
|
|
146
|
+
// caller happens to reach.
|
|
147
|
+
if (dynamicSpecifier !== undefined && inferHere)
|
|
148
|
+
info.imports.push(resolvedSpecifier(dynamicSpecifier));
|
|
149
|
+
if (ts.isTypeReferenceNode(n)) {
|
|
150
|
+
const seg = segmentsOf(n.typeName);
|
|
151
|
+
if (seg !== undefined)
|
|
152
|
+
info.refs.push(seg);
|
|
153
|
+
n.typeArguments?.forEach((a) => visit(a, inferHere));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (ts.isExpressionWithTypeArguments(n)) {
|
|
157
|
+
const seg = segmentsOf(n.expression);
|
|
158
|
+
if (seg !== undefined)
|
|
159
|
+
info.refs.push(seg);
|
|
160
|
+
else
|
|
161
|
+
visit(n.expression, true);
|
|
162
|
+
n.typeArguments?.forEach((a) => visit(a, inferHere));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (ts.isTypeQueryNode(n)) {
|
|
166
|
+
const seg = segmentsOf(n.exprName);
|
|
167
|
+
if (seg !== undefined)
|
|
168
|
+
info.refs.push(seg);
|
|
169
|
+
n.typeArguments?.forEach((a) => visit(a, inferHere));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (ts.isImportTypeNode(n)) {
|
|
173
|
+
if (importTypeSpecifier !== undefined) {
|
|
174
|
+
info.imports.push({ ...resolvedSpecifier(importTypeSpecifier), qualifier: n.qualifier ? segmentsOf(n.qualifier) : undefined });
|
|
175
|
+
}
|
|
176
|
+
n.typeArguments?.forEach((a) => visit(a, inferHere));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (ts.isComputedPropertyName(n)) {
|
|
180
|
+
visit(n.expression, true);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// An export specifier inside a namespace or ambient-module body
|
|
184
|
+
// (`export { X }`, `export type { X }`) references X the same way a
|
|
185
|
+
// value expression references an identifier - nothing else visits a
|
|
186
|
+
// nested ExportDeclaration, since the top-level walk only reads
|
|
187
|
+
// export declarations that are direct children of a source file.
|
|
188
|
+
if (ts.isExportDeclaration(n)) {
|
|
189
|
+
const spec = n.moduleSpecifier !== undefined && ts.isStringLiteral(n.moduleSpecifier) ? n.moduleSpecifier : undefined;
|
|
190
|
+
if (n.exportClause !== undefined && ts.isNamedExports(n.exportClause)) {
|
|
191
|
+
for (const element of n.exportClause.elements) {
|
|
192
|
+
const local = (element.propertyName ?? element.name).text;
|
|
193
|
+
if (spec !== undefined)
|
|
194
|
+
info.imports.push({ ...resolvedSpecifier(spec), qualifier: [local] });
|
|
195
|
+
else
|
|
196
|
+
info.refs.push([local]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (spec !== undefined) {
|
|
200
|
+
info.imports.push(resolvedSpecifier(spec));
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (inferHere) {
|
|
205
|
+
const seg = (ts.isPropertyAccessExpression(n) || ts.isIdentifier(n)) ? segmentsOf(n) : undefined;
|
|
206
|
+
if (seg !== undefined) {
|
|
207
|
+
info.refs.push(seg);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// An annotated function-like/constructor/setter contributes only its
|
|
212
|
+
// signature (parameters, type parameters, return type) - its body is
|
|
213
|
+
// never inferred, since the annotation already fixes what the
|
|
214
|
+
// checker needs. An unannotated one has its body walked in inferring
|
|
215
|
+
// mode instead (the inference rule, below).
|
|
216
|
+
if (isFunctionLikeWithBody(n) || ts.isConstructorDeclaration(n) || ts.isSetAccessorDeclaration(n)) {
|
|
217
|
+
n.typeParameters?.forEach((p) => visit(p, inferHere));
|
|
218
|
+
n.parameters.forEach((p) => visit(p, inferHere));
|
|
219
|
+
if (ts.isMethodDeclaration(n) && n.name !== undefined && ts.isComputedPropertyName(n.name))
|
|
220
|
+
visit(n.name, inferHere);
|
|
221
|
+
if (n.type !== undefined)
|
|
222
|
+
visit(n.type, inferHere);
|
|
223
|
+
else if (n.body !== undefined && isFunctionLikeWithBody(n))
|
|
224
|
+
visit(n.body, true);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (ts.isBindingElement(n)) {
|
|
228
|
+
if (n.name !== undefined && ts.isComputedPropertyName(n.name))
|
|
229
|
+
visit(n.name, inferHere);
|
|
230
|
+
else if (!ts.isIdentifier(n.name))
|
|
231
|
+
visit(n.name, inferHere);
|
|
232
|
+
if (inferHere && n.propertyName !== undefined)
|
|
233
|
+
visit(n.propertyName, inferHere);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (ts.isVariableDeclaration(n) || ts.isPropertyDeclaration(n) || ts.isParameter(n)) {
|
|
237
|
+
if (n.name !== undefined && ts.isComputedPropertyName(n.name))
|
|
238
|
+
visit(n.name, inferHere);
|
|
239
|
+
else if (n.name !== undefined && !ts.isIdentifier(n.name))
|
|
240
|
+
visit(n.name, inferHere);
|
|
241
|
+
if (n.type !== undefined)
|
|
242
|
+
visit(n.type, inferHere);
|
|
243
|
+
else if (n.initializer !== undefined)
|
|
244
|
+
visit(n.initializer, true);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
ts.forEachChild(n, (child) => visit(child, inferHere));
|
|
248
|
+
};
|
|
249
|
+
visit(node, inferring);
|
|
250
|
+
}
|
|
251
|
+
function summarize(inputs, summaries, file) {
|
|
252
|
+
const cached = summaries.get(file);
|
|
253
|
+
if (cached !== undefined)
|
|
254
|
+
return cached;
|
|
255
|
+
const text = inputs.readFile(file);
|
|
256
|
+
if (text === undefined) {
|
|
257
|
+
const empty = {
|
|
258
|
+
missing: true, isScript: false, imports: new Map(), exportsLocal: new Map(), reexports: new Map(),
|
|
259
|
+
stars: [], decls: new Map(), defaultInfo: undefined, defaultAlias: undefined, exportEquals: false,
|
|
260
|
+
exportEqualsInfo: undefined, ambient: [],
|
|
261
|
+
};
|
|
262
|
+
summaries.set(file, empty);
|
|
263
|
+
return empty;
|
|
264
|
+
}
|
|
265
|
+
const sourceFileOptions = inputs.sourceFileOptionsFor(file);
|
|
266
|
+
const sf = ts.createSourceFile(file, text, {
|
|
267
|
+
languageVersion: inputs.languageVersion,
|
|
268
|
+
impliedNodeFormat: sourceFileOptions.impliedNodeFormat,
|
|
269
|
+
}, true, inputs.scriptKindFor(file));
|
|
270
|
+
const resolvedSpecifier = (literal) => ({
|
|
271
|
+
specifier: literal.text,
|
|
272
|
+
key: resolutionKey({
|
|
273
|
+
specifier: literal.text,
|
|
274
|
+
mode: ts.getModeForUsageLocation(sf, literal, sourceFileOptions.compilerOptions),
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
const summary = {
|
|
278
|
+
missing: false, isScript: !ts.isExternalModule(sf), imports: new Map(), exportsLocal: new Map(),
|
|
279
|
+
reexports: new Map(), stars: [], decls: new Map(), defaultInfo: undefined, defaultAlias: undefined,
|
|
280
|
+
exportEquals: false, exportEqualsInfo: undefined, ambient: [],
|
|
281
|
+
};
|
|
282
|
+
const addDecl = (name, info) => {
|
|
283
|
+
const existing = summary.decls.get(name);
|
|
284
|
+
if (existing === undefined)
|
|
285
|
+
summary.decls.set(name, [info]);
|
|
286
|
+
else
|
|
287
|
+
existing.push(info);
|
|
288
|
+
};
|
|
289
|
+
for (const statement of sf.statements) {
|
|
290
|
+
if (ts.isImportDeclaration(statement)) {
|
|
291
|
+
const spec = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier : undefined;
|
|
292
|
+
const clause = statement.importClause;
|
|
293
|
+
if (clause === undefined || spec === undefined)
|
|
294
|
+
continue;
|
|
295
|
+
if (clause.name !== undefined)
|
|
296
|
+
summary.imports.set(clause.name.text, { ...resolvedSpecifier(spec), importedName: "default" });
|
|
297
|
+
const bindings = clause.namedBindings;
|
|
298
|
+
if (bindings !== undefined && ts.isNamespaceImport(bindings)) {
|
|
299
|
+
summary.imports.set(bindings.name.text, { ...resolvedSpecifier(spec), importedName: "*" });
|
|
300
|
+
}
|
|
301
|
+
else if (bindings !== undefined) {
|
|
302
|
+
for (const element of bindings.elements) {
|
|
303
|
+
summary.imports.set(element.name.text, { ...resolvedSpecifier(spec), importedName: (element.propertyName ?? element.name).text });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (ts.isImportEqualsDeclaration(statement)) {
|
|
309
|
+
// `import x = require("./y")` is out of scope for analysis
|
|
310
|
+
// entirely (module-graph.ts's own walk counts it as unsupported
|
|
311
|
+
// syntax and never resolves it) - `import x = SomeNamespace.Y`
|
|
312
|
+
// (the other, in-scope form) is a real local reference, walked
|
|
313
|
+
// the same way any other declaration is.
|
|
314
|
+
if (!ts.isExternalModuleReference(statement.moduleReference)) {
|
|
315
|
+
const info = newInfo();
|
|
316
|
+
const seg = segmentsOf(statement.moduleReference);
|
|
317
|
+
if (seg !== undefined)
|
|
318
|
+
info.refs.push(seg);
|
|
319
|
+
info.position = positionOf(statement, sf, file);
|
|
320
|
+
info.isImportEquals = true;
|
|
321
|
+
addDecl(statement.name.text, info);
|
|
322
|
+
if (hasExportModifier(statement))
|
|
323
|
+
summary.exportsLocal.set(statement.name.text, statement.name.text);
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (ts.isExportDeclaration(statement)) {
|
|
328
|
+
const spec = statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier : undefined;
|
|
329
|
+
if (statement.exportClause === undefined) {
|
|
330
|
+
if (spec !== undefined)
|
|
331
|
+
summary.stars.push(resolvedSpecifier(spec));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (ts.isNamespaceExport(statement.exportClause)) {
|
|
335
|
+
if (spec !== undefined)
|
|
336
|
+
summary.reexports.set(statement.exportClause.name.text, { ...resolvedSpecifier(spec), importedName: "*" });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
for (const element of statement.exportClause.elements) {
|
|
340
|
+
const exported = element.name.text;
|
|
341
|
+
const local = (element.propertyName ?? element.name).text;
|
|
342
|
+
if (spec !== undefined)
|
|
343
|
+
summary.reexports.set(exported, { ...resolvedSpecifier(spec), importedName: local });
|
|
344
|
+
else
|
|
345
|
+
summary.exportsLocal.set(exported, local);
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (ts.isExportAssignment(statement)) {
|
|
350
|
+
const info = newInfo();
|
|
351
|
+
const seg = segmentsOf(statement.expression);
|
|
352
|
+
if (seg !== undefined)
|
|
353
|
+
info.refs.push(seg);
|
|
354
|
+
else
|
|
355
|
+
collect(statement.expression, info, true, sf, sourceFileOptions.compilerOptions);
|
|
356
|
+
// `export default <expr>` (an anonymous expression, no separate
|
|
357
|
+
// named declaration of its own): the checker's own declaration for
|
|
358
|
+
// it is this ExportAssignment statement itself - EXCEPT when the
|
|
359
|
+
// expression is a single identifier (`export default Foo;`),
|
|
360
|
+
// which is an alias to Foo's own declaration (FileSummary's own
|
|
361
|
+
// `defaultAlias` comment; a qualified name has no local name to
|
|
362
|
+
// resolve, so it is left for the resolver to report unresolvable).
|
|
363
|
+
info.position = positionOf(statement, sf, file);
|
|
364
|
+
if (statement.isExportEquals === true) {
|
|
365
|
+
summary.exportEquals = true;
|
|
366
|
+
summary.exportEqualsInfo = info;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
summary.defaultInfo = info;
|
|
370
|
+
summary.defaultAlias = seg === undefined ? undefined : seg.length === 1 ? { name: seg[0] } : { qualified: true };
|
|
371
|
+
}
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
// `declare global` (GlobalAugmentation) and `declare module "literal
|
|
375
|
+
// name"` (a StringLiteral name) bind ambient names no import ever
|
|
376
|
+
// names - a plain `namespace X {}`/`declare namespace X {}` (an
|
|
377
|
+
// Identifier name, handled below with every other ordinary
|
|
378
|
+
// declaration) is reachable the normal way and must not double up
|
|
379
|
+
// here.
|
|
380
|
+
if (ts.isModuleDeclaration(statement) &&
|
|
381
|
+
(statement.name.kind === ts.SyntaxKind.StringLiteral || (statement.flags & ts.NodeFlags.GlobalAugmentation) !== 0)) {
|
|
382
|
+
const info = newInfo();
|
|
383
|
+
collect(statement, info, false, sf, sourceFileOptions.compilerOptions);
|
|
384
|
+
summary.ambient.push(info);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (ts.isVariableStatement(statement)) {
|
|
388
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
389
|
+
const info = newInfo();
|
|
390
|
+
collect(declaration, info, false, sf, sourceFileOptions.compilerOptions);
|
|
391
|
+
// Each bound name gets its OWN position (bindingElements' own
|
|
392
|
+
// comment) - a shared `info` object would give every name
|
|
393
|
+
// destructured from the same declarator the same position,
|
|
394
|
+
// which the checker itself never does.
|
|
395
|
+
for (const { name, node } of bindingElements(declaration.name, declaration, [])) {
|
|
396
|
+
addDecl(name, { refs: info.refs, imports: info.imports, position: positionOf(node, sf, file) });
|
|
397
|
+
if (hasExportModifier(statement))
|
|
398
|
+
summary.exportsLocal.set(name, name);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isClassDeclaration(statement) ||
|
|
404
|
+
ts.isFunctionDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) {
|
|
405
|
+
const name = statement.name !== undefined && ts.isIdentifier(statement.name) ? statement.name.text : undefined;
|
|
406
|
+
const info = newInfo();
|
|
407
|
+
collect(statement, info, false, sf, sourceFileOptions.compilerOptions);
|
|
408
|
+
info.position = positionOf(statement, sf, file);
|
|
409
|
+
const local = name ?? "default";
|
|
410
|
+
addDecl(local, info);
|
|
411
|
+
if (hasExportModifier(statement)) {
|
|
412
|
+
if (hasDefaultModifier(statement))
|
|
413
|
+
summary.exportsLocal.set("default", local);
|
|
414
|
+
else
|
|
415
|
+
summary.exportsLocal.set(local, local);
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
// An expression statement, a bare block, ... - not a declaration and
|
|
420
|
+
// not a form any rule above needs; a script file's own effect (R6,
|
|
421
|
+
// below) never depends on what a statement here says.
|
|
422
|
+
}
|
|
423
|
+
summaries.set(file, summary);
|
|
424
|
+
return summary;
|
|
425
|
+
}
|
|
426
|
+
// A resolved edge that isn't real Program source (module-graph.ts's own
|
|
427
|
+
// resolvedSpecifiers can carry one - a resolved .js with no declaration
|
|
428
|
+
// file, a JSON import, ...) is not a target either builder below can ever
|
|
429
|
+
// load - lifted to module scope (not a closure over one `inputs`) so both
|
|
430
|
+
// `buildTypeClosure` and `computeSyntacticNamedDeclarations` share the identical
|
|
431
|
+
// answer for the identical edge, never two independently-written copies
|
|
432
|
+
// that could drift.
|
|
433
|
+
function resolveTarget(inputs, file, specifier) {
|
|
434
|
+
const target = inputs.resolvedSpecifiers.get(file)?.get(specifier.key);
|
|
435
|
+
return target !== undefined && isProgramSource(target) ? target : undefined;
|
|
436
|
+
}
|
|
437
|
+
// Whether `file` exports `name` at all, following `export *` (a cyclic
|
|
438
|
+
// chain answers false past its own start - the same reasoning
|
|
439
|
+
// `reachExport`'s own `done` guard applies, restated per-call here since
|
|
440
|
+
// this can run inside more than one root's own walk). This function
|
|
441
|
+
// alone does NOT enforce real ESM's own rule that `export *` never
|
|
442
|
+
// carries a "default" - it still answers true for a star target with a
|
|
443
|
+
// direct default of its own (the `name === "default" &&
|
|
444
|
+
// summary.defaultInfo !== undefined` disjunct below matches on THAT
|
|
445
|
+
// target file directly, regardless of how it was reached). `reachExport`
|
|
446
|
+
// above tolerates the resulting over-inclusion (a star-only re-export of
|
|
447
|
+
// "default" it can never really satisfy still marks the target file
|
|
448
|
+
// reached) since a bigger-than-needed closure is still a correct one;
|
|
449
|
+
// `computeSyntacticNamedDeclarations`'s own resolveNamed cannot tolerate
|
|
450
|
+
// that for a NAMED answer, so it never lets `name === "default"` reach
|
|
451
|
+
// this function's own stars branch at all (`resolveNamed`'s own comment
|
|
452
|
+
// on `export *` and "default" has the caller-side guard).
|
|
453
|
+
function hasExport(inputs, summaries, file, name, seen = new Set(), visitedFiles) {
|
|
454
|
+
if (seen.has(file))
|
|
455
|
+
return false;
|
|
456
|
+
seen.add(file);
|
|
457
|
+
visitedFiles?.add(file);
|
|
458
|
+
const summary = summarize(inputs, summaries, file);
|
|
459
|
+
if (summary.exportEquals)
|
|
460
|
+
return true;
|
|
461
|
+
if (summary.exportsLocal.has(name) || summary.reexports.has(name) || (name === "default" && summary.defaultInfo !== undefined))
|
|
462
|
+
return true;
|
|
463
|
+
if (name === "default")
|
|
464
|
+
return false;
|
|
465
|
+
return summary.stars.some((spec) => {
|
|
466
|
+
const target = resolveTarget(inputs, file, spec);
|
|
467
|
+
return target !== undefined && hasExport(inputs, summaries, target, name, seen, visitedFiles);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
export function buildTypeClosure(inputs) {
|
|
471
|
+
const summaries = new Map();
|
|
472
|
+
const closure = new Set();
|
|
473
|
+
const done = new Set();
|
|
474
|
+
function mark(file) { closure.add(file); }
|
|
475
|
+
function resolve(file, specifier) {
|
|
476
|
+
return resolveTarget(inputs, file, specifier);
|
|
477
|
+
}
|
|
478
|
+
// A declaration actually reached: every rule-followed reference/import
|
|
479
|
+
// is dispatched. module-graph.ts's own safety net (a resolution failure
|
|
480
|
+
// rule 6 itself reports while walking the closure Program this
|
|
481
|
+
// function's own output becomes a root list for - see
|
|
482
|
+
// rules/type-leak.ts's own ReportUnresolvedReference) is a separate
|
|
483
|
+
// pass over the real Program, not this syntactic walk; a net built from
|
|
484
|
+
// the same rules it is meant to catch a gap in could never fire.
|
|
485
|
+
//
|
|
486
|
+
// The inference rule reaches only the identifiers the declaration's
|
|
487
|
+
// initializer or body references, resolved the same way every other
|
|
488
|
+
// reference is - `collect` (above) already recorded every one of them
|
|
489
|
+
// into `info.refs` while walking an unannotated declaration's own
|
|
490
|
+
// value position, so the loop below needs no separate step for it.
|
|
491
|
+
// Reaching every import binding of the file instead doubles rule 6's
|
|
492
|
+
// own Program on a 23,000-file codebase, with no change in findings:
|
|
493
|
+
// every identifier that can shape an inferred type already appears in
|
|
494
|
+
// the expression itself.
|
|
495
|
+
function processInfo(file, info) {
|
|
496
|
+
for (const ref of info.refs)
|
|
497
|
+
reachRef(file, ref);
|
|
498
|
+
for (const use of info.imports) {
|
|
499
|
+
const target = resolve(file, use);
|
|
500
|
+
if (target === undefined)
|
|
501
|
+
continue;
|
|
502
|
+
if (use.qualifier !== undefined)
|
|
503
|
+
reachExport(target, use.qualifier[0], use.qualifier.slice(1));
|
|
504
|
+
else
|
|
505
|
+
reachWhole(target);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function reachRef(file, segments) {
|
|
509
|
+
const summary = summarize(inputs, summaries, file);
|
|
510
|
+
const [head, ...rest] = segments;
|
|
511
|
+
if (head === undefined)
|
|
512
|
+
return;
|
|
513
|
+
if (summary.decls.has(head))
|
|
514
|
+
reachLocal(file, head);
|
|
515
|
+
const binding = summary.imports.get(head);
|
|
516
|
+
if (binding !== undefined) {
|
|
517
|
+
const target = resolve(file, binding);
|
|
518
|
+
if (target === undefined)
|
|
519
|
+
return;
|
|
520
|
+
if (binding.importedName === "*") {
|
|
521
|
+
if (rest.length > 0)
|
|
522
|
+
reachExport(target, rest[0], rest.slice(1));
|
|
523
|
+
else
|
|
524
|
+
reachWhole(target);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
reachExport(target, binding.importedName, rest);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function reachLocal(file, name) {
|
|
532
|
+
const key = `${file}\0L\0${name}`;
|
|
533
|
+
if (done.has(key))
|
|
534
|
+
return;
|
|
535
|
+
done.add(key);
|
|
536
|
+
mark(file);
|
|
537
|
+
const summary = summarize(inputs, summaries, file);
|
|
538
|
+
for (const info of summary.decls.get(name) ?? [])
|
|
539
|
+
processInfo(file, info);
|
|
540
|
+
// A declaration and an import can share one local name only when the
|
|
541
|
+
// import is itself the declaration (there is no local decls entry
|
|
542
|
+
// for a plain re-bound import name) - checked regardless, since a
|
|
543
|
+
// name that is only ever an import binding never appears in `decls`
|
|
544
|
+
// at all, and this call would otherwise never follow it.
|
|
545
|
+
if (summary.imports.has(name))
|
|
546
|
+
reachRef(file, [name]);
|
|
547
|
+
}
|
|
548
|
+
function reachExport(file, name, rest = []) {
|
|
549
|
+
const key = `${file}\0E\0${name}\0${rest.join(".")}`;
|
|
550
|
+
if (done.has(key))
|
|
551
|
+
return;
|
|
552
|
+
done.add(key);
|
|
553
|
+
const summary = summarize(inputs, summaries, file);
|
|
554
|
+
if (summary.missing)
|
|
555
|
+
return;
|
|
556
|
+
if (summary.exportEquals) {
|
|
557
|
+
mark(file);
|
|
558
|
+
if (summary.exportEqualsInfo !== undefined)
|
|
559
|
+
processInfo(file, summary.exportEqualsInfo);
|
|
560
|
+
reachWhole(file);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const local = summary.exportsLocal.get(name);
|
|
564
|
+
if (local !== undefined) {
|
|
565
|
+
mark(file);
|
|
566
|
+
// `export { ns }` where `ns` is itself a namespace import: the
|
|
567
|
+
// export chain's own target is the imported module, qualified by
|
|
568
|
+
// whatever the caller still needs past this hop.
|
|
569
|
+
if (rest.length > 0 && summary.imports.get(local)?.importedName === "*")
|
|
570
|
+
reachRef(file, [local, ...rest]);
|
|
571
|
+
else
|
|
572
|
+
reachLocal(file, local);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (name === "default" && summary.defaultInfo !== undefined) {
|
|
576
|
+
mark(file);
|
|
577
|
+
processInfo(file, summary.defaultInfo);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const reexport = summary.reexports.get(name);
|
|
581
|
+
if (reexport !== undefined) {
|
|
582
|
+
mark(file);
|
|
583
|
+
const target = resolve(file, reexport);
|
|
584
|
+
if (target === undefined)
|
|
585
|
+
return;
|
|
586
|
+
if (reexport.importedName === "*") {
|
|
587
|
+
if (rest.length > 0)
|
|
588
|
+
reachExport(target, rest[0], rest.slice(1));
|
|
589
|
+
else
|
|
590
|
+
reachAllExports(target);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
reachExport(target, reexport.importedName, rest);
|
|
594
|
+
}
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
for (const spec of summary.stars) {
|
|
598
|
+
const target = resolve(file, spec);
|
|
599
|
+
if (target !== undefined && hasExport(inputs, summaries, target, name)) {
|
|
600
|
+
mark(file);
|
|
601
|
+
reachExport(target, name, rest);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function reachAllExports(file) {
|
|
606
|
+
const key = `${file}\0AE`;
|
|
607
|
+
if (done.has(key))
|
|
608
|
+
return;
|
|
609
|
+
done.add(key);
|
|
610
|
+
const summary = summarize(inputs, summaries, file);
|
|
611
|
+
if (summary.missing)
|
|
612
|
+
return;
|
|
613
|
+
mark(file);
|
|
614
|
+
for (const name of summary.exportsLocal.keys())
|
|
615
|
+
reachExport(file, name);
|
|
616
|
+
for (const name of summary.reexports.keys())
|
|
617
|
+
reachExport(file, name);
|
|
618
|
+
if (summary.defaultInfo !== undefined)
|
|
619
|
+
reachExport(file, "default");
|
|
620
|
+
if (summary.exportEquals)
|
|
621
|
+
reachExport(file, "=");
|
|
622
|
+
for (const spec of summary.stars) {
|
|
623
|
+
const target = resolve(file, spec);
|
|
624
|
+
if (target !== undefined)
|
|
625
|
+
reachAllExports(target);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function reachWhole(file) {
|
|
629
|
+
const key = `${file}\0W`;
|
|
630
|
+
if (done.has(key))
|
|
631
|
+
return;
|
|
632
|
+
done.add(key);
|
|
633
|
+
const summary = summarize(inputs, summaries, file);
|
|
634
|
+
if (summary.missing)
|
|
635
|
+
return;
|
|
636
|
+
mark(file);
|
|
637
|
+
reachAllExports(file);
|
|
638
|
+
for (const name of summary.decls.keys())
|
|
639
|
+
reachLocal(file, name);
|
|
640
|
+
for (const info of summary.ambient)
|
|
641
|
+
processInfo(file, info);
|
|
642
|
+
}
|
|
643
|
+
for (const file of inputs.surfaceFiles)
|
|
644
|
+
reachAllExports(file);
|
|
645
|
+
for (const file of inputs.extraRoots ?? [])
|
|
646
|
+
reachWhole(file);
|
|
647
|
+
// Ambient roots: a script file (no import, no export at all) or a file
|
|
648
|
+
// holding `declare global`/`declare module "..."` binds names no
|
|
649
|
+
// import statement ever names, so nothing above can discover it by
|
|
650
|
+
// following an edge - `inputs.ambientFiles` already names every one
|
|
651
|
+
// (module-graph.ts's own per-file walk flagged each from real syntax,
|
|
652
|
+
// during the pass every build already makes over every analyzed file);
|
|
653
|
+
// this loop parses exactly these, never the rest.
|
|
654
|
+
//
|
|
655
|
+
// reachAmbientRoot reaches only the ambient content itself
|
|
656
|
+
// (`summary.ambient`, every `declare global`/`declare module "..."`
|
|
657
|
+
// body) and, for an actual script (no import/export at all - the
|
|
658
|
+
// checker treats it as a global scope, not a module), every one of its
|
|
659
|
+
// top-level declarations - never `reachAllExports` and never every
|
|
660
|
+
// `decls` key unconditionally: an ordinary MODULE that also augments the
|
|
661
|
+
// global scope (`export {}; declare global { ... }`) binds nothing
|
|
662
|
+
// ambient beyond that block, so its own unrelated exports and locals
|
|
663
|
+
// stay reachable the normal way, by whoever actually imports them - not
|
|
664
|
+
// forced in as a root just because the same file also happens to hold a
|
|
665
|
+
// `declare global`. `processInfo` on each reached declaration already
|
|
666
|
+
// follows every type position, inferred reference, and import it
|
|
667
|
+
// actually uses, the same as any other reached declaration - a plain
|
|
668
|
+
// import at the top of an ambient file joins the closure only when one
|
|
669
|
+
// of these declarations references it, never unconditionally.
|
|
670
|
+
function reachAmbientRoot(file) {
|
|
671
|
+
const key = `${file}\0AR`;
|
|
672
|
+
if (done.has(key))
|
|
673
|
+
return;
|
|
674
|
+
done.add(key);
|
|
675
|
+
const summary = summarize(inputs, summaries, file);
|
|
676
|
+
if (summary.missing)
|
|
677
|
+
return;
|
|
678
|
+
mark(file);
|
|
679
|
+
for (const info of summary.ambient)
|
|
680
|
+
processInfo(file, info);
|
|
681
|
+
if (summary.isScript) {
|
|
682
|
+
for (const name of summary.decls.keys())
|
|
683
|
+
reachLocal(file, name);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (const file of inputs.ambientFiles)
|
|
687
|
+
reachAmbientRoot(file);
|
|
688
|
+
return { files: [...closure].sort() };
|
|
689
|
+
}
|
|
690
|
+
// The one place a checker-derived declaration (type-leak.ts's own
|
|
691
|
+
// `collectNamedDeclarations`) and a syntactically-parsed one (this
|
|
692
|
+
// module's own `computeSyntacticNamedDeclarations`, below) are turned into
|
|
693
|
+
// the same string, so the two can be compared - or merged into one set -
|
|
694
|
+
// by value, never by object identity (a syntactic parse has no bound
|
|
695
|
+
// Program, so it can never share a node reference with the checker's
|
|
696
|
+
// own). Position alone (file, 1-based line, 1-based column of the
|
|
697
|
+
// declaration's own start) is enough: two different declarations can
|
|
698
|
+
// never share one file's one offset, so a declared NAME is never part of
|
|
699
|
+
// the key at all.
|
|
700
|
+
export function declarationKey(file, line, column) {
|
|
701
|
+
return `${file.replaceAll("\\", "/")}\0${line}\0${column}`;
|
|
702
|
+
}
|
|
703
|
+
// A SourceFile and its first declaration can start at the same offset.
|
|
704
|
+
// Reusing a position key is refused because it can make that declaration public.
|
|
705
|
+
export function sourceFileKey(file) {
|
|
706
|
+
return `${file.replaceAll("\\", "/")}\0<sourcefile>`;
|
|
707
|
+
}
|
|
708
|
+
// The syntactic twin of type-leak.ts's own `collectNamedDeclarations`:
|
|
709
|
+
// every declaration a consumer can already reach under some public name,
|
|
710
|
+
// computed from syntax alone (export chains, `export *`, aliases - the
|
|
711
|
+
// same rules `reachExport`/`hasExport` above already follow for file
|
|
712
|
+
// reachability), instead of a bound checker walking a real Program. Exists
|
|
713
|
+
// so rule 6 can be scoped to one module's own surface files - a closure
|
|
714
|
+
// Program that never loads every OTHER surface's own files still needs to
|
|
715
|
+
// know every name those other surfaces expose, the one thing scoping the
|
|
716
|
+
// Program cannot also scope away (a type a consumer can already import
|
|
717
|
+
// from a module outside the closure is still not a leak).
|
|
718
|
+
//
|
|
719
|
+
// `visitedFiles` contains each file whose export syntax can affect the
|
|
720
|
+
// answer. Returning this set is required because an augmentation can add
|
|
721
|
+
// a name there. Treating every analyzed augmentation as relevant is
|
|
722
|
+
// refused because ambient roots already apply unrelated augmentations.
|
|
723
|
+
//
|
|
724
|
+
// `unresolvable`: true the moment this walk crosses a shape it cannot
|
|
725
|
+
// answer as confidently as the checker would (an `export =` target, an
|
|
726
|
+
// `import x = SomeNamespace.Y` export, or a specifier reached from a
|
|
727
|
+
// non-analyzed project file whose edges were not recorded - DeclInfo's own
|
|
728
|
+
// `isImportEquals` comment covers the second case). The caller falls back to
|
|
729
|
+
// treating every surface as a closure root and the checker's own named set
|
|
730
|
+
// when this is true, the same as if scoping had never been requested. Silently
|
|
731
|
+
// omitting a name here instead would read an already-named declaration as
|
|
732
|
+
// unnamed on its next surface - a false leak - so an unresolvable shape stops
|
|
733
|
+
// the whole computation rather than under-reporting one name and continuing.
|
|
734
|
+
export function computeSyntacticNamedDeclarations(inputs, surfaceFiles) {
|
|
735
|
+
const summaries = new Map();
|
|
736
|
+
const keys = new Set();
|
|
737
|
+
const visitedFiles = new Set();
|
|
738
|
+
let unresolvable = false;
|
|
739
|
+
const resolvedAllExportsOf = new Set();
|
|
740
|
+
const resolvedNames = new Set();
|
|
741
|
+
// The graph records specifier resolutions only for analyzed files. A
|
|
742
|
+
// non-analyzed project file reached through one of those edges can therefore
|
|
743
|
+
// have real declarations beyond a missing record. Guessing that it exports
|
|
744
|
+
// nothing would create false leaks. An analyzed file's missing record is a
|
|
745
|
+
// real unresolved or non-program target, and an external package file can
|
|
746
|
+
// safely keep the checker's own no-declaration answer.
|
|
747
|
+
function resolveWalkTarget(file, specifier) {
|
|
748
|
+
const target = resolveTarget(inputs, file, specifier);
|
|
749
|
+
if (target === undefined && !inputs.analyzedFiles.has(file) &&
|
|
750
|
+
!file.split(/[\\/]/).includes("node_modules"))
|
|
751
|
+
unresolvable = true;
|
|
752
|
+
return target;
|
|
753
|
+
}
|
|
754
|
+
function addDeclInfo(info) {
|
|
755
|
+
// Every DeclInfo this walk can reach `summarize` sets a position on -
|
|
756
|
+
// `isImportEquals` is the one shape known in advance to need the
|
|
757
|
+
// fallback instead (DeclInfo's own comment); a missing position on
|
|
758
|
+
// any other shape would be this module's own bug, not a real project
|
|
759
|
+
// input, so it takes the same safe path rather than silently
|
|
760
|
+
// dropping the name.
|
|
761
|
+
if (info.isImportEquals === true || info.position === undefined) {
|
|
762
|
+
unresolvable = true;
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
keys.add(declarationKey(info.position.file, info.position.line, info.position.column));
|
|
766
|
+
}
|
|
767
|
+
// A namespace-shaped resolution names the target SourceFile. A position
|
|
768
|
+
// key is refused because the first declaration can start at the same offset.
|
|
769
|
+
function addSourceFileKey(target) {
|
|
770
|
+
visitedFiles.add(target);
|
|
771
|
+
keys.add(sourceFileKey(target));
|
|
772
|
+
}
|
|
773
|
+
// `export default <expr>` where `expr` is a single identifier
|
|
774
|
+
// (`export default Foo;`) is an alias - resolved exactly like a named
|
|
775
|
+
// export of that local name (FileSummary's own `defaultAlias`
|
|
776
|
+
// comment), never at the ExportAssignment's own position. Every other
|
|
777
|
+
// shape (a class/function expression, a literal, ...) keeps the
|
|
778
|
+
// ExportAssignment's own position, already on `defaultInfo`.
|
|
779
|
+
function resolveDefault(file, summary) {
|
|
780
|
+
const alias = summary.defaultAlias;
|
|
781
|
+
if (alias === undefined) {
|
|
782
|
+
addDeclInfo(summary.defaultInfo);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if ("qualified" in alias) {
|
|
786
|
+
unresolvable = true;
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const decls = summary.decls.get(alias.name);
|
|
790
|
+
if (decls !== undefined) {
|
|
791
|
+
for (const info of decls)
|
|
792
|
+
addDeclInfo(info);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const binding = summary.imports.get(alias.name);
|
|
796
|
+
if (binding !== undefined) {
|
|
797
|
+
const target = resolveWalkTarget(file, binding);
|
|
798
|
+
if (target === undefined)
|
|
799
|
+
return;
|
|
800
|
+
if (binding.importedName === "*") {
|
|
801
|
+
addSourceFileKey(target);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
resolveNamed(target, binding.importedName);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
unresolvable = true;
|
|
808
|
+
}
|
|
809
|
+
// Resolves `file`'s own export named `name` to every real declaration
|
|
810
|
+
// it names - the same priority order `reachExport` follows above (a
|
|
811
|
+
// local export, then `default`, then a named re-export, then `export
|
|
812
|
+
// *`, first match in declared order wins), which is what makes this
|
|
813
|
+
// agree with a real checker on which of two `export *` sources naming
|
|
814
|
+
// the same identifier wins, and on a local export that shadows one.
|
|
815
|
+
function resolveNamed(file, name) {
|
|
816
|
+
const key = `${file}\0${name}`;
|
|
817
|
+
if (resolvedNames.has(key))
|
|
818
|
+
return;
|
|
819
|
+
resolvedNames.add(key);
|
|
820
|
+
visitedFiles.add(file);
|
|
821
|
+
const summary = summarize(inputs, summaries, file);
|
|
822
|
+
if (summary.missing)
|
|
823
|
+
return; // an edge with no real target: the checker's own symbol there has no declarations either
|
|
824
|
+
if (summary.exportEquals) {
|
|
825
|
+
unresolvable = true;
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const local = summary.exportsLocal.get(name);
|
|
829
|
+
if (local !== undefined) {
|
|
830
|
+
const decls = summary.decls.get(local);
|
|
831
|
+
if (decls !== undefined) {
|
|
832
|
+
for (const info of decls)
|
|
833
|
+
addDeclInfo(info);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const binding = summary.imports.get(local);
|
|
837
|
+
if (binding !== undefined) {
|
|
838
|
+
const target = resolveWalkTarget(file, binding);
|
|
839
|
+
if (target === undefined)
|
|
840
|
+
return;
|
|
841
|
+
// A re-exported namespace import (`import * as NS from "./x";
|
|
842
|
+
// export { NS };`): the checker's own declaration for it is the
|
|
843
|
+
// target's own SourceFile, the same as `export * as ns` below.
|
|
844
|
+
if (binding.importedName === "*") {
|
|
845
|
+
addSourceFileKey(target);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
resolveNamed(target, binding.importedName);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
// `exportsLocal` named a local that is neither a real declaration
|
|
852
|
+
// nor an import binding - not a shape this walk expects to exist;
|
|
853
|
+
// treated as unresolvable rather than guessed at.
|
|
854
|
+
unresolvable = true;
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (name === "default" && summary.defaultInfo !== undefined) {
|
|
858
|
+
resolveDefault(file, summary);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const reexport = summary.reexports.get(name);
|
|
862
|
+
if (reexport !== undefined) {
|
|
863
|
+
const target = resolveWalkTarget(file, reexport);
|
|
864
|
+
if (target === undefined)
|
|
865
|
+
return;
|
|
866
|
+
// `export * as ns from "./x"`: the checker's own declaration for
|
|
867
|
+
// `ns` is `./x`'s own SourceFile (measured directly against a real
|
|
868
|
+
// checker), not any one declaration inside it.
|
|
869
|
+
if (reexport.importedName === "*") {
|
|
870
|
+
addSourceFileKey(target);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
resolveNamed(target, reexport.importedName);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
// `export *` never carries a "default" of its own - hasExport's own
|
|
877
|
+
// header - so a plain `export {default as X} from` a star-only
|
|
878
|
+
// source resolves to nothing here too, matching a real checker
|
|
879
|
+
// exactly (measured directly: `getExportsOfModule` gives that name no
|
|
880
|
+
// declarations at all in that shape).
|
|
881
|
+
if (name === "default")
|
|
882
|
+
return;
|
|
883
|
+
for (const spec of summary.stars) {
|
|
884
|
+
const target = resolveWalkTarget(file, spec);
|
|
885
|
+
if (target !== undefined && hasExport(inputs, summaries, target, name, new Set(), visitedFiles)) {
|
|
886
|
+
resolveNamed(target, name);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// Every name reachable through `file`'s own `export *` chain (not
|
|
892
|
+
// `file`'s own direct names - callers already have those from
|
|
893
|
+
// `exportsLocal`/`reexports`/`defaultInfo` directly) - never "default"
|
|
894
|
+
// (hasExport's own header: `export *` carries no default). `seen`
|
|
895
|
+
// guards a cyclic chain the same way `hasExport`'s own does.
|
|
896
|
+
function collectStarNames(file, seen = new Set()) {
|
|
897
|
+
const names = new Set();
|
|
898
|
+
if (seen.has(file))
|
|
899
|
+
return names;
|
|
900
|
+
seen.add(file);
|
|
901
|
+
visitedFiles.add(file);
|
|
902
|
+
const summary = summarize(inputs, summaries, file);
|
|
903
|
+
if (summary.missing)
|
|
904
|
+
return names;
|
|
905
|
+
for (const name of summary.exportsLocal.keys())
|
|
906
|
+
names.add(name);
|
|
907
|
+
for (const name of summary.reexports.keys())
|
|
908
|
+
names.add(name);
|
|
909
|
+
for (const spec of summary.stars) {
|
|
910
|
+
const target = resolveWalkTarget(file, spec);
|
|
911
|
+
if (target !== undefined)
|
|
912
|
+
for (const name of collectStarNames(target, seen))
|
|
913
|
+
names.add(name);
|
|
914
|
+
}
|
|
915
|
+
return names;
|
|
916
|
+
}
|
|
917
|
+
// Every name `file` itself claims to export, including one it only
|
|
918
|
+
// gets through `export *` - the syntactic mirror of `reachAllExports`
|
|
919
|
+
// above, over export NAMES instead of files to load. Every name -
|
|
920
|
+
// direct or star-inherited alike - resolves through `resolveNamed(file,
|
|
921
|
+
// name)`, never `resolveNamed(target, name)` on a star's own target
|
|
922
|
+
// directly: `resolveNamed`'s own stars loop is what decides which of
|
|
923
|
+
// several `export *` sources naming the same identifier wins (the
|
|
924
|
+
// first, in declared order) - calling straight into a star's own
|
|
925
|
+
// target here instead would add every one of them, the same
|
|
926
|
+
// over-inclusion `reachAllExports` above tolerates for file
|
|
927
|
+
// reachability (see hasExport's own header) but a NAMED answer cannot:
|
|
928
|
+
// the checker names exactly one declaration for a colliding name, not
|
|
929
|
+
// every source that happens to offer one.
|
|
930
|
+
function resolveAllExportsOf(file) {
|
|
931
|
+
if (resolvedAllExportsOf.has(file))
|
|
932
|
+
return;
|
|
933
|
+
resolvedAllExportsOf.add(file);
|
|
934
|
+
visitedFiles.add(file);
|
|
935
|
+
const summary = summarize(inputs, summaries, file);
|
|
936
|
+
if (summary.missing)
|
|
937
|
+
return;
|
|
938
|
+
if (summary.exportEquals) {
|
|
939
|
+
unresolvable = true;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
for (const name of summary.exportsLocal.keys())
|
|
943
|
+
resolveNamed(file, name);
|
|
944
|
+
for (const name of summary.reexports.keys())
|
|
945
|
+
resolveNamed(file, name);
|
|
946
|
+
if (summary.defaultInfo !== undefined)
|
|
947
|
+
resolveNamed(file, "default");
|
|
948
|
+
for (const spec of summary.stars) {
|
|
949
|
+
const target = resolveWalkTarget(file, spec);
|
|
950
|
+
if (target === undefined)
|
|
951
|
+
continue;
|
|
952
|
+
for (const name of collectStarNames(target))
|
|
953
|
+
resolveNamed(file, name);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
for (const file of surfaceFiles)
|
|
957
|
+
resolveAllExportsOf(file);
|
|
958
|
+
return { keys, visitedFiles, unresolvable };
|
|
959
|
+
}
|