fallow-type-aware 0.0.0-bootstrap.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/LICENSE +21 -0
- package/README.md +48 -0
- package/fallow-type-aware.mjs +11 -0
- package/package.json +37 -0
- package/src/cli.mjs +167 -0
- package/src/file-identity.mjs +11 -0
- package/src/generated-protocol.mjs +27 -0
- package/src/graph-algorithms.mjs +114 -0
- package/src/project-state.mjs +194 -0
- package/src/protocol.mjs +407 -0
- package/src/response-normalization.mjs +28 -0
- package/src/semantic-identity.mjs +315 -0
- package/src/semantic.mjs +2142 -0
- package/src/symbol-impact.mjs +220 -0
- package/src/type-coupling.mjs +166 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { SymbolFlags } from "typescript/unstable/sync";
|
|
4
|
+
import {
|
|
5
|
+
isClassDeclaration,
|
|
6
|
+
isClassExpression,
|
|
7
|
+
isEnumDeclaration,
|
|
8
|
+
isExportSpecifier,
|
|
9
|
+
isFunctionDeclaration,
|
|
10
|
+
isGetAccessorDeclaration,
|
|
11
|
+
isInterfaceDeclaration,
|
|
12
|
+
isMethodDeclaration,
|
|
13
|
+
isMethodSignatureDeclaration,
|
|
14
|
+
isModuleDeclaration,
|
|
15
|
+
isPropertyDeclaration,
|
|
16
|
+
isPropertySignatureDeclaration,
|
|
17
|
+
isSetAccessorDeclaration,
|
|
18
|
+
isTypeAliasDeclaration,
|
|
19
|
+
isVariableDeclaration,
|
|
20
|
+
} from "typescript/unstable/ast/is";
|
|
21
|
+
|
|
22
|
+
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
23
|
+
|
|
24
|
+
const DECLARATION_KIND_RULES = [
|
|
25
|
+
[[isClassDeclaration, isClassExpression], "class"],
|
|
26
|
+
[[isInterfaceDeclaration], "interface"],
|
|
27
|
+
[[isTypeAliasDeclaration], "type_alias"],
|
|
28
|
+
[[isEnumDeclaration], "enum"],
|
|
29
|
+
[[isFunctionDeclaration], "function"],
|
|
30
|
+
[[isModuleDeclaration], "namespace"],
|
|
31
|
+
[[isVariableDeclaration], "variable"],
|
|
32
|
+
[
|
|
33
|
+
[
|
|
34
|
+
isMethodDeclaration,
|
|
35
|
+
isMethodSignatureDeclaration,
|
|
36
|
+
isGetAccessorDeclaration,
|
|
37
|
+
isSetAccessorDeclaration,
|
|
38
|
+
],
|
|
39
|
+
"class_method",
|
|
40
|
+
],
|
|
41
|
+
[[isPropertyDeclaration, isPropertySignatureDeclaration], "class_property"],
|
|
42
|
+
];
|
|
43
|
+
const TYPE_ONLY_DECLARATIONS = [
|
|
44
|
+
isInterfaceDeclaration,
|
|
45
|
+
isTypeAliasDeclaration,
|
|
46
|
+
isPropertySignatureDeclaration,
|
|
47
|
+
];
|
|
48
|
+
const DUAL_NAMESPACE_DECLARATIONS = [isClassDeclaration, isClassExpression, isEnumDeclaration];
|
|
49
|
+
const DECLARATION_OWNER_NODES = [isClassDeclaration, isClassExpression, isInterfaceDeclaration];
|
|
50
|
+
|
|
51
|
+
const slash = (value) => value.split(path.sep).join("/");
|
|
52
|
+
const matchesAny = (node, checks) => checks.some((check) => check(node));
|
|
53
|
+
|
|
54
|
+
export const relativePath = (root, fileName) =>
|
|
55
|
+
slash(path.relative(canonicalFileIdentity(root), canonicalFileIdentity(fileName)));
|
|
56
|
+
|
|
57
|
+
export const sourceFileIdentity = (sourceFile) => canonicalFileIdentity(sourceFile.fileName);
|
|
58
|
+
|
|
59
|
+
export const positions = (node) => {
|
|
60
|
+
const sourceFile = node.getSourceFile();
|
|
61
|
+
const nodes = node.name ? [node, node.name] : [node];
|
|
62
|
+
return nodes.map((candidate) => {
|
|
63
|
+
const start = candidate.getStart(sourceFile);
|
|
64
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(start);
|
|
65
|
+
const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0);
|
|
66
|
+
return {
|
|
67
|
+
line: line + 1,
|
|
68
|
+
col: Buffer.byteLength(sourceFile.text.slice(lineStart, start), "utf8"),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const nodeText = (node) => node?.text;
|
|
74
|
+
|
|
75
|
+
export const declarationKind = (node) =>
|
|
76
|
+
DECLARATION_KIND_RULES.find(([checks]) => matchesAny(node, checks))?.[1];
|
|
77
|
+
|
|
78
|
+
export const declarationNamespaces = (node) =>
|
|
79
|
+
matchesAny(node, TYPE_ONLY_DECLARATIONS)
|
|
80
|
+
? new Set(["type"])
|
|
81
|
+
: new Set(matchesAny(node, DUAL_NAMESPACE_DECLARATIONS) ? ["type", "value"] : ["value"]);
|
|
82
|
+
|
|
83
|
+
export const declarationName = (node) => node.name?.text;
|
|
84
|
+
|
|
85
|
+
const ownerName = (node) => {
|
|
86
|
+
if (!node) return null;
|
|
87
|
+
if (matchesAny(node, DECLARATION_OWNER_NODES)) return nodeText(node.name) ?? null;
|
|
88
|
+
return ownerName(node.parent);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const declarationOwner = (node) => ownerName(node.parent);
|
|
92
|
+
|
|
93
|
+
export const ownerDeclaration = (node) => {
|
|
94
|
+
let current = node?.parent;
|
|
95
|
+
while (current) {
|
|
96
|
+
if (matchesAny(current, DECLARATION_OWNER_NODES)) return current;
|
|
97
|
+
current = current.parent;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const isDeclaration = (node) => declarationKind(node) !== undefined;
|
|
103
|
+
|
|
104
|
+
const visit = (node, callback) => {
|
|
105
|
+
callback(node);
|
|
106
|
+
node.forEachChild((child) => {
|
|
107
|
+
visit(child, callback);
|
|
108
|
+
return undefined;
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const declarationIndexes = new WeakMap();
|
|
113
|
+
|
|
114
|
+
const anchorKey = ({ declarationKind: kind, localName, exportedName, owner, line, col }) =>
|
|
115
|
+
[kind, localName, exportedName, owner ?? "", line, col].join("\0");
|
|
116
|
+
|
|
117
|
+
const setExportAnchor = (index, node, localName, exportedName, owner, position) => {
|
|
118
|
+
index.set(
|
|
119
|
+
anchorKey({
|
|
120
|
+
declarationKind: "export",
|
|
121
|
+
localName,
|
|
122
|
+
exportedName,
|
|
123
|
+
owner,
|
|
124
|
+
...position,
|
|
125
|
+
}),
|
|
126
|
+
node,
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const exportSpecifierNames = (node) => {
|
|
131
|
+
const exportedName = nodeText(node.name);
|
|
132
|
+
return {
|
|
133
|
+
exportedName,
|
|
134
|
+
localName: nodeText(node.propertyName) ?? exportedName,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const exportSpecifierPositions = (node) =>
|
|
139
|
+
[node, node.name, node.propertyName].filter(Boolean).flatMap((candidate) => positions(candidate));
|
|
140
|
+
|
|
141
|
+
const indexExportPosition = (index, node, names, position) => {
|
|
142
|
+
for (const localName of new Set([names.localName, names.exportedName])) {
|
|
143
|
+
setExportAnchor(index, node, localName, names.exportedName, null, position);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const indexExportSpecifier = (index, node) => {
|
|
148
|
+
const names = exportSpecifierNames(node);
|
|
149
|
+
exportSpecifierPositions(node).forEach((position) =>
|
|
150
|
+
indexExportPosition(index, node, names, position),
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const defaultModifierFor = (node) =>
|
|
155
|
+
node.modifiers?.find((modifier) => modifier.getText(node.getSourceFile()) === "default");
|
|
156
|
+
|
|
157
|
+
const indexNamedDeclaration = (index, node, localName) => {
|
|
158
|
+
for (const position of positions(node)) {
|
|
159
|
+
const common = {
|
|
160
|
+
localName,
|
|
161
|
+
exportedName: localName,
|
|
162
|
+
owner: declarationOwner(node),
|
|
163
|
+
...position,
|
|
164
|
+
};
|
|
165
|
+
index.set(anchorKey({ declarationKind: declarationKind(node), ...common }), node);
|
|
166
|
+
index.set(anchorKey({ declarationKind: "export", ...common }), node);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const indexDefaultDeclaration = (index, node, localName, defaultModifier) => {
|
|
171
|
+
if (!defaultModifier) return;
|
|
172
|
+
for (const position of [...positions(node), ...positions(defaultModifier)]) {
|
|
173
|
+
for (const identityLocalName of new Set([localName, "default"])) {
|
|
174
|
+
setExportAnchor(index, node, identityLocalName, "default", declarationOwner(node), position);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const declarationLocalName = (node, defaultModifier) =>
|
|
180
|
+
declarationName(node) ?? (defaultModifier ? "default" : undefined);
|
|
181
|
+
|
|
182
|
+
const indexDeclarationNode = (index, node) => {
|
|
183
|
+
if (!isDeclaration(node)) return;
|
|
184
|
+
const defaultModifier = defaultModifierFor(node);
|
|
185
|
+
const localName = declarationLocalName(node, defaultModifier);
|
|
186
|
+
if (!localName) return;
|
|
187
|
+
indexNamedDeclaration(index, node, localName);
|
|
188
|
+
indexDefaultDeclaration(index, node, localName, defaultModifier);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const declarationIndex = (sourceFile) => {
|
|
192
|
+
const cached = declarationIndexes.get(sourceFile);
|
|
193
|
+
if (cached) return cached;
|
|
194
|
+
const index = new Map();
|
|
195
|
+
visit(sourceFile, (node) => {
|
|
196
|
+
if (isExportSpecifier(node)) {
|
|
197
|
+
indexExportSpecifier(index, node);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
indexDeclarationNode(index, node);
|
|
201
|
+
});
|
|
202
|
+
declarationIndexes.set(sourceFile, index);
|
|
203
|
+
return index;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const findDeclaration = (sourceFile, identity) => {
|
|
207
|
+
const anchor = declarationIndex(sourceFile).get(anchorKey(identity));
|
|
208
|
+
if (!anchor || isExportSpecifier(anchor)) return anchor;
|
|
209
|
+
return declarationNamespaces(anchor).has(identity.namespace) ? anchor : undefined;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const symbolForDeclaration = (project, declaration) =>
|
|
213
|
+
project.checker.getSymbolAtLocation(declaration.name ?? declaration);
|
|
214
|
+
|
|
215
|
+
export const resolveAlias = (checker, symbol) => {
|
|
216
|
+
if (!symbol) return undefined;
|
|
217
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) return symbol;
|
|
218
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
219
|
+
return checker.isUnknownSymbol(aliased) ? symbol : aliased;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const declarationsForSymbol = (project, symbol) =>
|
|
223
|
+
(symbol?.declarations ?? []).map((handle) => handle.resolve(project)).filter(Boolean);
|
|
224
|
+
|
|
225
|
+
export const stableDeclarationKey = (node, namespace = "value") => {
|
|
226
|
+
const first = positions(node)[0];
|
|
227
|
+
return [
|
|
228
|
+
sourceFileIdentity(node.getSourceFile()),
|
|
229
|
+
namespace,
|
|
230
|
+
declarationKind(node) ?? "unknown",
|
|
231
|
+
declarationName(node) ?? "default",
|
|
232
|
+
first.line,
|
|
233
|
+
first.col,
|
|
234
|
+
declarationOwner(node) ?? "",
|
|
235
|
+
].join("\0");
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const stableSymbolIdentity = (root, declaration, namespace, exportedName) => {
|
|
239
|
+
const position = positions(declaration)[0];
|
|
240
|
+
return {
|
|
241
|
+
path: relativePath(root, declaration.getSourceFile().fileName),
|
|
242
|
+
namespace,
|
|
243
|
+
declaration_kind: declarationKind(declaration) ?? "unknown",
|
|
244
|
+
exported_name: exportedName,
|
|
245
|
+
local_name: declarationName(declaration) ?? exportedName,
|
|
246
|
+
line: position.line,
|
|
247
|
+
col: position.col,
|
|
248
|
+
owner: declarationOwner(declaration),
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const semanticQueryIdentity = (root, query, resolved) => {
|
|
253
|
+
if (query.symbol.declarationKind !== "export") {
|
|
254
|
+
return stableSymbolIdentity(
|
|
255
|
+
root,
|
|
256
|
+
resolved.declaration,
|
|
257
|
+
query.symbol.namespace,
|
|
258
|
+
query.symbol.exportedName,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
path: relativePath(root, query.symbol.absolutePath),
|
|
263
|
+
namespace: query.symbol.namespace,
|
|
264
|
+
declaration_kind: query.symbol.declarationKind,
|
|
265
|
+
exported_name: query.symbol.exportedName,
|
|
266
|
+
local_name: query.symbol.localName,
|
|
267
|
+
line: query.symbol.line,
|
|
268
|
+
col: query.symbol.col,
|
|
269
|
+
owner: query.symbol.owner,
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export const isProjectSource = (project, sourceFile) =>
|
|
274
|
+
Boolean(sourceFile) &&
|
|
275
|
+
!sourceFile.isDeclarationFile &&
|
|
276
|
+
!project.program.isSourceFileDefaultLibrary(sourceFile) &&
|
|
277
|
+
!project.program.isSourceFileFromExternalLibrary(sourceFile);
|
|
278
|
+
|
|
279
|
+
export const projectSourceFiles = (project) =>
|
|
280
|
+
project.program
|
|
281
|
+
.getSourceFileNames()
|
|
282
|
+
.map((fileName) => project.program.getSourceFile(fileName))
|
|
283
|
+
.filter((sourceFile) => isProjectSource(project, sourceFile));
|
|
284
|
+
|
|
285
|
+
const projectExportIndexes = new WeakMap();
|
|
286
|
+
|
|
287
|
+
const exportedDeclarations = (project, sourceFile) => {
|
|
288
|
+
const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile);
|
|
289
|
+
if (!moduleSymbol) return [];
|
|
290
|
+
return project.checker.getExportsOfModule(moduleSymbol).flatMap((exported) => {
|
|
291
|
+
const target = resolveAlias(project.checker, exported);
|
|
292
|
+
return declarationsForSymbol(project, target).flatMap((declaration) =>
|
|
293
|
+
[...declarationNamespaces(declaration)].map((namespace) => ({
|
|
294
|
+
exportedName: exported.name,
|
|
295
|
+
declaration,
|
|
296
|
+
namespace,
|
|
297
|
+
})),
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const exportIndexKey = ({ exportedName, declaration, namespace }) =>
|
|
303
|
+
`${exportedName}\0${stableDeclarationKey(declaration, namespace)}`;
|
|
304
|
+
|
|
305
|
+
export const projectExportIndex = (project) => {
|
|
306
|
+
const cached = projectExportIndexes.get(project);
|
|
307
|
+
if (cached) return cached;
|
|
308
|
+
const index = new Set(
|
|
309
|
+
projectSourceFiles(project).flatMap((sourceFile) =>
|
|
310
|
+
exportedDeclarations(project, sourceFile).map(exportIndexKey),
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
projectExportIndexes.set(project, index);
|
|
314
|
+
return index;
|
|
315
|
+
};
|