@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
package/dist/src/conventions.js
CHANGED
|
@@ -12,7 +12,14 @@ import { documentStrings } from "./configuration-files.js";
|
|
|
12
12
|
import { globExpression } from "./glob.js";
|
|
13
13
|
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
|
|
14
14
|
import { satisfies } from "./version-range.js";
|
|
15
|
+
import { yamlDocument } from "./yaml-document.js";
|
|
15
16
|
const JSON_EXTENSION = ".json";
|
|
17
|
+
/**
|
|
18
|
+
* A configuration file its tool reads as a YAML document, read as the JSON document it
|
|
19
|
+
* denotes. Its nodes' offsets are that JSON text's, so no move may read one: a failure's
|
|
20
|
+
* line would name the wrong line.
|
|
21
|
+
*/
|
|
22
|
+
const YAML_NAME = /\.ya?ml$/u;
|
|
16
23
|
/** A configuration file with no extension that its tool reads as a JSON document. */
|
|
17
24
|
const JSON_RC = /^\.[\w-]+rc$/u;
|
|
18
25
|
/** The sections an installed package is required by, so its absence fails the run. */
|
|
@@ -614,6 +621,14 @@ function readingFiles(host, parse, dir, files) {
|
|
|
614
621
|
catch {
|
|
615
622
|
continue;
|
|
616
623
|
}
|
|
624
|
+
if (YAML_NAME.test(name)) {
|
|
625
|
+
const value = yamlDocument(text);
|
|
626
|
+
if (value !== undefined) {
|
|
627
|
+
const json = JSON.stringify(value);
|
|
628
|
+
found.push({ name, text: json, file: parse(`${path}${JSON_EXTENSION}`, json) });
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
617
632
|
found.push({
|
|
618
633
|
name,
|
|
619
634
|
text,
|
package/dist/src/dependencies.js
CHANGED
|
@@ -664,6 +664,91 @@ 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 packages one manifest's scripts preload by a runtime flag's bare module specifier,
|
|
701
|
+
* a flag counting only in a command whose program passes it to the runtime, and for Node
|
|
702
|
+
* only before the script it runs, whose own arguments come after it.
|
|
703
|
+
*/
|
|
704
|
+
function scriptPreloadsOf(manifest) {
|
|
705
|
+
const scripts = manifest["scripts"];
|
|
706
|
+
const found = new Set();
|
|
707
|
+
if (!isRecord(scripts)) {
|
|
708
|
+
return found;
|
|
709
|
+
}
|
|
710
|
+
for (const script of Object.values(scripts)) {
|
|
711
|
+
if (typeof script !== "string") {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
for (const command of script.split(COMMAND_SEPARATORS)) {
|
|
715
|
+
const tokens = scriptTokens(command);
|
|
716
|
+
let at = 0;
|
|
717
|
+
while (at < tokens.length &&
|
|
718
|
+
(ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))) {
|
|
719
|
+
at += 1;
|
|
720
|
+
}
|
|
721
|
+
const path = tokens[at] ?? "";
|
|
722
|
+
const program = path.slice(path.lastIndexOf("/") + 1);
|
|
723
|
+
if (!PRELOADING_PROGRAMS.has(program)) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
for (at += 1; at < tokens.length; at += 1) {
|
|
727
|
+
const token = tokens[at] ?? "";
|
|
728
|
+
if (!token.startsWith("-")) {
|
|
729
|
+
if (program === "node") {
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const equals = token.indexOf("=");
|
|
735
|
+
const flag = equals < 0 ? token : token.slice(0, equals);
|
|
736
|
+
if (!PRELOAD_FLAGS.has(flag)) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (equals < 0) {
|
|
740
|
+
at += 1;
|
|
741
|
+
}
|
|
742
|
+
const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
|
|
743
|
+
const name = value === undefined ? undefined : packageOfSpecifier(value);
|
|
744
|
+
if (name !== undefined) {
|
|
745
|
+
found.add(name);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return found;
|
|
751
|
+
}
|
|
667
752
|
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
668
753
|
function isInside(path, dir) {
|
|
669
754
|
return dir === "" || path.startsWith(`${dir}/`);
|
|
@@ -745,12 +830,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
745
830
|
continue;
|
|
746
831
|
}
|
|
747
832
|
const tokens = scriptTokensOf(read.value);
|
|
833
|
+
const preloads = scriptPreloadsOf(read.value);
|
|
748
834
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
749
835
|
const found = installedOf(host, dir, dependency.name);
|
|
750
836
|
if (found === undefined && uninstalledHere.has(dir)) {
|
|
751
837
|
continue;
|
|
752
838
|
}
|
|
753
839
|
declared.push(dependency);
|
|
840
|
+
if (preloads.has(dependency.name)) {
|
|
841
|
+
ran.add(declarationKey(dependency));
|
|
842
|
+
}
|
|
754
843
|
if (found === undefined) {
|
|
755
844
|
continue;
|
|
756
845
|
}
|
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
|
}
|
|
@@ -105,6 +105,19 @@ function reachabilityClasses(input) {
|
|
|
105
105
|
return published.has(id) ? outside : "certain";
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether one test of dead code references a target symbol that is published API of a run
|
|
110
|
+
* that declares no consumer, which outside programs the run cannot see may use, so the
|
|
111
|
+
* target counts as live for the test and the test is no test of dead code.
|
|
112
|
+
*/
|
|
113
|
+
export function testsUnseenAPI(input) {
|
|
114
|
+
if (input.boundary.consumers.declared.length > 0) {
|
|
115
|
+
return () => false;
|
|
116
|
+
}
|
|
117
|
+
const union = input.swept.matrix.union;
|
|
118
|
+
const published = publishedIds(union);
|
|
119
|
+
return (id) => testTargets(union, id).some((target) => published.has(target));
|
|
120
|
+
}
|
|
108
121
|
/** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
|
|
109
122
|
function testTargets(union, id) {
|
|
110
123
|
return (union.out[union.at(id)] ?? []).flatMap((edge) => {
|
|
@@ -151,13 +164,16 @@ function testUsedSubjects(input) {
|
|
|
151
164
|
}
|
|
152
165
|
/**
|
|
153
166
|
* 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
|
-
*
|
|
167
|
+
* reference, found dead only because no root reaches them, is `probable` unless the
|
|
168
|
+
* configuration declares the roots complete and no configuration holding the subject loads
|
|
169
|
+
* a module the analysis cannot resolve, because a root the analysis cannot see would make
|
|
170
|
+
* it live; every other declaration has none below `certain`.
|
|
157
171
|
*/
|
|
158
|
-
function rootsCeiling(input, finding, candidate) {
|
|
172
|
+
function rootsCeiling(input, finding, candidate, configurations) {
|
|
159
173
|
const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
|
|
160
|
-
|
|
174
|
+
const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
|
|
175
|
+
const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
|
|
176
|
+
return unreachable && !complete ? "probable" : "certain";
|
|
161
177
|
}
|
|
162
178
|
/**
|
|
163
179
|
* The component each dead declaration of the run falls in, and none for a live one, with
|
|
@@ -170,8 +186,24 @@ function findingComponents(swept, components, cascade) {
|
|
|
170
186
|
held.set(member, component);
|
|
171
187
|
}
|
|
172
188
|
}
|
|
189
|
+
const listed = membersListed(swept);
|
|
190
|
+
return (id) => {
|
|
191
|
+
const component = held.get(id);
|
|
192
|
+
return component === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: {
|
|
195
|
+
id: component.id,
|
|
196
|
+
root: component.roots.includes(id),
|
|
197
|
+
symbolCount: component.members.length,
|
|
198
|
+
deletableLines: component.deletableLines,
|
|
199
|
+
...(cascade === "full" ? { members: listed(component.members) } : {}),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** The members of a component as the run lists them, positioned and in the canonical order. */
|
|
204
|
+
function membersListed(swept) {
|
|
173
205
|
const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
|
|
174
|
-
|
|
206
|
+
return (members) => members
|
|
175
207
|
.flatMap((member) => {
|
|
176
208
|
const symbol = symbols.get(member);
|
|
177
209
|
return symbol === undefined
|
|
@@ -188,18 +220,6 @@ function findingComponents(swept, components, cascade) {
|
|
|
188
220
|
];
|
|
189
221
|
})
|
|
190
222
|
.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
223
|
}
|
|
204
224
|
/**
|
|
205
225
|
* The run's dead components with each file a `DS1502` finding reports dead among them, so
|
|
@@ -277,11 +297,12 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
277
297
|
let minted = computed;
|
|
278
298
|
const dead = new Set(components.map((component) => component.id));
|
|
279
299
|
const fallingWith = new Map();
|
|
280
|
-
for (const { heldByTests } of swept.sweep.candidates) {
|
|
300
|
+
for (const { id, heldByTests } of swept.sweep.candidates) {
|
|
281
301
|
if (heldByTests !== undefined) {
|
|
282
|
-
fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ??
|
|
302
|
+
fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
|
|
283
303
|
}
|
|
284
304
|
}
|
|
305
|
+
const listed = membersListed(swept);
|
|
285
306
|
const done = findings.map((finding) => {
|
|
286
307
|
const row = kinds.get(finding.code);
|
|
287
308
|
if (row === undefined) {
|
|
@@ -326,11 +347,25 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
326
347
|
let component = found;
|
|
327
348
|
if (component === undefined) {
|
|
328
349
|
minted += 1;
|
|
350
|
+
const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
|
|
329
351
|
component = {
|
|
330
352
|
id: componentId(minted),
|
|
331
353
|
root: true,
|
|
332
|
-
symbolCount:
|
|
354
|
+
symbolCount: falling?.length ?? 1,
|
|
333
355
|
deletableLines: 0,
|
|
356
|
+
...(input.config.reporters.cascade !== "full"
|
|
357
|
+
? {}
|
|
358
|
+
: falling === undefined
|
|
359
|
+
? {
|
|
360
|
+
members: [
|
|
361
|
+
{
|
|
362
|
+
ref: finding.symbol.ref,
|
|
363
|
+
name: finding.symbol.name,
|
|
364
|
+
position: finding.position,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
: { members: listed(falling) }),
|
|
334
369
|
};
|
|
335
370
|
}
|
|
336
371
|
const counts = candidate === undefined
|
|
@@ -341,7 +376,11 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
341
376
|
? candidate !== undefined && testUsed.declarations.has(judged.id)
|
|
342
377
|
: finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
|
|
343
378
|
const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
|
|
344
|
-
const
|
|
379
|
+
const configurations = candidate?.configurations ??
|
|
380
|
+
(judged === undefined && !part
|
|
381
|
+
? swept.matrix.configurations
|
|
382
|
+
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
|
|
383
|
+
const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate, configurations)), judged === undefined ? "certain" : ceilingOf(judged.id));
|
|
345
384
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
346
385
|
const fixability = generated
|
|
347
386
|
? "none"
|
|
@@ -372,10 +411,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
372
411
|
generated,
|
|
373
412
|
component,
|
|
374
413
|
retainedBy: [],
|
|
375
|
-
configurations
|
|
376
|
-
(judged === undefined && !part
|
|
377
|
-
? swept.matrix.configurations
|
|
378
|
-
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
414
|
+
configurations,
|
|
379
415
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
380
416
|
fixability,
|
|
381
417
|
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,
|
|
@@ -772,10 +772,9 @@ function calleeSteers(project, signature, at) {
|
|
|
772
772
|
}
|
|
773
773
|
/**
|
|
774
774
|
* The parts with each name of `contextual` judged where the compiler gives its function a
|
|
775
|
-
* contextual type:
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
* name read.
|
|
775
|
+
* contextual type: a name an object pattern binds is marked as one, and an annotated
|
|
776
|
+
* parameter with whether the call's inference may read its annotation. A question the
|
|
777
|
+
* checker leaves unanswered keeps the name read.
|
|
779
778
|
*/
|
|
780
779
|
function withContextualRead(project, parts, contextual) {
|
|
781
780
|
if (contextual.length === 0) {
|
|
@@ -791,15 +790,17 @@ function withContextualRead(project, parts, contextual) {
|
|
|
791
790
|
continue;
|
|
792
791
|
}
|
|
793
792
|
const place = one.part.parameter;
|
|
794
|
-
judged.set(one.part,
|
|
793
|
+
judged.set(one.part, answer === UNANSWERED || place === undefined
|
|
795
794
|
? { ...one.part, used: true }
|
|
796
|
-
:
|
|
797
|
-
...one.part,
|
|
798
|
-
|
|
799
|
-
...
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
795
|
+
: one.pattern
|
|
796
|
+
? { ...one.part, parameter: { ...place, contextualPattern: true } }
|
|
797
|
+
: {
|
|
798
|
+
...one.part,
|
|
799
|
+
parameter: {
|
|
800
|
+
...place,
|
|
801
|
+
annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
|
|
802
|
+
},
|
|
803
|
+
});
|
|
803
804
|
}
|
|
804
805
|
return parts.map((part) => judged.get(part) ?? part);
|
|
805
806
|
}
|
|
@@ -884,15 +885,28 @@ function functionOf(block) {
|
|
|
884
885
|
}
|
|
885
886
|
}
|
|
886
887
|
/**
|
|
887
|
-
* The locals one block declares with `let`, `const
|
|
888
|
-
* block that spell it. A `var` of a nested
|
|
889
|
-
*
|
|
888
|
+
* The locals one block declares with `let`, `const`, `var` or a function declaration
|
|
889
|
+
* inside a function, each with the names of the block that spell it. A `var` of a nested
|
|
890
|
+
* block is one where no nested function of its function names it, since the function scope
|
|
891
|
+
* holds it past the block's end.
|
|
890
892
|
*/
|
|
891
893
|
function localsOf(block) {
|
|
892
894
|
const declared = [];
|
|
893
895
|
const scope = functionOf(block);
|
|
894
896
|
const outlives = scope?.body !== block;
|
|
895
897
|
block.statements.forEach((statement, at) => {
|
|
898
|
+
if (isFunctionDeclaration(statement)) {
|
|
899
|
+
if (scope !== undefined && statement.name !== undefined && statement.body !== undefined) {
|
|
900
|
+
declared.push({
|
|
901
|
+
name: statement.name,
|
|
902
|
+
statement: at,
|
|
903
|
+
initialized: true,
|
|
904
|
+
outlives: false,
|
|
905
|
+
hoisted: true,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
896
910
|
if (!isVariableStatement(statement)) {
|
|
897
911
|
return;
|
|
898
912
|
}
|
|
@@ -902,6 +916,11 @@ function localsOf(block) {
|
|
|
902
916
|
return;
|
|
903
917
|
}
|
|
904
918
|
for (const declaration of list.declarations) {
|
|
919
|
+
// A write to a local typed `never` is the compiler's exhaustiveness check, which
|
|
920
|
+
// fails to compile once a case goes unhandled.
|
|
921
|
+
if (declaration.type?.kind === SyntaxKind.NeverKeyword) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
905
924
|
for (const name of boundNames(declaration.name)) {
|
|
906
925
|
if (!blockScoped &&
|
|
907
926
|
outlives &&
|
|
@@ -913,6 +932,7 @@ function localsOf(block) {
|
|
|
913
932
|
statement: at,
|
|
914
933
|
initialized: declaration.initializer !== undefined,
|
|
915
934
|
outlives: !blockScoped && outlives,
|
|
935
|
+
hoisted: false,
|
|
916
936
|
});
|
|
917
937
|
}
|
|
918
938
|
}
|
|
@@ -953,12 +973,20 @@ function deadStores(file, root, block, holder, locals, ids) {
|
|
|
953
973
|
used: false,
|
|
954
974
|
});
|
|
955
975
|
for (const local of locals) {
|
|
956
|
-
const
|
|
976
|
+
const own = local.hoisted ? block.statements[local.statement] : undefined;
|
|
977
|
+
// A function's reads of itself keep it no more than its absence would.
|
|
978
|
+
const uses = local.uses.filter((use) => readsBinding(use, local.name, ids) && (own === undefined || !within(use, own)));
|
|
957
979
|
if (!local.outlives && uses.every(isPlainWrite)) {
|
|
958
|
-
|
|
980
|
+
// A declaration with no initializer writes `undefined`, the one store left to name.
|
|
981
|
+
const writes = [...(local.initialized ? [local.name] : []), ...uses];
|
|
982
|
+
const targets = writes.length === 0 ? [local.name] : writes;
|
|
959
983
|
found.push(...targets.map((target) => deadAt(local, target)));
|
|
960
984
|
continue;
|
|
961
985
|
}
|
|
986
|
+
// A read anywhere in the block reads a function declaration, which is written first.
|
|
987
|
+
if (local.hoisted) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
962
990
|
if (uses.some((use) => inNestedFunction(use, block))) {
|
|
963
991
|
continue;
|
|
964
992
|
}
|