@cplieger/deadset-ts 2.2.0 → 2.3.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 +5 -3
- package/dist/src/analysis.js +109 -0
- package/dist/src/config.js +6 -0
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/index.js +1 -0
- package/dist/src/manifest.js +120 -0
- package/dist/src/references.js +15 -79
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +48 -3
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +169 -0
- package/src/config.ts +7 -0
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +174 -0
- package/src/references.ts +15 -83
- package/src/roots.ts +467 -0
- package/src/run.ts +55 -4
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
package/dist/src/references.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isArrayLiteralExpression, isBinaryExpression, isDeleteExpression, isElementAccessExpression, isForInStatement, isForOfStatement, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
2
|
+
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
3
|
+
import { globExpression } from "./glob.js";
|
|
2
4
|
import { nodeKey } from "./inventory.js";
|
|
3
5
|
import { byPosition, renderPosition } from "./position.js";
|
|
4
6
|
/**
|
|
@@ -31,21 +33,6 @@ const ASSIGNMENTS = new Set([
|
|
|
31
33
|
SyntaxKind.QuestionQuestionEqualsToken,
|
|
32
34
|
SyntaxKind.CaretEqualsToken,
|
|
33
35
|
]);
|
|
34
|
-
/**
|
|
35
|
-
* The declaration forms that name something declared elsewhere. Each is a link rather
|
|
36
|
-
* than a declaration of what it names, so a reference through one names the next link
|
|
37
|
-
* and, at the end of the chain, the declaration itself: a re-export something imports
|
|
38
|
-
* is used, and so is the declaration behind it.
|
|
39
|
-
*/
|
|
40
|
-
const ALIASES = new Set([
|
|
41
|
-
SyntaxKind.ImportClause,
|
|
42
|
-
SyntaxKind.ImportEqualsDeclaration,
|
|
43
|
-
SyntaxKind.ImportSpecifier,
|
|
44
|
-
SyntaxKind.NamespaceImport,
|
|
45
|
-
SyntaxKind.ExportAssignment,
|
|
46
|
-
SyntaxKind.ExportSpecifier,
|
|
47
|
-
SyntaxKind.NamespaceExport,
|
|
48
|
-
]);
|
|
49
36
|
/** Whether one node is an identifier or a private name, the two forms a name takes. */
|
|
50
37
|
function isName(node) {
|
|
51
38
|
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
|
|
@@ -197,62 +184,6 @@ function sitesOf(file, declarations, fileId) {
|
|
|
197
184
|
file.forEachChild(visit);
|
|
198
185
|
return found;
|
|
199
186
|
}
|
|
200
|
-
/**
|
|
201
|
-
* One literal run of a pattern, with every character the expression syntax claims
|
|
202
|
-
* escaped.
|
|
203
|
-
*
|
|
204
|
-
* The set is the syntax characters and the solidus, and it is closed: an escape of any
|
|
205
|
-
* other character is refused outright by the expression syntax under the unicode flag,
|
|
206
|
-
* so escaping a hyphen, which needs none outside a character class, would refuse every
|
|
207
|
-
* pattern that names a hyphenated path.
|
|
208
|
-
*/
|
|
209
|
-
function quoted(text) {
|
|
210
|
-
return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
|
|
214
|
-
* do not, a brace list is an alternation of its members, and every other character
|
|
215
|
-
* stands for itself.
|
|
216
|
-
*/
|
|
217
|
-
function expressionOf(pattern) {
|
|
218
|
-
let source = "^";
|
|
219
|
-
let at = 0;
|
|
220
|
-
while (at < pattern.length) {
|
|
221
|
-
const rest = pattern.slice(at);
|
|
222
|
-
if (rest.startsWith("**/")) {
|
|
223
|
-
source += "(?:[^/]*/)*";
|
|
224
|
-
at += 3;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (rest.startsWith("**")) {
|
|
228
|
-
source += ".*";
|
|
229
|
-
at += 2;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
const character = pattern[at] ?? "";
|
|
233
|
-
at += 1;
|
|
234
|
-
if (character === "*") {
|
|
235
|
-
source += "[^/]*";
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (character === "?") {
|
|
239
|
-
source += "[^/]";
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (character === "{") {
|
|
243
|
-
const close = pattern.indexOf("}", at);
|
|
244
|
-
if (close < 0) {
|
|
245
|
-
source += "\\{";
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
|
|
249
|
-
at = close + 1;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
source += quoted(character);
|
|
253
|
-
}
|
|
254
|
-
return new RegExp(`${source}$`, "u");
|
|
255
|
-
}
|
|
256
187
|
/**
|
|
257
188
|
* The name one test-file rule takes. A rule's name is a word list the report's own
|
|
258
189
|
* vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
|
|
@@ -284,7 +215,7 @@ function compare(a, b) {
|
|
|
284
215
|
export function references(project, held, targetRoot, options) {
|
|
285
216
|
const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
|
|
286
217
|
const declarations = held.declarations;
|
|
287
|
-
const patterns = options.testFiles.map(
|
|
218
|
+
const patterns = options.testFiles.map(globExpression);
|
|
288
219
|
const matched = options.testFiles.map(() => 0);
|
|
289
220
|
const found = [];
|
|
290
221
|
const reached = new Map();
|
|
@@ -303,11 +234,17 @@ export function references(project, held, targetRoot, options) {
|
|
|
303
234
|
* is the declaration behind it, so both are named; a link this project does not
|
|
304
235
|
* declare is stepped over rather than recorded.
|
|
305
236
|
*
|
|
306
|
-
* A
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
237
|
+
* A symbol is asked to step only where the binder flagged it an alias, which is the
|
|
238
|
+
* same condition the step itself asserts on. The declaration's syntax is not that
|
|
239
|
+
* condition and does not stand in for it: a default export of an expression is
|
|
240
|
+
* written as one of the alias forms and names nothing declared elsewhere, so it is
|
|
241
|
+
* recorded as the declaration it is and the chain ends there.
|
|
242
|
+
*
|
|
243
|
+
* A declaration handle carries the file it is in, so the question the walk asks of a
|
|
244
|
+
* declaration is answered before it is resolved: a declaration outside the project's
|
|
245
|
+
* own files belongs to another program and is never read, which is what keeps the
|
|
246
|
+
* pass from fetching a library file to discover that the inventory does not hold what
|
|
247
|
+
* is in it.
|
|
311
248
|
*/
|
|
312
249
|
const targetsOf = (symbol) => {
|
|
313
250
|
const cached = reached.get(symbol.id);
|
|
@@ -320,9 +257,8 @@ export function references(project, held, targetRoot, options) {
|
|
|
320
257
|
let stepped = false;
|
|
321
258
|
while (at !== undefined && !walked.has(at.id)) {
|
|
322
259
|
walked.add(at.id);
|
|
323
|
-
|
|
260
|
+
const alias = (at.flags & SymbolFlags.Alias) !== 0;
|
|
324
261
|
for (const handle of at.declarations) {
|
|
325
|
-
alias = alias || ALIASES.has(handle.kind);
|
|
326
262
|
if (!ownFiles.has(handle.path)) {
|
|
327
263
|
continue;
|
|
328
264
|
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root set of one project: every declaration the analysis keeps live without a
|
|
3
|
+
* reference, and why.
|
|
4
|
+
*
|
|
5
|
+
* A root makes its declaration live under REACHABILITY and plays no part in
|
|
6
|
+
* reference counting, which asks only whether some declaration of the loaded graph
|
|
7
|
+
* names it. The two relations are different sets, so an exported declaration that
|
|
8
|
+
* something outside the program reaches is live under the first and still a
|
|
9
|
+
* candidate under the second.
|
|
10
|
+
*/
|
|
11
|
+
import { REPOSITORY_DOCUMENT } from "./config.js";
|
|
12
|
+
import { entryPoints } from "./entry-points.js";
|
|
13
|
+
import { globExpression } from "./glob.js";
|
|
14
|
+
import { nodeKey } from "./inventory.js";
|
|
15
|
+
import { byPosition } from "./position.js";
|
|
16
|
+
import { sourceFilesOf } from "./source-files.js";
|
|
17
|
+
/** The code the Contract gives a configured root that matches nothing. */
|
|
18
|
+
const UNMATCHED_ROOT_CODE = "DS1704";
|
|
19
|
+
/** The subject kind a finding under that code carries, from the Contract's vocabulary. */
|
|
20
|
+
const UNMATCHED_ROOT_SUBJECT = "root";
|
|
21
|
+
/** The fixed position of a finding about a document rather than about a line of one. */
|
|
22
|
+
const DOCUMENT_POSITION = 1;
|
|
23
|
+
/** Whether a configured string carries either special character. */
|
|
24
|
+
function isPattern(text) {
|
|
25
|
+
return text.includes("*") || text.includes("?");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether ref matches a pattern whose only special characters are `*`, which stands
|
|
29
|
+
* for any run of characters including the solidus, and `?`, which stands for exactly
|
|
30
|
+
* one character counted as a code point. A star keeps the position it last stood at,
|
|
31
|
+
* so the shortest run it can stand for is tried first.
|
|
32
|
+
*/
|
|
33
|
+
function matchGlob(pattern, ref) {
|
|
34
|
+
let next = 0;
|
|
35
|
+
let star = -1;
|
|
36
|
+
let resume = 0;
|
|
37
|
+
let at = 0;
|
|
38
|
+
while (at < ref.length) {
|
|
39
|
+
if (next < pattern.length && (pattern[next] === "?" || pattern[next] === ref[at])) {
|
|
40
|
+
next += 1;
|
|
41
|
+
at += 1;
|
|
42
|
+
}
|
|
43
|
+
else if (next < pattern.length && pattern[next] === "*") {
|
|
44
|
+
star = next;
|
|
45
|
+
resume = at;
|
|
46
|
+
next += 1;
|
|
47
|
+
}
|
|
48
|
+
else if (star >= 0) {
|
|
49
|
+
resume += 1;
|
|
50
|
+
next = star + 1;
|
|
51
|
+
at = resume;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
while (next < pattern.length && pattern[next] === "*") {
|
|
58
|
+
next += 1;
|
|
59
|
+
}
|
|
60
|
+
return next === pattern.length;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether one configured string names the symbol reference ref. A string carrying
|
|
64
|
+
* neither special character names only the reference it spells.
|
|
65
|
+
*
|
|
66
|
+
* Both sides are read as code points: a character outside the basic plane is two
|
|
67
|
+
* string units, and `?` stands for one character.
|
|
68
|
+
*/
|
|
69
|
+
export function matchRef(pattern, ref) {
|
|
70
|
+
if (!isPattern(pattern)) {
|
|
71
|
+
return pattern === ref;
|
|
72
|
+
}
|
|
73
|
+
return matchGlob(Array.from(pattern), Array.from(ref));
|
|
74
|
+
}
|
|
75
|
+
/** Two strings ordered bytewise, which is the order every set of a run is read in. */
|
|
76
|
+
function compare(a, b) {
|
|
77
|
+
if (a === b) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
return a < b ? -1 : 1;
|
|
81
|
+
}
|
|
82
|
+
/** The members of one declaration, indexed by the container that holds them. */
|
|
83
|
+
function childrenOf(held) {
|
|
84
|
+
const byParent = new Map();
|
|
85
|
+
for (const symbol of held.symbols) {
|
|
86
|
+
const siblings = byParent.get(symbol.parent);
|
|
87
|
+
if (siblings === undefined) {
|
|
88
|
+
byParent.set(symbol.parent, [symbol]);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
siblings.push(symbol);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return byParent;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The roots one project holds, and every configured string that named nothing in it.
|
|
98
|
+
*
|
|
99
|
+
* A file a manifest member, an entry-file pattern or a tool's configuration names
|
|
100
|
+
* is a root with everything its module exports: each name of the module's export
|
|
101
|
+
* table, and where a name is a re-export, every link of the chain to the declaration
|
|
102
|
+
* it carries, so a re-exported declaration is a root of the file that publishes it.
|
|
103
|
+
* A file a runtime executes without reading its exports, a test file or a worker, is
|
|
104
|
+
* a root by itself.
|
|
105
|
+
*/
|
|
106
|
+
export function roots(project, held, targetRoot, options) {
|
|
107
|
+
const files = sourceFilesOf(project, targetRoot);
|
|
108
|
+
const declared = held.declarations;
|
|
109
|
+
const fileIds = new Map();
|
|
110
|
+
for (const file of files.byName.values()) {
|
|
111
|
+
const id = declared.get(nodeKey(file, file));
|
|
112
|
+
if (id !== undefined) {
|
|
113
|
+
fileIds.set(file.fileName, id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const found = new Map();
|
|
117
|
+
const add = (id, kind, source) => {
|
|
118
|
+
found.set([id, kind, source].join("\u0000"), { id, kind, source });
|
|
119
|
+
};
|
|
120
|
+
const entered = [];
|
|
121
|
+
const enter = (file, kind, source, exports) => {
|
|
122
|
+
entered.push({ file, kind, source, exports });
|
|
123
|
+
};
|
|
124
|
+
const published = [];
|
|
125
|
+
const publishes = (entry) => entry.published &&
|
|
126
|
+
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
|
|
127
|
+
for (const entry of options.manifest.entries) {
|
|
128
|
+
for (const file of files.named(entry.path)) {
|
|
129
|
+
enter(file, entry.role === "run" ? "manifest-binary" : "manifest-entry", entry.member, true);
|
|
130
|
+
if (options.publishedAPI && publishes(entry)) {
|
|
131
|
+
published.push(file);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const pattern of options.entryFiles) {
|
|
136
|
+
const expression = globExpression(pattern);
|
|
137
|
+
for (const [path, file] of files.byPath) {
|
|
138
|
+
if (expression.test(path)) {
|
|
139
|
+
enter(file, "entry-file", pattern, true);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const point of entryPoints(project, files, options.testFiles)) {
|
|
144
|
+
enter(point.file, point.rule, point.source, point.exports);
|
|
145
|
+
}
|
|
146
|
+
const exportsOf = exportTables(project, held, [...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published], files.byName);
|
|
147
|
+
for (const entry of entered) {
|
|
148
|
+
const id = fileIds.get(entry.file.fileName);
|
|
149
|
+
if (id === undefined) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
add(id, entry.kind, entry.source);
|
|
153
|
+
if (entry.exports) {
|
|
154
|
+
for (const exported of exportsOf.get(entry.file.fileName) ?? []) {
|
|
155
|
+
add(exported, entry.kind, entry.source);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (options.publishedAPI) {
|
|
160
|
+
for (const id of publishedAPI(published, exportsOf, held)) {
|
|
161
|
+
add(id, "published-api", "");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const unmatched = configured(held, options.patterns, add);
|
|
165
|
+
const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
|
|
166
|
+
const ordered = [...found.values()].sort((a, b) => {
|
|
167
|
+
const left = sites.get(a.id);
|
|
168
|
+
const right = sites.get(b.id);
|
|
169
|
+
const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
|
|
170
|
+
return site || compare(a.kind, b.kind) || compare(a.source, b.source);
|
|
171
|
+
});
|
|
172
|
+
return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every declaration each file's module exports, by the file's name: the declaration
|
|
176
|
+
* each name of the export table is, and every link of a re-export's chain. A name
|
|
177
|
+
* whose declaration is outside the project's own files belongs to another program
|
|
178
|
+
* and names nothing here.
|
|
179
|
+
*
|
|
180
|
+
* One batch resolves every file to its module and one request per module reads
|
|
181
|
+
* its export table, star re-exports resolved into it. A file that is no module
|
|
182
|
+
* exports nothing.
|
|
183
|
+
*/
|
|
184
|
+
function exportTables(project, held, wanted, ownFiles) {
|
|
185
|
+
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
186
|
+
const tables = new Map();
|
|
187
|
+
if (unique.length === 0) {
|
|
188
|
+
return tables;
|
|
189
|
+
}
|
|
190
|
+
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
191
|
+
const chains = new Map();
|
|
192
|
+
const chainOf = (symbol) => {
|
|
193
|
+
const cached = chains.get(symbol.id);
|
|
194
|
+
if (cached !== undefined) {
|
|
195
|
+
return cached;
|
|
196
|
+
}
|
|
197
|
+
const ids = [];
|
|
198
|
+
const walked = new Set();
|
|
199
|
+
let at = symbol;
|
|
200
|
+
while (at !== undefined && !walked.has(at.id)) {
|
|
201
|
+
walked.add(at.id);
|
|
202
|
+
for (const handle of at.declarations) {
|
|
203
|
+
if (!ownFiles.has(handle.path)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const node = project.declarationAt(handle)?.node;
|
|
207
|
+
const id = node === undefined
|
|
208
|
+
? undefined
|
|
209
|
+
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
210
|
+
if (id !== undefined && !ids.includes(id)) {
|
|
211
|
+
ids.push(id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
at = project.aliasStepOf(at);
|
|
215
|
+
}
|
|
216
|
+
chains.set(symbol.id, ids);
|
|
217
|
+
return ids;
|
|
218
|
+
};
|
|
219
|
+
unique.forEach((file, index) => {
|
|
220
|
+
const module = modules[index];
|
|
221
|
+
if (module === undefined) {
|
|
222
|
+
tables.set(file.fileName, []);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const ids = new Set();
|
|
226
|
+
for (const exported of project.checker.getExportsOfModule(module)) {
|
|
227
|
+
for (const id of chainOf(exported)) {
|
|
228
|
+
ids.add(id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
tables.set(file.fileName, [...ids]);
|
|
232
|
+
});
|
|
233
|
+
return tables;
|
|
234
|
+
}
|
|
235
|
+
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
236
|
+
const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
|
|
237
|
+
/**
|
|
238
|
+
* Every declaration a consumer of the library can name: what the published files
|
|
239
|
+
* export, and below each, the members a consumer reaches through it.
|
|
240
|
+
*
|
|
241
|
+
* The published files are the ones `exports` reaches where the manifest declares
|
|
242
|
+
* one, because a package that declares `exports` has said exactly what a consumer may
|
|
243
|
+
* import; where it declares none, they are the ones the members that preceded it
|
|
244
|
+
* name. Below a published module or namespace the walk follows what its export table
|
|
245
|
+
* names; below any other published declaration it follows every member a consumer
|
|
246
|
+
* can write: a public one, and a protected one, which a consumer's subclass names. A
|
|
247
|
+
* private member is not published, nor is anything below one, and a type parameter
|
|
248
|
+
* is not published either, because a consumer supplies a type argument by position.
|
|
249
|
+
*
|
|
250
|
+
* A published file is not itself published. A consumer imports a module and names
|
|
251
|
+
* what it exports; the file is a root because the manifest names it, which is a
|
|
252
|
+
* different reason.
|
|
253
|
+
*/
|
|
254
|
+
function publishedAPI(published, exportsOf, held) {
|
|
255
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
256
|
+
const children = childrenOf(held);
|
|
257
|
+
const reached = new Set();
|
|
258
|
+
const pending = [];
|
|
259
|
+
for (const file of published) {
|
|
260
|
+
for (const id of exportsOf.get(file.fileName) ?? []) {
|
|
261
|
+
if (byId.get(id)?.kind === "file") {
|
|
262
|
+
pending.push(id);
|
|
263
|
+
}
|
|
264
|
+
else if (!reached.has(id)) {
|
|
265
|
+
reached.add(id);
|
|
266
|
+
pending.push(id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
271
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
272
|
+
for (const member of children.get(next) ?? []) {
|
|
273
|
+
const reachable = exporting
|
|
274
|
+
? member.exported
|
|
275
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
276
|
+
if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
reached.add(member.id);
|
|
280
|
+
pending.push(member.id);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return [...reached];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Keeps every declaration the configuration names and returns every string that
|
|
287
|
+
* named none, in configuration order. A string the configuration lists twice is one
|
|
288
|
+
* configured root.
|
|
289
|
+
*/
|
|
290
|
+
function configured(held, patterns, add) {
|
|
291
|
+
const unmatched = [];
|
|
292
|
+
for (const pattern of new Set(patterns)) {
|
|
293
|
+
const kind = isPattern(pattern) ? "pattern" : "configured";
|
|
294
|
+
let matched = false;
|
|
295
|
+
for (const symbol of held.symbols) {
|
|
296
|
+
if (matchRef(pattern, symbol.ref)) {
|
|
297
|
+
add(symbol.id, kind, pattern);
|
|
298
|
+
matched = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!matched) {
|
|
302
|
+
unmatched.push(pattern);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return unmatched;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Every configured string that named no declaration in ANY project of the run, in
|
|
309
|
+
* configuration order and each once.
|
|
310
|
+
*
|
|
311
|
+
* A finding about a configured root is about the configuration and not about a
|
|
312
|
+
* declaration, so there is one per run: a string that names a declaration of one
|
|
313
|
+
* project has named something, and the projects it named nothing in are not a
|
|
314
|
+
* finding. A run with no project names nothing with any of them.
|
|
315
|
+
*/
|
|
316
|
+
export function unmatchedEverywhere(patterns, perProject) {
|
|
317
|
+
return [...new Set(patterns)].filter((pattern) => perProject.every((project) => project.unmatched.includes(pattern)));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The findings a run reports for the configured strings that named nothing.
|
|
321
|
+
*
|
|
322
|
+
* `document` is the path of the document that declared the roots, below the target
|
|
323
|
+
* root; where the run read them from no document of the target it is empty and the
|
|
324
|
+
* finding names the conventional repository configuration, which is the document a
|
|
325
|
+
* maintainer writes a configured root in. Every finding of one run sits at that
|
|
326
|
+
* document's first position: the roots are written as an array whose members carry
|
|
327
|
+
* no line of their own, so the string each finding names is what tells one from
|
|
328
|
+
* another.
|
|
329
|
+
*/
|
|
330
|
+
export function unmatchedRoots(sources, document) {
|
|
331
|
+
const position = {
|
|
332
|
+
path: document === "" ? REPOSITORY_DOCUMENT : document,
|
|
333
|
+
line: DOCUMENT_POSITION,
|
|
334
|
+
column: DOCUMENT_POSITION,
|
|
335
|
+
endLine: DOCUMENT_POSITION,
|
|
336
|
+
};
|
|
337
|
+
return sources.map((source) => ({
|
|
338
|
+
code: UNMATCHED_ROOT_CODE,
|
|
339
|
+
position,
|
|
340
|
+
symbol: { ref: source, kind: UNMATCHED_ROOT_SUBJECT, name: source, sizeLines: 1 },
|
|
341
|
+
message: isPattern(source)
|
|
342
|
+
? "configured root pattern matches no symbol of the inventory"
|
|
343
|
+
: "configured root matches no symbol of the inventory",
|
|
344
|
+
}));
|
|
345
|
+
}
|
package/dist/src/run.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runRoots } from "./analysis.js";
|
|
2
|
+
import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
|
|
2
3
|
import { diagnosticErrors, discoverProjects, DiscoveryError, renderDiagnostic, } from "./discover.js";
|
|
3
4
|
import { joinPath, resolvePath } from "./paths.js";
|
|
4
5
|
import { printConfig } from "./print.js";
|
|
@@ -6,10 +7,9 @@ import { resolve } from "./resolve.js";
|
|
|
6
7
|
import { readScope, ScopeError, scopeForDir } from "./scope.js";
|
|
7
8
|
import { diagnosticsOf, openEngine, runSession } from "./session.js";
|
|
8
9
|
import { CONTRACT_VERSION } from "./version.js";
|
|
9
|
-
/** The repository configuration's name at the target root. */
|
|
10
|
-
const REPOSITORY_DOCUMENT = "deadset.json";
|
|
11
10
|
/** The exit codes the Contract's exit-code table names. */
|
|
12
11
|
const EXIT_CLEAN = 0;
|
|
12
|
+
const EXIT_FINDINGS = 1;
|
|
13
13
|
const EXIT_USAGE = 2;
|
|
14
14
|
const EXIT_FAILURE = 3;
|
|
15
15
|
const VERBS = [
|
|
@@ -171,6 +171,9 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
|
|
|
171
171
|
if (verb === "print-projects") {
|
|
172
172
|
return printProjectsVerb(args.slice(1), out, err, host, openClient);
|
|
173
173
|
}
|
|
174
|
+
if (verb === "print-roots") {
|
|
175
|
+
return printRootsVerb(args.slice(1), out, err, host, openClient);
|
|
176
|
+
}
|
|
174
177
|
if (verb !== undefined && VERBS.includes(verb)) {
|
|
175
178
|
err.write(`deadset-ts: ${verb} is not implemented\n`);
|
|
176
179
|
return EXIT_USAGE;
|
|
@@ -227,6 +230,48 @@ function printProjectsVerb(args, out, err, host, openClient) {
|
|
|
227
230
|
return refuse(error, err);
|
|
228
231
|
}
|
|
229
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* One root as a line: the declaration's reference, why it is a root, and the string
|
|
235
|
+
* that named it, separated by tabs. A run of several configurations adds the ones
|
|
236
|
+
* whose project holds the root as a fourth field, and writes the third present and
|
|
237
|
+
* empty where no string named the root, so every line of one run holds the same
|
|
238
|
+
* fields.
|
|
239
|
+
*/
|
|
240
|
+
function rootLine(root, several) {
|
|
241
|
+
const line = `${root.ref}\t${root.kind}`;
|
|
242
|
+
if (several) {
|
|
243
|
+
return `${line}\t${root.source}\t${root.configurations.join(" ")}`;
|
|
244
|
+
}
|
|
245
|
+
return root.source === "" ? line : `${line}\t${root.source}`;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Writes the root set of the run, one root per line, and then names on the error
|
|
249
|
+
* stream, by its issue kind, every configured root or pattern that named nothing;
|
|
250
|
+
* one such string fails the run. The line shape is this command's own rather than a
|
|
251
|
+
* Contract format, so that sort and cut read it.
|
|
252
|
+
*/
|
|
253
|
+
function printRootsVerb(args, out, err, host, openClient) {
|
|
254
|
+
let engine;
|
|
255
|
+
try {
|
|
256
|
+
const options = readOptions(args);
|
|
257
|
+
const { config, provenance } = resolve(inputsOf(host, options));
|
|
258
|
+
const scope = scopeOf(host, options);
|
|
259
|
+
engine = openClient(false);
|
|
260
|
+
const answer = runRoots(engine, host, scope, config, provenance);
|
|
261
|
+
const several = answer.configurations.length > 1;
|
|
262
|
+
for (const root of answer.roots) {
|
|
263
|
+
out.write(`${rootLine(root, several)}\n`);
|
|
264
|
+
}
|
|
265
|
+
for (const finding of answer.findings) {
|
|
266
|
+
err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
|
|
267
|
+
}
|
|
268
|
+
return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
engine?.close();
|
|
272
|
+
return refuse(error, err);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
230
275
|
/** Turns one refusal into the exit code the Contract's table gives it. */
|
|
231
276
|
function refuse(error, err) {
|
|
232
277
|
if (error instanceof ConfigError) {
|
package/dist/src/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { API } from "@typescript/native/unstable/sync";
|
|
1
|
+
import { API, SymbolFlags } from "@typescript/native/unstable/sync";
|
|
2
2
|
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
3
3
|
/**
|
|
4
4
|
* Opens the compiler client the analysis reads through.
|
|
@@ -53,6 +53,11 @@ function viewOf(project) {
|
|
|
53
53
|
resolvedSymbolAt: ({ node }) => isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
|
|
54
54
|
shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
|
|
55
55
|
aliasStepOf: (symbol) => {
|
|
56
|
+
// The step is asked of an alias only: asked of any other symbol, the compiler
|
|
57
|
+
// fails an assertion and the session ends.
|
|
58
|
+
if ((symbol.flags & SymbolFlags.Alias) === 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
56
61
|
const held = project.checker.getImmediateAliasedSymbol(symbol);
|
|
57
62
|
// An alias that resolves to nothing answers with the checker's own unknown
|
|
58
63
|
// symbol, which declares nothing; a symbol that is no alias answers with
|