@cplieger/deadset-ts 7.1.0-dev.2 → 7.2.0-dev.1
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/alias-chain.js +6 -2
- package/dist/src/analysis.js +3 -1
- package/dist/src/config.js +4 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/decorator.js +3 -2
- package/dist/src/findings/unused-declarations.js +3 -3
- package/dist/src/injection-container.js +28 -25
- package/dist/src/intra-function-parts.js +72 -11
- package/dist/src/inventory.js +18 -3
- package/dist/src/kinds.js +2 -2
- package/dist/src/ref.js +3 -2
- package/dist/src/references.js +38 -20
- package/dist/src/report.js +1 -1
- package/dist/src/resolve.js +37 -4
- package/dist/src/schema.js +4 -1
- package/dist/src/suppress-file.js +43 -9
- package/dist/src/suppress-inline.js +35 -12
- package/dist/src/suppress.js +3 -2
- package/dist/src/verbs/analyze.js +7 -3
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alias-chain.ts +7 -2
- package/src/analysis.ts +3 -3
- package/src/config.ts +9 -1
- package/src/conformance-record.ts +2 -2
- package/src/decorator.ts +8 -2
- package/src/findings/unused-declarations.ts +3 -3
- package/src/injection-container.ts +31 -26
- package/src/intra-function-parts.ts +95 -11
- package/src/inventory.ts +26 -2
- package/src/kinds.ts +2 -2
- package/src/ref.ts +3 -2
- package/src/references.ts +55 -21
- package/src/report.ts +1 -1
- package/src/resolve.ts +56 -6
- package/src/schema.ts +10 -14
- package/src/signatures.ts +6 -1
- package/src/suppress-file.ts +48 -9
- package/src/suppress-inline.ts +57 -15
- package/src/suppress.ts +3 -2
- package/src/verbs/analyze.ts +7 -2
- package/src/version.ts +1 -1
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
isCatchClause,
|
|
20
20
|
isClassDeclaration,
|
|
21
21
|
isClassExpression,
|
|
22
|
+
isConstructorDeclaration,
|
|
22
23
|
isContinueStatement,
|
|
23
24
|
isDecorator,
|
|
24
25
|
isDefaultClause,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
isMethodDeclaration,
|
|
38
39
|
isModuleDeclaration,
|
|
39
40
|
isNamespaceImport,
|
|
41
|
+
isNewExpression,
|
|
40
42
|
isNoSubstitutionTemplateLiteral,
|
|
41
43
|
isNumericLiteral,
|
|
42
44
|
isObjectBindingPattern,
|
|
@@ -61,6 +63,7 @@ import {
|
|
|
61
63
|
type Block,
|
|
62
64
|
type CallExpression,
|
|
63
65
|
type CatchClause,
|
|
66
|
+
type ConstructorDeclaration,
|
|
64
67
|
type FunctionExpression,
|
|
65
68
|
type Identifier,
|
|
66
69
|
type Node,
|
|
@@ -68,7 +71,7 @@ import {
|
|
|
68
71
|
type Statement,
|
|
69
72
|
} from "@typescript/native/ast";
|
|
70
73
|
import type { FindingPosition } from "./finding.ts";
|
|
71
|
-
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
74
|
+
import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
|
|
72
75
|
import { positionKey, renderPosition } from "./position.ts";
|
|
73
76
|
import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
|
|
74
77
|
import type { Reference } from "./references.ts";
|
|
@@ -117,8 +120,10 @@ interface SignatureFact {
|
|
|
117
120
|
readonly declaration: string;
|
|
118
121
|
/** Whether the body is empty or does nothing but throw. */
|
|
119
122
|
readonly stub: boolean;
|
|
120
|
-
/** Whether a class hierarchy fixes the signature
|
|
123
|
+
/** Whether a class hierarchy fixes the signature, or a decorator a constructor's. */
|
|
121
124
|
readonly fixed: boolean;
|
|
125
|
+
/** Whether the signature is a constructor's, which its class's identifier stands for. */
|
|
126
|
+
readonly constructor: boolean;
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/** What one project answers about the parts of its declarations. */
|
|
@@ -129,6 +134,8 @@ export interface ProjectParts {
|
|
|
129
134
|
readonly calls: ReadonlyMap<string, CallUse>;
|
|
130
135
|
/** Each name an import or an export specifier writes, by position, which uses no value. */
|
|
131
136
|
readonly specifiers: ReadonlySet<string>;
|
|
137
|
+
/** Each name of {@link constructionNames}, by position, which uses no constructor as a value. */
|
|
138
|
+
readonly constructions: ReadonlySet<string>;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
/** The codes of the parts this module reads. */
|
|
@@ -391,6 +398,50 @@ function hierarchyFixes(declared: Node, extended: ReadonlySet<string>): boolean
|
|
|
391
398
|
return owner.name !== undefined && extended.has(owner.name.text);
|
|
392
399
|
}
|
|
393
400
|
|
|
401
|
+
/** Whether a decorator on a constructor's class or on one of its parameters fixes its signature. */
|
|
402
|
+
function decoratorFixes(constructor: ConstructorDeclaration): boolean {
|
|
403
|
+
const owner = constructor.parent;
|
|
404
|
+
return (
|
|
405
|
+
((owner as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator) ||
|
|
406
|
+
constructor.parameters.some((parameter) =>
|
|
407
|
+
((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator),
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The name an expression spells a class by: an identifier, or the member a property access names. */
|
|
413
|
+
function spelledName(expression: Node): Identifier | undefined {
|
|
414
|
+
const node = unwrapped(expression);
|
|
415
|
+
if (isIdentifier(node)) {
|
|
416
|
+
return node;
|
|
417
|
+
}
|
|
418
|
+
return isPropertyAccessExpression(node) && isIdentifier(node.name) ? node.name : undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The names one node writes that use a class without using its constructor as a value:
|
|
423
|
+
* the class a `new` expression calls, the class an `extends` or `implements` clause
|
|
424
|
+
* names, and the name a type reference spells.
|
|
425
|
+
*/
|
|
426
|
+
function constructionNames(node: Node): readonly Identifier[] {
|
|
427
|
+
if (isNewExpression(node)) {
|
|
428
|
+
return [spelledName(node.expression) ?? []].flat();
|
|
429
|
+
}
|
|
430
|
+
if (
|
|
431
|
+
isHeritageClause(node) &&
|
|
432
|
+
(isClassDeclaration(node.parent) || isClassExpression(node.parent))
|
|
433
|
+
) {
|
|
434
|
+
return node.types.flatMap((type) =>
|
|
435
|
+
isTypeReferenceNode(type) ? [] : (spelledName(type.expression) ?? []),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (isTypeReferenceNode(node)) {
|
|
439
|
+
const named = isQualifiedName(node.typeName) ? node.typeName.right : node.typeName;
|
|
440
|
+
return isIdentifier(named) ? [named] : [];
|
|
441
|
+
}
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
|
|
394
445
|
/** One declaration of the inventory that declares a function, and that function. */
|
|
395
446
|
interface Declared {
|
|
396
447
|
readonly id: string;
|
|
@@ -483,13 +534,17 @@ export function projectParts<Brand>(
|
|
|
483
534
|
const signatures: SignatureFact[] = [];
|
|
484
535
|
const calls = new Map<string, CallUse>();
|
|
485
536
|
const specifiers = new Set<string>();
|
|
537
|
+
const constructions = new Set<string>();
|
|
486
538
|
const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
|
|
487
539
|
const contextual: ContextualParameter[] = [];
|
|
488
540
|
|
|
489
541
|
for (const file of files) {
|
|
542
|
+
// A parameter property's own parts stay with the parameter when its member goes.
|
|
490
543
|
const declarationAt = (node: Node): string | undefined => {
|
|
491
544
|
const id = held.declarations.get(nodeKey(file, node));
|
|
492
|
-
return id === undefined || kinds.get(id) === "file"
|
|
545
|
+
return id === undefined || kinds.get(id) === "file" || isParameterProperty(node)
|
|
546
|
+
? undefined
|
|
547
|
+
: id;
|
|
493
548
|
};
|
|
494
549
|
// A part no declaration holds belongs to the file, which is judged while the file is live.
|
|
495
550
|
const fileId = held.declarations.get(nodeKey(file, file));
|
|
@@ -543,6 +598,18 @@ export function projectParts<Brand>(
|
|
|
543
598
|
walk.declared.push({ id, node, signature });
|
|
544
599
|
covered.add(signature);
|
|
545
600
|
}
|
|
601
|
+
// A constructor is no declaration, so the class it belongs to holds its parts.
|
|
602
|
+
const constructs =
|
|
603
|
+
isConstructorDeclaration(node) && node.body !== undefined
|
|
604
|
+
? declarationAt(node.parent)
|
|
605
|
+
: undefined;
|
|
606
|
+
if (constructs !== undefined && isConstructorDeclaration(node)) {
|
|
607
|
+
walk.declared.push({ id: constructs, node, signature: node });
|
|
608
|
+
covered.add(node);
|
|
609
|
+
}
|
|
610
|
+
for (const name of constructionNames(node)) {
|
|
611
|
+
constructions.add(position(name));
|
|
612
|
+
}
|
|
546
613
|
const value = covered.has(node) ? undefined : valueSignature(node);
|
|
547
614
|
const valueHolder = id ?? holderOf(node);
|
|
548
615
|
if (value !== undefined && valueHolder !== undefined && !isStub(value)) {
|
|
@@ -628,11 +695,15 @@ export function projectParts<Brand>(
|
|
|
628
695
|
const ids = resolvedIds(project, [...new Set(asked)]);
|
|
629
696
|
|
|
630
697
|
for (const one of walk.declared) {
|
|
631
|
-
|
|
698
|
+
const constructor = isConstructorDeclaration(one.node);
|
|
699
|
+
if (!constructor) {
|
|
700
|
+
sites.push({ file, declared: one });
|
|
701
|
+
}
|
|
632
702
|
signatures.push({
|
|
633
703
|
declaration: one.id,
|
|
634
704
|
stub: isStub(one.signature),
|
|
635
|
-
fixed: hierarchyFixes(one.node, extended),
|
|
705
|
+
fixed: constructor ? decoratorFixes(one.node) : hierarchyFixes(one.node, extended),
|
|
706
|
+
constructor,
|
|
636
707
|
});
|
|
637
708
|
const reader = readers.get(one.signature);
|
|
638
709
|
if (reader !== undefined) {
|
|
@@ -699,7 +770,13 @@ export function projectParts<Brand>(
|
|
|
699
770
|
}
|
|
700
771
|
|
|
701
772
|
parts.push(...resultParts(project, targetRoot, sites, references, calls));
|
|
702
|
-
return {
|
|
773
|
+
return {
|
|
774
|
+
parts: withContextualRead(project, parts, contextual),
|
|
775
|
+
signatures,
|
|
776
|
+
calls,
|
|
777
|
+
specifiers,
|
|
778
|
+
constructions,
|
|
779
|
+
};
|
|
703
780
|
}
|
|
704
781
|
|
|
705
782
|
/**
|
|
@@ -722,8 +799,8 @@ function valueSignature(node: Node): SignatureNode | undefined {
|
|
|
722
799
|
|
|
723
800
|
/**
|
|
724
801
|
* Every name one function's parameters bind, a binding pattern's names one by one. A
|
|
725
|
-
* `this` parameter receives no argument
|
|
726
|
-
*
|
|
802
|
+
* `this` parameter receives no argument, a decorated one is read by its decorator, and a
|
|
803
|
+
* parameter property is read by the write to its member, so none binds a subject.
|
|
727
804
|
*/
|
|
728
805
|
function parameterNames(signature: SignatureNode): ParameterName[] {
|
|
729
806
|
const found: ParameterName[] = [];
|
|
@@ -750,7 +827,8 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
|
|
|
750
827
|
signature.parameters.forEach((parameter, at) => {
|
|
751
828
|
const name = parameter.name;
|
|
752
829
|
if (
|
|
753
|
-
((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator)
|
|
830
|
+
((parameter as { readonly modifiers?: readonly Node[] }).modifiers ?? []).some(isDecorator) ||
|
|
831
|
+
isParameterProperty(parameter)
|
|
754
832
|
) {
|
|
755
833
|
return;
|
|
756
834
|
}
|
|
@@ -1096,7 +1174,7 @@ export function intraFunctionFacts(
|
|
|
1096
1174
|
for (const [at, value] of one.calls) {
|
|
1097
1175
|
calls.set(at, value);
|
|
1098
1176
|
}
|
|
1099
|
-
for (const at of one.specifiers) {
|
|
1177
|
+
for (const at of [...one.specifiers, ...one.constructions]) {
|
|
1100
1178
|
specifiers.add(at);
|
|
1101
1179
|
}
|
|
1102
1180
|
}
|
|
@@ -1127,10 +1205,16 @@ export function intraFunctionFacts(
|
|
|
1127
1205
|
.map((one) => one.declaration)
|
|
1128
1206
|
.filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)),
|
|
1129
1207
|
);
|
|
1208
|
+
// A class used as a value fixes its constructor's whole signature, whatever reads follow.
|
|
1209
|
+
const constructors = new Set(
|
|
1210
|
+
signatures.filter((one) => one.constructor).map((one) => one.declaration),
|
|
1211
|
+
);
|
|
1130
1212
|
const freeButValued = new Set(
|
|
1131
1213
|
signatures
|
|
1132
1214
|
.map((one) => one.declaration)
|
|
1133
|
-
.filter(
|
|
1215
|
+
.filter(
|
|
1216
|
+
(id) => !fixed.has(id) && !exempted.has(id) && valued.has(id) && !constructors.has(id),
|
|
1217
|
+
),
|
|
1134
1218
|
);
|
|
1135
1219
|
// Awaiting a thenable result sequences on it, so a call that awaits one uses it.
|
|
1136
1220
|
const sequenced = new Set(
|
package/src/inventory.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isClassDeclaration,
|
|
3
3
|
isComputedPropertyName,
|
|
4
|
+
isConstructorDeclaration,
|
|
4
5
|
isEnumDeclaration,
|
|
5
6
|
isEnumMember,
|
|
6
7
|
isExportAssignment,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
isModuleBlock,
|
|
16
17
|
isModuleDeclaration,
|
|
17
18
|
isNamespaceExport,
|
|
19
|
+
isParameterDeclaration,
|
|
18
20
|
isParenthesizedTypeNode,
|
|
19
21
|
isPrivateIdentifier,
|
|
20
22
|
isPropertyDeclaration,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
isUnionTypeNode,
|
|
27
29
|
isVariableStatement,
|
|
28
30
|
ModifierFlags,
|
|
31
|
+
type ClassDeclaration,
|
|
29
32
|
type Node,
|
|
30
33
|
type SourceFile,
|
|
31
34
|
} from "@typescript/native/ast";
|
|
@@ -490,6 +493,26 @@ function unnamedStart(node: Node): number {
|
|
|
490
493
|
return prefix === null ? start : start + prefix[0].length;
|
|
491
494
|
}
|
|
492
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Whether one node is a parameter property: a constructor parameter carrying an
|
|
498
|
+
* accessibility, `readonly` or `override` modifier, which also declares an instance
|
|
499
|
+
* member of the class, with the parameter's node as the member's declaration.
|
|
500
|
+
*/
|
|
501
|
+
export function isParameterProperty(node: Node): boolean {
|
|
502
|
+
return (
|
|
503
|
+
isParameterDeclaration(node) &&
|
|
504
|
+
isConstructorDeclaration(node.parent) &&
|
|
505
|
+
(modifiersOf(node) & ModifierFlags.ParameterPropertyModifier) !== 0
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** The parameter properties every constructor of one class declares. */
|
|
510
|
+
function parameterPropertiesOf(node: ClassDeclaration): readonly Node[] {
|
|
511
|
+
return node.members.flatMap((member) =>
|
|
512
|
+
isConstructorDeclaration(member) ? member.parameters.filter(isParameterProperty) : [],
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
493
516
|
/** The kind a class element takes: a method, or a property or accessor. */
|
|
494
517
|
function classMemberKind(node: Node): SymbolKind {
|
|
495
518
|
return isMethodDeclaration(node) ? "method" : "class-member";
|
|
@@ -647,7 +670,7 @@ export function inventory<Brand>(
|
|
|
647
670
|
owner: record,
|
|
648
671
|
declaration: node,
|
|
649
672
|
nameNode: node.name,
|
|
650
|
-
members: node.members,
|
|
673
|
+
members: [...node.members, ...parameterPropertiesOf(node)],
|
|
651
674
|
memberKind: "class-member",
|
|
652
675
|
isClass: true,
|
|
653
676
|
membersAreExports: false,
|
|
@@ -974,7 +997,8 @@ export function inventory<Brand>(
|
|
|
974
997
|
isPropertyDeclaration(node) ||
|
|
975
998
|
isMethodDeclaration(node) ||
|
|
976
999
|
isGetAccessorDeclaration(node) ||
|
|
977
|
-
isSetAccessorDeclaration(node)
|
|
1000
|
+
isSetAccessorDeclaration(node) ||
|
|
1001
|
+
isParameterProperty(node)
|
|
978
1002
|
) {
|
|
979
1003
|
return classMemberKind(node);
|
|
980
1004
|
}
|
package/src/kinds.ts
CHANGED
|
@@ -191,7 +191,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
191
191
|
code: "DS1301",
|
|
192
192
|
name: "write-only-symbol",
|
|
193
193
|
languages: ["go", "ts"],
|
|
194
|
-
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
|
|
194
|
+
rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. A TypeScript parameter property, a constructor parameter that carries a modifier, declares a member of its class that the constructor writes with its argument, so the member is judged under this rule, and under every rule that reads a class member, as a member the class body declares is. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
|
|
195
195
|
defaultSeverity: "deny",
|
|
196
196
|
maxClass: "certain",
|
|
197
197
|
fixability: "deletable",
|
|
@@ -332,7 +332,7 @@ const ROWS: readonly KindRow[] = [
|
|
|
332
332
|
languages: ["go", "ts"],
|
|
333
333
|
rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
|
|
334
334
|
precondition:
|
|
335
|
-
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, 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 function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. 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 parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type is not reported when it carries a type annotation, because the inference of the call that holds the function may read the annotation, or when it is an object binding pattern, because the pattern reads each property it names from the argument and the code that hands the argument may act on that read. No name such a pattern binds is reported either. Any other 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.",
|
|
335
|
+
"The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, 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 function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. 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 parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type is not reported when it carries a type annotation, because the inference of the call that holds the function may read the annotation, or when it is an object binding pattern, because the pattern reads each property it names from the argument and the code that hands the argument may act on that read. No name such a pattern binds is reported either. Any other 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. A TypeScript constructor is used as a value where its class is used as a value other than as the callee of a `new` expression or the class an `extends` clause names, and its signature is not free where its class carries a decorator or a parameter of the constructor carries one, because the code that decorator or a container runs may construct the class. A parameter property is read by the write to its member the constructor makes, so it is never reported under this kind, and its member is judged under DS1301. 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.",
|
|
336
336
|
defaultSeverity: "warn",
|
|
337
337
|
maxClass: "certain",
|
|
338
338
|
fixability: "manual",
|
package/src/ref.ts
CHANGED
|
@@ -27,10 +27,11 @@ const TS_COMPUTED = String.raw`\[[^\]\r\n\t ]+\]`;
|
|
|
27
27
|
const TS_HEAD = `(?:${TS_IDENT}|${TS_QUOTED})`;
|
|
28
28
|
const TS_MEMBER = `(?:${TS_IDENT}|#${TS_IDENT}|${TS_QUOTED}|${TS_COMPUTED})`;
|
|
29
29
|
const TS_PACKAGE = String.raw`(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+`;
|
|
30
|
-
const
|
|
30
|
+
const TS_DIRS = String.raw`(?:[^/#\r\n]+/)*`;
|
|
31
|
+
const TS_FILE = String.raw`${TS_DIRS}[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
|
|
31
32
|
|
|
32
33
|
const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
|
|
33
|
-
const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}
|
|
34
|
+
const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_DIRS}package\\.json`;
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* One expression per form the grammar defines, each anchored and each the whole of
|
package/src/references.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
isVoidExpression,
|
|
33
33
|
SyntaxKind,
|
|
34
34
|
type Node,
|
|
35
|
+
type ParameterDeclaration,
|
|
35
36
|
type SourceFile,
|
|
36
37
|
} from "@typescript/native/ast";
|
|
37
38
|
import type { Symbol as TSSymbol } from "@typescript/native/sync";
|
|
@@ -41,7 +42,13 @@ import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuri
|
|
|
41
42
|
import { specifierCandidates } from "./configuration-files.ts";
|
|
42
43
|
import { namespaceUses } from "./namespace-objects.ts";
|
|
43
44
|
import { globExpression } from "./glob.ts";
|
|
44
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
declarationsByName,
|
|
47
|
+
isParameterProperty,
|
|
48
|
+
nodeKey,
|
|
49
|
+
type Inventory,
|
|
50
|
+
type InventorySymbol,
|
|
51
|
+
} from "./inventory.ts";
|
|
45
52
|
import { augmentationReferences } from "./augmentations.ts";
|
|
46
53
|
import { elementAccessReferences } from "./element-access.ts";
|
|
47
54
|
import { indexedAccessReferences } from "./indexed-access.ts";
|
|
@@ -478,6 +485,13 @@ interface Decorated {
|
|
|
478
485
|
readonly declaration: string;
|
|
479
486
|
}
|
|
480
487
|
|
|
488
|
+
/** One parameter property, which its constructor writes, and the declaration that writes it. */
|
|
489
|
+
interface ParameterWrite {
|
|
490
|
+
readonly name: Node;
|
|
491
|
+
readonly from: string;
|
|
492
|
+
readonly member: string;
|
|
493
|
+
}
|
|
494
|
+
|
|
481
495
|
/** One property a pattern reads, and the declaration the pattern is written inside. */
|
|
482
496
|
interface Read extends PropertyRead {
|
|
483
497
|
readonly from: string;
|
|
@@ -494,6 +508,7 @@ interface FileSites {
|
|
|
494
508
|
readonly wholes: readonly WholeRead[];
|
|
495
509
|
readonly urls: readonly ModuleURL[];
|
|
496
510
|
readonly decorated: readonly Decorated[];
|
|
511
|
+
readonly parameterWrites: readonly ParameterWrite[];
|
|
497
512
|
readonly keyed: readonly KeyedAccess[];
|
|
498
513
|
}
|
|
499
514
|
|
|
@@ -682,6 +697,7 @@ function sitesOf(
|
|
|
682
697
|
const wholes: WholeRead[] = [];
|
|
683
698
|
const urls: ModuleURL[] = [];
|
|
684
699
|
const decorated: Decorated[] = [];
|
|
700
|
+
const parameterWrites: ParameterWrite[] = [];
|
|
685
701
|
const namespaces = namespaceUses(file);
|
|
686
702
|
const literals: LiteralAt[] = [];
|
|
687
703
|
const flows: FlowAt[] = [];
|
|
@@ -766,7 +782,15 @@ function sitesOf(
|
|
|
766
782
|
}
|
|
767
783
|
const held = declarations.get(nodeKey(file, node));
|
|
768
784
|
const outer = enclosing;
|
|
769
|
-
|
|
785
|
+
// A parameter property's annotation, initializer and decorators stay with the
|
|
786
|
+
// parameter when the member goes, so they are written inside the constructor's class.
|
|
787
|
+
if (held !== undefined && isParameterProperty(node)) {
|
|
788
|
+
parameterWrites.push({
|
|
789
|
+
name: (node as ParameterDeclaration).name,
|
|
790
|
+
from: outer,
|
|
791
|
+
member: held,
|
|
792
|
+
});
|
|
793
|
+
} else if (held !== undefined) {
|
|
770
794
|
enclosing = held;
|
|
771
795
|
for (const modifier of (node as { readonly modifiers?: readonly Node[] }).modifiers ?? []) {
|
|
772
796
|
if (isDecorator(modifier)) {
|
|
@@ -789,6 +813,7 @@ function sitesOf(
|
|
|
789
813
|
wholes,
|
|
790
814
|
urls,
|
|
791
815
|
decorated,
|
|
816
|
+
parameterWrites,
|
|
792
817
|
keyed,
|
|
793
818
|
};
|
|
794
819
|
}
|
|
@@ -884,15 +909,12 @@ function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string |
|
|
|
884
909
|
return names;
|
|
885
910
|
}
|
|
886
911
|
|
|
887
|
-
/**
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
*/
|
|
891
|
-
function importBindings(file: SourceFile): readonly { node: Node; whole: boolean }[] {
|
|
892
|
-
const found: { node: Node; whole: boolean }[] = [];
|
|
912
|
+
/** The name node of every import binding one file's top level declares. */
|
|
913
|
+
function importBindings(file: SourceFile): readonly { node: Node }[] {
|
|
914
|
+
const found: { node: Node }[] = [];
|
|
893
915
|
for (const statement of file.statements) {
|
|
894
916
|
if (isImportEqualsDeclaration(statement)) {
|
|
895
|
-
found.push({ node: statement.name
|
|
917
|
+
found.push({ node: statement.name });
|
|
896
918
|
continue;
|
|
897
919
|
}
|
|
898
920
|
const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
|
|
@@ -900,16 +922,16 @@ function importBindings(file: SourceFile): readonly { node: Node; whole: boolean
|
|
|
900
922
|
continue;
|
|
901
923
|
}
|
|
902
924
|
if (clause.name !== undefined) {
|
|
903
|
-
found.push({ node: clause.name
|
|
925
|
+
found.push({ node: clause.name });
|
|
904
926
|
}
|
|
905
927
|
const bindings = clause.namedBindings;
|
|
906
928
|
if (bindings === undefined) {
|
|
907
929
|
continue;
|
|
908
930
|
}
|
|
909
931
|
if (bindings.kind === SyntaxKind.NamespaceImport) {
|
|
910
|
-
found.push({ node: bindings.name
|
|
932
|
+
found.push({ node: bindings.name });
|
|
911
933
|
} else {
|
|
912
|
-
found.push(...bindings.elements.map((element) => ({ node: element.name
|
|
934
|
+
found.push(...bindings.elements.map((element) => ({ node: element.name })));
|
|
913
935
|
}
|
|
914
936
|
}
|
|
915
937
|
return found;
|
|
@@ -1286,6 +1308,7 @@ export function references<Brand>(
|
|
|
1286
1308
|
wholes,
|
|
1287
1309
|
urls,
|
|
1288
1310
|
decorated,
|
|
1311
|
+
parameterWrites,
|
|
1289
1312
|
keyed,
|
|
1290
1313
|
} of walked) {
|
|
1291
1314
|
const test = testFile.get(file) === true;
|
|
@@ -1307,6 +1330,18 @@ export function references<Brand>(
|
|
|
1307
1330
|
});
|
|
1308
1331
|
}
|
|
1309
1332
|
|
|
1333
|
+
// The constructor stores its argument in the member a parameter property declares.
|
|
1334
|
+
for (const { name, from, member } of parameterWrites) {
|
|
1335
|
+
found.push({
|
|
1336
|
+
from,
|
|
1337
|
+
to: member,
|
|
1338
|
+
position: renderPosition(file, root, name.getStart()),
|
|
1339
|
+
use: "write",
|
|
1340
|
+
resolution: "syntax",
|
|
1341
|
+
test,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1310
1345
|
// One batch per capped run of the file's name nodes, the re-exports' nodes after
|
|
1311
1346
|
// the uses and the evaluated modules' specifiers last, each node once: a bare star
|
|
1312
1347
|
// re-export's specifier is a link and an evaluation both. A shorthand is left out
|
|
@@ -1456,15 +1491,14 @@ export function references<Brand>(
|
|
|
1456
1491
|
symbol === UNANSWERED
|
|
1457
1492
|
? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
|
|
1458
1493
|
: chains.chainOf(symbol);
|
|
1459
|
-
// The markup may name any export of a module a binding
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
: chained;
|
|
1494
|
+
// The markup may name any export of a module a binding's chain reaches, whether a
|
|
1495
|
+
// namespace import binds it or a named or default import of a namespace re-export.
|
|
1496
|
+
const targets = chained.flatMap((target) => [
|
|
1497
|
+
target,
|
|
1498
|
+
...declaredIn(target.id)
|
|
1499
|
+
.filter((one) => one.exported)
|
|
1500
|
+
.map((one) => ({ ...target, id: one.id })),
|
|
1501
|
+
]);
|
|
1468
1502
|
for (const target of targets) {
|
|
1469
1503
|
found.push({
|
|
1470
1504
|
from: fileId,
|
package/src/report.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { StaleSuppression } from "./findings/self-check.ts";
|
|
|
19
19
|
import type { TestFileRule } from "./references.ts";
|
|
20
20
|
|
|
21
21
|
/** The version of the report schema this analyzer writes a report to. */
|
|
22
|
-
const SCHEMA_VERSION = "8.
|
|
22
|
+
const SCHEMA_VERSION = "8.2.0";
|
|
23
23
|
|
|
24
24
|
/** Every report schema version this analyzer reads: the one the Contract it implements admits. */
|
|
25
25
|
export const SCHEMA_VERSIONS_ACCEPTED: readonly string[] = [SCHEMA_VERSION];
|
package/src/resolve.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type Sort,
|
|
20
20
|
type Source,
|
|
21
21
|
type TargetKind,
|
|
22
|
+
type TargetKindSetting,
|
|
22
23
|
type TemplateDelimiters,
|
|
23
24
|
} from "./config.ts";
|
|
24
25
|
import { CONVENTION_ROWS } from "./convention-rows.ts";
|
|
@@ -57,9 +58,9 @@ const PROJECT_MEMBER = "project";
|
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* One segment of a project path: not empty, not `.` or `..`, and holding no
|
|
60
|
-
* solidus
|
|
61
|
+
* solidus or control character.
|
|
61
62
|
*/
|
|
62
|
-
const PROJECT_SEGMENT = String.raw`(?:[
|
|
63
|
+
const PROJECT_SEGMENT = String.raw`(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+)`;
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* A project path: segments joined by a solidus, so the path is relative, below
|
|
@@ -85,7 +86,7 @@ const CASCADES: readonly Cascade[] = ["roots", "full"];
|
|
|
85
86
|
*/
|
|
86
87
|
interface Doc {
|
|
87
88
|
readonly contractVersion: string | undefined;
|
|
88
|
-
readonly targetKind:
|
|
89
|
+
readonly targetKind: TargetKindSetting | undefined;
|
|
89
90
|
readonly languages: readonly Language[] | undefined;
|
|
90
91
|
readonly minConfidence: Confidence | undefined;
|
|
91
92
|
readonly generatedFiles: GeneratedFiles | undefined;
|
|
@@ -281,6 +282,55 @@ function readStrings<T extends string>(
|
|
|
281
282
|
return value as readonly T[];
|
|
282
283
|
}
|
|
283
284
|
|
|
285
|
+
/**
|
|
286
|
+
* The target kind, a string or an object naming every language. The walk has
|
|
287
|
+
* already refused an undeclared member, so an object is refused for the first
|
|
288
|
+
* language it leaves out, then for the first language whose value is refused,
|
|
289
|
+
* each in the vocabulary's language order.
|
|
290
|
+
*/
|
|
291
|
+
function readTargetKind(
|
|
292
|
+
target: Record<string, unknown> | undefined,
|
|
293
|
+
label: string,
|
|
294
|
+
): TargetKindSetting | undefined {
|
|
295
|
+
const value = member(target, "kind");
|
|
296
|
+
const perLanguage = `an object naming the kind of each of ${spell(LANGUAGES)}`;
|
|
297
|
+
if (value === undefined) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
if (typeof value === "string") {
|
|
301
|
+
if (!(TARGET_KINDS as readonly string[]).includes(value)) {
|
|
302
|
+
throw malformed(
|
|
303
|
+
label,
|
|
304
|
+
"target.kind",
|
|
305
|
+
`${JSON.stringify(value)} is not one of ${spell(TARGET_KINDS)}, nor ${perLanguage}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return value as TargetKind;
|
|
309
|
+
}
|
|
310
|
+
if (!isRecord(value)) {
|
|
311
|
+
throw malformed(
|
|
312
|
+
label,
|
|
313
|
+
"target.kind",
|
|
314
|
+
`${JSON.stringify(value)} is neither one of ${spell(TARGET_KINDS)} nor ${perLanguage}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const missing = LANGUAGES.find((language) => !Object.hasOwn(value, language));
|
|
318
|
+
if (missing !== undefined) {
|
|
319
|
+
throw malformed(label, `target.kind.${missing}`, `is not set, and ${perLanguage} names each`);
|
|
320
|
+
}
|
|
321
|
+
for (const language of LANGUAGES) {
|
|
322
|
+
const kind = value[language];
|
|
323
|
+
if (typeof kind !== "string" || !(TARGET_KINDS as readonly string[]).includes(kind)) {
|
|
324
|
+
throw malformed(
|
|
325
|
+
label,
|
|
326
|
+
`target.kind.${language}`,
|
|
327
|
+
`${JSON.stringify(kind)} is not one of ${spell(TARGET_KINDS)}`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { go: value["go"] as TargetKind, ts: value["ts"] as TargetKind };
|
|
332
|
+
}
|
|
333
|
+
|
|
284
334
|
/** A component file's extension: a full stop followed by letters and digits. */
|
|
285
335
|
const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
|
|
286
336
|
|
|
@@ -355,7 +405,7 @@ function readProject(
|
|
|
355
405
|
label,
|
|
356
406
|
path,
|
|
357
407
|
`${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
|
|
358
|
-
"none empty, . or .., and no
|
|
408
|
+
"none empty, . or .., and no control character",
|
|
359
409
|
);
|
|
360
410
|
}
|
|
361
411
|
return { shape: "project", id, project };
|
|
@@ -795,7 +845,7 @@ function readDoc(value: unknown, label: string): Doc {
|
|
|
795
845
|
|
|
796
846
|
return {
|
|
797
847
|
contractVersion,
|
|
798
|
-
targetKind:
|
|
848
|
+
targetKind: readTargetKind(target, label),
|
|
799
849
|
languages: readStrings(analysis, "languages", "analysis.languages", label, 0, LANGUAGES),
|
|
800
850
|
minConfidence: readEnum(
|
|
801
851
|
analysis,
|
|
@@ -1052,7 +1102,7 @@ function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Or
|
|
|
1052
1102
|
|
|
1053
1103
|
const config: Config = {
|
|
1054
1104
|
contractVersion: at("contract_version", defaults.contractVersion, (doc) => doc.contractVersion),
|
|
1055
|
-
targetKind: at<
|
|
1105
|
+
targetKind: at<TargetKindSetting | "">("target.kind", "", (doc) => doc.targetKind),
|
|
1056
1106
|
analysis: {
|
|
1057
1107
|
languages: at("analysis.languages", defaults.analysis.languages, (doc) => doc.languages),
|
|
1058
1108
|
minConfidence: at(
|