fallow-type-aware 3.16.0 → 3.18.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/README.md +7 -0
- package/fallow-type-aware.mjs +4 -1
- package/package.json +3 -2
- package/src/generated-protocol.mjs +2 -2
- package/src/project-state.mjs +111 -10
- package/src/semantic-identity.mjs +9 -3
- package/src/semantic.mjs +195 -58
- package/src/type-coupling.mjs +1 -2
- package/src/windows-child-process.mjs +25 -0
package/README.md
CHANGED
|
@@ -25,6 +25,13 @@ and SHA-256 declaration guard, but Fallow owns the final decision and fix
|
|
|
25
25
|
policy. The sidecar does not emit TypeScript compiler diagnostics as Fallow
|
|
26
26
|
findings and does not implement generic typed lint rules.
|
|
27
27
|
|
|
28
|
+
The raw TypeScript-Go host cannot currently expose Svelte virtual-module named
|
|
29
|
+
exports. If source code imports or re-exports such a name and checker resolution
|
|
30
|
+
has no declaration target, the sidecar returns
|
|
31
|
+
`svelte-virtual-module-exports` instead of claiming complete evidence. Run
|
|
32
|
+
`svelte-check` for framework diagnostics; Fallow stays fail-closed until a
|
|
33
|
+
supported host seam can preserve virtual source identity and mappings.
|
|
34
|
+
|
|
28
35
|
## Run locally
|
|
29
36
|
|
|
30
37
|
```sh
|
package/fallow-type-aware.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { installWindowsChildProcessPolicy } from "./src/windows-child-process.mjs";
|
|
4
|
+
|
|
5
|
+
installWindowsChildProcessPolicy();
|
|
4
6
|
|
|
5
7
|
try {
|
|
8
|
+
const { assertTypescriptBackendResolvable } = await import("./src/backend-preflight.mjs");
|
|
6
9
|
assertTypescriptBackendResolvable();
|
|
7
10
|
const { run } = await import("./src/cli.mjs");
|
|
8
11
|
await run({ input: process.stdin, output: process.stdout, args: process.argv.slice(2) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fallow-type-aware",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.0",
|
|
4
4
|
"description": "Optional TypeScript-Go semantic refinement sidecar for Fallow",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@codspeed/tinybench-plugin": "5.7.1",
|
|
35
|
-
"tinybench": "6.1.2"
|
|
35
|
+
"tinybench": "6.1.2",
|
|
36
|
+
"zod": "4.4.3"
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated from crates/api/type-aware-protocol.json. Do not edit.
|
|
2
2
|
export const TYPE_AWARE_PROTOCOL = Object.freeze({
|
|
3
3
|
schema_version: 1,
|
|
4
|
-
wire_protocol_version:
|
|
5
|
-
semantic_schema_version:
|
|
4
|
+
wire_protocol_version: 7,
|
|
5
|
+
semantic_schema_version: 3,
|
|
6
6
|
analysis_operation: "semantic-queries",
|
|
7
7
|
status_operation: "status",
|
|
8
8
|
query_operations: ["symbol-use", "symbol-trace", "api-surface", "symbol-impact", "type-coupling"],
|
package/src/project-state.mjs
CHANGED
|
@@ -2,18 +2,107 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { SymbolFlags } from "typescript/unstable/sync";
|
|
6
|
+
import {
|
|
7
|
+
isExportDeclaration,
|
|
8
|
+
isImportDeclaration,
|
|
9
|
+
isNamedExports,
|
|
10
|
+
isNamedImports,
|
|
11
|
+
} from "typescript/unstable/ast/is";
|
|
12
|
+
|
|
5
13
|
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
6
|
-
import { relativePath } from "./semantic-identity.mjs";
|
|
14
|
+
import { projectSourceFiles, relativePath } from "./semantic-identity.mjs";
|
|
7
15
|
|
|
8
16
|
const INFERRED_PROJECT = "<inferred>";
|
|
9
17
|
const compareText = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
10
18
|
const slash = (value) => value.split(path.sep).join("/");
|
|
11
19
|
|
|
12
|
-
const
|
|
13
|
-
project.program.getConfigFileParsingDiagnostics()
|
|
14
|
-
project.program.getProgramDiagnostics()
|
|
15
|
-
project.program.getSyntacticDiagnostics()
|
|
16
|
-
project.program.getBindDiagnostics()
|
|
20
|
+
const structuralDiagnostics = (project) => [
|
|
21
|
+
...project.program.getConfigFileParsingDiagnostics(),
|
|
22
|
+
...project.program.getProgramDiagnostics(),
|
|
23
|
+
...project.program.getSyntacticDiagnostics(),
|
|
24
|
+
...project.program.getBindDiagnostics(),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const isProjectLocalDiagnostic = (project, diagnostic) => {
|
|
28
|
+
if (!diagnostic.fileName) return true;
|
|
29
|
+
const sourceFile = project.program.getSourceFile(diagnostic.fileName);
|
|
30
|
+
if (!sourceFile) return true;
|
|
31
|
+
return (
|
|
32
|
+
!project.program.isSourceFileDefaultLibrary(sourceFile) &&
|
|
33
|
+
!project.program.isSourceFileFromExternalLibrary(sourceFile)
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isSvelteSpecifier = (node) =>
|
|
38
|
+
typeof node.moduleSpecifier?.text === "string" && node.moduleSpecifier.text.endsWith(".svelte");
|
|
39
|
+
|
|
40
|
+
const isUnknownAlias = (checker, specifier) => {
|
|
41
|
+
const symbol = checker.getSymbolAtLocation(specifier.name);
|
|
42
|
+
if (!symbol) return true;
|
|
43
|
+
return checker.isUnknownSymbol(checker.getAliasedSymbol(symbol));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const concreteExportSymbol = (checker, symbol) => {
|
|
47
|
+
const target =
|
|
48
|
+
(symbol.flags & SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol);
|
|
49
|
+
return !checker.isUnknownSymbol(target) && (target.declarations?.length ?? 0) > 0;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const hasProvableNamedExports = (project, declaration) => {
|
|
53
|
+
const moduleSymbol = project.checker.getSymbolAtLocation(declaration.moduleSpecifier);
|
|
54
|
+
if (!moduleSymbol) return false;
|
|
55
|
+
const hasConcreteModuleDeclaration = moduleSymbol.declarations?.some((moduleDeclaration) => {
|
|
56
|
+
const declarationPath =
|
|
57
|
+
moduleDeclaration.path ??
|
|
58
|
+
moduleDeclaration.fileName ??
|
|
59
|
+
moduleDeclaration.getSourceFile?.().fileName ??
|
|
60
|
+
"";
|
|
61
|
+
return declarationPath.endsWith(".d.svelte") || declarationPath.endsWith(".d.svelte.ts");
|
|
62
|
+
});
|
|
63
|
+
if (!hasConcreteModuleDeclaration) return false;
|
|
64
|
+
const namedExports = project.checker
|
|
65
|
+
.getExportsOfModule(moduleSymbol)
|
|
66
|
+
.filter((symbol) => symbol.name !== "default");
|
|
67
|
+
return namedExports.every((symbol) => concreteExportSymbol(project.checker, symbol));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const svelteDeclarationHasGap = (project, node) => {
|
|
71
|
+
if (!isSvelteSpecifier(node)) return false;
|
|
72
|
+
if (isImportDeclaration(node)) {
|
|
73
|
+
const bindings = node.importClause?.namedBindings;
|
|
74
|
+
return (
|
|
75
|
+
bindings !== undefined &&
|
|
76
|
+
isNamedImports(bindings) &&
|
|
77
|
+
bindings.elements.some((item) => isUnknownAlias(project.checker, item))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!isExportDeclaration(node)) return false;
|
|
81
|
+
if (!node.exportClause) return !hasProvableNamedExports(project, node);
|
|
82
|
+
if (!isNamedExports(node.exportClause)) return !hasProvableNamedExports(project, node);
|
|
83
|
+
return node.exportClause.elements.some((item) => isUnknownAlias(project.checker, item));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const sourceHasSvelteVirtualModuleGap = (project, sourceFile) => {
|
|
87
|
+
let gap = false;
|
|
88
|
+
const visit = (node) => {
|
|
89
|
+
if (gap) return;
|
|
90
|
+
gap = svelteDeclarationHasGap(project, node);
|
|
91
|
+
if (gap) return;
|
|
92
|
+
node.forEachChild((child) => {
|
|
93
|
+
visit(child);
|
|
94
|
+
return undefined;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
visit(sourceFile);
|
|
98
|
+
return gap;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const hasSvelteVirtualModuleGap = (project) =>
|
|
102
|
+
projectSourceFiles(project).some(
|
|
103
|
+
(sourceFile) =>
|
|
104
|
+
sourceFile.text.includes(".svelte") && sourceHasSvelteVirtualModuleGap(project, sourceFile),
|
|
105
|
+
);
|
|
17
106
|
|
|
18
107
|
const configPath = (root, project) => {
|
|
19
108
|
const normalized = slash(project.configFileName);
|
|
@@ -173,15 +262,27 @@ const effectiveProjectConfigHash = (root, project) => {
|
|
|
173
262
|
};
|
|
174
263
|
|
|
175
264
|
export const projectState = (root, project, source) => {
|
|
176
|
-
const
|
|
265
|
+
const diagnostics = structuralDiagnostics(project);
|
|
266
|
+
const localDiagnosticCount = diagnostics.filter((diagnostic) =>
|
|
267
|
+
isProjectLocalDiagnostic(project, diagnostic),
|
|
268
|
+
).length;
|
|
269
|
+
const hasLocalDiagnostics = localDiagnosticCount > 0;
|
|
270
|
+
const svelteVirtualModuleGap = !hasLocalDiagnostics && hasSvelteVirtualModuleGap(project);
|
|
271
|
+
const reasonCode = hasLocalDiagnostics
|
|
272
|
+
? "blocking-diagnostics"
|
|
273
|
+
: svelteVirtualModuleGap
|
|
274
|
+
? "svelte-virtual-module-exports"
|
|
275
|
+
: diagnostics.length > 0
|
|
276
|
+
? "blocking-diagnostics"
|
|
277
|
+
: null;
|
|
177
278
|
return {
|
|
178
279
|
project,
|
|
179
280
|
config: configPath(root, project),
|
|
180
281
|
effective_config_hash: effectiveProjectConfigHash(root, project),
|
|
181
282
|
source,
|
|
182
|
-
status:
|
|
183
|
-
reason_code:
|
|
184
|
-
blocking_diagnostic_count:
|
|
283
|
+
status: reasonCode === null ? "complete" : "unavailable",
|
|
284
|
+
reason_code: reasonCode,
|
|
285
|
+
blocking_diagnostic_count: reasonCode === "blocking-diagnostics" ? diagnostics.length : 0,
|
|
185
286
|
source_file_count: project.program.getSourceFileNames().length,
|
|
186
287
|
program_reused: false,
|
|
187
288
|
candidate_count: 0,
|
|
@@ -237,9 +237,15 @@ export const symbolForDeclaration = (project, declaration) =>
|
|
|
237
237
|
|
|
238
238
|
export const resolveAlias = (checker, symbol) => {
|
|
239
239
|
if (!symbol) return undefined;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
let current = symbol;
|
|
242
|
+
while ((current.flags & SymbolFlags.Alias) !== 0 && !seen.has(current)) {
|
|
243
|
+
seen.add(current);
|
|
244
|
+
const aliased = checker.getAliasedSymbol(current);
|
|
245
|
+
if (checker.isUnknownSymbol(aliased) || aliased === current) return current;
|
|
246
|
+
current = aliased;
|
|
247
|
+
}
|
|
248
|
+
return current;
|
|
243
249
|
};
|
|
244
250
|
|
|
245
251
|
export const declarationsForSymbol = (project, symbol) =>
|
package/src/semantic.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isImportTypeNode,
|
|
21
21
|
isNamespaceImport,
|
|
22
22
|
isPropertyAccessExpression,
|
|
23
|
+
isQualifiedName,
|
|
23
24
|
isPrivateIdentifier,
|
|
24
25
|
isSetAccessorDeclaration,
|
|
25
26
|
isStringLiteralLikeNode,
|
|
@@ -157,6 +158,10 @@ const REASON_ACTIONS = new Map([
|
|
|
157
158
|
"blocking-diagnostics",
|
|
158
159
|
"Repair structural TypeScript diagnostics in every selected project and retry.",
|
|
159
160
|
],
|
|
161
|
+
[
|
|
162
|
+
"svelte-virtual-module-exports",
|
|
163
|
+
"Run svelte-check for framework diagnostics. See https://docs.fallow.tools/analysis/type-aware#svelte-virtual-module-exports for supported Svelte project setup.",
|
|
164
|
+
],
|
|
160
165
|
[
|
|
161
166
|
"unknown-entry-point",
|
|
162
167
|
"Refresh the package entry points or pass project-relative source entry points.",
|
|
@@ -166,21 +171,22 @@ const DEFAULT_REASON_ACTION =
|
|
|
166
171
|
"Narrow the query to a specific symbol, entry point, or healthy TypeScript project and retry.";
|
|
167
172
|
const REASON_PRIORITY = new Map([
|
|
168
173
|
["blocking-diagnostics", 0],
|
|
169
|
-
["
|
|
170
|
-
["
|
|
171
|
-
["
|
|
172
|
-
["
|
|
173
|
-
["unknown-
|
|
174
|
-
["
|
|
175
|
-
["
|
|
176
|
-
["
|
|
177
|
-
["
|
|
178
|
-
["
|
|
179
|
-
["
|
|
180
|
-
["
|
|
181
|
-
["
|
|
182
|
-
["
|
|
183
|
-
["
|
|
174
|
+
["svelte-virtual-module-exports", 1],
|
|
175
|
+
["incomplete-project-coverage", 2],
|
|
176
|
+
["framework-contract-provenance", 3],
|
|
177
|
+
["ambiguous-project", 4],
|
|
178
|
+
["unknown-symbol", 5],
|
|
179
|
+
["unknown-entry-point", 6],
|
|
180
|
+
["decorated-declaration", 7],
|
|
181
|
+
["optional-contract", 8],
|
|
182
|
+
["accessor-pair", 9],
|
|
183
|
+
["overload-set", 10],
|
|
184
|
+
["attached-comment", 11],
|
|
185
|
+
["abstract-declaration", 12],
|
|
186
|
+
["dynamic-member-access", 13],
|
|
187
|
+
["virtual-dispatch", 14],
|
|
188
|
+
["dynamic-behavior", 15],
|
|
189
|
+
["evidence-limit", 16],
|
|
184
190
|
]);
|
|
185
191
|
|
|
186
192
|
const actionForReason = (reasonCode) => REASON_ACTIONS.get(reasonCode) ?? DEFAULT_REASON_ACTION;
|
|
@@ -201,6 +207,22 @@ const combineOmissions = (omissions) => {
|
|
|
201
207
|
return [...counts].map(([reason_code, count]) => ({ reason_code, count }));
|
|
202
208
|
};
|
|
203
209
|
|
|
210
|
+
const projectStateReason = (state) => state.reason_code ?? "blocking-diagnostics";
|
|
211
|
+
|
|
212
|
+
const unavailableProjectOmissions = (states) =>
|
|
213
|
+
combineOmissions(
|
|
214
|
+
states
|
|
215
|
+
.filter((state) => state.status !== "complete")
|
|
216
|
+
.map((state) => ({ reason_code: projectStateReason(state), count: 1 })),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const unavailableProjectReason = (states) => {
|
|
220
|
+
if (states.length === 0) return "no-project";
|
|
221
|
+
return (
|
|
222
|
+
unavailableProjectOmissions(states).toSorted(compareOmissions)[0]?.reason_code ?? "no-project"
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
204
226
|
const resultStatus = (partial) => (partial ? "partial" : "complete");
|
|
205
227
|
const resultReason = (omissions) => (omissions.length > 0 ? omissions[0].reason_code : null);
|
|
206
228
|
const resultActions = (omissions) =>
|
|
@@ -410,30 +432,60 @@ const registerSymbolTargets = (root, state, entry) => {
|
|
|
410
432
|
state.exportEntriesByProject.set(project, projectEntries);
|
|
411
433
|
});
|
|
412
434
|
}
|
|
413
|
-
entry.resolved.ownerContexts.forEach(({
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
entry.query.symbol.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
435
|
+
entry.resolved.ownerContexts.forEach(({ declaration, anchor }) => {
|
|
436
|
+
const namespaces =
|
|
437
|
+
entry.query.symbol.declarationKind === "export"
|
|
438
|
+
? declarationNamespaces(declaration)
|
|
439
|
+
: new Set([entry.query.symbol.namespace]);
|
|
440
|
+
namespaces.forEach((namespace) => addSymbolTarget(root, state, entry, declaration, namespace));
|
|
441
|
+
if (entry.query.symbol.declarationKind === "export" && anchor) {
|
|
442
|
+
declarationNamespaces(anchor).forEach((namespace) =>
|
|
443
|
+
addSymbolTarget(root, state, entry, anchor, namespace),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
422
446
|
});
|
|
423
447
|
state.contractRelationsByQuery.set(entry.query.id, entry.resolved.contractRelations);
|
|
424
448
|
};
|
|
425
449
|
|
|
426
450
|
const matchingSymbolEntries = (project, symbol, namespace, targetsByKey) => {
|
|
451
|
+
const declarations = declarationsForSymbol(project, symbol);
|
|
452
|
+
const matchingLane = declarations.filter((declaration) =>
|
|
453
|
+
declarationNamespaces(declaration).has(namespace),
|
|
454
|
+
);
|
|
455
|
+
if (matchingLane.length > 0) {
|
|
456
|
+
return new Set(
|
|
457
|
+
matchingLane.flatMap(
|
|
458
|
+
(declaration) => targetsByKey.get(stableDeclarationKey(declaration, namespace)) ?? [],
|
|
459
|
+
),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
427
462
|
return new Set(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
463
|
+
declarations.flatMap((declaration) =>
|
|
464
|
+
[...declarationNamespaces(declaration)].flatMap(
|
|
465
|
+
(declaredNamespace) =>
|
|
466
|
+
targetsByKey.get(stableDeclarationKey(declaration, declaredNamespace)) ?? [],
|
|
467
|
+
),
|
|
468
|
+
),
|
|
434
469
|
);
|
|
435
470
|
};
|
|
436
471
|
|
|
472
|
+
const matchingAliasEntries = (project, symbol, namespace, targetsByKey) => {
|
|
473
|
+
const entries = new Set();
|
|
474
|
+
const seen = new Set();
|
|
475
|
+
let current = symbol;
|
|
476
|
+
while (current && !seen.has(current)) {
|
|
477
|
+
seen.add(current);
|
|
478
|
+
matchingSymbolEntries(project, current, namespace, targetsByKey).forEach((entry) =>
|
|
479
|
+
entries.add(entry),
|
|
480
|
+
);
|
|
481
|
+
if ((current.flags & SymbolFlags.Alias) === 0) break;
|
|
482
|
+
const aliased = project.checker.getAliasedSymbol(current);
|
|
483
|
+
if (project.checker.isUnknownSymbol(aliased) || aliased === current) break;
|
|
484
|
+
current = aliased;
|
|
485
|
+
}
|
|
486
|
+
return entries;
|
|
487
|
+
};
|
|
488
|
+
|
|
437
489
|
const isDeclarationReference = (root, state, entry, node) => {
|
|
438
490
|
const locations = state.declarationLocationsByQuery.get(entry.query.id);
|
|
439
491
|
if (locations.has(locationKey(location(root, node)))) return true;
|
|
@@ -467,13 +519,37 @@ const importTypeSpecifier = (node) => {
|
|
|
467
519
|
const isDynamicImportCall = (node) =>
|
|
468
520
|
isCallExpression(node) && node.expression.getText(node.getSourceFile()) === "import";
|
|
469
521
|
|
|
522
|
+
const MODULE_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
523
|
+
|
|
524
|
+
const lexicalModuleIdentityForText = (project, sourceFile, specifierText) => {
|
|
525
|
+
if (!specifierText?.startsWith(".")) return undefined;
|
|
526
|
+
const resolved = path.resolve(path.dirname(sourceFile.fileName), specifierText);
|
|
527
|
+
const extension = path.extname(resolved);
|
|
528
|
+
const stem = extension ? resolved.slice(0, -extension.length) : resolved;
|
|
529
|
+
const candidates = [
|
|
530
|
+
resolved,
|
|
531
|
+
...MODULE_SOURCE_EXTENSIONS.map((candidate) => `${stem}${candidate}`),
|
|
532
|
+
...MODULE_SOURCE_EXTENSIONS.map((candidate) => path.join(resolved, `index${candidate}`)),
|
|
533
|
+
];
|
|
534
|
+
for (const candidate of candidates) {
|
|
535
|
+
const matched = project.program.getSourceFile(candidate);
|
|
536
|
+
if (matched) return sourceFileIdentity(matched);
|
|
537
|
+
if (existsSync(candidate)) return canonicalFileIdentity(candidate);
|
|
538
|
+
}
|
|
539
|
+
return undefined;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const lexicalModuleIdentity = (project, specifier) =>
|
|
543
|
+
specifier
|
|
544
|
+
? lexicalModuleIdentityForText(project, specifier.getSourceFile(), specifier.text)
|
|
545
|
+
: undefined;
|
|
546
|
+
|
|
470
547
|
const moduleIdentityForSpecifier = (project, specifier) => {
|
|
471
548
|
if (!specifier) return undefined;
|
|
549
|
+
const lexicalIdentity = lexicalModuleIdentity(project, specifier);
|
|
550
|
+
if (lexicalIdentity) return lexicalIdentity;
|
|
472
551
|
const moduleSymbol = project.checker.getSymbolAtLocation(specifier);
|
|
473
|
-
const moduleDeclaration = declarationsForSymbol(
|
|
474
|
-
project,
|
|
475
|
-
resolveAlias(project.checker, moduleSymbol),
|
|
476
|
-
)[0];
|
|
552
|
+
const moduleDeclaration = declarationsForSymbol(project, moduleSymbol)[0];
|
|
477
553
|
return moduleDeclaration ? sourceFileIdentity(moduleDeclaration.getSourceFile()) : undefined;
|
|
478
554
|
};
|
|
479
555
|
|
|
@@ -486,7 +562,9 @@ const namespaceImportDeclaration = (project, node) => {
|
|
|
486
562
|
: undefined
|
|
487
563
|
: isElementAccessExpression(parent) && parent.argumentExpression === node
|
|
488
564
|
? parent.expression
|
|
489
|
-
:
|
|
565
|
+
: isQualifiedName(parent) && parent.right === node
|
|
566
|
+
? parent.left
|
|
567
|
+
: undefined;
|
|
490
568
|
if (!namespace) return undefined;
|
|
491
569
|
if (!isIdentifier(namespace)) return undefined;
|
|
492
570
|
const symbol = project.checker.getSymbolAtLocation(namespace);
|
|
@@ -495,9 +573,28 @@ const namespaceImportDeclaration = (project, node) => {
|
|
|
495
573
|
.find(isNamespaceImport);
|
|
496
574
|
};
|
|
497
575
|
|
|
576
|
+
const localAliasModuleEdgeDeclaration = (project, node) => {
|
|
577
|
+
const symbol = project.checker.getSymbolAtLocation(node);
|
|
578
|
+
return symbol?.declarations
|
|
579
|
+
?.map((declaration) => moduleEdgeDeclaration(declaration.resolve(project)))
|
|
580
|
+
.find(Boolean);
|
|
581
|
+
};
|
|
582
|
+
|
|
498
583
|
const referencedModuleIdentity = (project, node) => {
|
|
584
|
+
const importType = ancestorImportType(node);
|
|
585
|
+
const lexicalImportType = importType
|
|
586
|
+
?.getText(importType.getSourceFile())
|
|
587
|
+
.match(/import\s*\(\s*["']([^"']+)["']/u)?.[1];
|
|
588
|
+
const lexicalIdentity = lexicalModuleIdentityForText(
|
|
589
|
+
project,
|
|
590
|
+
node.getSourceFile(),
|
|
591
|
+
lexicalImportType,
|
|
592
|
+
);
|
|
593
|
+
if (lexicalIdentity) return lexicalIdentity;
|
|
499
594
|
const declaration =
|
|
500
|
-
moduleEdgeDeclaration(node) ??
|
|
595
|
+
moduleEdgeDeclaration(node) ??
|
|
596
|
+
moduleEdgeDeclaration(namespaceImportDeclaration(project, node)) ??
|
|
597
|
+
localAliasModuleEdgeDeclaration(project, node);
|
|
501
598
|
const declarationSpecifier = declaration?.moduleSpecifier;
|
|
502
599
|
const specifier =
|
|
503
600
|
declarationSpecifier && isStringLiteralLikeNode(declarationSpecifier)
|
|
@@ -506,13 +603,22 @@ const referencedModuleIdentity = (project, node) => {
|
|
|
506
603
|
return moduleIdentityForSpecifier(project, specifier);
|
|
507
604
|
};
|
|
508
605
|
|
|
509
|
-
const isExactExportReference = (project, entry, node) =>
|
|
510
|
-
entry.query.symbol.declarationKind !== "export"
|
|
511
|
-
referencedModuleIdentity(project, node)
|
|
512
|
-
|
|
606
|
+
const isExactExportReference = (project, entry, node) => {
|
|
607
|
+
if (entry.query.symbol.declarationKind !== "export") return true;
|
|
608
|
+
const referencedModule = referencedModuleIdentity(project, node);
|
|
609
|
+
if (!referencedModule) return false;
|
|
610
|
+
const queryModule = canonicalFileIdentity(entry.query.symbol.absolutePath);
|
|
611
|
+
return (
|
|
612
|
+
referencedModule === queryModule ||
|
|
613
|
+
sourceFileIdentity(entry.resolved.declaration.getSourceFile()) === queryModule
|
|
614
|
+
);
|
|
615
|
+
};
|
|
513
616
|
|
|
514
617
|
const recordSymbolUse = (root, state, project, entry, node, namespace) => {
|
|
515
618
|
if (isDeclarationReference(root, state, entry, node)) return;
|
|
619
|
+
if (isDeclaration(node.parent) && node.parent.name === node) return;
|
|
620
|
+
const moduleEdge = moduleEdgeDeclaration(node);
|
|
621
|
+
if (moduleEdge) return;
|
|
516
622
|
if (!isExactExportReference(project, entry, node)) return;
|
|
517
623
|
const evidence = state.evidenceByQuery.get(entry.query.id);
|
|
518
624
|
state.totalByQuery.set(entry.query.id, state.totalByQuery.get(entry.query.id) + 1);
|
|
@@ -614,6 +720,26 @@ const referenceNamespaces = (project, node, symbol) => {
|
|
|
614
720
|
return namespaces.size > 0 ? [...namespaces] : [isTypePosition(node) ? "type" : "value"];
|
|
615
721
|
};
|
|
616
722
|
|
|
723
|
+
const surfaceEntriesForNode = (state, project, node, namespace) => {
|
|
724
|
+
if (moduleEdgeDeclaration(node)) return new Set();
|
|
725
|
+
const moduleIdentity = referencedModuleIdentity(project, node);
|
|
726
|
+
if (!moduleIdentity) return new Set();
|
|
727
|
+
const direct = [...(state.exportEntriesByModule.get(moduleIdentity) ?? [])].filter(
|
|
728
|
+
(entry) =>
|
|
729
|
+
entry.query.symbol.exportedName === node.text && entry.query.symbol.namespace === namespace,
|
|
730
|
+
);
|
|
731
|
+
const entries = new Set(direct);
|
|
732
|
+
for (const entry of direct) {
|
|
733
|
+
for (const { declaration } of entry.resolved.ownerContexts) {
|
|
734
|
+
const symbol = resolveAlias(project.checker, symbolForDeclaration(project, declaration));
|
|
735
|
+
matchingSymbolEntries(project, symbol, namespace, state.targetsByKey).forEach((related) =>
|
|
736
|
+
entries.add(related),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return entries;
|
|
741
|
+
};
|
|
742
|
+
|
|
617
743
|
const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImports) => {
|
|
618
744
|
const projectEntries = classMemberEntrySetForProject(state, project);
|
|
619
745
|
const nodes = [];
|
|
@@ -623,7 +749,7 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
|
|
|
623
749
|
recordStringDispatchedMemberAccess(state, projectEntries, node);
|
|
624
750
|
recordComputedMemberAccess(state, project, projectEntries, node);
|
|
625
751
|
}
|
|
626
|
-
if (isIdentifierReference(node,
|
|
752
|
+
if (isIdentifierReference(node, undefined, includeDefaultImports)) {
|
|
627
753
|
nodes.push(node);
|
|
628
754
|
return;
|
|
629
755
|
}
|
|
@@ -631,9 +757,13 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
|
|
|
631
757
|
});
|
|
632
758
|
const symbols = project.checker.getSymbolAtLocation(nodes);
|
|
633
759
|
nodes.forEach((node, index) => {
|
|
634
|
-
const
|
|
760
|
+
const rawSymbol = symbols[index];
|
|
761
|
+
const symbol = resolveAlias(project.checker, rawSymbol);
|
|
635
762
|
referenceNamespaces(project, node, symbol).forEach((namespace) => {
|
|
636
|
-
const entries =
|
|
763
|
+
const entries = new Set([
|
|
764
|
+
...matchingAliasEntries(project, rawSymbol, namespace, state.targetsByKey),
|
|
765
|
+
...surfaceEntriesForNode(state, project, node, namespace),
|
|
766
|
+
]);
|
|
637
767
|
entries.forEach((entry) => recordSymbolUse(root, state, project, entry, node, namespace));
|
|
638
768
|
});
|
|
639
769
|
});
|
|
@@ -681,8 +811,6 @@ const symbolResolutionError = (query, reasonCode, action) => ({
|
|
|
681
811
|
});
|
|
682
812
|
|
|
683
813
|
const isCompleteProjectState = (state) => state?.status === "complete";
|
|
684
|
-
const selectedProjectName = (state) => state?.config ?? "the selected project";
|
|
685
|
-
|
|
686
814
|
const owningProjectContexts = (statesByProject, absolutePath) =>
|
|
687
815
|
[...statesByProject.entries()]
|
|
688
816
|
.filter(([project]) => project.program.getSourceFile(absolutePath))
|
|
@@ -722,13 +850,16 @@ const selectSymbolContext = (
|
|
|
722
850
|
}
|
|
723
851
|
const completeOwners = owners.filter(({ state }) => isCompleteProjectState(state));
|
|
724
852
|
if (completeOwners.length === 0) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
"blocking-diagnostics",
|
|
730
|
-
`Repair structural diagnostics in ${selectedProjectName(owners[0].state)} and retry.`,
|
|
853
|
+
const selectedOwner = [...owners].toSorted((left, right) =>
|
|
854
|
+
compareOmissions(
|
|
855
|
+
{ reason_code: projectStateReason(left.state) },
|
|
856
|
+
{ reason_code: projectStateReason(right.state) },
|
|
731
857
|
),
|
|
858
|
+
)[0];
|
|
859
|
+
const reasonCode = projectStateReason(selectedOwner.state);
|
|
860
|
+
return {
|
|
861
|
+
...selectedOwner,
|
|
862
|
+
...symbolResolutionError(query, reasonCode, actionForReason(reasonCode)),
|
|
732
863
|
};
|
|
733
864
|
}
|
|
734
865
|
return { owners, completeOwners };
|
|
@@ -934,7 +1065,7 @@ const resolvedOwnerContext = (query, owner) => {
|
|
|
934
1065
|
const anchor = symbolQueryAnchor(owner.project, query);
|
|
935
1066
|
const target = resolvedAnchorTarget(owner.project, anchor, query.symbol);
|
|
936
1067
|
return symbolQueryResolved(owner.project, query.symbol, anchor, target)
|
|
937
|
-
? { ...owner, ...target }
|
|
1068
|
+
? { ...owner, anchor, ...target }
|
|
938
1069
|
: undefined;
|
|
939
1070
|
};
|
|
940
1071
|
|
|
@@ -1377,6 +1508,13 @@ const signatureTypes = (project, type, anchor) =>
|
|
|
1377
1508
|
);
|
|
1378
1509
|
|
|
1379
1510
|
const checkerTypeChildren = (project, type, anchor, hasNamedDeclaration) => {
|
|
1511
|
+
if (type.isTypeParameter()) {
|
|
1512
|
+
const constraint = safeCheckerValue(
|
|
1513
|
+
() => project.checker.getConstraintOfTypeParameter(type),
|
|
1514
|
+
undefined,
|
|
1515
|
+
);
|
|
1516
|
+
return constraint ? [constraint] : [];
|
|
1517
|
+
}
|
|
1380
1518
|
const structural = [
|
|
1381
1519
|
...(type.getTypes() ?? []),
|
|
1382
1520
|
...safeCheckerValue(() => type.getAliasTypeArguments(), []),
|
|
@@ -1547,12 +1685,9 @@ const graphProjects = (snapshot, explicitProjects) =>
|
|
|
1547
1685
|
const readyProjectStates = (query, states) => {
|
|
1548
1686
|
const ready = states.filter((state) => state.status === "complete");
|
|
1549
1687
|
if (ready.length > 0) return { ready };
|
|
1688
|
+
const reasonCode = unavailableProjectReason(states);
|
|
1550
1689
|
return {
|
|
1551
|
-
error: unavailable(
|
|
1552
|
-
query,
|
|
1553
|
-
states.length === 0 ? "no-project" : "blocking-diagnostics",
|
|
1554
|
-
"Pass a healthy tsconfig containing the package entry points and retry.",
|
|
1555
|
-
),
|
|
1690
|
+
error: unavailable(query, reasonCode, actionForReason(reasonCode)),
|
|
1556
1691
|
};
|
|
1557
1692
|
};
|
|
1558
1693
|
|
|
@@ -1684,7 +1819,7 @@ const analyzeApiSurface = (root, query, states, evidenceLimit) => {
|
|
|
1684
1819
|
reason_code: "evidence-limit",
|
|
1685
1820
|
count: omissionCount,
|
|
1686
1821
|
},
|
|
1687
|
-
|
|
1822
|
+
...unavailableProjectOmissions(states),
|
|
1688
1823
|
{ reason_code: "unknown-entry-point", count: missingEntryPointCount },
|
|
1689
1824
|
],
|
|
1690
1825
|
});
|
|
@@ -1763,6 +1898,7 @@ const analyzeGraphSemanticQuery = (root, query, graphStates, evidenceLimit) => {
|
|
|
1763
1898
|
missingEntryPoints,
|
|
1764
1899
|
publicApiGraph,
|
|
1765
1900
|
readyProjectStates,
|
|
1901
|
+
unavailableProjectOmissions,
|
|
1766
1902
|
uniqueSorted,
|
|
1767
1903
|
},
|
|
1768
1904
|
);
|
|
@@ -1842,6 +1978,7 @@ const recordAbstainedProjectOutcome = (state, result) => {
|
|
|
1842
1978
|
"no-project",
|
|
1843
1979
|
"ambiguous-project",
|
|
1844
1980
|
"blocking-diagnostics",
|
|
1981
|
+
"svelte-virtual-module-exports",
|
|
1845
1982
|
"unknown-symbol",
|
|
1846
1983
|
"incomplete-project-coverage",
|
|
1847
1984
|
]);
|
package/src/type-coupling.mjs
CHANGED
|
@@ -118,7 +118,6 @@ export const analyzeTypeCoupling = ({ root, query, states, evidenceLimit }, serv
|
|
|
118
118
|
const highCouplingThreshold = percentile(degrees, 0.9);
|
|
119
119
|
const topContributors = topCouplingContributors(perFile);
|
|
120
120
|
const cycles = query.includeCycles ? findCycles(edges) : [];
|
|
121
|
-
const unavailableProjectCount = states.length - readiness.ready.length;
|
|
122
121
|
const missingEntryPointCount = services.missingEntryPoints(query, resolvedEntryPoints);
|
|
123
122
|
const nestedFileOmissionCount = nestedCouplingOmissionCount(perFile, evidenceLimit);
|
|
124
123
|
const boundFile = (entry) => boundCoupledFile(entry, evidenceLimit);
|
|
@@ -159,7 +158,7 @@ export const analyzeTypeCoupling = ({ root, query, states, evidenceLimit }, serv
|
|
|
159
158
|
Math.max(0, cycles.length - evidenceLimit) +
|
|
160
159
|
nestedFileOmissionCount,
|
|
161
160
|
},
|
|
162
|
-
|
|
161
|
+
...services.unavailableProjectOmissions(states),
|
|
163
162
|
{ reason_code: "unknown-entry-point", count: missingEntryPointCount },
|
|
164
163
|
],
|
|
165
164
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
3
|
+
|
|
4
|
+
const INSTALL_MARKER = Symbol.for("fallow.type-aware.windows-child-process-policy");
|
|
5
|
+
|
|
6
|
+
const hiddenOptions = (options) => ({ ...options, windowsHide: true });
|
|
7
|
+
|
|
8
|
+
/** Keep TypeScript-Go child processes hidden when the sidecar runs on Windows. */
|
|
9
|
+
export const installWindowsChildProcessPolicy = ({
|
|
10
|
+
childProcess: processApi = childProcess,
|
|
11
|
+
platform = process.platform,
|
|
12
|
+
syncBuiltinESMExports: syncExports = syncBuiltinESMExports,
|
|
13
|
+
} = {}) => {
|
|
14
|
+
if (platform !== "win32" || processApi[INSTALL_MARKER] === true) return;
|
|
15
|
+
|
|
16
|
+
const originalSpawn = processApi.spawn;
|
|
17
|
+
processApi.spawn = function spawnHidden(command, args, options) {
|
|
18
|
+
if (Array.isArray(args)) {
|
|
19
|
+
return originalSpawn.call(this, command, args, hiddenOptions(options));
|
|
20
|
+
}
|
|
21
|
+
return originalSpawn.call(this, command, hiddenOptions(args));
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(processApi, INSTALL_MARKER, { value: true });
|
|
24
|
+
syncExports();
|
|
25
|
+
};
|