@cplieger/deadset-ts 5.2.2 → 6.0.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 +1 -1
- package/dist/src/analysis.js +28 -5
- package/dist/src/augmentations.js +50 -0
- package/dist/src/components.js +15 -15
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +17 -2
- package/dist/src/conventions.js +82 -1
- package/dist/src/dependencies.js +42 -26
- package/dist/src/entry-points.js +28 -14
- package/dist/src/exempt.js +1 -0
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +47 -12
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/unused-declarations.js +13 -1
- package/dist/src/findings-pass.js +1 -0
- package/dist/src/graph.js +6 -6
- package/dist/src/implementations.js +1 -1
- package/dist/src/indexed-access.js +215 -0
- package/dist/src/interface-satisfaction.js +85 -23
- package/dist/src/intra-function-parts.js +139 -30
- package/dist/src/inventory.js +6 -1
- package/dist/src/kinds.js +13 -12
- package/dist/src/matrix.js +34 -9
- package/dist/src/module-declarations.js +180 -0
- package/dist/src/outside-tests.js +17 -15
- package/dist/src/overrides.js +70 -9
- package/dist/src/query.js +7 -0
- package/dist/src/ref.js +1 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +3 -1
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/jsr.json +1 -1
- package/package.json +8 -2
- package/src/analysis.ts +40 -4
- package/src/augmentations.ts +72 -0
- package/src/components.ts +15 -13
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +25 -3
- package/src/conventions.ts +106 -2
- package/src/dependencies.ts +51 -31
- package/src/entry-points.ts +32 -19
- package/src/exempt.ts +4 -1
- package/src/findings/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +71 -12
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/unused-declarations.ts +16 -1
- package/src/findings-pass.ts +4 -1
- package/src/graph.ts +6 -6
- package/src/implementations.ts +1 -1
- package/src/indexed-access.ts +295 -0
- package/src/interface-satisfaction.ts +118 -27
- package/src/intra-function-parts.ts +196 -35
- package/src/inventory.ts +8 -0
- package/src/kinds.ts +14 -12
- package/src/matrix.ts +51 -8
- package/src/module-declarations.ts +241 -0
- package/src/outside-tests.ts +18 -16
- package/src/overrides.ts +80 -14
- package/src/query.ts +28 -0
- package/src/ref.ts +1 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +4 -1
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
- package/src/workspace-run.ts +3 -4
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* call sites that use or discard it, each statement control flow cannot reach, each
|
|
5
5
|
* write to a local that no read reaches, and each case an earlier case covers.
|
|
6
6
|
*/
|
|
7
|
-
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isCallExpression, isCaseClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
7
|
+
import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isBindingElement, isCallExpression, isCaseClause, isCatchClause, isClassDeclaration, isClassExpression, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
8
|
import { nodeKey } from "./inventory.js";
|
|
9
9
|
import { positionKey, renderPosition } from "./position.js";
|
|
10
10
|
import { isAnswered, UNANSWERED } from "./query.js";
|
|
@@ -24,8 +24,6 @@ const NO_VALUE = new Set([
|
|
|
24
24
|
"Promise<undefined>",
|
|
25
25
|
"Promise<never>",
|
|
26
26
|
]);
|
|
27
|
-
/** The prefix that marks a parameter as unused on purpose, as the compiler's own rule reads it. */
|
|
28
|
-
const UNUSED_PREFIX = "_";
|
|
29
27
|
/** The rendered position of one node, from its first token to its last line. */
|
|
30
28
|
function spanOf(file, root, node) {
|
|
31
29
|
const start = renderPosition(file, root, node.getStart());
|
|
@@ -284,6 +282,8 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
284
282
|
const id = held.declarations.get(nodeKey(file, node));
|
|
285
283
|
return id === undefined || kinds.get(id) === "file" ? undefined : id;
|
|
286
284
|
};
|
|
285
|
+
// A part no declaration holds belongs to the file, which is judged while the file is live.
|
|
286
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
287
287
|
const holderOf = (node) => {
|
|
288
288
|
for (let at = node.parent; at.kind !== SyntaxKind.SourceFile; at = at.parent) {
|
|
289
289
|
const id = declarationAt(at);
|
|
@@ -291,9 +291,17 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
291
291
|
return id;
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
-
return
|
|
294
|
+
return fileId;
|
|
295
295
|
};
|
|
296
|
-
const walk = {
|
|
296
|
+
const walk = {
|
|
297
|
+
declared: [],
|
|
298
|
+
values: [],
|
|
299
|
+
catches: [],
|
|
300
|
+
blocks: [],
|
|
301
|
+
unreachable: [],
|
|
302
|
+
cases: [],
|
|
303
|
+
};
|
|
304
|
+
const covered = new Set();
|
|
297
305
|
const position = (node) => positionKey(renderPosition(file, targetRoot, node.getStart()));
|
|
298
306
|
// Code below an unreachable statement is still walked for its calls and specifiers,
|
|
299
307
|
// and reports no part of its own: the unreachable statement is the one finding.
|
|
@@ -322,6 +330,19 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
322
330
|
const signature = id === undefined ? undefined : signatureOf(node);
|
|
323
331
|
if (id !== undefined && signature !== undefined) {
|
|
324
332
|
walk.declared.push({ id, node, signature });
|
|
333
|
+
covered.add(signature);
|
|
334
|
+
}
|
|
335
|
+
const value = covered.has(node) ? undefined : valueSignature(node);
|
|
336
|
+
const valueHolder = id ?? holderOf(node);
|
|
337
|
+
if (value !== undefined && valueHolder !== undefined && !isStub(value)) {
|
|
338
|
+
walk.values.push({ holder: valueHolder, signature: value });
|
|
339
|
+
}
|
|
340
|
+
if (isCatchClause(node) && node.variableDeclaration !== undefined) {
|
|
341
|
+
const name = node.variableDeclaration.name;
|
|
342
|
+
const holder = holderOf(node);
|
|
343
|
+
if (isIdentifier(name) && holder !== undefined && silenced === 0) {
|
|
344
|
+
walk.catches.push({ clause: node, name, holder });
|
|
345
|
+
}
|
|
325
346
|
}
|
|
326
347
|
if (isCallExpression(node)) {
|
|
327
348
|
const name = calleeName(node);
|
|
@@ -371,15 +392,21 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
371
392
|
// One batch per file resolves every name a parameter or a local is read through.
|
|
372
393
|
const asked = [];
|
|
373
394
|
const readers = new Map();
|
|
374
|
-
for (const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
395
|
+
for (const signature of [
|
|
396
|
+
...walk.declared.map((one) => one.signature),
|
|
397
|
+
...walk.values.map((one) => one.signature),
|
|
398
|
+
]) {
|
|
399
|
+
const names = parameterNames(signature);
|
|
400
|
+
const declaredNames = new Set(names.map((one) => one.name));
|
|
401
|
+
const spelled = new Set(names.map((one) => one.name.text));
|
|
402
|
+
const uses = identifiersNaming(signature, spelled).filter((use) => !declaredNames.has(use));
|
|
403
|
+
readers.set(signature, { names, uses });
|
|
404
|
+
asked.push(...names.map((one) => one.name), ...uses);
|
|
382
405
|
}
|
|
406
|
+
const caught = walk.catches.map(({ clause, name }) => identifiersNaming(clause.block, new Set([name.text])));
|
|
407
|
+
walk.catches.forEach(({ name }, at) => {
|
|
408
|
+
asked.push(name, ...(caught[at] ?? []));
|
|
409
|
+
});
|
|
383
410
|
const stores = walk.blocks.map((block) => localsOf(block));
|
|
384
411
|
for (const store of stores) {
|
|
385
412
|
asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
|
|
@@ -392,9 +419,28 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
392
419
|
stub: isStub(one.signature),
|
|
393
420
|
fixed: hierarchyFixes(one.node, extended),
|
|
394
421
|
});
|
|
395
|
-
const
|
|
396
|
-
|
|
422
|
+
const reader = readers.get(one.signature);
|
|
423
|
+
if (reader !== undefined) {
|
|
424
|
+
parts.push(...parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const one of walk.values) {
|
|
428
|
+
const reader = readers.get(one.signature);
|
|
429
|
+
if (reader !== undefined) {
|
|
430
|
+
parts.push(...parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true));
|
|
431
|
+
}
|
|
397
432
|
}
|
|
433
|
+
walk.catches.forEach(({ name, holder }, at) => {
|
|
434
|
+
parts.push({
|
|
435
|
+
code: DEAD_STORE,
|
|
436
|
+
declaration: holder,
|
|
437
|
+
kind: "store",
|
|
438
|
+
name: name.text,
|
|
439
|
+
position: spanOf(file, targetRoot, name),
|
|
440
|
+
message: `value written to ${name.text} is never read`,
|
|
441
|
+
used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
|
|
442
|
+
});
|
|
443
|
+
});
|
|
398
444
|
walk.blocks.forEach((block, at) => {
|
|
399
445
|
const holder = holderOf(block);
|
|
400
446
|
if (holder !== undefined) {
|
|
@@ -427,31 +473,91 @@ export function projectParts(project, held, references, targetRoot) {
|
|
|
427
473
|
parts.push(...resultParts(project, targetRoot, sites, references, calls));
|
|
428
474
|
return { parts, signatures, calls, specifiers };
|
|
429
475
|
}
|
|
430
|
-
/**
|
|
431
|
-
function
|
|
432
|
-
|
|
476
|
+
/**
|
|
477
|
+
* A function written in place that no declaration declares: an arrow, a function
|
|
478
|
+
* expression, a nested function declaration, or a method of an object literal.
|
|
479
|
+
*/
|
|
480
|
+
function valueSignature(node) {
|
|
481
|
+
if (isArrowFunction(node) || isFunctionExpression(node)) {
|
|
482
|
+
return node;
|
|
483
|
+
}
|
|
484
|
+
if (isFunctionDeclaration(node)) {
|
|
485
|
+
return node.body === undefined ? undefined : node;
|
|
486
|
+
}
|
|
487
|
+
return isMethodDeclaration(node) &&
|
|
488
|
+
node.body !== undefined &&
|
|
489
|
+
isObjectLiteralExpression(node.parent)
|
|
490
|
+
? node
|
|
491
|
+
: undefined;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Every name one function's parameters bind, a binding pattern's names one by one. A
|
|
495
|
+
* `this` parameter receives no argument and a decorated one is read by its decorator, so
|
|
496
|
+
* neither binds a subject.
|
|
497
|
+
*/
|
|
498
|
+
function parameterNames(signature) {
|
|
499
|
+
const found = [];
|
|
500
|
+
const patternNames = (pattern, at) => {
|
|
501
|
+
if (isIdentifier(pattern)) {
|
|
502
|
+
found.push({ name: pattern, at, pattern: true, exempt: false });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const rest = isObjectBindingPattern(pattern) &&
|
|
506
|
+
pattern.elements.some((element) => element.dotDotDotToken !== undefined);
|
|
507
|
+
for (const element of pattern.elements) {
|
|
508
|
+
if (!isBindingElement(element) || element.name === undefined) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (isIdentifier(element.name)) {
|
|
512
|
+
const exempt = rest && element.dotDotDotToken === undefined;
|
|
513
|
+
found.push({ name: element.name, at, pattern: true, exempt });
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
patternNames(element.name, at);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
signature.parameters.forEach((parameter, at) => {
|
|
521
|
+
const name = parameter.name;
|
|
522
|
+
if ((parameter.modifiers ?? []).some(isDecorator)) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (isIdentifier(name)) {
|
|
526
|
+
if (name.text !== "this") {
|
|
527
|
+
found.push({ name, at, pattern: false, exempt: false });
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
patternNames(name, at);
|
|
532
|
+
});
|
|
533
|
+
return found;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Each name one function's parameters bind and whether the function reads it. A binding
|
|
537
|
+
* pattern counts as read where its function reads any name it binds.
|
|
538
|
+
*/
|
|
539
|
+
function parameterParts(file, root, declaration, signature, reader, ids, inValue) {
|
|
433
540
|
// A function that reads `arguments` reads every parameter by position.
|
|
434
541
|
const readsArguments = !isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
|
|
542
|
+
const read = reader.names.map(({ name }) => readsArguments ||
|
|
543
|
+
reader.uses.some((use) => use.text === name.text && readsBinding(use, name, ids)));
|
|
544
|
+
const lastRead = Math.max(-1, ...reader.names.filter((_, at) => read[at]).map((one) => one.at));
|
|
435
545
|
const found = [];
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
name.text === "this" ||
|
|
440
|
-
name.text.startsWith(UNUSED_PREFIX) ||
|
|
441
|
-
(parameter.modifiers ?? []).some(isDecorator)) {
|
|
442
|
-
continue;
|
|
546
|
+
reader.names.forEach(({ name, at, pattern, exempt }, index) => {
|
|
547
|
+
if (exempt) {
|
|
548
|
+
return;
|
|
443
549
|
}
|
|
444
550
|
found.push({
|
|
445
551
|
code: UNUSED_PARAMETER,
|
|
446
|
-
declaration
|
|
552
|
+
declaration,
|
|
447
553
|
kind: "parameter",
|
|
448
554
|
name: name.text,
|
|
449
555
|
position: spanOf(file, root, name),
|
|
450
556
|
message: `parameter ${name.text} is never read in the body`,
|
|
451
|
-
used:
|
|
452
|
-
|
|
557
|
+
used: read[index] === true,
|
|
558
|
+
parameter: { beforeARead: !pattern && at < lastRead, inValue },
|
|
453
559
|
});
|
|
454
|
-
}
|
|
560
|
+
});
|
|
455
561
|
return found;
|
|
456
562
|
}
|
|
457
563
|
/** The locals one block declares with `let` or `const`, each with the names that spell it. */
|
|
@@ -655,6 +761,9 @@ export function intraFunctionFacts(projects, references, exempted) {
|
|
|
655
761
|
const free = new Set(signatures
|
|
656
762
|
.map((one) => one.declaration)
|
|
657
763
|
.filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)));
|
|
764
|
+
const freeButValued = new Set(signatures
|
|
765
|
+
.map((one) => one.declaration)
|
|
766
|
+
.filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)));
|
|
658
767
|
const discardedEverywhere = new Set([...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id));
|
|
659
|
-
return { parts: [...parts.values()], free, discardedEverywhere };
|
|
768
|
+
return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
|
|
660
769
|
}
|
package/dist/src/inventory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/unstable/ast";
|
|
1
|
+
import { isClassDeclaration, isComputedPropertyName, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isStringLiteral, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/unstable/ast";
|
|
2
2
|
import { dirnamePath, joinPath, relativePath } from "./paths.js";
|
|
3
3
|
import { byPosition, positionKey, renderPosition, writtenLength, } from "./position.js";
|
|
4
4
|
import { computedComponent, nameComponent, renderRef } from "./ref.js";
|
|
@@ -260,6 +260,7 @@ export function inventory(project, host, targetRoot) {
|
|
|
260
260
|
const exportScopes = [];
|
|
261
261
|
const containers = [];
|
|
262
262
|
const starReExports = [];
|
|
263
|
+
const modules = [];
|
|
263
264
|
let outsideOwnFiles = 0;
|
|
264
265
|
const keep = (record) => {
|
|
265
266
|
building.push(record);
|
|
@@ -450,6 +451,9 @@ export function inventory(project, host, targetRoot) {
|
|
|
450
451
|
localName: undefined,
|
|
451
452
|
});
|
|
452
453
|
exportScopes.push({ owner: record, nameNode: node.name });
|
|
454
|
+
if (isStringLiteral(node.name)) {
|
|
455
|
+
modules.push([record, node.name.text]);
|
|
456
|
+
}
|
|
453
457
|
const body = node.body;
|
|
454
458
|
if (body === undefined) {
|
|
455
459
|
return;
|
|
@@ -710,6 +714,7 @@ export function inventory(project, host, targetRoot) {
|
|
|
710
714
|
.filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
|
|
711
715
|
.map((symbol) => symbol.id),
|
|
712
716
|
byName: namesOf(building),
|
|
717
|
+
modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
|
|
713
718
|
};
|
|
714
719
|
}
|
|
715
720
|
/** Whether one declaration is, or is declared under, a scope or container left unanswered. */
|
package/dist/src/kinds.js
CHANGED
|
@@ -70,8 +70,8 @@ const ROWS = [
|
|
|
70
70
|
code: "DS1101",
|
|
71
71
|
name: "unnecessary-export",
|
|
72
72
|
languages: ["go", "ts"],
|
|
73
|
-
rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method
|
|
74
|
-
precondition: "Closed world only.
|
|
73
|
+
rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
|
|
74
|
+
precondition: "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
|
|
75
75
|
defaultSeverity: "warn",
|
|
76
76
|
maxClass: "certain",
|
|
77
77
|
fixability: "narrowable",
|
|
@@ -81,7 +81,7 @@ const ROWS = [
|
|
|
81
81
|
code: "DS1102",
|
|
82
82
|
name: "unnecessary-exposure",
|
|
83
83
|
languages: ["go"],
|
|
84
|
-
rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method
|
|
84
|
+
rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
|
|
85
85
|
precondition: "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
|
|
86
86
|
defaultSeverity: "warn",
|
|
87
87
|
maxClass: "certain",
|
|
@@ -103,7 +103,7 @@ const ROWS = [
|
|
|
103
103
|
name: "redundant-export-keyword",
|
|
104
104
|
languages: ["ts"],
|
|
105
105
|
rule: "An exported declaration used only inside its own file, reported as a candidate for removing the export keyword. The finding names the narrower visibility the references support.",
|
|
106
|
-
precondition: "Closed world only. Reported in a file that is not an entry file and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file
|
|
106
|
+
precondition: "Closed world only. Reported in a file that is not an entry file and that either belongs to a project whose consumer set the configuration declares complete or is reached by no manifest export. A declared cross-language edge counts as a reference from outside the file. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first.",
|
|
107
107
|
defaultSeverity: "warn",
|
|
108
108
|
maxClass: "certain",
|
|
109
109
|
fixability: "narrowable",
|
|
@@ -123,8 +123,8 @@ const ROWS = [
|
|
|
123
123
|
code: "DS1203",
|
|
124
124
|
name: "uncalled-interface-method",
|
|
125
125
|
languages: ["go", "ts"],
|
|
126
|
-
rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions.",
|
|
127
|
-
precondition: "
|
|
126
|
+
rule: "An interface method that no call site invokes or selects through the interface, whatever the number of implementations. The finding names the concrete implementations and their positions. Each method that implements it and that nothing else keeps is reported under the code its own references select. Such a method counts as referenced by the interface method alone, so it belongs to this finding's dead component, whose root member is the interface method.",
|
|
127
|
+
precondition: "In Go, every method of an interface that declares an unexported method is exempt. That is the sum-type shape, whose method set exists to restrict the implementors, and a TypeScript interface cannot take it because it declares no member less visible than itself. In both languages, every marker method is exempt, an interface method whose every implementation carries an empty body.",
|
|
128
128
|
defaultSeverity: "warn",
|
|
129
129
|
maxClass: "certain",
|
|
130
130
|
fixability: "manual",
|
|
@@ -176,7 +176,7 @@ const ROWS = [
|
|
|
176
176
|
name: "file-never-built",
|
|
177
177
|
languages: ["go", "ts"],
|
|
178
178
|
rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint is the ignore tag is never reported: the toolchain applies no build constraint to a file named on its own command line, so that tag is the toolchain's convention for a file built by hand. A file any other custom tag excludes is reported under a matrix the configuration declares complete, because completeness is the maintainer's assertion that the listed configurations are every one the target builds, and a configuration the maintainer builds by hand belongs in that list.",
|
|
179
|
-
precondition: 'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled
|
|
179
|
+
precondition: 'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled. Such a file is recorded as excluded by cgo rather than as never built.',
|
|
180
180
|
defaultSeverity: "deny",
|
|
181
181
|
maxClass: "certain",
|
|
182
182
|
fixability: "deletable",
|
|
@@ -197,7 +197,7 @@ const ROWS = [
|
|
|
197
197
|
name: "unused-dependency",
|
|
198
198
|
languages: ["go", "ts"],
|
|
199
199
|
rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
|
|
200
|
-
precondition: "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and
|
|
200
|
+
precondition: "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and four more dependencies are needed. The first is one whose installed manifest declares a command, because a command is run by name rather than imported. The second is one that the installed manifest of a needed dependency declares as a peer and does not mark optional. The third is every required peer of a dependency so held, to a fixpoint. The fourth is one that a string names under a manifest key of an applied convention row, as grammar/analysis.md states. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
|
|
201
201
|
defaultSeverity: "deny",
|
|
202
202
|
maxClass: "certain",
|
|
203
203
|
fixability: "manual",
|
|
@@ -208,7 +208,7 @@ const ROWS = [
|
|
|
208
208
|
name: "unused-module-directive",
|
|
209
209
|
languages: ["go"],
|
|
210
210
|
rule: "A replace directive whose target module is absent from the build list, the one case in which the directive is exactly a no-op.",
|
|
211
|
-
precondition: "Only a replace whose target is absent from the build list. No other module-file directive is reported
|
|
211
|
+
precondition: "Only a replace whose target is absent from the build list. No other module-file directive is reported. An exclude directive is not, because not selected is a counterfactual about a resolution that did not happen. A workspace use entry and a tool directive are not, because each is an external entry point whose invocation lives outside anything the analysis reads.",
|
|
212
212
|
defaultSeverity: "warn",
|
|
213
213
|
maxClass: "certain",
|
|
214
214
|
fixability: "deletable",
|
|
@@ -228,7 +228,7 @@ const ROWS = [
|
|
|
228
228
|
code: "DS1702",
|
|
229
229
|
name: "unscoped-ignore-entry",
|
|
230
230
|
languages: ["go", "ts"],
|
|
231
|
-
rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project
|
|
231
|
+
rule: "An ignore-file entry or a baseline row that names a symbol and no file path. The entry is reported rather than matched, so a bare name cannot mask a match anywhere else in the project. The inline directive is scoped by its position and cannot be unscoped.",
|
|
232
232
|
defaultSeverity: "deny",
|
|
233
233
|
maxClass: "certain",
|
|
234
234
|
fixability: "none",
|
|
@@ -279,7 +279,7 @@ const ROWS = [
|
|
|
279
279
|
name: "unused-parameter",
|
|
280
280
|
languages: ["go", "ts"],
|
|
281
281
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
282
|
-
precondition: "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change.",
|
|
282
|
+
precondition: "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported when its body reads no parameter after it, and an unread parameter before the last read one is not. A TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. No name exempts a parameter. A TypeScript parameter whose name starts with an underscore is judged as any other is, and a Go parameter named by the blank identifier declares no name a body could read and is never reported.",
|
|
283
283
|
defaultSeverity: "warn",
|
|
284
284
|
maxClass: "certain",
|
|
285
285
|
fixability: "manual",
|
|
@@ -326,11 +326,12 @@ const ROWS = [
|
|
|
326
326
|
name: "dead-store",
|
|
327
327
|
languages: ["go", "ts"],
|
|
328
328
|
rule: "A write to a local variable with no read before the next write to it or the end of its scope, computed on the same read-and-write classification the write-only-symbol kind uses. The finding names the write position.",
|
|
329
|
+
precondition: "In TypeScript the binding of a catch clause is a local variable the clause writes when it catches, so a binding that is a name and that the clause's block never reads is a dead store, positioned at the binding: the clause written with no binding behaves the same.",
|
|
329
330
|
defaultSeverity: "deny",
|
|
330
331
|
maxClass: "certain",
|
|
331
332
|
fixability: "deletable",
|
|
332
333
|
fixed: false,
|
|
333
|
-
overlap: ["eslint no-useless-assignment"],
|
|
334
|
+
overlap: ["eslint no-useless-assignment", "@typescript-eslint/no-unused-vars"],
|
|
334
335
|
},
|
|
335
336
|
{
|
|
336
337
|
code: "DS1809",
|
package/dist/src/matrix.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { componentsOf } from "./components.js";
|
|
7
7
|
import { graphOf, OUTSIDE } from "./graph.js";
|
|
8
8
|
import { byPosition } from "./position.js";
|
|
9
|
-
import { sweep } from "./sweep.js";
|
|
9
|
+
import { reachedWithTheTests, sweep, } from "./sweep.js";
|
|
10
10
|
/** Two strings ordered bytewise. */
|
|
11
11
|
function compare(a, b) {
|
|
12
12
|
if (a === b) {
|
|
@@ -103,37 +103,62 @@ export function sweepMatrix(matrix, input) {
|
|
|
103
103
|
const dead = union.symbols.map(() => false);
|
|
104
104
|
const testOfDeadCode = union.symbols.map(() => false);
|
|
105
105
|
const heldBy = union.symbols.map(() => undefined);
|
|
106
|
-
const
|
|
107
|
-
union
|
|
106
|
+
const unreferencedTest = union.symbols.map(() => false);
|
|
107
|
+
const testReached = reachedWithTheTests(union, [
|
|
108
|
+
...input.marked,
|
|
109
|
+
...(input.exempt ?? []).map((record) => record.id),
|
|
110
|
+
...(input.unanswered ?? []),
|
|
111
|
+
]);
|
|
112
|
+
const judged = union.symbols.map((symbol, at) => {
|
|
108
113
|
const held = matrix.heldIn[at] ?? [];
|
|
109
114
|
const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
|
|
110
|
-
|
|
115
|
+
return held.length === 0 || found.some((candidate) => candidate === undefined)
|
|
116
|
+
? undefined
|
|
117
|
+
: found.filter((candidate) => candidate !== undefined);
|
|
118
|
+
});
|
|
119
|
+
// A configuration that does not hold a test's subject's caller finds the subject dead,
|
|
120
|
+
// so a test is of dead code only where every production subject it names is dead in
|
|
121
|
+
// every configuration that holds it.
|
|
122
|
+
const subjectsDead = (at) => (union.out[at] ?? []).every((edge) => union.test[edge.to] === true ||
|
|
123
|
+
union.support[edge.to] === true ||
|
|
124
|
+
union.subject[edge.to] !== true ||
|
|
125
|
+
judged[edge.to] !== undefined);
|
|
126
|
+
const candidates = [];
|
|
127
|
+
union.symbols.forEach((symbol, at) => {
|
|
128
|
+
const all = judged[at];
|
|
129
|
+
if (all === undefined) {
|
|
111
130
|
return;
|
|
112
131
|
}
|
|
113
|
-
const
|
|
114
|
-
const admitted = all.every((candidate) => candidate.testOfDeadCode);
|
|
132
|
+
const held = matrix.heldIn[at] ?? [];
|
|
133
|
+
const admitted = all.every((candidate) => candidate.testOfDeadCode) && subjectsDead(at);
|
|
115
134
|
const testRefs = union.made[at]?.test ?? 0;
|
|
116
135
|
const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
|
|
117
136
|
(union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
|
|
137
|
+
const unreferenced = union.test[at] === true && !admitted && testReached[at] !== true;
|
|
118
138
|
dead[at] = true;
|
|
119
139
|
testOfDeadCode[at] = admitted;
|
|
120
140
|
heldBy[at] = heldByTests;
|
|
141
|
+
unreferencedTest[at] = unreferenced;
|
|
142
|
+
const productionRefs = union.made[at]?.production ?? 0;
|
|
143
|
+
const counted = all.every((candidate) => candidate.relation === "reference-counting");
|
|
121
144
|
candidates.push({
|
|
122
145
|
id: symbol.id,
|
|
123
|
-
productionRefs
|
|
146
|
+
productionRefs,
|
|
124
147
|
testRefs,
|
|
125
|
-
|
|
148
|
+
// The run that judges a test-file declaration counts its test references.
|
|
149
|
+
relation: (unreferenced ? productionRefs + testRefs === 0 : counted)
|
|
126
150
|
? "reference-counting"
|
|
127
151
|
: "reachability",
|
|
128
152
|
testOfDeadCode: admitted,
|
|
129
153
|
configurations: held,
|
|
130
154
|
heldByTests,
|
|
155
|
+
unreferencedTest: unreferenced,
|
|
131
156
|
});
|
|
132
157
|
});
|
|
133
158
|
return {
|
|
134
159
|
liveUnder: liveAnywhere(union.symbols, per),
|
|
135
160
|
candidates,
|
|
136
|
-
components: componentsOf(union, dead.map((one, at) => one && heldBy[at] === undefined), testOfDeadCode),
|
|
161
|
+
components: componentsOf(union, dead.map((one, at) => one && heldBy[at] === undefined), testOfDeadCode, unreferencedTest),
|
|
137
162
|
};
|
|
138
163
|
}
|
|
139
164
|
/**
|