@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4
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 +1 -1
- package/dist/src/analysis.js +61 -10
- package/dist/src/augmentations.js +63 -1
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/convention-rows.js +39 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +98 -1
- package/dist/src/discover.js +14 -82
- package/dist/src/entry-points.js +9 -18
- package/dist/src/exempt.js +8 -0
- package/dist/src/findings/completion.js +66 -28
- 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 +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +38 -4
- package/dist/src/query.js +1 -0
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +197 -42
- package/dist/src/serialization-contract.js +590 -12
- 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 -3
- package/dist/src/workflow-steps.js +32 -15
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +109 -12
- package/src/augmentations.ts +77 -2
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +64 -12
- package/src/convention-rows.ts +47 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +110 -1
- package/src/discover.ts +14 -94
- package/src/entry-points.ts +10 -25
- package/src/exempt.ts +16 -0
- package/src/file-facts.ts +3 -0
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +76 -30
- 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 +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +51 -4
- package/src/query.ts +11 -0
- package/src/report.ts +56 -16
- package/src/reporters/annotations.ts +21 -10
- package/src/reporters/reporter.ts +1 -3
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +226 -46
- package/src/serialization-contract.ts +758 -12
- package/src/sweep.ts +23 -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 -3
- package/src/workflow-steps.ts +35 -17
- package/src/yaml-document.ts +377 -0
package/dist/src/dependencies.js
CHANGED
|
@@ -14,7 +14,7 @@ import { scriptTokens } from "./manifest.js";
|
|
|
14
14
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
15
15
|
import { UNANSWERED } from "./query.js";
|
|
16
16
|
import { sourceFilesOf } from "./source-files.js";
|
|
17
|
-
import { workflowTokens } from "./workflow-steps.js";
|
|
17
|
+
import { workflowCommands, workflowTokens } from "./workflow-steps.js";
|
|
18
18
|
const MANIFEST = "package.json";
|
|
19
19
|
const MODULES_DIR = "node_modules";
|
|
20
20
|
const MODULES_SEGMENT = `/${MODULES_DIR}/`;
|
|
@@ -664,6 +664,98 @@ function scriptTokensOf(manifest) {
|
|
|
664
664
|
}
|
|
665
665
|
return found;
|
|
666
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* The runtime flags whose value is a module loaded before the entry, as `--flag value` or
|
|
669
|
+
* `--flag=value`: https://nodejs.org/api/cli.html#--importmodule,
|
|
670
|
+
* https://nodejs.org/api/cli.html#--experimental-loadermodule,
|
|
671
|
+
* https://nodejs.org/api/cli.html#-r---require-module
|
|
672
|
+
*/
|
|
673
|
+
const PRELOAD_FLAGS = new Set([
|
|
674
|
+
"--experimental-loader",
|
|
675
|
+
"--import",
|
|
676
|
+
"--loader",
|
|
677
|
+
"--require",
|
|
678
|
+
"-r",
|
|
679
|
+
]);
|
|
680
|
+
/**
|
|
681
|
+
* The programs that pass a preload flag to the runtime they start: Node itself, and the
|
|
682
|
+
* runners that forward Node's flags or read the same `--require`:
|
|
683
|
+
* https://mochajs.org/#-require-module-r-module, https://tsx.is/node-enhancement,
|
|
684
|
+
* https://typestrong.org/ts-node/docs/options#require,
|
|
685
|
+
* https://github.com/remy/nodemon#running-non-node-scripts
|
|
686
|
+
*/
|
|
687
|
+
const PRELOADING_PROGRAMS = new Set([
|
|
688
|
+
"mocha",
|
|
689
|
+
"node",
|
|
690
|
+
"nodemon",
|
|
691
|
+
"ts-node",
|
|
692
|
+
"tsx",
|
|
693
|
+
]);
|
|
694
|
+
/** The programs that run the command after them, and an environment assignment before it. */
|
|
695
|
+
const LAUNCHERS = new Set(["cross-env", "env", "npx"]);
|
|
696
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
697
|
+
/** The commands one script string runs, split at the shell's list and pipeline operators. */
|
|
698
|
+
const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
|
|
699
|
+
/**
|
|
700
|
+
* The command strings one manifest runs: each of its `scripts`, and the `exec` command of
|
|
701
|
+
* nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
|
|
702
|
+
* it, which nodemon runs the watched script with:
|
|
703
|
+
* https://github.com/remy/nodemon#config-files
|
|
704
|
+
*/
|
|
705
|
+
function manifestCommands(host, dir, manifest) {
|
|
706
|
+
const scripts = manifest["scripts"];
|
|
707
|
+
const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
|
|
708
|
+
return [
|
|
709
|
+
...(isRecord(scripts) ? Object.values(scripts) : []),
|
|
710
|
+
...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
|
|
711
|
+
].filter((command) => typeof command === "string");
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* The packages `scripts` preload by a runtime flag's bare module specifier, a flag
|
|
715
|
+
* counting only in a command whose program passes it to the runtime, and for Node only
|
|
716
|
+
* before the script it runs, whose own arguments come after it.
|
|
717
|
+
*/
|
|
718
|
+
function preloadsOf(scripts) {
|
|
719
|
+
const found = new Set();
|
|
720
|
+
for (const script of scripts) {
|
|
721
|
+
for (const command of script.split(COMMAND_SEPARATORS)) {
|
|
722
|
+
const tokens = scriptTokens(command);
|
|
723
|
+
let at = 0;
|
|
724
|
+
while (at < tokens.length &&
|
|
725
|
+
(ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))) {
|
|
726
|
+
at += 1;
|
|
727
|
+
}
|
|
728
|
+
const path = tokens[at] ?? "";
|
|
729
|
+
const program = path.slice(path.lastIndexOf("/") + 1);
|
|
730
|
+
if (!PRELOADING_PROGRAMS.has(program)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
for (at += 1; at < tokens.length; at += 1) {
|
|
734
|
+
const token = tokens[at] ?? "";
|
|
735
|
+
if (!token.startsWith("-")) {
|
|
736
|
+
if (program === "node") {
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const equals = token.indexOf("=");
|
|
742
|
+
const flag = equals < 0 ? token : token.slice(0, equals);
|
|
743
|
+
if (!PRELOAD_FLAGS.has(flag)) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (equals < 0) {
|
|
747
|
+
at += 1;
|
|
748
|
+
}
|
|
749
|
+
const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
|
|
750
|
+
const name = value === undefined ? undefined : packageOfSpecifier(value);
|
|
751
|
+
if (name !== undefined) {
|
|
752
|
+
found.add(name);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return found;
|
|
758
|
+
}
|
|
667
759
|
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
668
760
|
function isInside(path, dir) {
|
|
669
761
|
return dir === "" || path.startsWith(`${dir}/`);
|
|
@@ -735,6 +827,7 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
735
827
|
}
|
|
736
828
|
}
|
|
737
829
|
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
830
|
+
const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
|
|
738
831
|
const declared = [];
|
|
739
832
|
const ran = new Set();
|
|
740
833
|
const peers = new Map();
|
|
@@ -745,12 +838,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
745
838
|
continue;
|
|
746
839
|
}
|
|
747
840
|
const tokens = scriptTokensOf(read.value);
|
|
841
|
+
const preloads = preloadsOf(manifestCommands(host, dir, read.value));
|
|
748
842
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
749
843
|
const found = installedOf(host, dir, dependency.name);
|
|
750
844
|
if (found === undefined && uninstalledHere.has(dir)) {
|
|
751
845
|
continue;
|
|
752
846
|
}
|
|
753
847
|
declared.push(dependency);
|
|
848
|
+
if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
|
|
849
|
+
ran.add(declarationKey(dependency));
|
|
850
|
+
}
|
|
754
851
|
if (found === undefined) {
|
|
755
852
|
continue;
|
|
756
853
|
}
|
package/dist/src/discover.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DiagnosticCategory } from "@typescript/native/sync";
|
|
2
2
|
import { baseUrlPaths } from "./base-url.js";
|
|
3
|
-
import { dirnamePath,
|
|
3
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
4
4
|
import { SetupError } from "./setup-failure.js";
|
|
5
5
|
import { writtenPath } from "./workspace-programs.js";
|
|
6
6
|
/** Directory names discovery does not descend into. */
|
|
@@ -224,47 +224,6 @@ export function consumerProjects(engine, host, root) {
|
|
|
224
224
|
function derivedId(targetRoot, configFile) {
|
|
225
225
|
return relativePath(targetRoot, configFile) ?? configFile;
|
|
226
226
|
}
|
|
227
|
-
/**
|
|
228
|
-
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
229
|
-
* passed. The parse reports it and returns, and a snapshot opened from it never answers:
|
|
230
|
-
* https://github.com/microsoft/TypeScript/issues/64754
|
|
231
|
-
*/
|
|
232
|
-
const CIRCULAR_EXTENDS = 18000;
|
|
233
|
-
/** The configuration files one parsed configuration's own `extends` names by path. */
|
|
234
|
-
function extendedPaths(parsed, configFile) {
|
|
235
|
-
const { raw } = parsed;
|
|
236
|
-
const named = typeof raw === "object" && raw !== null ? Reflect.get(raw, "extends") : [];
|
|
237
|
-
return (Array.isArray(named) ? named : [named])
|
|
238
|
-
.filter((one) => typeof one === "string")
|
|
239
|
-
.filter((one) => one.startsWith("./") || one.startsWith("../") || isAbsolutePath(one))
|
|
240
|
-
.map((one) => resolvePath(dirnamePath(configFile), one));
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* The `extends` chain from `configFile` to the first configuration it reaches twice, each
|
|
244
|
-
* link by path, or undefined where no chain written by path returns.
|
|
245
|
-
*/
|
|
246
|
-
function extendsCycle(engine, configFile, parsed) {
|
|
247
|
-
const walk = (chain, at) => {
|
|
248
|
-
for (const next of extendedPaths(at, chain.at(-1) ?? configFile)) {
|
|
249
|
-
if (chain.includes(next)) {
|
|
250
|
-
return [...chain, next];
|
|
251
|
-
}
|
|
252
|
-
let read;
|
|
253
|
-
try {
|
|
254
|
-
read = engine.parseConfigFile(next);
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const cycle = walk([...chain, next], read);
|
|
260
|
-
if (cycle !== undefined) {
|
|
261
|
-
return cycle;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return undefined;
|
|
265
|
-
};
|
|
266
|
-
return walk([configFile], parsed);
|
|
267
|
-
}
|
|
268
227
|
/** The configuration files one parsed configuration's `references` name. */
|
|
269
228
|
function referencesOf(parsed) {
|
|
270
229
|
// The parse answers a directory reference as the directory; the compiler reads a
|
|
@@ -272,53 +231,26 @@ function referencesOf(parsed) {
|
|
|
272
231
|
// https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/core/projectreference.go#L18
|
|
273
232
|
return (parsed.projectReferences ?? []).map(({ path }) => path.endsWith(".json") ? path : joinPath(path, "tsconfig.json"));
|
|
274
233
|
}
|
|
275
|
-
/** Why one parsed configuration's own `extends` chain cannot be opened, where it is circular. */
|
|
276
|
-
function circularity(engine, configFile, parsed) {
|
|
277
|
-
const chain = extendsCycle(engine, configFile, parsed);
|
|
278
|
-
if (chain === undefined && !parsed.errors.some(({ code }) => code === CIRCULAR_EXTENDS)) {
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
281
|
-
return `TS${String(CIRCULAR_EXTENDS)}: Circularity detected while resolving configuration: ${(chain ?? [configFile]).join(" -> ")}`;
|
|
282
|
-
}
|
|
283
234
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
235
|
+
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
236
|
+
* passed. A snapshot that opens two configurations whose `extends` chains reach one such
|
|
237
|
+
* cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
|
|
238
|
+
* so the analysis refuses one before any snapshot is asked to open it.
|
|
287
239
|
*/
|
|
288
|
-
|
|
289
|
-
for (const reference of references) {
|
|
290
|
-
if (visited.has(reference)) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
visited.add(reference);
|
|
294
|
-
let parsed;
|
|
295
|
-
try {
|
|
296
|
-
parsed = engine.parseConfigFile(reference);
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
const reason = circularity(engine, reference, parsed) ??
|
|
302
|
-
referencedCircularity(engine, referencesOf(parsed), visited);
|
|
303
|
-
if (reason !== undefined) {
|
|
304
|
-
return reason;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return undefined;
|
|
308
|
-
}
|
|
240
|
+
const CIRCULAR_EXTENDS = 18000;
|
|
309
241
|
/**
|
|
310
|
-
* The
|
|
311
|
-
*
|
|
312
|
-
* configuration it references is circular, which the analysis refuses before any
|
|
313
|
-
* snapshot is asked to open it.
|
|
242
|
+
* The configurations one configuration file's `references` name, or why it cannot be read:
|
|
243
|
+
* the compiler throws, or its parse reports the `extends` chain circular.
|
|
314
244
|
*/
|
|
315
245
|
function readConfiguration(engine, configFile) {
|
|
316
246
|
try {
|
|
317
247
|
const parsed = engine.parseConfigFile(configFile);
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return
|
|
248
|
+
const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
|
|
249
|
+
// The compiler leaves the message's `{0}` unfilled:
|
|
250
|
+
// https://github.com/microsoft/TypeScript/pull/64755
|
|
251
|
+
return circular === undefined
|
|
252
|
+
? { references: referencesOf(parsed) }
|
|
253
|
+
: { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
|
|
322
254
|
}
|
|
323
255
|
catch (error) {
|
|
324
256
|
return { reason: error instanceof Error ? error.message : String(error) };
|
package/dist/src/entry-points.js
CHANGED
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
* value that is not a literal is not evaluated and not guessed, so a file it would
|
|
11
11
|
* have named is entered by nothing here.
|
|
12
12
|
*/
|
|
13
|
-
import { isArrayLiteralExpression, isAsExpression,
|
|
13
|
+
import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
|
|
14
14
|
import { SymbolFlags } from "@typescript/native/sync";
|
|
15
15
|
import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
|
|
16
|
+
import { buildScriptEntries } from "./build-scripts.js";
|
|
16
17
|
import { globExpression } from "./glob.js";
|
|
17
18
|
import { htmlEntries } from "./html-entries.js";
|
|
18
19
|
import { documentEntries } from "./mdx-entries.js";
|
|
19
20
|
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
20
21
|
import { UNANSWERED } from "./query.js";
|
|
22
|
+
import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.js";
|
|
21
23
|
import { workflowEntries } from "./workflow-steps.js";
|
|
22
24
|
/** The extensions the test runner and the browser-test runner load a configuration from. */
|
|
23
25
|
const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
|
|
@@ -210,7 +212,7 @@ function configurationEntries(host, targetRoot, files, parse, selected) {
|
|
|
210
212
|
strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
|
|
211
213
|
}
|
|
212
214
|
for (const { file } of outside) {
|
|
213
|
-
strings(dirnamePath(file.fileName),
|
|
215
|
+
strings(dirnamePath(file.fileName), unselected(file));
|
|
214
216
|
}
|
|
215
217
|
return found;
|
|
216
218
|
}
|
|
@@ -398,17 +400,6 @@ function workersOf(project, file, files) {
|
|
|
398
400
|
}
|
|
399
401
|
return found;
|
|
400
402
|
}
|
|
401
|
-
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
402
|
-
function leadingLiteral(node) {
|
|
403
|
-
const at = unwrapped(node);
|
|
404
|
-
if (isTemplateExpression(at)) {
|
|
405
|
-
return at.head.text;
|
|
406
|
-
}
|
|
407
|
-
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
408
|
-
return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
|
|
409
|
-
}
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
412
403
|
/** Whether one call is `import.meta.glob(...)`. */
|
|
413
404
|
function isMetaGlob(node) {
|
|
414
405
|
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
|
|
@@ -432,11 +423,8 @@ function computedImportsOf(file, files) {
|
|
|
432
423
|
const dir = dirnamePath(file.fileName);
|
|
433
424
|
const found = [];
|
|
434
425
|
const below = (prefix) => {
|
|
435
|
-
const target =
|
|
436
|
-
if (
|
|
437
|
-
!prefix.endsWith("/") ||
|
|
438
|
-
target === dir ||
|
|
439
|
-
relativePath(target, dir) !== undefined) {
|
|
426
|
+
const target = computedLoadDirectory(dir, prefix);
|
|
427
|
+
if (target === undefined) {
|
|
440
428
|
return;
|
|
441
429
|
}
|
|
442
430
|
for (const candidate of files.byName.values()) {
|
|
@@ -522,6 +510,9 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse,
|
|
|
522
510
|
...(names === undefined ? {} : { names }),
|
|
523
511
|
});
|
|
524
512
|
}
|
|
513
|
+
for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
|
|
514
|
+
found.push({ file, rule: "bundler-entry", source, exports: true });
|
|
515
|
+
}
|
|
525
516
|
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
526
517
|
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
527
518
|
}
|
package/dist/src/exempt.js
CHANGED
|
@@ -106,11 +106,19 @@ export function evidenceRecords(input, detectors, disabled) {
|
|
|
106
106
|
site: evidence.site,
|
|
107
107
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
108
108
|
...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
|
|
109
|
+
...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
return found;
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The records that hold given the declarations a write the mode counts stores into: one
|
|
117
|
+
* that holds only while written holds where its declaration is among them.
|
|
118
|
+
*/
|
|
119
|
+
export function writtenRecords(records, written) {
|
|
120
|
+
return records.flatMap(({ whileWritten, ...record }) => whileWritten === undefined || written.has(record.id) ? [record] : []);
|
|
121
|
+
}
|
|
114
122
|
/** The records that hold under one mode, beside the evidence a production mode found in a test file. */
|
|
115
123
|
export function heldRecords(records, holding) {
|
|
116
124
|
const found = [];
|
|
@@ -87,10 +87,12 @@ function testedByIn(input) {
|
|
|
87
87
|
* published API, a public member of a published type included, is `certain` where the
|
|
88
88
|
* run loaded every consumer the scope declared and declared one, `probable` where one did
|
|
89
89
|
* not load, and `possible` where it declared none, whatever the configuration says of the
|
|
90
|
-
* consumer set. A member an `in` expression tests by name
|
|
90
|
+
* consumer set. A member an `in` expression tests by name, and one a function outside the
|
|
91
|
+
* analysis fills, is `possible`.
|
|
91
92
|
*/
|
|
92
93
|
function reachabilityClasses(input) {
|
|
93
94
|
const tested = testedByIn(input);
|
|
95
|
+
const filled = input.boundary.filledOutside ?? new Set();
|
|
94
96
|
const published = publishedIds(input.swept.matrix.union);
|
|
95
97
|
const { declared } = input.boundary.consumers;
|
|
96
98
|
const outside = declared.length === 0
|
|
@@ -99,12 +101,25 @@ function reachabilityClasses(input) {
|
|
|
99
101
|
? "certain"
|
|
100
102
|
: "probable";
|
|
101
103
|
return (id) => {
|
|
102
|
-
if (tested.has(id)) {
|
|
104
|
+
if (tested.has(id) || filled.has(id)) {
|
|
103
105
|
return "possible";
|
|
104
106
|
}
|
|
105
107
|
return published.has(id) ? outside : "certain";
|
|
106
108
|
};
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether one test of dead code references a target symbol that is published API of a run
|
|
112
|
+
* that declares no consumer, which outside programs the run cannot see may use, so the
|
|
113
|
+
* target counts as live for the test and the test is no test of dead code.
|
|
114
|
+
*/
|
|
115
|
+
export function testsUnseenAPI(input) {
|
|
116
|
+
if (input.boundary.consumers.declared.length > 0) {
|
|
117
|
+
return () => false;
|
|
118
|
+
}
|
|
119
|
+
const union = input.swept.matrix.union;
|
|
120
|
+
const published = publishedIds(union);
|
|
121
|
+
return (id) => testTargets(union, id).some((target) => published.has(target));
|
|
122
|
+
}
|
|
108
123
|
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
109
124
|
function testTargets(union, id) {
|
|
110
125
|
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
@@ -151,13 +166,16 @@ function testUsedSubjects(input) {
|
|
|
151
166
|
}
|
|
152
167
|
/**
|
|
153
168
|
* The ceiling the roots the run found put on one dead subject: a subject other declarations
|
|
154
|
-
* reference, found dead only because no root reaches them, is `probable` unless
|
|
155
|
-
*
|
|
156
|
-
*
|
|
169
|
+
* reference, found dead only because no root reaches them, is `probable` unless the
|
|
170
|
+
* configuration declares the roots complete and no configuration holding the subject loads
|
|
171
|
+
* a module the analysis cannot resolve, because a root the analysis cannot see would make
|
|
172
|
+
* it live; every other declaration has none below `certain`.
|
|
157
173
|
*/
|
|
158
|
-
function rootsCeiling(input, finding, candidate) {
|
|
174
|
+
function rootsCeiling(input, finding, candidate, configurations) {
|
|
159
175
|
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
160
|
-
|
|
176
|
+
const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
|
|
177
|
+
const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
|
|
178
|
+
return unreachable && !complete ? "probable" : "certain";
|
|
161
179
|
}
|
|
162
180
|
/**
|
|
163
181
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
@@ -170,8 +188,24 @@ function findingComponents(swept, components, cascade) {
|
|
|
170
188
|
held.set(member, component);
|
|
171
189
|
}
|
|
172
190
|
}
|
|
191
|
+
const listed = membersListed(swept);
|
|
192
|
+
return (id) => {
|
|
193
|
+
const component = held.get(id);
|
|
194
|
+
return component === undefined
|
|
195
|
+
? undefined
|
|
196
|
+
: {
|
|
197
|
+
id: component.id,
|
|
198
|
+
root: component.roots.includes(id),
|
|
199
|
+
symbolCount: component.members.length,
|
|
200
|
+
deletableLines: component.deletableLines,
|
|
201
|
+
...(cascade === "full" ? { members: listed(component.members) } : {}),
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** The members of a component as the run lists them, positioned and in the canonical order. */
|
|
206
|
+
function membersListed(swept) {
|
|
173
207
|
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
174
|
-
|
|
208
|
+
return (members) => members
|
|
175
209
|
.flatMap((member) => {
|
|
176
210
|
const symbol = symbols.get(member);
|
|
177
211
|
return symbol === undefined
|
|
@@ -188,18 +222,6 @@ function findingComponents(swept, components, cascade) {
|
|
|
188
222
|
];
|
|
189
223
|
})
|
|
190
224
|
.sort((a, b) => byPosition(a.position, b.position));
|
|
191
|
-
return (id) => {
|
|
192
|
-
const component = held.get(id);
|
|
193
|
-
return component === undefined
|
|
194
|
-
? undefined
|
|
195
|
-
: {
|
|
196
|
-
id: component.id,
|
|
197
|
-
root: component.roots.includes(id),
|
|
198
|
-
symbolCount: component.members.length,
|
|
199
|
-
deletableLines: component.deletableLines,
|
|
200
|
-
...(cascade === "full" ? { members: listed(component) } : {}),
|
|
201
|
-
};
|
|
202
|
-
};
|
|
203
225
|
}
|
|
204
226
|
/**
|
|
205
227
|
* The run's dead components with each file a `DS1502` finding reports dead among them, so
|
|
@@ -277,11 +299,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
277
299
|
let minted = computed;
|
|
278
300
|
const dead = new Set(components.map((component) => component.id));
|
|
279
301
|
const fallingWith = new Map();
|
|
280
|
-
for (const { heldByTests } of swept.sweep.candidates) {
|
|
302
|
+
for (const { id, heldByTests } of swept.sweep.candidates) {
|
|
281
303
|
if (heldByTests !== undefined) {
|
|
282
|
-
fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ??
|
|
304
|
+
fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
|
|
283
305
|
}
|
|
284
306
|
}
|
|
307
|
+
const listed = membersListed(swept);
|
|
285
308
|
const done = findings.map((finding) => {
|
|
286
309
|
const row = kinds.get(finding.code);
|
|
287
310
|
if (row === undefined) {
|
|
@@ -326,11 +349,25 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
326
349
|
let component = found;
|
|
327
350
|
if (component === undefined) {
|
|
328
351
|
minted += 1;
|
|
352
|
+
const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
|
|
329
353
|
component = {
|
|
330
354
|
id: componentId(minted),
|
|
331
355
|
root: true,
|
|
332
|
-
symbolCount:
|
|
356
|
+
symbolCount: falling?.length ?? 1,
|
|
333
357
|
deletableLines: 0,
|
|
358
|
+
...(input.config.reporters.cascade !== "full"
|
|
359
|
+
? {}
|
|
360
|
+
: falling === undefined
|
|
361
|
+
? {
|
|
362
|
+
members: [
|
|
363
|
+
{
|
|
364
|
+
ref: finding.symbol.ref,
|
|
365
|
+
name: finding.symbol.name,
|
|
366
|
+
position: finding.position,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
}
|
|
370
|
+
: { members: listed(falling) }),
|
|
334
371
|
};
|
|
335
372
|
}
|
|
336
373
|
const counts = candidate === undefined
|
|
@@ -341,7 +378,11 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
341
378
|
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
342
379
|
: finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
|
|
343
380
|
const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
|
|
344
|
-
const
|
|
381
|
+
const configurations = candidate?.configurations ??
|
|
382
|
+
(judged === undefined && !part
|
|
383
|
+
? swept.matrix.configurations
|
|
384
|
+
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
|
|
385
|
+
const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate, configurations)), judged === undefined ? "certain" : ceilingOf(judged.id));
|
|
345
386
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
346
387
|
const fixability = generated
|
|
347
388
|
? "none"
|
|
@@ -372,10 +413,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
372
413
|
generated,
|
|
373
414
|
component,
|
|
374
415
|
retainedBy: [],
|
|
375
|
-
configurations
|
|
376
|
-
(judged === undefined && !part
|
|
377
|
-
? swept.matrix.configurations
|
|
378
|
-
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
416
|
+
configurations,
|
|
379
417
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
380
418
|
fixability,
|
|
381
419
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
@@ -28,7 +28,8 @@ function overlapClause(overlap, part) {
|
|
|
28
28
|
* caller may pass a function used as a value more arguments than it declares, so there one
|
|
29
29
|
* a read parameter follows is renamed to an underscore name, which the compilers read as
|
|
30
30
|
* declared unread, and one no read parameter follows is deleted: kept where the call's
|
|
31
|
-
* inference may read its annotation, at `probable` where nothing reads the annotation
|
|
31
|
+
* inference may read its annotation, at `probable` where nothing reads the annotation or
|
|
32
|
+
* where it is a name a contextually typed object pattern binds.
|
|
32
33
|
*/
|
|
33
34
|
function parameterVerdict(part, free, freeButValued) {
|
|
34
35
|
const place = part.parameter;
|
|
@@ -45,7 +46,8 @@ function parameterVerdict(part, free, freeButValued) {
|
|
|
45
46
|
if (place?.annotated === "steers") {
|
|
46
47
|
return undefined;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
const unseen = place?.annotated === "free" || place?.contextualPattern === true;
|
|
50
|
+
return { fix: "delete", confidence: unseen ? "probable" : "certain" };
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Whether one part is reported. A parameter needs its function's signature free to
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { positionKey } from "../position.js";
|
|
2
2
|
import { declarationKey } from "../ref.js";
|
|
3
|
-
/** The containers whose type parameters `DS1303` reports: a function and a
|
|
4
|
-
const SIGNATURES = new Set(["function", "method"]);
|
|
3
|
+
/** The containers whose type parameters `DS1303` reports: a function, a class method and a type alias. */
|
|
4
|
+
const SIGNATURES = new Set(["function", "method", "type"]);
|
|
5
5
|
function indexed(input) {
|
|
6
6
|
const { matrix, sweep } = input.swept;
|
|
7
7
|
return {
|
|
@@ -165,10 +165,11 @@ function unusedEnumMembers(run) {
|
|
|
165
165
|
return unnamed(run, "enum-member").map((symbol) => findingAbout(symbol, "DS1302", `enum member ${symbol.name} is named nowhere`));
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
|
-
* `DS1303`: a type parameter of a function
|
|
169
|
-
* declaration's signature nor its body names.
|
|
170
|
-
* a
|
|
171
|
-
*
|
|
168
|
+
* `DS1303`: a type parameter of a function, a class method or a type alias that neither
|
|
169
|
+
* the declaration's signature nor its body names. An alias names no type of its own, so
|
|
170
|
+
* a parameter its type never names makes no two instantiations distinct. One of a class,
|
|
171
|
+
* an interface or a method signature is never reported: a class or an interface merges
|
|
172
|
+
* only with declarations of the same parameters.
|
|
172
173
|
*/
|
|
173
174
|
function unusedTypeParameters(run) {
|
|
174
175
|
return unnamed(run, "type-parameter")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { testsUnseenAPI } from "./completion.js";
|
|
1
2
|
import { unreachableExports } from "./visibility-narrowing.js";
|
|
2
3
|
/** The kinds of declaration that are members of a class, an interface, a type or an enum. */
|
|
3
4
|
const MEMBERS = new Set([
|
|
@@ -48,7 +49,7 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
|
|
|
48
49
|
function codeOf(judged) {
|
|
49
50
|
const { candidate, symbol } = judged;
|
|
50
51
|
if (candidate.testOfDeadCode) {
|
|
51
|
-
return "DS1005";
|
|
52
|
+
return judged.testsUnseenAPI ? undefined : "DS1005";
|
|
52
53
|
}
|
|
53
54
|
if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
|
|
54
55
|
return undefined;
|
|
@@ -199,6 +200,7 @@ function familyFindings(input, fallen) {
|
|
|
199
200
|
const union = input.swept.matrix.union;
|
|
200
201
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
201
202
|
const unreachable = unreachableExports(input);
|
|
203
|
+
const unseenAPI = testsUnseenAPI(input);
|
|
202
204
|
let brands;
|
|
203
205
|
const found = [];
|
|
204
206
|
for (const candidate of input.swept.sweep.candidates) {
|
|
@@ -217,6 +219,7 @@ function familyFindings(input, fallen) {
|
|
|
217
219
|
test: union.test[at] === true,
|
|
218
220
|
deprecated: input.deprecated.has(symbol.id),
|
|
219
221
|
unreachable: unreachable.has(symbol.id),
|
|
222
|
+
testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
|
|
220
223
|
};
|
|
221
224
|
const code = codeOf(judged);
|
|
222
225
|
if (code === undefined ||
|
package/dist/src/graph.js
CHANGED
|
@@ -34,9 +34,9 @@ const SWEPT = {
|
|
|
34
34
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
35
35
|
* tool reads what it loads and what its strings name, a runtime runs a test, setup or
|
|
36
36
|
* worker file, a page's module script, a command, a script or a workflow step, the manifest
|
|
37
|
-
* names what enters the package, an alias global stands for its export,
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* names what enters the package, an alias global stands for its export, an outside
|
|
38
|
+
* declaration names a global interface merging into it, and an entry file, a configured root
|
|
39
|
+
* and a pattern assert a caller. A published API supposes a consumer: reachability alone.
|
|
40
40
|
*/
|
|
41
41
|
const NAMES_A_CALLER = {
|
|
42
42
|
"entry-file": true,
|
|
@@ -49,8 +49,10 @@ const NAMES_A_CALLER = {
|
|
|
49
49
|
pattern: true,
|
|
50
50
|
"type-error": true,
|
|
51
51
|
"type-query-alias": true,
|
|
52
|
+
"global-augmentation": true,
|
|
52
53
|
"configuration-file": true,
|
|
53
54
|
"configuration-string": true,
|
|
55
|
+
"bundler-entry": true,
|
|
54
56
|
"test-runner": true,
|
|
55
57
|
"browser-tests": true,
|
|
56
58
|
worker: true,
|