@cplieger/deadset-ts 4.0.0 → 5.0.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 +68 -50
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +230 -37
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +186 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +383 -0
- package/dist/src/conventions.js +456 -0
- package/dist/src/dependencies.js +30 -3
- package/dist/src/discover.js +11 -4
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +47 -32
- package/dist/src/exempt.js +11 -3
- package/dist/src/findings/completion.js +19 -29
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/unused-declarations.js +7 -2
- package/dist/src/graph.js +13 -5
- package/dist/src/inventory.js +4 -2
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/references.js +209 -30
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +6 -3
- package/dist/src/resolve.js +31 -6
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +4 -2
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +395 -71
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +235 -0
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +440 -0
- package/src/conventions.ts +640 -0
- package/src/dependencies.ts +36 -2
- package/src/discover.ts +11 -7
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +66 -36
- package/src/exempt.ts +21 -3
- package/src/findings/completion.ts +24 -33
- package/src/findings/emitters.ts +32 -1
- package/src/findings/unused-declarations.ts +9 -2
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/inventory.ts +13 -2
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/references.ts +227 -28
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +6 -3
- package/src/resolve.ts +59 -6
- package/src/roots.ts +60 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +4 -2
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The convention rows a run applies, and the files they root. A row applies to a
|
|
3
|
+
* manifest that declares its enabling package when the installed package's version is
|
|
4
|
+
* in the row's range; its globs are read against that manifest's directory, with every
|
|
5
|
+
* directory a configuration property moves read from the property's literal value.
|
|
6
|
+
* No configuration file is run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isAsExpression,
|
|
11
|
+
isBinaryExpression,
|
|
12
|
+
isCallExpression,
|
|
13
|
+
isClassDeclaration,
|
|
14
|
+
isComputedPropertyName,
|
|
15
|
+
isExportAssignment,
|
|
16
|
+
isExportDeclaration,
|
|
17
|
+
isExpressionStatement,
|
|
18
|
+
isFunctionDeclaration,
|
|
19
|
+
isIdentifier,
|
|
20
|
+
isImportDeclaration,
|
|
21
|
+
isNamedExports,
|
|
22
|
+
isNamespaceImport,
|
|
23
|
+
isNoSubstitutionTemplateLiteral,
|
|
24
|
+
isNonNullExpression,
|
|
25
|
+
isObjectLiteralExpression,
|
|
26
|
+
isParenthesizedExpression,
|
|
27
|
+
isPropertyAccessExpression,
|
|
28
|
+
isPropertyAssignment,
|
|
29
|
+
isSatisfiesExpression,
|
|
30
|
+
isShorthandPropertyAssignment,
|
|
31
|
+
isSpreadAssignment,
|
|
32
|
+
isStringLiteral,
|
|
33
|
+
isVariableStatement,
|
|
34
|
+
SyntaxKind,
|
|
35
|
+
type AsExpression,
|
|
36
|
+
type Node,
|
|
37
|
+
type NonNullExpression,
|
|
38
|
+
type ObjectLiteralExpression,
|
|
39
|
+
type ParenthesizedExpression,
|
|
40
|
+
type SatisfiesExpression,
|
|
41
|
+
type SourceFile,
|
|
42
|
+
} from "@typescript/native/unstable/ast";
|
|
43
|
+
import { lineOf } from "./component-files.ts";
|
|
44
|
+
import {
|
|
45
|
+
CONVENTION_ROWS,
|
|
46
|
+
type ConventionRow,
|
|
47
|
+
type DirectoryMove,
|
|
48
|
+
type OptionsCall,
|
|
49
|
+
} from "./convention-rows.ts";
|
|
50
|
+
import { globExpression } from "./glob.ts";
|
|
51
|
+
import type { Host } from "./host.ts";
|
|
52
|
+
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
|
|
53
|
+
import type { SetupFailure } from "./setup-failure.ts";
|
|
54
|
+
import type { SourceFiles } from "./source-files.ts";
|
|
55
|
+
import { satisfies } from "./version-range.ts";
|
|
56
|
+
|
|
57
|
+
/** One manifest the analysis reads. */
|
|
58
|
+
export interface ConventionManifest {
|
|
59
|
+
/** The manifest's directory, absolute. */
|
|
60
|
+
readonly dir: string;
|
|
61
|
+
/** The manifest's path below the target root, `/`-separated. */
|
|
62
|
+
readonly path: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One row a run applied, as the report lists it. */
|
|
66
|
+
export interface AppliedConvention {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
readonly package: string;
|
|
69
|
+
/** The version the installed package's own manifest names. */
|
|
70
|
+
readonly version: string;
|
|
71
|
+
/** The declaring manifest's path below the target root. */
|
|
72
|
+
readonly manifest: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One glob of an applied row, read against its manifest's directory. */
|
|
76
|
+
interface ConventionGlob {
|
|
77
|
+
readonly dir: string;
|
|
78
|
+
readonly row: string;
|
|
79
|
+
readonly expression: RegExp;
|
|
80
|
+
/** The expressions of the row's excludes, read against the same directory. */
|
|
81
|
+
readonly excludes: readonly RegExp[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A setup failure a row met, at the directory of the manifest that declared it. */
|
|
85
|
+
interface ConventionFailure {
|
|
86
|
+
readonly dir: string;
|
|
87
|
+
readonly failure: SetupFailure;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What the rows decided for one run. */
|
|
91
|
+
export interface Conventions {
|
|
92
|
+
/** The rows applied, ordered by name, then manifest, then their compact encoding. */
|
|
93
|
+
readonly applied: readonly AppliedConvention[];
|
|
94
|
+
readonly globs: readonly ConventionGlob[];
|
|
95
|
+
readonly failures: readonly ConventionFailure[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One file a row roots. */
|
|
99
|
+
interface ConventionEntry {
|
|
100
|
+
readonly file: SourceFile;
|
|
101
|
+
/** The row's name. */
|
|
102
|
+
readonly row: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Parses one file's text with no program. */
|
|
106
|
+
type ParseSource = (fileName: string, text: string) => SourceFile;
|
|
107
|
+
|
|
108
|
+
const DEPENDENCY_SECTIONS = [
|
|
109
|
+
"dependencies",
|
|
110
|
+
"devDependencies",
|
|
111
|
+
"peerDependencies",
|
|
112
|
+
"optionalDependencies",
|
|
113
|
+
] as const;
|
|
114
|
+
|
|
115
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readJSON(host: Host, path: string): unknown {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(host.readFile(path));
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function declares(manifest: unknown, name: string): boolean {
|
|
128
|
+
if (!isRecord(manifest)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return DEPENDENCY_SECTIONS.some((section) => {
|
|
132
|
+
const held = manifest[section];
|
|
133
|
+
return isRecord(held) && Object.hasOwn(held, name);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The version the installed package's manifest names, read from the nearest
|
|
139
|
+
* `node_modules` at or above `dir` that holds it, `""` where that manifest names
|
|
140
|
+
* none, or `undefined` where no `node_modules` holds it.
|
|
141
|
+
*/
|
|
142
|
+
function installedVersion(host: Host, dir: string, name: string): string | undefined {
|
|
143
|
+
for (let at = normalizePath(dir); ; at = dirnamePath(at)) {
|
|
144
|
+
const path = joinPath(at, "node_modules", name, "package.json");
|
|
145
|
+
if (host.kindOf(path) === "file") {
|
|
146
|
+
const manifest = readJSON(host, path);
|
|
147
|
+
const version = isRecord(manifest) ? manifest["version"] : undefined;
|
|
148
|
+
return typeof version === "string" ? version : "";
|
|
149
|
+
}
|
|
150
|
+
if (dirnamePath(at) === at) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Whether one node wraps an expression without changing its value. */
|
|
157
|
+
function isWrapper(
|
|
158
|
+
node: Node,
|
|
159
|
+
): node is ParenthesizedExpression | AsExpression | SatisfiesExpression | NonNullExpression {
|
|
160
|
+
return (
|
|
161
|
+
isParenthesizedExpression(node) ||
|
|
162
|
+
isAsExpression(node) ||
|
|
163
|
+
isSatisfiesExpression(node) ||
|
|
164
|
+
isNonNullExpression(node)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The expression a wrapper that changes no value holds. */
|
|
169
|
+
function unwrapped(node: Node): Node {
|
|
170
|
+
let at = node;
|
|
171
|
+
while (isWrapper(at)) {
|
|
172
|
+
at = at.expression;
|
|
173
|
+
}
|
|
174
|
+
return at;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The key one property is written with, where it is written as a name or a literal. */
|
|
178
|
+
function keyOf(property: Node): string | undefined {
|
|
179
|
+
if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property)) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const { name } = property;
|
|
183
|
+
if (isComputedPropertyName(name)) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (isIdentifier(name) || isStringLiteral(name) || isNoSubstitutionTemplateLiteral(name)) {
|
|
187
|
+
return name.text;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The values one file's top-level variable declarations bind, by name. */
|
|
193
|
+
function declaredValues(file: SourceFile): ReadonlyMap<string, Node> {
|
|
194
|
+
const values = new Map<string, Node>();
|
|
195
|
+
for (const statement of file.statements) {
|
|
196
|
+
if (isVariableStatement(statement)) {
|
|
197
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
198
|
+
if (isIdentifier(declaration.name) && declaration.initializer !== undefined) {
|
|
199
|
+
values.set(declaration.name.text, declaration.initializer);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return values;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isModuleExports(node: Node): boolean {
|
|
208
|
+
return (
|
|
209
|
+
isPropertyAccessExpression(node) &&
|
|
210
|
+
isIdentifier(node.expression) &&
|
|
211
|
+
node.expression.text === "module" &&
|
|
212
|
+
node.name.text === "exports"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Every expression or declaration one file exports as its default, in any module syntax. */
|
|
217
|
+
function defaultExports(file: SourceFile): readonly Node[] {
|
|
218
|
+
const found: Node[] = [];
|
|
219
|
+
for (const statement of file.statements) {
|
|
220
|
+
if (isExportAssignment(statement)) {
|
|
221
|
+
found.push(statement.expression);
|
|
222
|
+
} else if (
|
|
223
|
+
(isFunctionDeclaration(statement) || isClassDeclaration(statement)) &&
|
|
224
|
+
statement.modifiers?.some((one) => one.kind === SyntaxKind.DefaultKeyword) === true
|
|
225
|
+
) {
|
|
226
|
+
found.push(statement);
|
|
227
|
+
} else if (
|
|
228
|
+
isExportDeclaration(statement) &&
|
|
229
|
+
statement.moduleSpecifier === undefined &&
|
|
230
|
+
statement.exportClause !== undefined &&
|
|
231
|
+
isNamedExports(statement.exportClause)
|
|
232
|
+
) {
|
|
233
|
+
for (const element of statement.exportClause.elements) {
|
|
234
|
+
if (element.name.text === "default") {
|
|
235
|
+
found.push(element.propertyName ?? element.name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else if (
|
|
239
|
+
isExpressionStatement(statement) &&
|
|
240
|
+
isBinaryExpression(statement.expression) &&
|
|
241
|
+
statement.expression.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
242
|
+
isModuleExports(statement.expression.left)
|
|
243
|
+
) {
|
|
244
|
+
found.push(statement.expression.right);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return found;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The first argument of every call one file makes to one module's export, through a
|
|
252
|
+
* named or a namespace import; `undefined` for a call with no argument.
|
|
253
|
+
*/
|
|
254
|
+
function optionsArguments(file: SourceFile, call: OptionsCall): readonly (Node | undefined)[] {
|
|
255
|
+
const names = new Set<string>();
|
|
256
|
+
const namespaces = new Set<string>();
|
|
257
|
+
for (const statement of file.statements) {
|
|
258
|
+
const bindings = isImportDeclaration(statement)
|
|
259
|
+
? statement.importClause?.namedBindings
|
|
260
|
+
: undefined;
|
|
261
|
+
if (
|
|
262
|
+
bindings === undefined ||
|
|
263
|
+
!isImportDeclaration(statement) ||
|
|
264
|
+
!isStringLiteral(statement.moduleSpecifier) ||
|
|
265
|
+
statement.moduleSpecifier.text !== call.module
|
|
266
|
+
) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (isNamespaceImport(bindings)) {
|
|
270
|
+
namespaces.add(bindings.name.text);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
for (const element of bindings.elements) {
|
|
274
|
+
if ((element.propertyName ?? element.name).text === call.export) {
|
|
275
|
+
names.add(element.name.text);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const found: (Node | undefined)[] = [];
|
|
280
|
+
const visit = (node: Node): void => {
|
|
281
|
+
if (isCallExpression(node)) {
|
|
282
|
+
const callee = unwrapped(node.expression);
|
|
283
|
+
const named = isIdentifier(callee) && names.has(callee.text);
|
|
284
|
+
const throughNamespace =
|
|
285
|
+
isPropertyAccessExpression(callee) &&
|
|
286
|
+
isIdentifier(callee.expression) &&
|
|
287
|
+
namespaces.has(callee.expression.text) &&
|
|
288
|
+
callee.name.text === call.export;
|
|
289
|
+
if (named || throughNamespace) {
|
|
290
|
+
found.push(node.arguments[0]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
node.forEachChild(visit);
|
|
294
|
+
};
|
|
295
|
+
file.forEachChild(visit);
|
|
296
|
+
return found;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The object literal one expression stands for, read through the file's top-level
|
|
301
|
+
* declarations and, where `throughCalls`, through a call's first argument: `undefined`
|
|
302
|
+
* for a call with no argument, and any other node where the expression is not written
|
|
303
|
+
* out as an object literal.
|
|
304
|
+
*/
|
|
305
|
+
function objectOf(
|
|
306
|
+
node: Node,
|
|
307
|
+
declared: ReadonlyMap<string, Node>,
|
|
308
|
+
throughCalls: boolean,
|
|
309
|
+
seen: ReadonlySet<string> = new Set(),
|
|
310
|
+
): Node | undefined {
|
|
311
|
+
const at = unwrapped(node);
|
|
312
|
+
if (isIdentifier(at)) {
|
|
313
|
+
const value = declared.get(at.text);
|
|
314
|
+
if (value !== undefined && !seen.has(at.text)) {
|
|
315
|
+
return objectOf(value, declared, throughCalls, new Set([...seen, at.text]));
|
|
316
|
+
}
|
|
317
|
+
} else if (throughCalls && isCallExpression(at)) {
|
|
318
|
+
const [first] = at.arguments;
|
|
319
|
+
return first === undefined ? undefined : objectOf(first, declared, throughCalls, seen);
|
|
320
|
+
}
|
|
321
|
+
return at;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Reads the values at one path of keys below one object literal into `values`, and
|
|
326
|
+
* answers the first member that does not write its value out: a spread, a value that is
|
|
327
|
+
* not a literal at the path's end, or one that is not an object literal before it.
|
|
328
|
+
*/
|
|
329
|
+
function readPath(
|
|
330
|
+
object: ObjectLiteralExpression,
|
|
331
|
+
keys: readonly string[],
|
|
332
|
+
values: string[],
|
|
333
|
+
): Node | undefined {
|
|
334
|
+
const [key, ...rest] = keys;
|
|
335
|
+
for (const member of object.properties) {
|
|
336
|
+
if (isSpreadAssignment(member)) {
|
|
337
|
+
return member;
|
|
338
|
+
}
|
|
339
|
+
if (keyOf(member) !== key) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const held = isPropertyAssignment(member) ? unwrapped(member.initializer) : undefined;
|
|
343
|
+
if (rest.length === 0) {
|
|
344
|
+
if (held === undefined || !(isStringLiteral(held) || isNoSubstitutionTemplateLiteral(held))) {
|
|
345
|
+
return member;
|
|
346
|
+
}
|
|
347
|
+
values.push(held.text);
|
|
348
|
+
} else {
|
|
349
|
+
if (held === undefined || !isObjectLiteralExpression(held)) {
|
|
350
|
+
return member;
|
|
351
|
+
}
|
|
352
|
+
const failed = readPath(held, rest, values);
|
|
353
|
+
if (failed !== undefined) {
|
|
354
|
+
return failed;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** What one configuration file states about one property. */
|
|
362
|
+
interface PropertyReading {
|
|
363
|
+
/** The literal values the file sets the property to. */
|
|
364
|
+
readonly values: readonly string[];
|
|
365
|
+
/** The position of the first value that is not a literal, where there is one. */
|
|
366
|
+
readonly notLiteral?: Node;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Every value one file writes at one property's path inside the object it is read from:
|
|
371
|
+
* the options argument of every call `call` names, or the file's default export. A string
|
|
372
|
+
* literal or a template literal with no substitution is the value. A read object, or an
|
|
373
|
+
* object along the path, that is not written out as an object literal, or that spreads
|
|
374
|
+
* another object into itself, is not a literal, and neither is any other value at the path.
|
|
375
|
+
*/
|
|
376
|
+
export function readProperty(
|
|
377
|
+
file: SourceFile,
|
|
378
|
+
property: string,
|
|
379
|
+
call?: OptionsCall,
|
|
380
|
+
): PropertyReading {
|
|
381
|
+
const declared = declaredValues(file);
|
|
382
|
+
const objects =
|
|
383
|
+
call === undefined
|
|
384
|
+
? defaultExports(file).map((one) => objectOf(one, declared, true))
|
|
385
|
+
: optionsArguments(file, call).map((one) =>
|
|
386
|
+
one === undefined ? undefined : objectOf(one, declared, false),
|
|
387
|
+
);
|
|
388
|
+
const values: string[] = [];
|
|
389
|
+
for (const object of objects) {
|
|
390
|
+
if (object === undefined) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const notLiteral = isObjectLiteralExpression(object)
|
|
394
|
+
? readPath(object, property.split("."), values)
|
|
395
|
+
: object;
|
|
396
|
+
if (notLiteral !== undefined) {
|
|
397
|
+
return { values, notLiteral };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { values };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** A directory below a manifest's, `.` for the directory itself, `undefined` outside it. */
|
|
404
|
+
function below(dir: string, path: string): string | undefined {
|
|
405
|
+
return normalizePath(path) === normalizePath(dir) ? "." : relativePath(dir, path);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Every text a template stands for, each placeholder replaced by every directory its move
|
|
410
|
+
* resolved to. A placeholder whose move resolved outside the manifest's directory leaves
|
|
411
|
+
* the text standing for nothing.
|
|
412
|
+
*/
|
|
413
|
+
function expand(
|
|
414
|
+
template: string,
|
|
415
|
+
resolved: ReadonlyMap<string, readonly (string | undefined)[]>,
|
|
416
|
+
): readonly string[] {
|
|
417
|
+
const match = /<([A-Za-z]+)>/u.exec(template);
|
|
418
|
+
if (match === null) {
|
|
419
|
+
return [template];
|
|
420
|
+
}
|
|
421
|
+
const [placeholder, id = ""] = match;
|
|
422
|
+
const out: string[] = [];
|
|
423
|
+
for (const value of resolved.get(id) ?? []) {
|
|
424
|
+
if (value === undefined) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const replaced =
|
|
428
|
+
value === "." && template.startsWith(`${placeholder}/`, match.index)
|
|
429
|
+
? template.replace(`${placeholder}/`, "")
|
|
430
|
+
: template.replace(placeholder, value);
|
|
431
|
+
out.push(...expand(replaced, resolved));
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The configuration files in one directory that one glob names, by name. */
|
|
437
|
+
function configurationFiles(host: Host, dir: string, glob: string): readonly string[] {
|
|
438
|
+
const expression = globExpression(glob);
|
|
439
|
+
let entries;
|
|
440
|
+
try {
|
|
441
|
+
entries = host.readDirectory(dir);
|
|
442
|
+
} catch {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
return entries
|
|
446
|
+
.filter((entry) => !entry.directory && expression.test(entry.name))
|
|
447
|
+
.map((entry) => entry.name)
|
|
448
|
+
.sort();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** What one move resolved to: its directories, or the failure its property met. */
|
|
452
|
+
type MoveResult = { readonly dirs: readonly (string | undefined)[] } | { readonly failure: string };
|
|
453
|
+
|
|
454
|
+
function resolveMove(
|
|
455
|
+
host: Host,
|
|
456
|
+
parse: ParseSource,
|
|
457
|
+
dir: string,
|
|
458
|
+
row: ConventionRow,
|
|
459
|
+
move: DirectoryMove,
|
|
460
|
+
resolved: ReadonlyMap<string, readonly (string | undefined)[]>,
|
|
461
|
+
): MoveResult {
|
|
462
|
+
const set: string[] = [];
|
|
463
|
+
for (const reading of move.readings) {
|
|
464
|
+
for (const glob of reading.files) {
|
|
465
|
+
for (const name of configurationFiles(host, dir, glob)) {
|
|
466
|
+
const path = joinPath(dir, name);
|
|
467
|
+
let text: string;
|
|
468
|
+
try {
|
|
469
|
+
text = host.readFile(path);
|
|
470
|
+
} catch {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const file = parse(path, text);
|
|
474
|
+
const read = readProperty(file, reading.property, reading.call);
|
|
475
|
+
if (read.notLiteral !== undefined) {
|
|
476
|
+
const line = lineOf(text, read.notLiteral.getStart());
|
|
477
|
+
return {
|
|
478
|
+
failure:
|
|
479
|
+
`${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
|
|
480
|
+
`the convention row ${row.name} reads, to a value that is not a string literal: ` +
|
|
481
|
+
`write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
|
|
482
|
+
`files in ts.entry_files`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
set.push(...read.values);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const dirs: (string | undefined)[] = [];
|
|
490
|
+
for (const base of expand(move.base ?? ".", resolved)) {
|
|
491
|
+
const against = joinPath(dir, base);
|
|
492
|
+
const texts = set.length > 0 ? set : move.defaults.flatMap((one) => expand(one, resolved));
|
|
493
|
+
for (const text of texts) {
|
|
494
|
+
dirs.push(below(dir, resolvePath(against, text)));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { dirs: [...new Set(dirs)] };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Two strings ordered bytewise. */
|
|
501
|
+
function compare(a: string, b: string): number {
|
|
502
|
+
if (a === b) {
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
return a < b ? -1 : 1;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* What the rows decide over the manifests the analysis reads. A row `disabled` names
|
|
510
|
+
* is skipped before anything is read for it.
|
|
511
|
+
*/
|
|
512
|
+
export function readConventions(
|
|
513
|
+
host: Host,
|
|
514
|
+
parse: ParseSource,
|
|
515
|
+
manifests: readonly ConventionManifest[],
|
|
516
|
+
disabled: readonly string[],
|
|
517
|
+
rows: readonly ConventionRow[] = CONVENTION_ROWS,
|
|
518
|
+
): Conventions {
|
|
519
|
+
const applied: AppliedConvention[] = [];
|
|
520
|
+
const globs: ConventionGlob[] = [];
|
|
521
|
+
const failures: ConventionFailure[] = [];
|
|
522
|
+
for (const manifest of manifests) {
|
|
523
|
+
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
524
|
+
const versions = new Map<string, string | undefined>();
|
|
525
|
+
const missing = new Set<string>();
|
|
526
|
+
for (const row of rows) {
|
|
527
|
+
if (disabled.includes(row.name) || !declares(declared, row.package)) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (!versions.has(row.package)) {
|
|
531
|
+
versions.set(row.package, installedVersion(host, manifest.dir, row.package));
|
|
532
|
+
}
|
|
533
|
+
const version = versions.get(row.package);
|
|
534
|
+
if (version === undefined) {
|
|
535
|
+
if (missing.has(row.package)) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
missing.add(row.package);
|
|
539
|
+
failures.push({
|
|
540
|
+
dir: manifest.dir,
|
|
541
|
+
failure: {
|
|
542
|
+
setupClass: "missing-module",
|
|
543
|
+
detail:
|
|
544
|
+
`${manifest.path} declares ${row.package}, which no node_modules at or ` +
|
|
545
|
+
`above its directory holds: run the package manager's install`,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (!satisfies(version, row.range)) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const resolved = new Map<string, readonly (string | undefined)[]>();
|
|
554
|
+
let failed: string | undefined;
|
|
555
|
+
for (const move of row.moves) {
|
|
556
|
+
const result = resolveMove(host, parse, manifest.dir, row, move, resolved);
|
|
557
|
+
if ("failure" in result) {
|
|
558
|
+
failed = result.failure;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
resolved.set(move.id, result.dirs);
|
|
562
|
+
}
|
|
563
|
+
if (failed !== undefined) {
|
|
564
|
+
const where = dirnamePath(manifest.path);
|
|
565
|
+
failures.push({
|
|
566
|
+
dir: manifest.dir,
|
|
567
|
+
failure: {
|
|
568
|
+
setupClass: "convention-not-literal",
|
|
569
|
+
detail: where === "." ? failed : `${where}/${failed}`,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
applied.push({
|
|
575
|
+
name: row.name,
|
|
576
|
+
package: row.package,
|
|
577
|
+
version,
|
|
578
|
+
manifest: manifest.path,
|
|
579
|
+
});
|
|
580
|
+
const excludes = [
|
|
581
|
+
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
582
|
+
].map(globExpression);
|
|
583
|
+
for (const glob of new Set(row.entries.flatMap((entry) => expand(entry, resolved)))) {
|
|
584
|
+
globs.push({
|
|
585
|
+
dir: manifest.dir,
|
|
586
|
+
row: row.name,
|
|
587
|
+
expression: globExpression(glob),
|
|
588
|
+
excludes,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const unique = [...new Map(applied.map((one) => [JSON.stringify(one), one])).values()];
|
|
594
|
+
unique.sort(
|
|
595
|
+
(a, b) =>
|
|
596
|
+
compare(a.name, b.name) ||
|
|
597
|
+
compare(a.manifest, b.manifest) ||
|
|
598
|
+
compare(JSON.stringify(a), JSON.stringify(b)),
|
|
599
|
+
);
|
|
600
|
+
return { applied: unique, globs, failures };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Every own file of one project an applied row's globs match, once per row. */
|
|
604
|
+
export function conventionEntries(
|
|
605
|
+
conventions: Conventions,
|
|
606
|
+
files: SourceFiles,
|
|
607
|
+
): readonly ConventionEntry[] {
|
|
608
|
+
const found = new Map<string, ConventionEntry>();
|
|
609
|
+
for (const glob of conventions.globs) {
|
|
610
|
+
for (const file of files.byName.values()) {
|
|
611
|
+
const path = relativePath(glob.dir, file.fileName);
|
|
612
|
+
if (
|
|
613
|
+
path !== undefined &&
|
|
614
|
+
glob.expression.test(path) &&
|
|
615
|
+
!glob.excludes.some((one) => one.test(path))
|
|
616
|
+
) {
|
|
617
|
+
found.set(`${file.fileName}\u0000${glob.row}`, { file, row: glob.row });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return [...found.values()];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The failures the rows met that hold for one configuration: those of a manifest whose
|
|
626
|
+
* directory holds the configuration file or one of the files the project owns.
|
|
627
|
+
*/
|
|
628
|
+
export function conventionFailuresFor(
|
|
629
|
+
conventions: Conventions,
|
|
630
|
+
configFile: string,
|
|
631
|
+
ownFiles: readonly string[],
|
|
632
|
+
): readonly SetupFailure[] {
|
|
633
|
+
return conventions.failures
|
|
634
|
+
.filter(
|
|
635
|
+
({ dir }) =>
|
|
636
|
+
below(dir, dirnamePath(configFile)) !== undefined ||
|
|
637
|
+
ownFiles.some((file) => relativePath(dir, file) !== undefined),
|
|
638
|
+
)
|
|
639
|
+
.map(({ failure }) => failure);
|
|
640
|
+
}
|
package/src/dependencies.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The dependencies the target's manifest declares, and what the projects' own files
|
|
3
3
|
* need from outside the target. A file needs a package it imports, re-exports from,
|
|
4
4
|
* augments or references the types of, by name or through the file the specifier
|
|
5
|
-
* resolves to; a configuration needs one it names
|
|
5
|
+
* resolves to; a compiler configuration needs one it names, and a configuration file a
|
|
6
|
+
* tool loads needs each dependency its strings spell. A package one dependency pulls in
|
|
6
7
|
* for itself is that dependency's need, not the target's.
|
|
7
8
|
*/
|
|
8
9
|
|
|
@@ -21,6 +22,12 @@ import {
|
|
|
21
22
|
type Node,
|
|
22
23
|
type SourceFile,
|
|
23
24
|
} from "@typescript/native/unstable/ast";
|
|
25
|
+
import {
|
|
26
|
+
configurationFiles,
|
|
27
|
+
declaredIn as declaredBeside,
|
|
28
|
+
dependenciesNamed,
|
|
29
|
+
moduleStrings,
|
|
30
|
+
} from "./configuration-files.ts";
|
|
24
31
|
import type { FindingPosition } from "./finding.ts";
|
|
25
32
|
import type { Host } from "./host.ts";
|
|
26
33
|
import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
|
|
@@ -28,6 +35,7 @@ import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
|
28
35
|
import type { DependencySection, Module } from "./ref.ts";
|
|
29
36
|
import { UNANSWERED } from "./query.ts";
|
|
30
37
|
import type { Handle, ProjectView } from "./session.ts";
|
|
38
|
+
import { sourceFilesOf } from "./source-files.ts";
|
|
31
39
|
|
|
32
40
|
const MANIFEST = "package.json";
|
|
33
41
|
const MODULES_DIR = "node_modules";
|
|
@@ -268,12 +276,14 @@ function bindingsOf(node: Node | undefined): readonly Texted[] {
|
|
|
268
276
|
* Every package one project needs, and which declaration each use is written in.
|
|
269
277
|
* One local walk and one batched resolution per file: a use of an import binding is
|
|
270
278
|
* an identifier resolving to the binding's own symbol, so a local declaration that
|
|
271
|
-
* shadows the name is not one.
|
|
279
|
+
* shadows the name is not one. A string of a configuration file uses each dependency
|
|
280
|
+
* the manifest beside the file declares that it spells.
|
|
272
281
|
*/
|
|
273
282
|
export function projectNeeds<Brand>(
|
|
274
283
|
project: ProjectView<Brand>,
|
|
275
284
|
held: Inventory,
|
|
276
285
|
host: Host,
|
|
286
|
+
targetRoot: string,
|
|
277
287
|
): ProjectNeeds {
|
|
278
288
|
const programPackages = new Set<string>();
|
|
279
289
|
for (const name of project.program.getSourceFileNames()) {
|
|
@@ -308,6 +318,30 @@ export function projectNeeds<Brand>(
|
|
|
308
318
|
for (const file of project.ownSourceFiles()) {
|
|
309
319
|
unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
|
|
310
320
|
}
|
|
321
|
+
|
|
322
|
+
const beside = new Map<string, readonly string[]>();
|
|
323
|
+
const declaredAt = (path: string): readonly string[] => {
|
|
324
|
+
const dir = dirnamePath(path);
|
|
325
|
+
let names = beside.get(dir);
|
|
326
|
+
if (names === undefined) {
|
|
327
|
+
names = declaredBeside(host, dir);
|
|
328
|
+
beside.set(dir, names);
|
|
329
|
+
}
|
|
330
|
+
return names;
|
|
331
|
+
};
|
|
332
|
+
const configuration = configurationFiles(host, targetRoot, sourceFilesOf(project, targetRoot));
|
|
333
|
+
for (const { file } of configuration.modules) {
|
|
334
|
+
const names = declaredAt(file.fileName);
|
|
335
|
+
for (const { node, text } of moduleStrings(file)) {
|
|
336
|
+
use(ownersOf(file, held, node), dependenciesNamed(text, names));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const document of configuration.documents) {
|
|
340
|
+
const names = declaredAt(document.path);
|
|
341
|
+
for (const text of document.strings) {
|
|
342
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
311
345
|
return { packages, uses, unanswered };
|
|
312
346
|
}
|
|
313
347
|
|