@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.3
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 +27 -5
- package/dist/src/augmentations.js +62 -0
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +39 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +89 -0
- package/dist/src/entry-points.js +9 -18
- package/dist/src/findings/completion.js +62 -26
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +4 -1
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +7 -7
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +38 -4
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +138 -11
- package/dist/src/serialization-contract.js +258 -8
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -2
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +45 -7
- package/src/augmentations.ts +76 -1
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +47 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +96 -0
- package/src/entry-points.ts +10 -25
- package/src/file-facts.ts +3 -0
- package/src/findings/completion.ts +72 -28
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +6 -1
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +7 -7
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +51 -4
- package/src/report.ts +56 -16
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +162 -12
- package/src/serialization-contract.ts +309 -7
- package/src/sweep.ts +21 -9
- package/src/test-globals.ts +275 -0
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -2
- package/src/yaml-document.ts +377 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loads of a project's production files whose target the analysis cannot resolve: an
|
|
3
|
+
* `import()` or `require()` call whose path is computed and starts with no relative literal
|
|
4
|
+
* naming a directory below the calling file's. The files such a load runs are roots the
|
|
5
|
+
* analysis does not see, so the configuration's roots are never complete.
|
|
6
|
+
*/
|
|
7
|
+
import { isAsExpression, isBinaryExpression, isCallExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
|
|
8
|
+
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
9
|
+
import { renderPosition } from "./position.js";
|
|
10
|
+
import { UNANSWERED } from "./query.js";
|
|
11
|
+
import { sourceFilesOf } from "./source-files.js";
|
|
12
|
+
/** The expression a wrapper that changes no value holds: parentheses and type assertions. */
|
|
13
|
+
function unwrapped(node) {
|
|
14
|
+
let at = node;
|
|
15
|
+
while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
16
|
+
at = at.expression;
|
|
17
|
+
}
|
|
18
|
+
return at;
|
|
19
|
+
}
|
|
20
|
+
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
21
|
+
export function leadingLiteral(node) {
|
|
22
|
+
const at = unwrapped(node);
|
|
23
|
+
if (isTemplateExpression(at)) {
|
|
24
|
+
return at.head.text;
|
|
25
|
+
}
|
|
26
|
+
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
27
|
+
const left = unwrapped(at.left);
|
|
28
|
+
return isStringLiteral(left) || isNoSubstitutionTemplateLiteral(left)
|
|
29
|
+
? left.text
|
|
30
|
+
: leadingLiteral(at.left);
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The directory below `dir` a computed load's leading literal names, which the entry
|
|
36
|
+
* rules root every file of, or none where it names a package, `dir` itself or a
|
|
37
|
+
* directory holding it, each of which would name files no rule roots.
|
|
38
|
+
*/
|
|
39
|
+
export function computedLoadDirectory(dir, prefix) {
|
|
40
|
+
if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const target = resolvePath(dir, prefix);
|
|
44
|
+
return target === dir || relativePath(target, dir) !== undefined ? undefined : target;
|
|
45
|
+
}
|
|
46
|
+
/** Whether a computed path's leading literal names a package, whose files are none of the target's. */
|
|
47
|
+
function namesPackage(prefix) {
|
|
48
|
+
return (prefix !== undefined &&
|
|
49
|
+
prefix !== "" &&
|
|
50
|
+
!prefix.startsWith(".") &&
|
|
51
|
+
!prefix.startsWith("/") &&
|
|
52
|
+
!/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every unresolved load the project's production files write, in file then source order.
|
|
56
|
+
* `testFiles` holds the test files by their path below the target root. A `require` call
|
|
57
|
+
* loads a module only where the name resolves to no declaration of the project's own
|
|
58
|
+
* files: one the project declares is its own function.
|
|
59
|
+
*/
|
|
60
|
+
export function unseenLoadsOf(project, configuration, targetRoot, testFiles) {
|
|
61
|
+
const tests = new Set(testFiles);
|
|
62
|
+
const sites = [];
|
|
63
|
+
for (const [path, file] of sourceFilesOf(project, targetRoot).byPath) {
|
|
64
|
+
if (tests.has(path) || file.isDeclarationFile) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const dir = dirnamePath(file.fileName);
|
|
68
|
+
const visit = (node) => {
|
|
69
|
+
if (isCallExpression(node)) {
|
|
70
|
+
const callee = node.expression;
|
|
71
|
+
const required = isIdentifier(callee) && callee.text === "require";
|
|
72
|
+
const [first] = node.arguments;
|
|
73
|
+
const argument = first === undefined ? undefined : unwrapped(first);
|
|
74
|
+
if ((callee.kind === SyntaxKind.ImportKeyword || required) &&
|
|
75
|
+
argument !== undefined &&
|
|
76
|
+
!isStringLiteral(argument) &&
|
|
77
|
+
!isNoSubstitutionTemplateLiteral(argument)) {
|
|
78
|
+
const prefix = leadingLiteral(argument);
|
|
79
|
+
const seen = namesPackage(prefix) ||
|
|
80
|
+
(prefix !== undefined && computedLoadDirectory(dir, prefix) !== undefined);
|
|
81
|
+
if (!seen) {
|
|
82
|
+
sites.push({
|
|
83
|
+
position: renderPosition(file, targetRoot, node.getStart()),
|
|
84
|
+
require: required ? callee : undefined,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
node.forEachChild(visit);
|
|
90
|
+
};
|
|
91
|
+
file.forEachChild(visit);
|
|
92
|
+
}
|
|
93
|
+
const requires = sites.flatMap((site) => (site.require === undefined ? [] : [site.require]));
|
|
94
|
+
const own = project.ownPaths();
|
|
95
|
+
const local = new Set();
|
|
96
|
+
project.symbolsAt(requires.map((node) => project.handle(node))).forEach((symbol, at) => {
|
|
97
|
+
const node = requires[at];
|
|
98
|
+
if (node !== undefined &&
|
|
99
|
+
symbol !== UNANSWERED &&
|
|
100
|
+
symbol?.declarations.some((handle) => own.has(handle.path)) === true) {
|
|
101
|
+
local.add(node);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return sites
|
|
105
|
+
.filter((site) => site.require === undefined || !local.has(site.require))
|
|
106
|
+
.map((site) => ({ configuration, position: site.position }));
|
|
107
|
+
}
|
|
@@ -3,7 +3,7 @@ import { ConfigError, kindOf } from "../config.js";
|
|
|
3
3
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
|
|
4
4
|
import { moduleIdentity } from "../consumers.js";
|
|
5
5
|
import { analyzerProvenance, verdictOf } from "../findings-pass.js";
|
|
6
|
-
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
6
|
+
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
7
7
|
import { partialNotes } from "../partial.js";
|
|
8
8
|
import { buildReport, LANGUAGE, capFindings, sortFindings, } from "../report.js";
|
|
9
9
|
import { json } from "../reporters/json.js";
|
|
@@ -77,6 +77,24 @@ function unmappedNotes(entries, targetRoot) {
|
|
|
77
77
|
};
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* One note per load of a production file the run cannot resolve, each site once: a file
|
|
82
|
+
* it loads is a root the run does not see, so `ts.roots_complete` lifts no cap there.
|
|
83
|
+
*/
|
|
84
|
+
function unseenLoadNotes(loads) {
|
|
85
|
+
return [
|
|
86
|
+
...new Map(loads.map((load) => {
|
|
87
|
+
const { path, line, column } = load.position;
|
|
88
|
+
const site = `${path}:${String(line)}:${String(column)}`;
|
|
89
|
+
const note = {
|
|
90
|
+
kind: "computed-load",
|
|
91
|
+
path: dirnamePath(path),
|
|
92
|
+
message: `${site} loads a module by a computed path the analysis cannot resolve, so the configurations compiling it report what no root reaches at probable at most, whatever ts.roots_complete says`,
|
|
93
|
+
};
|
|
94
|
+
return [site, note];
|
|
95
|
+
})).values(),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
80
98
|
/** Each project the run dropped as a report names it, by its configuration file below the target. */
|
|
81
99
|
function notBuiltOf(dropped, targetRoot) {
|
|
82
100
|
return dropped.map((one) => {
|
|
@@ -222,7 +240,11 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
222
240
|
typeErrorSkips: run.typeErrorSkips,
|
|
223
241
|
unanswered: unansweredCounts(analysis),
|
|
224
242
|
conventionsApplied: run.conventionsApplied,
|
|
225
|
-
|
|
243
|
+
listsComponents: config.reporters.cascade === "full",
|
|
244
|
+
notes: [
|
|
245
|
+
...unmappedNotes(run.unmappedEntries, resolvePath(invoked, targetRoot)),
|
|
246
|
+
...unseenLoadNotes(run.unseenLoads),
|
|
247
|
+
],
|
|
226
248
|
});
|
|
227
249
|
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
228
250
|
verdict = verdictOf(result, config);
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value of a YAML configuration document in the subset tool configurations write:
|
|
3
|
+
* block mappings and sequences, flow mappings and sequences, and plain, single-quoted and
|
|
4
|
+
* double-quoted scalars, with comments. A document using anything else, an anchor, an
|
|
5
|
+
* alias, a tag, a block scalar, a complex key or a second document, has no value here,
|
|
6
|
+
* as a JSON document that does not parse has none.
|
|
7
|
+
*/
|
|
8
|
+
/** A failure to read the document within the subset; it ends the whole reading. */
|
|
9
|
+
class Unsupported extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** The characters after which a quote opens a quoted scalar rather than standing in a plain one. */
|
|
12
|
+
const SCALAR_START = new Set(["", ":", "-", "[", "{", ","]);
|
|
13
|
+
/** The plain scalars YAML's core schema reads as something other than a string. */
|
|
14
|
+
const CORE = new Map([
|
|
15
|
+
["true", true],
|
|
16
|
+
["True", true],
|
|
17
|
+
["TRUE", true],
|
|
18
|
+
["false", false],
|
|
19
|
+
["False", false],
|
|
20
|
+
["FALSE", false],
|
|
21
|
+
["null", null],
|
|
22
|
+
["Null", null],
|
|
23
|
+
["NULL", null],
|
|
24
|
+
["~", null],
|
|
25
|
+
["", null],
|
|
26
|
+
]);
|
|
27
|
+
const NUMBER = /^[-+]?(?:\d+|\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?$/u;
|
|
28
|
+
/** The text of one line before its comment: a `#` at the line's start or after a blank, outside quotes. */
|
|
29
|
+
function withoutComment(line) {
|
|
30
|
+
let quote;
|
|
31
|
+
let previous = "";
|
|
32
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
33
|
+
const char = line.charAt(at);
|
|
34
|
+
if (quote !== undefined) {
|
|
35
|
+
if (quote === '"' && char === "\\") {
|
|
36
|
+
at += 1;
|
|
37
|
+
}
|
|
38
|
+
else if (char === quote) {
|
|
39
|
+
quote = undefined;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if ((char === "'" || char === '"') && SCALAR_START.has(previous)) {
|
|
44
|
+
quote = char;
|
|
45
|
+
}
|
|
46
|
+
else if (char === "#" && (at === 0 || /\s/u.test(line.charAt(at - 1)))) {
|
|
47
|
+
return line.slice(0, at);
|
|
48
|
+
}
|
|
49
|
+
if (!/\s/u.test(char)) {
|
|
50
|
+
previous = char;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return line;
|
|
54
|
+
}
|
|
55
|
+
/** The value one scalar's text denotes, quoted or plain. */
|
|
56
|
+
function scalar(text) {
|
|
57
|
+
const value = text.trim();
|
|
58
|
+
if (value.startsWith("'")) {
|
|
59
|
+
if (!value.endsWith("'") || value.length < 2) {
|
|
60
|
+
throw new Unsupported();
|
|
61
|
+
}
|
|
62
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
63
|
+
}
|
|
64
|
+
if (value.startsWith('"')) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(value);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Unsupported();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (/^[&*!|>%@`?]/u.test(value) || value === "---" || value === "...") {
|
|
73
|
+
throw new Unsupported();
|
|
74
|
+
}
|
|
75
|
+
if (/:(?:\s|$)/u.test(value)) {
|
|
76
|
+
throw new Unsupported();
|
|
77
|
+
}
|
|
78
|
+
if (CORE.has(value)) {
|
|
79
|
+
return CORE.get(value);
|
|
80
|
+
}
|
|
81
|
+
return NUMBER.test(value) ? Number(value) : value;
|
|
82
|
+
}
|
|
83
|
+
/** Reads one flow collection or flow scalar from `text` at `at`, answering the value and where it ends. */
|
|
84
|
+
function flow(text, start) {
|
|
85
|
+
let at = start;
|
|
86
|
+
const skip = () => {
|
|
87
|
+
while (at < text.length && /\s/u.test(text.charAt(at))) {
|
|
88
|
+
at += 1;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
skip();
|
|
92
|
+
const open = text.charAt(at);
|
|
93
|
+
if (open === "[" || open === "{") {
|
|
94
|
+
const close = open === "[" ? "]" : "}";
|
|
95
|
+
const items = [];
|
|
96
|
+
const object = {};
|
|
97
|
+
at += 1;
|
|
98
|
+
for (;;) {
|
|
99
|
+
skip();
|
|
100
|
+
if (text.charAt(at) === close) {
|
|
101
|
+
return { value: open === "[" ? items : object, end: at + 1 };
|
|
102
|
+
}
|
|
103
|
+
const item = flow(text, at);
|
|
104
|
+
at = item.end;
|
|
105
|
+
skip();
|
|
106
|
+
if (open === "{") {
|
|
107
|
+
if (text.charAt(at) !== ":" || typeof item.value !== "string") {
|
|
108
|
+
throw new Unsupported();
|
|
109
|
+
}
|
|
110
|
+
const held = flow(text, at + 1);
|
|
111
|
+
object[item.value] = held.value;
|
|
112
|
+
at = held.end;
|
|
113
|
+
skip();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
items.push(item.value);
|
|
117
|
+
}
|
|
118
|
+
if (text.charAt(at) === ",") {
|
|
119
|
+
at += 1;
|
|
120
|
+
}
|
|
121
|
+
else if (text.charAt(at) !== close) {
|
|
122
|
+
throw new Unsupported();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (open === "'" || open === '"') {
|
|
127
|
+
let end = at + 1;
|
|
128
|
+
for (; end < text.length; end += 1) {
|
|
129
|
+
const char = text.charAt(end);
|
|
130
|
+
if (open === '"' && char === "\\") {
|
|
131
|
+
end += 1;
|
|
132
|
+
}
|
|
133
|
+
else if (char === open && !(open === "'" && text.charAt(end + 1) === "'")) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
else if (char === open) {
|
|
137
|
+
end += 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (end >= text.length) {
|
|
141
|
+
throw new Unsupported();
|
|
142
|
+
}
|
|
143
|
+
return { value: scalar(text.slice(at, end + 1)), end: end + 1 };
|
|
144
|
+
}
|
|
145
|
+
let end = at;
|
|
146
|
+
while (end < text.length && !",]}".includes(text.charAt(end))) {
|
|
147
|
+
if (text.charAt(end) === ":" && /[\s,\]}]/u.test(text.charAt(end + 1) || " ")) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
end += 1;
|
|
151
|
+
}
|
|
152
|
+
return { value: scalar(text.slice(at, end)), end };
|
|
153
|
+
}
|
|
154
|
+
/** A value written on one or more lines of a block, read whole: a flow node, else one plain scalar. */
|
|
155
|
+
function flowValue(text) {
|
|
156
|
+
if (!/^["'[{]/u.test(text)) {
|
|
157
|
+
return scalar(text);
|
|
158
|
+
}
|
|
159
|
+
const read = flow(text, 0);
|
|
160
|
+
if (text.slice(read.end).trim() !== "") {
|
|
161
|
+
throw new Unsupported();
|
|
162
|
+
}
|
|
163
|
+
return read.value;
|
|
164
|
+
}
|
|
165
|
+
/** The position of a mapping key's `:` in one line's content, or -1 where the content is no mapping entry. */
|
|
166
|
+
function keyEnd(text) {
|
|
167
|
+
let quote;
|
|
168
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
169
|
+
const char = text.charAt(at);
|
|
170
|
+
if (quote !== undefined) {
|
|
171
|
+
if (quote === '"' && char === "\\") {
|
|
172
|
+
at += 1;
|
|
173
|
+
}
|
|
174
|
+
else if (char === quote) {
|
|
175
|
+
quote = undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (at === 0 && (char === "'" || char === '"')) {
|
|
179
|
+
quote = char;
|
|
180
|
+
}
|
|
181
|
+
else if (char === "[" || char === "{") {
|
|
182
|
+
return -1;
|
|
183
|
+
}
|
|
184
|
+
else if (char === ":" && (at + 1 === text.length || /\s/u.test(text.charAt(at + 1)))) {
|
|
185
|
+
return at;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return -1;
|
|
189
|
+
}
|
|
190
|
+
/** How many flow collections one text opens and leaves unclosed, quotes read through. */
|
|
191
|
+
function openCollections(text) {
|
|
192
|
+
let depth = 0;
|
|
193
|
+
let quote;
|
|
194
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
195
|
+
const char = text.charAt(at);
|
|
196
|
+
if (quote !== undefined) {
|
|
197
|
+
if (quote === '"' && char === "\\") {
|
|
198
|
+
at += 1;
|
|
199
|
+
}
|
|
200
|
+
else if (char === quote) {
|
|
201
|
+
quote = undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if ((char === "'" || char === '"') &&
|
|
205
|
+
SCALAR_START.has(text.slice(0, at).trim().slice(-1))) {
|
|
206
|
+
quote = char;
|
|
207
|
+
}
|
|
208
|
+
else if (char === "[" || char === "{") {
|
|
209
|
+
depth += 1;
|
|
210
|
+
}
|
|
211
|
+
else if (char === "]" || char === "}") {
|
|
212
|
+
depth -= 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return depth;
|
|
216
|
+
}
|
|
217
|
+
/** Whether one line's content is a block sequence item. */
|
|
218
|
+
function isItem(text) {
|
|
219
|
+
return text === "-" || text.startsWith("- ");
|
|
220
|
+
}
|
|
221
|
+
/** Reads the document's lines as one value. */
|
|
222
|
+
class Reader {
|
|
223
|
+
at = 0;
|
|
224
|
+
lines;
|
|
225
|
+
constructor(lines) {
|
|
226
|
+
this.lines = lines;
|
|
227
|
+
}
|
|
228
|
+
/** The whole document's value. */
|
|
229
|
+
document() {
|
|
230
|
+
const first = this.lines[0];
|
|
231
|
+
if (first === undefined) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const value = this.block(first.indent);
|
|
235
|
+
if (this.at < this.lines.length) {
|
|
236
|
+
throw new Unsupported();
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
/** The block node whose first line is the next one, at `indent`. */
|
|
241
|
+
block(indent) {
|
|
242
|
+
const line = this.lines[this.at];
|
|
243
|
+
if (line === undefined) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
if (isItem(line.text)) {
|
|
247
|
+
return this.sequence(indent);
|
|
248
|
+
}
|
|
249
|
+
if (keyEnd(line.text) >= 0) {
|
|
250
|
+
return this.mapping(indent);
|
|
251
|
+
}
|
|
252
|
+
return this.multiline(indent);
|
|
253
|
+
}
|
|
254
|
+
/** A scalar or a flow collection whose text runs over the next lines indented deeper than `indent`. */
|
|
255
|
+
multiline(indent) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
for (let line = this.lines[this.at]; line !== undefined && (parts.length === 0 || line.indent > indent); line = this.lines[this.at]) {
|
|
258
|
+
parts.push(line.text);
|
|
259
|
+
this.at += 1;
|
|
260
|
+
}
|
|
261
|
+
return flowValue(parts.join(" "));
|
|
262
|
+
}
|
|
263
|
+
/** The value after a key or an item marker: inline text, else the deeper block below it. */
|
|
264
|
+
valueAfter(inline, indent, sequenceAtIndent) {
|
|
265
|
+
if (inline !== "") {
|
|
266
|
+
const parts = [inline];
|
|
267
|
+
// A flow collection's closing bracket may stand at the key's own column.
|
|
268
|
+
for (let line = this.lines[this.at]; line !== undefined && (line.indent > indent || openCollections(parts.join(" ")) > 0); line = this.lines[this.at]) {
|
|
269
|
+
parts.push(line.text);
|
|
270
|
+
this.at += 1;
|
|
271
|
+
}
|
|
272
|
+
return flowValue(parts.join(" "));
|
|
273
|
+
}
|
|
274
|
+
const next = this.lines[this.at];
|
|
275
|
+
if (next === undefined) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if (next.indent > indent || (sequenceAtIndent && next.indent === indent && isItem(next.text))) {
|
|
279
|
+
return this.block(next.indent);
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
mapping(indent) {
|
|
284
|
+
const object = {};
|
|
285
|
+
for (let line = this.lines[this.at]; line?.indent === indent; line = this.lines[this.at]) {
|
|
286
|
+
const end = keyEnd(line.text);
|
|
287
|
+
if (end < 0) {
|
|
288
|
+
throw new Unsupported();
|
|
289
|
+
}
|
|
290
|
+
const key = scalar(line.text.slice(0, end));
|
|
291
|
+
if (typeof key === "object" && key !== null) {
|
|
292
|
+
throw new Unsupported();
|
|
293
|
+
}
|
|
294
|
+
this.at += 1;
|
|
295
|
+
object[String(key)] = this.valueAfter(line.text.slice(end + 1).trim(), indent, true);
|
|
296
|
+
}
|
|
297
|
+
const left = this.lines[this.at];
|
|
298
|
+
if (left !== undefined && left.indent > indent) {
|
|
299
|
+
throw new Unsupported();
|
|
300
|
+
}
|
|
301
|
+
return object;
|
|
302
|
+
}
|
|
303
|
+
sequence(indent) {
|
|
304
|
+
const items = [];
|
|
305
|
+
for (let line = this.lines[this.at]; line?.indent === indent && isItem(line.text); line = this.lines[this.at]) {
|
|
306
|
+
const content = line.text.slice(1);
|
|
307
|
+
const inline = content.trimStart();
|
|
308
|
+
if (inline !== "" && (isItem(inline) || keyEnd(inline) >= 0)) {
|
|
309
|
+
// A node on the item's own line continues at its own column: read it as a block there.
|
|
310
|
+
const moved = { indent: indent + 1 + content.length - inline.length, text: inline };
|
|
311
|
+
this.lines[this.at] = moved;
|
|
312
|
+
items.push(this.block(moved.indent));
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.at += 1;
|
|
316
|
+
items.push(this.valueAfter(inline, indent, false));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const left = this.lines[this.at];
|
|
320
|
+
if (left !== undefined && left.indent > indent) {
|
|
321
|
+
throw new Unsupported();
|
|
322
|
+
}
|
|
323
|
+
return items;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** The JSON value one YAML document's text denotes, or undefined where it lies outside the subset. */
|
|
327
|
+
export function yamlDocument(text) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
for (const raw of text.replace(/^\uFEFF/u, "").split(/\r?\n/u)) {
|
|
330
|
+
if (/^ *\t/u.test(raw)) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
const content = withoutComment(raw).trimEnd();
|
|
334
|
+
const trimmed = content.trimStart();
|
|
335
|
+
if (trimmed === "") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (trimmed === "---" && lines.length === 0) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (trimmed.startsWith("%") || trimmed === "---" || trimmed === "...") {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
lines.push({ indent: content.length - trimmed.length, text: trimmed });
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return new Reader(lines).document();
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof Unsupported) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/analysis.ts
CHANGED
|
@@ -107,6 +107,7 @@ import { references, testFileRulesOf, type Reference, type TestFileRule } from "
|
|
|
107
107
|
import {
|
|
108
108
|
roots,
|
|
109
109
|
unmatchedEverywhere,
|
|
110
|
+
type Root,
|
|
110
111
|
type RootKind,
|
|
111
112
|
type RootOptions,
|
|
112
113
|
type Roots,
|
|
@@ -114,7 +115,10 @@ import {
|
|
|
114
115
|
import type { Scope } from "./scope.ts";
|
|
115
116
|
import { serializationContract } from "./serialization-contract.ts";
|
|
116
117
|
import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
|
|
118
|
+
import { mergedGlobalInterfaces } from "./augmentations.ts";
|
|
119
|
+
import { unseenLoadsOf, type UnseenLoad } from "./unseen-loads.ts";
|
|
117
120
|
import { joinedTestFiles, withJoinedTests } from "./test-joins.ts";
|
|
121
|
+
import { GLOBALS_RUNNER, namesTestGlobals, withTestGlobals } from "./test-globals.ts";
|
|
118
122
|
import { supportReferences, testSupportFiles, type TestEvidence } from "./test-support.ts";
|
|
119
123
|
import {
|
|
120
124
|
diagnosticsOf,
|
|
@@ -428,11 +432,17 @@ function readProjects<Answer>(
|
|
|
428
432
|
const joins = discovered.derived
|
|
429
433
|
? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
|
|
430
434
|
: new Map<string, readonly string[]>();
|
|
431
|
-
const programs =
|
|
435
|
+
const programs = withTestGlobals(
|
|
432
436
|
engine,
|
|
433
437
|
host,
|
|
434
|
-
|
|
435
|
-
|
|
438
|
+
parse,
|
|
439
|
+
discovered.configFiles,
|
|
440
|
+
withBaseUrlPaths(
|
|
441
|
+
engine,
|
|
442
|
+
host,
|
|
443
|
+
[...discovered.configFiles, ...consumerOf.keys()],
|
|
444
|
+
workspace?.programs ?? new Map(),
|
|
445
|
+
),
|
|
436
446
|
);
|
|
437
447
|
const session = runSession(
|
|
438
448
|
engine,
|
|
@@ -804,6 +814,8 @@ interface ReadProject<Extra> {
|
|
|
804
814
|
readonly componentWarnings: readonly ComponentWarning[];
|
|
805
815
|
/** Per own file below the target root, the module specifiers its imports write. */
|
|
806
816
|
readonly imports: ReadonlyMap<string, readonly string[]>;
|
|
817
|
+
/** The global augmentations a declaration outside the project names. */
|
|
818
|
+
readonly augmented: readonly Root[];
|
|
807
819
|
readonly extra: Extra;
|
|
808
820
|
}
|
|
809
821
|
|
|
@@ -940,6 +952,9 @@ function readRun<Extra>(
|
|
|
940
952
|
disabled,
|
|
941
953
|
);
|
|
942
954
|
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
955
|
+
const injected = project.program
|
|
956
|
+
.getSourceFileNames()
|
|
957
|
+
.some((name) => name.endsWith(`/node_modules/${GLOBALS_RUNNER}/globals.d.ts`));
|
|
943
958
|
const pathOf = (file: (typeof components)[number]): string =>
|
|
944
959
|
relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
945
960
|
return {
|
|
@@ -948,12 +963,27 @@ function readRun<Extra>(
|
|
|
948
963
|
testFiles: resolved.testFilePaths,
|
|
949
964
|
evidence,
|
|
950
965
|
componentFiles: components.map(pathOf),
|
|
966
|
+
augmented: mergedGlobalInterfaces(
|
|
967
|
+
project,
|
|
968
|
+
projectRead.held,
|
|
969
|
+
sourceFilesOf(project, targetRoot),
|
|
970
|
+
).map((id): Root => ({ id, kind: "global-augmentation", source: "" })),
|
|
951
971
|
imports: new Map(
|
|
952
972
|
project.ownSourceFiles().flatMap((file) => {
|
|
953
973
|
const path = relativePath(targetRoot, file.fileName);
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
974
|
+
if (path === undefined) {
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
const specifiers = file.imports.filter(isStringLiteralLikeNode).map((one) => one.text);
|
|
978
|
+
// A file calling the globals a program declares is written for their runner.
|
|
979
|
+
return [
|
|
980
|
+
[
|
|
981
|
+
path,
|
|
982
|
+
injected && namesTestGlobals(project, file)
|
|
983
|
+
? [...specifiers, GLOBALS_RUNNER]
|
|
984
|
+
: specifiers,
|
|
985
|
+
],
|
|
986
|
+
];
|
|
957
987
|
}),
|
|
958
988
|
),
|
|
959
989
|
componentWarnings: components.flatMap((file) =>
|
|
@@ -1004,7 +1034,9 @@ function readRun<Extra>(
|
|
|
1004
1034
|
configuration: projectRead.configuration,
|
|
1005
1035
|
symbols: projectRead.held.symbols,
|
|
1006
1036
|
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
1007
|
-
|
|
1037
|
+
// The augmentations are rooted after test support is read: an outside declaration
|
|
1038
|
+
// naming one calls nothing of its file.
|
|
1039
|
+
roots: [...projectRead.rooted.liveUnderReachability, ...one.augmented],
|
|
1008
1040
|
testFiles: [...one.testFiles, ...supportFiles],
|
|
1009
1041
|
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
1010
1042
|
...(testUsedFiles.length === 0 ? {} : { testUsedFiles }),
|
|
@@ -1246,6 +1278,7 @@ interface EmitterExtra {
|
|
|
1246
1278
|
readonly directives: readonly InlineDirective[];
|
|
1247
1279
|
readonly declarationsNamed: readonly boolean[];
|
|
1248
1280
|
readonly parts: ProjectParts;
|
|
1281
|
+
readonly unseenLoads: readonly UnseenLoad[];
|
|
1249
1282
|
}
|
|
1250
1283
|
|
|
1251
1284
|
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
@@ -1281,6 +1314,7 @@ export function readEmitterRun(
|
|
|
1281
1314
|
),
|
|
1282
1315
|
declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
|
|
1283
1316
|
parts: projectParts(project, read.held, references, targetRoot),
|
|
1317
|
+
unseenLoads: unseenLoadsOf(project, read.configuration, targetRoot, testFiles),
|
|
1284
1318
|
}),
|
|
1285
1319
|
);
|
|
1286
1320
|
// Whether a declaration carries the marker decides its code, so one whose marker went
|
|
@@ -1386,6 +1420,7 @@ export function emitterInputOver(
|
|
|
1386
1420
|
heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
|
|
1387
1421
|
generated: new Set(projects.flatMap((one) => one.extra.generated)),
|
|
1388
1422
|
inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
|
|
1423
|
+
unseenLoads: projects.flatMap((one) => one.extra.unseenLoads),
|
|
1389
1424
|
},
|
|
1390
1425
|
boundary: {
|
|
1391
1426
|
consumers: {
|
|
@@ -1476,6 +1511,7 @@ function readAnalysis(
|
|
|
1476
1511
|
librariesUnfound: read.librariesUnfound,
|
|
1477
1512
|
conventionsApplied: read.conventionsApplied,
|
|
1478
1513
|
unmappedEntries: unmappedEverywhere(read.projects),
|
|
1514
|
+
unseenLoads: read.projects.flatMap((one) => one.extra.unseenLoads),
|
|
1479
1515
|
},
|
|
1480
1516
|
configured: read.projects.map((one) => one.configured),
|
|
1481
1517
|
symbols,
|
|
@@ -1574,6 +1610,8 @@ interface RunFacts {
|
|
|
1574
1610
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
1575
1611
|
/** The manifest entries no project of the run reads back to a file, each once. */
|
|
1576
1612
|
readonly unmappedEntries: readonly ManifestEntry[];
|
|
1613
|
+
/** The loads of production files whose target the run cannot resolve, per configuration. */
|
|
1614
|
+
readonly unseenLoads: readonly UnseenLoad[];
|
|
1577
1615
|
}
|
|
1578
1616
|
|
|
1579
1617
|
/** One configuration whose component files a run cannot read. */
|