@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.
Files changed (43) hide show
  1. package/dist/src/alias-chain.js +6 -2
  2. package/dist/src/analysis.js +3 -1
  3. package/dist/src/config.js +4 -0
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/decorator.js +3 -2
  6. package/dist/src/findings/unused-declarations.js +3 -3
  7. package/dist/src/injection-container.js +28 -25
  8. package/dist/src/intra-function-parts.js +72 -11
  9. package/dist/src/inventory.js +18 -3
  10. package/dist/src/kinds.js +2 -2
  11. package/dist/src/ref.js +3 -2
  12. package/dist/src/references.js +38 -20
  13. package/dist/src/report.js +1 -1
  14. package/dist/src/resolve.js +37 -4
  15. package/dist/src/schema.js +4 -1
  16. package/dist/src/suppress-file.js +43 -9
  17. package/dist/src/suppress-inline.js +35 -12
  18. package/dist/src/suppress.js +3 -2
  19. package/dist/src/verbs/analyze.js +7 -3
  20. package/dist/src/version.js +1 -1
  21. package/jsr.json +1 -1
  22. package/package.json +1 -1
  23. package/src/alias-chain.ts +7 -2
  24. package/src/analysis.ts +3 -3
  25. package/src/config.ts +9 -1
  26. package/src/conformance-record.ts +2 -2
  27. package/src/decorator.ts +8 -2
  28. package/src/findings/unused-declarations.ts +3 -3
  29. package/src/injection-container.ts +31 -26
  30. package/src/intra-function-parts.ts +95 -11
  31. package/src/inventory.ts +26 -2
  32. package/src/kinds.ts +2 -2
  33. package/src/ref.ts +3 -2
  34. package/src/references.ts +55 -21
  35. package/src/report.ts +1 -1
  36. package/src/resolve.ts +56 -6
  37. package/src/schema.ts +10 -14
  38. package/src/signatures.ts +6 -1
  39. package/src/suppress-file.ts +48 -9
  40. package/src/suppress-inline.ts +57 -15
  41. package/src/suppress.ts +3 -2
  42. package/src/verbs/analyze.ts +7 -2
  43. 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: the method overrides, or is overridden. */
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" ? undefined : id;
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
- sites.push({ file, declared: one });
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 { parts: withContextualRead(project, parts, contextual), signatures, calls, specifiers };
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 and a decorated one is read by its decorator, so
726
- * neither binds a subject.
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((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)),
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 TS_FILE = String.raw`(?:[^/#\r\n]+/)*[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
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}/package\\.json`;
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 { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
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
- if (held !== undefined) {
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
- * The name node of every import binding one file's top level declares, and whether it
889
- * binds a whole module.
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, whole: true });
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, whole: false });
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, whole: true });
932
+ found.push({ node: bindings.name });
911
933
  } else {
912
- found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
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 holds whole.
1460
- const targets = binding.whole
1461
- ? chained.flatMap((target) => [
1462
- target,
1463
- ...declaredIn(target.id)
1464
- .filter((one) => one.exported)
1465
- .map((one) => ({ ...target, id: one.id })),
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.1.0";
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, backslash or line break.
61
+ * solidus or control character.
61
62
  */
62
- const PROJECT_SEGMENT = String.raw`(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)`;
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: TargetKind | undefined;
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 backslash or line break",
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: readEnum(target, "kind", "target.kind", label, TARGET_KINDS),
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<TargetKind | "">("target.kind", "", (doc) => doc.targetKind),
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(