@cplieger/deadset-ts 8.0.0-dev.15 → 8.0.0-dev.16
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/dist/src/analysis.js +41 -37
- package/dist/src/commands/read.js +6 -7
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +3 -1
- package/dist/src/contract/config.generated.js +1 -1
- package/dist/src/contract/contract.generated.js +119 -2
- package/dist/src/dependencies.js +79 -48
- package/dist/src/entries/build-scripts.js +222 -0
- package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
- package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
- package/dist/src/entries/conventions.js +739 -0
- package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
- package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
- package/dist/src/entries/run-directories.js +77 -0
- package/dist/src/entries/sources.js +144 -0
- package/dist/src/entries/static-module.js +669 -0
- package/dist/src/entries/test-runners.js +684 -0
- package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
- package/dist/src/extensions.js +37 -0
- package/dist/src/facts.js +38 -0
- package/dist/src/findings/emitters.js +5 -8
- package/dist/src/findings/self-check.js +36 -20
- package/dist/src/findings-pass.js +5 -6
- package/dist/src/framework-lifecycle.js +1 -1
- package/dist/src/fs/manifest.js +15 -20
- package/dist/src/generated-file.js +172 -29
- package/dist/src/graph.js +2 -4
- package/dist/src/index.js +1 -1
- package/dist/src/injection-container.js +1 -1
- package/dist/src/inventory/commonjs.js +62 -17
- package/dist/src/inventory/enumerate.js +2 -1
- package/dist/src/javascript-file.js +26 -11
- package/dist/src/load/classify.js +28 -9
- package/dist/src/load/diagnostics.js +8 -1
- package/dist/src/load/discover.js +15 -7
- package/dist/src/load/plan.js +2 -2
- package/dist/src/load/synthesize.js +5 -5
- package/dist/src/load/workspace-manifests.js +1 -1
- package/dist/src/matrix.js +6 -13
- package/dist/src/outside-tests.js +1 -1
- package/dist/src/paths.js +16 -0
- package/dist/src/platform-calls.js +78 -0
- package/dist/src/platform-symbols.js +140 -0
- package/dist/src/references.js +202 -32
- package/dist/src/resolve.js +1 -1
- package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
- package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
- package/dist/src/{manifest.js → roots/manifest.js} +4 -4
- package/dist/src/roots/published-reach.js +412 -0
- package/dist/src/roots/root-set.js +489 -0
- package/dist/src/roots/unseen-loads.js +208 -0
- package/dist/src/serialization-contract.js +1 -1
- package/dist/src/source-files.js +5 -9
- package/dist/src/suppress.js +30 -32
- package/dist/src/test-support.js +4 -2
- package/dist/src/verbs/analyze.js +13 -1
- package/dist/src/verbs/describe.js +5 -3
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +75 -52
- package/src/automatic-types.ts +1 -1
- package/src/commands/read.ts +6 -8
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +4 -0
- package/src/contract/config.generated.ts +1 -1
- package/src/contract/contract.generated.ts +138 -2
- package/src/dependencies.ts +96 -49
- package/src/entries/build-scripts.ts +292 -0
- package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
- package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
- package/src/{conventions.ts → entries/conventions.ts} +347 -522
- package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
- package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
- package/src/entries/run-directories.ts +108 -0
- package/src/entries/sources.ts +266 -0
- package/src/entries/static-module.ts +857 -0
- package/src/entries/test-runners.ts +868 -0
- package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
- package/src/exempt.ts +3 -2
- package/src/extensions.ts +45 -0
- package/src/facts.ts +44 -0
- package/src/file-facts.ts +1 -1
- package/src/findings/emitters.ts +14 -10
- package/src/findings/self-check.ts +41 -22
- package/src/findings/visibility-narrowing.ts +1 -1
- package/src/findings-pass.ts +5 -12
- package/src/framework-lifecycle.ts +1 -1
- package/src/fs/manifest.ts +25 -21
- package/src/generated-file.ts +240 -36
- package/src/graph.ts +3 -5
- package/src/index.ts +1 -1
- package/src/injection-container.ts +1 -1
- package/src/inventory/commonjs.ts +102 -17
- package/src/inventory/enumerate.ts +2 -1
- package/src/javascript-file.ts +29 -12
- package/src/load/classify.ts +49 -13
- package/src/load/diagnostics.ts +11 -1
- package/src/load/discover.ts +27 -11
- package/src/load/plan.ts +11 -2
- package/src/load/synthesize.ts +6 -6
- package/src/load/test-globals.ts +1 -1
- package/src/load/workspace-manifests.ts +1 -1
- package/src/load/workspace-run.ts +1 -1
- package/src/matrix.ts +7 -14
- package/src/outside-tests.ts +1 -1
- package/src/paths.ts +18 -0
- package/src/platform-calls.ts +119 -0
- package/src/platform-symbols.ts +205 -0
- package/src/references.ts +281 -34
- package/src/report.ts +1 -1
- package/src/resolve.ts +1 -1
- package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
- package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
- package/src/{manifest.ts → roots/manifest.ts} +5 -5
- package/src/roots/published-reach.ts +481 -0
- package/src/{roots.ts → roots/root-set.ts} +203 -630
- package/src/roots/unseen-loads.ts +307 -0
- package/src/serialization-contract.ts +1 -1
- package/src/source-files.ts +5 -10
- package/src/suppress.ts +48 -33
- package/src/test-support.ts +8 -3
- package/src/verbs/analyze.ts +17 -4
- package/src/verbs/describe.ts +5 -2
- package/src/verbs/explain.ts +1 -1
- package/dist/src/build-scripts.js +0 -418
- package/dist/src/conventions.js +0 -907
- package/dist/src/entry-points.js +0 -444
- package/dist/src/own-body.js +0 -28
- package/dist/src/roots.js +0 -865
- package/dist/src/unseen-loads.js +0 -189
- package/src/build-scripts.ts +0 -536
- package/src/entry-points.ts +0 -584
- package/src/own-body.ts +0 -42
- package/src/unseen-loads.ts +0 -282
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loads by a computed path: those whose files the analysis resolves, and those it
|
|
3
|
+
* cannot, whose files are roots the analysis does not see, so the configuration's roots
|
|
4
|
+
* are never complete.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isBinaryExpression,
|
|
9
|
+
isCallExpression,
|
|
10
|
+
isIdentifier,
|
|
11
|
+
isMetaProperty,
|
|
12
|
+
isNoSubstitutionTemplateLiteral,
|
|
13
|
+
isPropertyAccessExpression,
|
|
14
|
+
isStringLiteral,
|
|
15
|
+
isTemplateExpression,
|
|
16
|
+
SyntaxKind,
|
|
17
|
+
type CallExpression,
|
|
18
|
+
type Identifier,
|
|
19
|
+
type Node,
|
|
20
|
+
type SourceFile,
|
|
21
|
+
} from "@typescript/native/ast";
|
|
22
|
+
import { membersNamed, textsOf, writtenValue, type Value } from "../entries/static-module.ts";
|
|
23
|
+
import { writtenName } from "../platform-calls.ts";
|
|
24
|
+
import { dirnamePath, relativePath, resolvePath } from "../paths.ts";
|
|
25
|
+
import type { Position } from "../position.ts";
|
|
26
|
+
import { unwrapped } from "../unwrap.ts";
|
|
27
|
+
import { globExpression } from "../text/glob.ts";
|
|
28
|
+
|
|
29
|
+
/** One load the analysis cannot resolve, in the configuration whose production file writes it. */
|
|
30
|
+
export interface UnseenLoad {
|
|
31
|
+
readonly configuration: string;
|
|
32
|
+
/** The position of the call. */
|
|
33
|
+
readonly position: Position;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
37
|
+
function leadingLiteral(node: Node): string | undefined {
|
|
38
|
+
const at = unwrapped(node);
|
|
39
|
+
if (isTemplateExpression(at)) {
|
|
40
|
+
return at.head.text;
|
|
41
|
+
}
|
|
42
|
+
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
43
|
+
const left = unwrapped(at.left);
|
|
44
|
+
return isStringLiteral(left) || isNoSubstitutionTemplateLiteral(left)
|
|
45
|
+
? left.text
|
|
46
|
+
: leadingLiteral(at.left);
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The directory below `dir` a computed load's leading literal names, every file of which
|
|
53
|
+
* the load reaches, or none where it names a package, `dir` itself or a directory holding
|
|
54
|
+
* it, each of which would name every sibling.
|
|
55
|
+
*/
|
|
56
|
+
function computedLoadDirectory(dir: string, prefix: string): string | undefined {
|
|
57
|
+
if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const target = resolvePath(dir, prefix);
|
|
61
|
+
return target === dir || relativePath(target, dir) !== undefined ? undefined : target;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One `import()`, `require()` or `import.meta.glob()` call, and the declaration whose code writes it. */
|
|
65
|
+
export interface LoadCall {
|
|
66
|
+
readonly call: CallExpression;
|
|
67
|
+
readonly from: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One file a computed load may load, by its id. */
|
|
71
|
+
interface Loadable {
|
|
72
|
+
readonly file: SourceFile;
|
|
73
|
+
readonly fileId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One load by a computed path whose files the analysis resolves. */
|
|
77
|
+
interface ComputedLoad {
|
|
78
|
+
readonly call: CallExpression;
|
|
79
|
+
readonly from: string;
|
|
80
|
+
/** The name a `require()` call is written with; {@link callsPlatform} owns which one loads. */
|
|
81
|
+
readonly require: Identifier | undefined;
|
|
82
|
+
readonly files: readonly Loadable[];
|
|
83
|
+
/** Whether the load reads each file as text or as a URL, which references no declaration of it. */
|
|
84
|
+
readonly asset: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the analysis names every file the load may load. An unresolved load names none,
|
|
87
|
+
* and leaves the configuration's roots incomplete (confidence.26).
|
|
88
|
+
*/
|
|
89
|
+
readonly resolved: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Whether the load is an eager glob, which Vite turns into static imports at the top of
|
|
92
|
+
* the calling module (https://vite.dev/guide/features.html#glob-import), so the file
|
|
93
|
+
* evaluates what it loads wherever the call is written.
|
|
94
|
+
*/
|
|
95
|
+
readonly eager: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Whether one node is a call that may load a module by a computed path. */
|
|
99
|
+
export function isLoadCall(node: Node): node is CallExpression {
|
|
100
|
+
if (!isCallExpression(node)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return (
|
|
104
|
+
node.expression.kind === SyntaxKind.ImportKeyword ||
|
|
105
|
+
writtenName(node.expression) !== undefined ||
|
|
106
|
+
isMetaGlob(node)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The files one `import.meta.glob` call loads, as Vite's `toAbsoluteGlob` reads its globs
|
|
112
|
+
* (https://vite.dev/guide/features.html#glob-import): `/` against the project root, `./`
|
|
113
|
+
* and `../` against a literal `base` (read against the root where it starts with `/`) or the
|
|
114
|
+
* calling file's directory, `**` as written, and `!` removing what it matches. Undefined
|
|
115
|
+
* where Vite reads a glob through an alias, the root is unknown, or a glob or base is computed.
|
|
116
|
+
*/
|
|
117
|
+
function globbed(
|
|
118
|
+
file: SourceFile,
|
|
119
|
+
first: Node,
|
|
120
|
+
options: Node | undefined,
|
|
121
|
+
others: readonly Loadable[],
|
|
122
|
+
rootOf: (fileName: string) => string | undefined,
|
|
123
|
+
): readonly Loadable[] | undefined {
|
|
124
|
+
const patterns = textsOf(writtenValue(first), true);
|
|
125
|
+
const bases = globOption(options, "base");
|
|
126
|
+
const [written, ...more] = bases ?? [];
|
|
127
|
+
const base = written === undefined ? undefined : textsOf(written, false);
|
|
128
|
+
if (
|
|
129
|
+
patterns === undefined ||
|
|
130
|
+
bases === undefined ||
|
|
131
|
+
more.length > 0 ||
|
|
132
|
+
(written !== undefined && base === undefined)
|
|
133
|
+
) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const dir = dirnamePath(file.fileName);
|
|
137
|
+
const root = (): string | undefined => rootOf(file.fileName);
|
|
138
|
+
const against = (at: string): string | undefined => {
|
|
139
|
+
if (!at.startsWith("/")) {
|
|
140
|
+
return resolvePath(dir, at);
|
|
141
|
+
}
|
|
142
|
+
const top = root();
|
|
143
|
+
return top === undefined ? undefined : resolvePath(top, `.${at}`);
|
|
144
|
+
};
|
|
145
|
+
const from = base?.[0] === undefined ? dir : against(base[0]);
|
|
146
|
+
const keep: RegExp[] = [];
|
|
147
|
+
const drop: RegExp[] = [];
|
|
148
|
+
for (const written of patterns) {
|
|
149
|
+
const negated = written.startsWith("!");
|
|
150
|
+
const pattern = negated ? written.slice(1) : written;
|
|
151
|
+
let path: string | undefined;
|
|
152
|
+
if (pattern.startsWith("/")) {
|
|
153
|
+
path = against(pattern);
|
|
154
|
+
} else if (pattern.startsWith("./") || pattern.startsWith("../")) {
|
|
155
|
+
path = from === undefined ? undefined : resolvePath(from, pattern);
|
|
156
|
+
} else if (pattern.startsWith("**")) {
|
|
157
|
+
path = pattern;
|
|
158
|
+
}
|
|
159
|
+
if (path === undefined) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
(negated ? drop : keep).push(globExpression(path));
|
|
163
|
+
}
|
|
164
|
+
return others.filter(
|
|
165
|
+
(candidate) =>
|
|
166
|
+
keep.some((glob) => glob.test(candidate.file.fileName)) &&
|
|
167
|
+
!drop.some((glob) => glob.test(candidate.file.fileName)),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The computed loads among one file's load calls, each loading the `candidates` but the
|
|
173
|
+
* file itself: an `import()` or `require()` whose path starts with a relative literal naming
|
|
174
|
+
* a directory below the file's loads every candidate below it, as a bundler's context module
|
|
175
|
+
* reads it, and an `import.meta.glob` every candidate its globs match ({@link globbed}). A
|
|
176
|
+
* call of a computed path that names no such directory and no package is an unresolved load.
|
|
177
|
+
*/
|
|
178
|
+
export function computedLoadsOf(
|
|
179
|
+
file: SourceFile,
|
|
180
|
+
calls: readonly LoadCall[],
|
|
181
|
+
candidates: readonly Loadable[],
|
|
182
|
+
rootOf: (fileName: string) => string | undefined,
|
|
183
|
+
): readonly ComputedLoad[] {
|
|
184
|
+
const dir = dirnamePath(file.fileName);
|
|
185
|
+
const others = candidates.filter((candidate) => candidate.file !== file);
|
|
186
|
+
return calls.flatMap(({ call, from }): ComputedLoad[] => {
|
|
187
|
+
const [first, options] = call.arguments;
|
|
188
|
+
if (first === undefined) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
if (isMetaGlob(call)) {
|
|
192
|
+
const files = globbed(file, first, options, others, rootOf);
|
|
193
|
+
const asset = readsAsAsset(options);
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
call,
|
|
197
|
+
from,
|
|
198
|
+
require: undefined,
|
|
199
|
+
files: files ?? [],
|
|
200
|
+
resolved: files !== undefined,
|
|
201
|
+
asset,
|
|
202
|
+
eager: isEager(options),
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
const argument = unwrapped(first);
|
|
207
|
+
const require =
|
|
208
|
+
call.expression.kind === SyntaxKind.ImportKeyword ? undefined : writtenName(call.expression);
|
|
209
|
+
if (!mayNamePath(argument)) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const prefix = leadingLiteral(argument);
|
|
213
|
+
if (namesPackage(prefix)) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const target = prefix === undefined ? undefined : computedLoadDirectory(dir, prefix);
|
|
217
|
+
const files =
|
|
218
|
+
target === undefined
|
|
219
|
+
? []
|
|
220
|
+
: others.filter((candidate) => relativePath(target, candidate.file.fileName) !== undefined);
|
|
221
|
+
return [
|
|
222
|
+
{ call, from, require, files, resolved: target !== undefined, asset: false, eager: false },
|
|
223
|
+
];
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Whether a computed argument may be a module path: anything but a string, which names its
|
|
229
|
+
* module, and a value no path is, a number, a boolean, `null`, an object, an array, a
|
|
230
|
+
* function or a class.
|
|
231
|
+
*/
|
|
232
|
+
function mayNamePath(argument: Node): boolean {
|
|
233
|
+
return !NO_PATH.has(argument.kind);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const NO_PATH: ReadonlySet<SyntaxKind> = new Set([
|
|
237
|
+
SyntaxKind.StringLiteral,
|
|
238
|
+
SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
239
|
+
SyntaxKind.NumericLiteral,
|
|
240
|
+
SyntaxKind.BigIntLiteral,
|
|
241
|
+
SyntaxKind.TrueKeyword,
|
|
242
|
+
SyntaxKind.FalseKeyword,
|
|
243
|
+
SyntaxKind.NullKeyword,
|
|
244
|
+
SyntaxKind.ObjectLiteralExpression,
|
|
245
|
+
SyntaxKind.ArrayLiteralExpression,
|
|
246
|
+
SyntaxKind.ArrowFunction,
|
|
247
|
+
SyntaxKind.FunctionExpression,
|
|
248
|
+
SyntaxKind.ClassExpression,
|
|
249
|
+
SyntaxKind.RegularExpressionLiteral,
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
/** Whether one call is `import.meta.glob(...)`. */
|
|
253
|
+
function isMetaGlob(node: CallExpression): boolean {
|
|
254
|
+
if (!isPropertyAccessExpression(node.expression)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const { expression, name } = node.expression;
|
|
258
|
+
return (
|
|
259
|
+
isIdentifier(name) &&
|
|
260
|
+
name.text === "glob" &&
|
|
261
|
+
isMetaProperty(expression) &&
|
|
262
|
+
expression.keywordToken === SyntaxKind.ImportKeyword
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The values the options of an `import.meta.glob` write for one key, undefined where a spread may hide one. */
|
|
267
|
+
function globOption(options: Node | undefined, key: string): readonly Value[] | undefined {
|
|
268
|
+
return options === undefined ? [] : membersNamed(writtenValue(options), key);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Whether the options of an `import.meta.glob` set `eager` to `true`. */
|
|
272
|
+
function isEager(options: Node | undefined): boolean {
|
|
273
|
+
return (
|
|
274
|
+
globOption(options, "eager")?.some((value) => value.kind === "boolean" && value.value) === true
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The query parameters that make Vite load a file as its text or its URL, each written bare. */
|
|
279
|
+
const ASSET_QUERY = /(?:^\?|&)(?:raw|url|inline)(?:&|$)/u;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Whether the options of an `import.meta.glob` read each file as text or as a URL: a
|
|
283
|
+
* `query` string holding a bare `raw`, `url` or `inline`, or an `as` of `raw` or `url`,
|
|
284
|
+
* which Vite reads as that query (https://vite.dev/guide/features.html#glob-import). Any
|
|
285
|
+
* other query is read as loading the module, which keeps more than it may.
|
|
286
|
+
*/
|
|
287
|
+
function readsAsAsset(options: Node | undefined): boolean {
|
|
288
|
+
const texts = (key: string): readonly string[] =>
|
|
289
|
+
(globOption(options, key) ?? []).flatMap((value) =>
|
|
290
|
+
value.kind === "text" ? [value.text] : [],
|
|
291
|
+
);
|
|
292
|
+
return (
|
|
293
|
+
texts("as").some((text) => text === "raw" || text === "url") ||
|
|
294
|
+
texts("query").some((text) => ASSET_QUERY.test(text.startsWith("?") ? text : `?${text}`))
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Whether a computed path's leading literal names a package, whose files are none of the target's. */
|
|
299
|
+
function namesPackage(prefix: string | undefined): boolean {
|
|
300
|
+
return (
|
|
301
|
+
prefix !== undefined &&
|
|
302
|
+
prefix !== "" &&
|
|
303
|
+
!prefix.startsWith(".") &&
|
|
304
|
+
!prefix.startsWith("/") &&
|
|
305
|
+
!/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix)
|
|
306
|
+
);
|
|
307
|
+
}
|
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
namesADeclaration,
|
|
73
73
|
resolveEntries,
|
|
74
74
|
spelledEntry,
|
|
75
|
-
} from "./configured-declarations.ts";
|
|
75
|
+
} from "./roots/configured-declarations.ts";
|
|
76
76
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
77
77
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
78
78
|
import { renderPosition, type Position } from "./position.ts";
|
package/src/source-files.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { SourceFile } from "@typescript/native/ast";
|
|
|
8
8
|
import { commonDirectory, emittedFrom } from "./load/emit-map.ts";
|
|
9
9
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
10
10
|
import type { ProjectView } from "./session.ts";
|
|
11
|
+
import { OUTPUT_EXTENSIONS } from "./extensions.ts";
|
|
11
12
|
|
|
12
13
|
/** The one character a subpath pattern of a manifest's `exports` holds. */
|
|
13
14
|
const SUBPATH_WILDCARD = "*";
|
|
@@ -15,9 +16,6 @@ const SUBPATH_WILDCARD = "*";
|
|
|
15
16
|
/** The extension a source file's name drops when the project writes it out. */
|
|
16
17
|
const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/u;
|
|
17
18
|
|
|
18
|
-
/** The extensions the project may write a source file's output under. */
|
|
19
|
-
const OUTPUT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".d.ts", ".d.mts", ".d.cts"] as const;
|
|
20
|
-
|
|
21
19
|
/**
|
|
22
20
|
* Whether one path is what a subpath pattern stands for: the text before the
|
|
23
21
|
* wildcard, any run of characters including the solidus, then the text after it.
|
|
@@ -90,13 +88,10 @@ function layoutOf<Brand>(
|
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
/**
|
|
93
|
-
* The lookups over one project's own files.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* extension is one the compiler emits from a source extension, and a path below the
|
|
98
|
-
* output directory is read below the directory the compiler emits from, which the
|
|
99
|
-
* configuration declares or which is the directory every input file shares.
|
|
91
|
+
* The lookups over one project's own files. A path naming an emitted file is read back to its
|
|
92
|
+
* source through the project's own emit mapping alone: the extension is one the compiler emits
|
|
93
|
+
* from a source extension, and a path below the output directory is read below the directory
|
|
94
|
+
* the compiler emits from, which the configuration declares or every input file shares.
|
|
100
95
|
*/
|
|
101
96
|
export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: string): SourceFiles {
|
|
102
97
|
const byName = new Map<string, SourceFile>();
|
package/src/suppress.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* ignore entry or a baseline row. A bound record marks its declaration live before the
|
|
5
5
|
* sweep, unless its code reports a part or a dependency row, and is in effect when it
|
|
6
6
|
* withholds a finding its code would have produced there, dormant when the configuration
|
|
7
|
-
* withholds every such finding anyway, and stale otherwise.
|
|
7
|
+
* or a hold withholds every such finding anyway, and stale otherwise.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
type CODE,
|
|
12
12
|
KIND_ROWS,
|
|
13
|
+
REMEDIES,
|
|
13
14
|
SYMBOL_SUBJECTS,
|
|
14
15
|
type Subject,
|
|
15
16
|
} from "./contract/contract.generated.ts";
|
|
@@ -147,6 +148,20 @@ export interface Dials<F extends Finding = Finding> {
|
|
|
147
148
|
readonly withholds: (finding: F) => boolean;
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
/**
|
|
152
|
+
* What the analysis holds because it cannot decide it, as the ledger reads it: a hold is a
|
|
153
|
+
* gap, not evidence that the code changed, so a record it touches is dormant.
|
|
154
|
+
*/
|
|
155
|
+
export interface Holds<F extends Finding = Finding> {
|
|
156
|
+
/** Whether a hold withholds one finding: a type error's skip, or a hold of its declaration. */
|
|
157
|
+
readonly withholds: (finding: F) => boolean;
|
|
158
|
+
/** Whether a hold keeps the declaration one record is bound to live, so no finding about it is computed. */
|
|
159
|
+
readonly holdsRecord: (record: SuppressionRecord) => boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** No hold. */
|
|
163
|
+
const NO_HOLDS: Holds = { withholds: () => false, holdsRecord: () => false };
|
|
164
|
+
|
|
150
165
|
/** What every record did, and the findings the records withhold. */
|
|
151
166
|
export interface Ledger {
|
|
152
167
|
/** One verdict per record, in reading order. */
|
|
@@ -172,36 +187,30 @@ function rowOf(subject: Subject | undefined): boolean {
|
|
|
172
187
|
return subject === "file" || subject === "dependency" || subject === "record";
|
|
173
188
|
}
|
|
174
189
|
|
|
175
|
-
/**
|
|
176
|
-
* Whether a finding's subject is a row of a document rather than a declaration. Apart
|
|
177
|
-
* from a dependency row, nothing withholds a finding about one.
|
|
178
|
-
*/
|
|
190
|
+
/** Whether a finding's subject is a row of a document rather than a declaration. */
|
|
179
191
|
export function isRowSubject(kind: string): boolean {
|
|
180
192
|
return rowOf(subjectOf(kind));
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
/**
|
|
184
|
-
export function
|
|
185
|
-
return
|
|
195
|
+
/** The subject a code reports, which kinds.json's remedies table answers for. */
|
|
196
|
+
export function subjectOfCode(code: string): Subject | undefined {
|
|
197
|
+
return SUBJECT_OF_CODE.get(code);
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
/**
|
|
189
|
-
* Whether a
|
|
190
|
-
*
|
|
191
|
-
*
|
|
201
|
+
* Whether a record of `mechanism` can name a finding about `subject`, as kinds.json's
|
|
202
|
+
* remedies table lists the suppressions of each subject. A subject the table does not
|
|
203
|
+
* know is a declaration's.
|
|
192
204
|
*/
|
|
193
|
-
export function
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
/** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
|
|
198
|
-
export function isDependencyCode(code: string): boolean {
|
|
199
|
-
return SUBJECT_OF_CODE.get(code) === "dependency";
|
|
205
|
+
export function names(subject: Subject | undefined, mechanism: Mechanism): boolean {
|
|
206
|
+
return (REMEDIES[subject ?? "declaration"].suppressions as readonly Mechanism[]).includes(
|
|
207
|
+
mechanism,
|
|
208
|
+
);
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
/** Whether a
|
|
203
|
-
export function
|
|
204
|
-
return
|
|
211
|
+
/** Whether a finding about a symbol of `kind` is one a record of `mechanism` can name. */
|
|
212
|
+
export function namesKind(kind: string, mechanism: Mechanism): boolean {
|
|
213
|
+
return names(subjectOf(kind), mechanism);
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
/**
|
|
@@ -238,16 +247,13 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
|
238
247
|
if (record.code !== finding.code) {
|
|
239
248
|
return false;
|
|
240
249
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
record.mechanism !== "inline" &&
|
|
244
|
-
record.symbol === finding.symbol.ref &&
|
|
245
|
-
record.path === finding.position.path
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
if (isRowSubject(finding.symbol.kind)) {
|
|
250
|
+
const subject = subjectOf(finding.symbol.kind);
|
|
251
|
+
if (!names(subject, record.mechanism)) {
|
|
249
252
|
return false;
|
|
250
253
|
}
|
|
254
|
+
if (subject === "dependency") {
|
|
255
|
+
return record.symbol === finding.symbol.ref && record.path === finding.position.path;
|
|
256
|
+
}
|
|
251
257
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
252
258
|
return true;
|
|
253
259
|
}
|
|
@@ -262,13 +268,14 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
|
|
|
262
268
|
* there: swept with no mark for a liveness kind, and as swept otherwise. The first record
|
|
263
269
|
* in reading order claims a finding, so a second naming it is stale; a part-kind record
|
|
264
270
|
* claims every finding of its code about a part of its declaration. A record is dormant
|
|
265
|
-
* when the dials withhold every finding it claims,
|
|
266
|
-
*
|
|
271
|
+
* when the dials or `holds` withhold every finding it claims, or when it claims none and
|
|
272
|
+
* its code is at `allow` or a hold keeps its declaration live.
|
|
267
273
|
*/
|
|
268
274
|
export function ledgerOf<F extends Finding>(
|
|
269
275
|
records: readonly SuppressionRecord[],
|
|
270
276
|
would: readonly F[],
|
|
271
277
|
dials: Dials<F>,
|
|
278
|
+
holds: Holds<F> = NO_HOLDS,
|
|
272
279
|
): Ledger {
|
|
273
280
|
const taken = new Set<string>();
|
|
274
281
|
const verdicts: Verdict[] = [];
|
|
@@ -278,13 +285,21 @@ export function ledgerOf<F extends Finding>(
|
|
|
278
285
|
const claimed =
|
|
279
286
|
first === undefined ? [] : isPartKind(record.code) ? would.filter(open) : [first];
|
|
280
287
|
if (claimed.length === 0) {
|
|
281
|
-
verdicts.push(
|
|
288
|
+
verdicts.push(
|
|
289
|
+
record.bound !== "" && (dials.allows(record.code) || holds.holdsRecord(record))
|
|
290
|
+
? "dormant"
|
|
291
|
+
: "stale",
|
|
292
|
+
);
|
|
282
293
|
continue;
|
|
283
294
|
}
|
|
284
295
|
for (const finding of claimed) {
|
|
285
296
|
taken.add(claimKey(finding));
|
|
286
297
|
}
|
|
287
|
-
verdicts.push(
|
|
298
|
+
verdicts.push(
|
|
299
|
+
claimed.every((finding) => dials.withholds(finding) || holds.withholds(finding))
|
|
300
|
+
? "dormant"
|
|
301
|
+
: "in-effect",
|
|
302
|
+
);
|
|
288
303
|
}
|
|
289
304
|
return {
|
|
290
305
|
verdicts,
|
package/src/test-support.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* test code.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./convention-rows.ts";
|
|
9
|
+
import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./entries/convention-rows.ts";
|
|
10
10
|
import { packageOfSpecifier } from "./dependencies.ts";
|
|
11
11
|
import type { InventorySymbol } from "./inventory.ts";
|
|
12
12
|
import type { Reference } from "./references.ts";
|
|
13
|
-
import type { Root } from "./roots.ts";
|
|
13
|
+
import type { Root } from "./roots/root-set.ts";
|
|
14
14
|
|
|
15
15
|
/** The root kind that never takes a file out of test-support code: the skip only withholds. */
|
|
16
16
|
const SKIP_ROOT = "type-error";
|
|
@@ -20,7 +20,12 @@ const SKIP_ROOT = "type-error";
|
|
|
20
20
|
* convention row reads test files for, and the convention table's test libraries.
|
|
21
21
|
*/
|
|
22
22
|
const TEST_PACKAGES: ReadonlySet<string> = new Set([
|
|
23
|
-
...CONVENTION_ROWS.filter(
|
|
23
|
+
...CONVENTION_ROWS.filter(
|
|
24
|
+
(row) =>
|
|
25
|
+
row.testFiles !== undefined ||
|
|
26
|
+
row.testDirectories !== undefined ||
|
|
27
|
+
row.testRunner !== undefined,
|
|
28
|
+
).map((row) => row.package),
|
|
24
29
|
...TEST_LIBRARIES,
|
|
25
30
|
]);
|
|
26
31
|
|
package/src/verbs/analyze.ts
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
import { kindOf } from "../config.ts";
|
|
10
10
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
|
|
11
11
|
import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
|
|
12
|
+
import { factsDocument } from "../facts.ts";
|
|
12
13
|
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
|
|
13
14
|
import type { Host } from "../host.ts";
|
|
14
|
-
import type { ManifestEntry } from "../manifest.ts";
|
|
15
|
-
import type { HeldScripts } from "../roots.ts";
|
|
15
|
+
import type { ManifestEntry } from "../roots/manifest.ts";
|
|
16
|
+
import type { HeldScripts } from "../roots/root-set.ts";
|
|
16
17
|
import { moduleIdentity, type NotBuilt } from "../load/discover.ts";
|
|
17
18
|
import { partialNotes } from "../partial.ts";
|
|
18
19
|
import {
|
|
@@ -29,7 +30,7 @@ import { RENDERINGS } from "../reporters/reporters.ts";
|
|
|
29
30
|
import { parseTemplate, TemplateError, type Template } from "../reporters/template.ts";
|
|
30
31
|
import { resolve } from "../resolve.ts";
|
|
31
32
|
import { otherSchemeRows, rowReason, writeBaseline, type Recorded } from "../suppress-file.ts";
|
|
32
|
-
import type { UnseenLoad } from "../unseen-loads.ts";
|
|
33
|
+
import type { UnseenLoad } from "../roots/unseen-loads.ts";
|
|
33
34
|
import type { UnreadInput } from "../load/plan.ts";
|
|
34
35
|
import type { UnresolvedSpecifier } from "../load/specifiers.ts";
|
|
35
36
|
|
|
@@ -42,7 +43,7 @@ import { ownVersion } from "./version.ts";
|
|
|
42
43
|
|
|
43
44
|
/** The options `analyze` takes beside the ones every verb takes. */
|
|
44
45
|
export const ANALYZE_OPTIONS: VerbOptions = {
|
|
45
|
-
plain: ["report", "baseline-write", "exit-code", "template"],
|
|
46
|
+
plain: ["report", "facts", "baseline-write", "exit-code", "template"],
|
|
46
47
|
repeatable: ["format"],
|
|
47
48
|
};
|
|
48
49
|
|
|
@@ -258,6 +259,7 @@ function templateOf(
|
|
|
258
259
|
/** Every document one invocation asked for: the report, each rendering beside it, a baseline. */
|
|
259
260
|
interface Documents {
|
|
260
261
|
readonly report: string;
|
|
262
|
+
readonly facts: string | undefined;
|
|
261
263
|
readonly formats: readonly Format[];
|
|
262
264
|
readonly baseline: string | undefined;
|
|
263
265
|
}
|
|
@@ -286,8 +288,13 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
286
288
|
const { config, provenance } = resolve(inputs());
|
|
287
289
|
const invoked = host.workingDirectory();
|
|
288
290
|
const baseline = option("baseline-write");
|
|
291
|
+
const facts = option("facts");
|
|
292
|
+
if (facts === "") {
|
|
293
|
+
throw new UsageError("--facts names the path the facts document is written to, and named none");
|
|
294
|
+
}
|
|
289
295
|
const documents: Documents = {
|
|
290
296
|
report: resolvePath(invoked, reportPath),
|
|
297
|
+
facts: facts === undefined ? undefined : resolvePath(invoked, facts),
|
|
291
298
|
formats: formatsOf(repeated("format"), config.reporters.formats),
|
|
292
299
|
baseline: baseline === undefined ? undefined : resolvePath(invoked, baseline),
|
|
293
300
|
};
|
|
@@ -378,6 +385,12 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
378
385
|
);
|
|
379
386
|
}
|
|
380
387
|
writeDocument(host, documents.report, "name a path the run can write", () => json(report));
|
|
388
|
+
const factsPath = documents.facts;
|
|
389
|
+
if (factsPath !== undefined) {
|
|
390
|
+
writeDocument(host, factsPath, "name a path the run can write", () => [
|
|
391
|
+
factsDocument(version, run.generated),
|
|
392
|
+
]);
|
|
393
|
+
}
|
|
381
394
|
const options = {
|
|
382
395
|
readSource: (path: string) => host.readFile(joinPath(targetRoot, path)),
|
|
383
396
|
template,
|
package/src/verbs/describe.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
CONTRACT_VERSION,
|
|
3
3
|
EXIT_CLEAN,
|
|
4
4
|
EXIT_USAGE,
|
|
5
|
+
FACTS_VERSION,
|
|
5
6
|
SCHEMA_VERSIONS,
|
|
6
7
|
} from "../contract/contract.generated.ts";
|
|
7
8
|
import { CONFORMANCE, type Conformance } from "../conformance.ts";
|
|
@@ -11,8 +12,9 @@ import { ownVersion } from "./version.ts";
|
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The document `describe` writes: the analyzer, the Contract version it implements, the
|
|
14
|
-
* report schema versions it reads, the languages it claims,
|
|
15
|
-
* the conformance corpus, a member the document omits where
|
|
15
|
+
* report schema versions it reads, the languages it claims, the facts version it writes,
|
|
16
|
+
* and its recorded result over the conformance corpus, a member the document omits where
|
|
17
|
+
* no run is recorded.
|
|
16
18
|
*/
|
|
17
19
|
export function describeDocument(version: string, recorded: Conformance | undefined): string {
|
|
18
20
|
const document = {
|
|
@@ -21,6 +23,7 @@ export function describeDocument(version: string, recorded: Conformance | undefi
|
|
|
21
23
|
contract_version: CONTRACT_VERSION,
|
|
22
24
|
schema_versions_accepted: SCHEMA_VERSIONS,
|
|
23
25
|
languages: [LANGUAGE],
|
|
26
|
+
facts_version_written: FACTS_VERSION,
|
|
24
27
|
...(recorded === undefined
|
|
25
28
|
? {}
|
|
26
29
|
: {
|
package/src/verbs/explain.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { resolvePath } from "../paths.ts";
|
|
|
11
11
|
import { positionKey } from "../position.ts";
|
|
12
12
|
import type { Reference } from "../references.ts";
|
|
13
13
|
import { resolve } from "../resolve.ts";
|
|
14
|
-
import type { Root } from "../roots.ts";
|
|
14
|
+
import type { Root } from "../roots/root-set.ts";
|
|
15
15
|
import { type Verb, type VerbOptions } from "./verb.ts";
|
|
16
16
|
import { UsageError } from "../usage.ts";
|
|
17
17
|
import { bytewise } from "../text/compare.ts";
|