@cplieger/deadset-ts 7.1.0 → 7.2.0-dev.2

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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/src/alias-chain.js +6 -2
  3. package/dist/src/analysis.js +3 -1
  4. package/dist/src/config.js +4 -0
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/decorator.js +3 -2
  7. package/dist/src/dependencies.js +0 -4
  8. package/dist/src/deprecation.js +1 -1
  9. package/dist/src/findings/completion.js +1 -1
  10. package/dist/src/findings/interfaces.js +1 -8
  11. package/dist/src/findings/unused-declarations.js +8 -11
  12. package/dist/src/html-entries.js +0 -3
  13. package/dist/src/injection-container.js +28 -25
  14. package/dist/src/intra-function-parts.js +71 -22
  15. package/dist/src/inventory.js +19 -6
  16. package/dist/src/kinds.js +2 -2
  17. package/dist/src/matrix.js +1 -8
  18. package/dist/src/module-declarations.js +0 -3
  19. package/dist/src/ref.js +3 -2
  20. package/dist/src/references.js +38 -20
  21. package/dist/src/report.js +1 -1
  22. package/dist/src/resolve.js +37 -4
  23. package/dist/src/schema.js +4 -1
  24. package/dist/src/suppress-file.js +43 -9
  25. package/dist/src/suppress-inline.js +35 -12
  26. package/dist/src/suppress.js +3 -2
  27. package/dist/src/type-errors.js +2 -1
  28. package/dist/src/verbs/analyze.js +7 -3
  29. package/dist/src/version.js +1 -1
  30. package/dist/src/workspace-resolution.js +5 -8
  31. package/dist/src/workspace.js +2 -2
  32. package/jsr.json +1 -1
  33. package/package.json +2 -2
  34. package/src/alias-chain.ts +7 -2
  35. package/src/analysis.ts +3 -3
  36. package/src/config.ts +9 -1
  37. package/src/conformance-record.ts +2 -2
  38. package/src/decorator.ts +8 -2
  39. package/src/dependencies.ts +0 -4
  40. package/src/deprecation.ts +1 -1
  41. package/src/findings/completion.ts +1 -4
  42. package/src/findings/interfaces.ts +1 -9
  43. package/src/findings/unused-declarations.ts +8 -15
  44. package/src/html-entries.ts +0 -3
  45. package/src/injection-container.ts +31 -26
  46. package/src/intra-function-parts.ts +96 -24
  47. package/src/inventory.ts +27 -5
  48. package/src/kinds.ts +2 -2
  49. package/src/matrix.ts +1 -9
  50. package/src/module-declarations.ts +0 -3
  51. package/src/ref.ts +3 -2
  52. package/src/references.ts +55 -21
  53. package/src/report.ts +1 -1
  54. package/src/resolve.ts +56 -6
  55. package/src/schema.ts +10 -14
  56. package/src/signatures.ts +6 -1
  57. package/src/suppress-file.ts +48 -9
  58. package/src/suppress-inline.ts +57 -15
  59. package/src/suppress.ts +3 -2
  60. package/src/type-errors.ts +2 -1
  61. package/src/verbs/analyze.ts +7 -2
  62. package/src/version.ts +1 -1
  63. package/src/workspace-resolution.ts +5 -8
  64. package/src/workspace.ts +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # deadset-ts
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@cplieger/deadset-ts)](https://www.npmjs.com/package/@cplieger/deadset-ts) [![JSR](https://jsr.io/badges/@cplieger/deadset-ts)](https://jsr.io/@cplieger/deadset-ts)
3
+ [![npm](https://img.shields.io/npm/v/@cplieger/deadset-ts)](https://www.npmjs.com/package/@cplieger/deadset-ts) [![JSR](https://jsr.io/badges/@cplieger/deadset-ts)](https://jsr.io/@cplieger/deadset-ts) [![Mutation (TS)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/deadset-ts/badges/mutation-ts.json)](https://github.com/cplieger/deadset-ts/issues?q=label%3Astryker-tracker)
4
4
 
5
5
  deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
6
6
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { isExportAssignment, isExportSpecifier, isIdentifier, isImportClause, isImportSpecifier, isNamespaceExport, isNamespaceImport, } from "@typescript/native/ast";
7
7
  import { SymbolFlags } from "@typescript/native/sync";
8
- import { declarationsByName, nodeKey } from "./inventory.js";
8
+ import { declarationsByName, isParameterProperty, nodeKey } from "./inventory.js";
9
9
  import { UNANSWERED } from "./query.js";
10
10
  /**
11
11
  * The chain walks over one project, against the declarations of `held`, the same
@@ -28,7 +28,11 @@ export function aliasChains(project, held) {
28
28
  }
29
29
  const node = project.declarationAt(handle)?.node;
30
30
  const id = node === undefined ? undefined : held.declarations.get(nodeKey(node.getSourceFile(), node));
31
- if (id !== undefined && !ids.includes(id)) {
31
+ // A parameter property's node declares the parameter too, whose reads are no member's.
32
+ const parameter = node !== undefined &&
33
+ isParameterProperty(node) &&
34
+ (symbol.flags & SymbolFlags.Property) === 0;
35
+ if (id !== undefined && !parameter && !ids.includes(id)) {
32
36
  ids.push(id);
33
37
  }
34
38
  }
@@ -6,6 +6,7 @@
6
6
  * another project's checker, which holds only for views the session hands out.
7
7
  */
8
8
  import { lineOf, readComponent } from "./component-files.js";
9
+ import { kindOf } from "./config.js";
9
10
  import { consumerLoads, consumerReferences, moduleIdentity, } from "./consumers.js";
10
11
  import { configuredDeclarations, configuredRef, declarationsNamed, } from "./configured-declarations.js";
11
12
  import { decorator } from "./decorator.js";
@@ -33,6 +34,7 @@ import { packageEntries, readManifest } from "./manifest.js";
33
34
  import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
34
35
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
35
36
  import { byPosition, isComponentFile } from "./position.js";
37
+ import { LANGUAGE } from "./report.js";
36
38
  import { SetupError, setupLine } from "./setup-failure.js";
37
39
  import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
38
40
  import { reflectiveLookup } from "./reflective-lookup.js";
@@ -158,7 +160,7 @@ function readProjects(engine, host, scope, config, stage) {
158
160
  conventions,
159
161
  testFiles: config.ts.testFiles,
160
162
  parse,
161
- publishedAPI: config.targetKind === "library",
163
+ publishedAPI: config.targetKind !== "" && kindOf(config.targetKind, LANGUAGE) === "library",
162
164
  publishedMembers: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
163
165
  };
164
166
  };
@@ -5,6 +5,10 @@ import { CONTRACT_VERSION } from "./version.js";
5
5
  * target supplied the setting it is about.
6
6
  */
7
7
  export const REPOSITORY_DOCUMENT = "deadset.json";
8
+ /** The kind a target kind setting gives one language's declarations. */
9
+ export function kindOf(setting, language) {
10
+ return typeof setting === "string" ? setting : setting[language];
11
+ }
8
12
  /** The severities ranked, the strongest highest. */
9
13
  const SEVERITY_RANK = { allow: 0, warn: 1, deny: 2 };
10
14
  /** Whether a finding at one severity fails a run under the failing severity. */
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "5.1.0",
5
+ corpusVersion: "5.2.0",
6
6
  result: "pass",
7
- digest: "sha256:befd5e68f8d68e859b2a0920464a5470a157d5d40a881d9cca28716a4575751d",
7
+ digest: "sha256:4050d1b67fb757bc12a60f8ece7317a3e8485d3383ec52919cd24b9dfd5509dc",
8
8
  },
9
9
  gaps: [],
10
10
  };
@@ -11,7 +11,7 @@
11
11
  * the inventory holds for the class, static members included.
12
12
  */
13
13
  import { decoratorsOf, spelledDecorator } from "./calls.js";
14
- import { nodeKey } from "./inventory.js";
14
+ import { isParameterProperty, nodeKey, } from "./inventory.js";
15
15
  import { renderPosition } from "./position.js";
16
16
  /** The kinds of declaration a decorator is attached to and holds back. */
17
17
  const DECORATED = new Set(["class", "method", "class-member"]);
@@ -39,7 +39,8 @@ export function decorator(input) {
39
39
  const visit = (node) => {
40
40
  const id = held.declarations.get(nodeKey(file, node));
41
41
  const symbol = id === undefined ? undefined : byId.get(id);
42
- if (symbol !== undefined && DECORATED.has(symbol.kind)) {
42
+ // A parameter property's decorator is attached to its parameter, and to no member.
43
+ if (symbol !== undefined && DECORATED.has(symbol.kind) && !isParameterProperty(node)) {
43
44
  for (const attached of decoratorsOf(node)) {
44
45
  const detail = `decorated by ${spelledDecorator(attached)}`;
45
46
  const site = renderPosition(file, targetRoot, attached.getStart());
@@ -466,15 +466,11 @@ function skipWhitespace(text, at) {
466
466
  */
467
467
  function membersAt(text, at) {
468
468
  const found = new Map();
469
- if (text.charAt(at) !== "{") {
470
- return found;
471
- }
472
469
  let cursor = skipWhitespace(text, at + 1);
473
470
  while (text.charAt(cursor) === '"') {
474
471
  const keyEnd = valueEnd(text, cursor);
475
472
  const key = JSON.parse(text.slice(cursor, keyEnd));
476
473
  const value = skipWhitespace(text, skipWhitespace(text, keyEnd) + 1);
477
- found.delete(key);
478
474
  found.set(key, { key: cursor, value });
479
475
  cursor = skipWhitespace(text, valueEnd(text, value));
480
476
  if (text.charAt(cursor) === ",") {
@@ -47,7 +47,7 @@ export function deprecatedDeclarations(project, held) {
47
47
  }
48
48
  }
49
49
  const id = held.declarations.get(nodeKey(file, node));
50
- if (id !== undefined && node !== file) {
50
+ if (id !== undefined) {
51
51
  named.push({ id, handle: project.handle(nameNode(node)) });
52
52
  }
53
53
  node.forEachChild(visit);
@@ -145,7 +145,7 @@ function findingComponents(swept, cascade) {
145
145
  },
146
146
  ];
147
147
  })
148
- .sort((a, b) => byPosition(a.position, b.position) || (a.ref === b.ref ? 0 : a.ref < b.ref ? -1 : 1));
148
+ .sort((a, b) => byPosition(a.position, b.position));
149
149
  return (id) => {
150
150
  const component = held.get(id);
151
151
  return component === undefined
@@ -10,13 +10,6 @@ const UNCALLED_MESSAGE = "no call site invokes or selects the method through the
10
10
  function positionOf(symbol) {
11
11
  return { ...symbol.position, endLine: Math.max(symbol.position.line, symbol.endLine) };
12
12
  }
13
- /** Two strings ordered bytewise. */
14
- function compare(a, b) {
15
- if (a === b) {
16
- return 0;
17
- }
18
- return a < b ? -1 : 1;
19
- }
20
13
  /** One run's declarations and candidates, indexed for the two kinds. */
21
14
  function indexed(input) {
22
15
  const union = input.swept.matrix.union;
@@ -31,7 +24,7 @@ function indexed(input) {
31
24
  .map(symbolOf)
32
25
  .filter((symbol) => symbol !== undefined)
33
26
  .map((symbol) => ({ ref: symbol.ref, name: symbol.name, position: positionOf(symbol) }))
34
- .sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref)),
27
+ .sort((a, b) => byPosition(a.position, b.position)),
35
28
  };
36
29
  }
37
30
  /** The finding one code makes about one declaration. */
@@ -39,28 +39,26 @@ const WORDS = {
39
39
  const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
40
40
  /**
41
41
  * The code one candidate is reported under, or none, each symbol once under the most
42
- * specific code: a test of dead code first; no other test-file declaration, member of a
43
- * dead container or enum member nothing names; a test-support declaration test code
44
- * references as test-only use; then deprecation, a test reference, and the unreferenced
45
- * codes, an unimportable unused export being the unreachable kind's.
42
+ * specific code: a test of dead code first; no other test-file declaration or enum
43
+ * member nothing names; a test-support declaration test code references as test-only
44
+ * use; then deprecation, a test reference, and the unreferenced codes, an unimportable
45
+ * unused export being the unreachable kind's.
46
46
  */
47
47
  function codeOf(judged) {
48
48
  const { candidate, symbol } = judged;
49
49
  if (candidate.testOfDeadCode) {
50
50
  return "DS1005";
51
51
  }
52
- if ((judged.test && !candidate.unreferencedTest) ||
53
- judged.containerDead ||
54
- NOT_THIS_FAMILY.has(symbol.kind)) {
52
+ if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
55
53
  return undefined;
56
54
  }
57
- if (candidate.unreferencedTest) {
58
- return unreferencedCode(symbol);
59
- }
60
55
  const referenced = candidate.productionRefs + candidate.testRefs > 0;
61
56
  if (symbol.kind === "enum-member" && !referenced) {
62
57
  return undefined;
63
58
  }
59
+ if (candidate.unreferencedTest) {
60
+ return unreferencedCode(symbol);
61
+ }
64
62
  if (candidate.heldByTests === candidate.id) {
65
63
  return "DS1004";
66
64
  }
@@ -192,7 +190,6 @@ function familyFindings(input, fallen) {
192
190
  candidate,
193
191
  symbol,
194
192
  test: union.test[at] === true,
195
- containerDead: containerDead && !fallen,
196
193
  deprecated: input.deprecated.has(symbol.id),
197
194
  unreachable: unreachable.has(symbol.id),
198
195
  };
@@ -102,9 +102,6 @@ function manifestDirectory(host, root, dir) {
102
102
  if (host.kindOf(joinPath(at, "package.json")) === "file") {
103
103
  return at;
104
104
  }
105
- if (at === root) {
106
- break;
107
- }
108
105
  }
109
106
  return root;
110
107
  }
@@ -1,23 +1,25 @@
1
1
  /**
2
2
  * The injection-container class: a container constructs a class passed to a call
3
3
  * `ts.injection_registrations` names, or one a parameter of whose constructor carries a
4
- * decorator, and populates each of its properties and accessors that carries one, a base
5
- * class's included. A constructor and its parameters are no declarations of the
6
- * inventory, so the class stands for them and the registration referencing it keeps it.
4
+ * decorator, passes every argument of its constructor, and populates each of its
5
+ * properties and accessors that carries a decorator, a base class's included. So each
6
+ * parameter property is injected, decorated or not. A constructor is no declaration of
7
+ * the inventory, so the class stands for it and the registration referencing it keeps it.
7
8
  */
8
9
  import { isClassDeclaration, isClassExpression, isConstructorDeclaration, isGetAccessorDeclaration, isPropertyDeclaration, isSetAccessorDeclaration, } from "@typescript/native/ast";
9
10
  import { aliasChains } from "./alias-chain.js";
10
11
  import { calleeName, callsOf, classesPassed, decoratorsOf, spelledDecorator } from "./calls.js";
11
12
  import { classMembers } from "./class-members.js";
12
13
  import { entriesAt, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
13
- import { nodeKey } from "./inventory.js";
14
+ import { isParameterProperty, nodeKey } from "./inventory.js";
14
15
  import { renderPosition } from "./position.js";
15
- /** Whether one member declaration is a property or an accessor carrying a decorator. */
16
+ /** Whether one member declaration is a parameter property, or a property or accessor carrying a decorator. */
16
17
  function isInjected(node) {
17
- return ((isPropertyDeclaration(node) ||
18
- isGetAccessorDeclaration(node) ||
19
- isSetAccessorDeclaration(node)) &&
20
- decoratorsOf(node).length > 0);
18
+ return (isParameterProperty(node) ||
19
+ ((isPropertyDeclaration(node) ||
20
+ isGetAccessorDeclaration(node) ||
21
+ isSetAccessorDeclaration(node)) &&
22
+ decoratorsOf(node).length > 0));
21
23
  }
22
24
  /** The classes of the target a call whose callee an entry names passes, with that call's entry. */
23
25
  function registrations(input) {
@@ -43,18 +45,20 @@ function registrations(input) {
43
45
  return uses.map((use) => ({ id: use.id, site: use.site, detail }));
44
46
  });
45
47
  }
46
- /** The classes of the target a parameter of whose constructor carries a decorator. */
48
+ /**
49
+ * The classes of the target a parameter of whose constructor carries a decorator, each
50
+ * with the first such decorator.
51
+ */
47
52
  function constructedForParameters(input) {
48
53
  const { project, held } = input;
49
- const found = new Set();
54
+ const found = new Map();
50
55
  for (const file of project.ownSourceFiles()) {
51
56
  const visit = (node) => {
52
57
  if (isClassDeclaration(node) || isClassExpression(node)) {
53
58
  const id = held.declarations.get(nodeKey(file, node));
54
- const asks = node.members.some((member) => isConstructorDeclaration(member) &&
55
- member.parameters.some((parameter) => decoratorsOf(parameter).length > 0));
56
- if (id !== undefined && asks) {
57
- found.add(id);
59
+ const [asks] = node.members.flatMap((member) => isConstructorDeclaration(member) ? member.parameters.flatMap(decoratorsOf) : []);
60
+ if (id !== undefined && asks !== undefined) {
61
+ found.set(id, asks);
58
62
  }
59
63
  }
60
64
  node.forEachChild(visit);
@@ -72,7 +76,7 @@ export function injectionContainer(input) {
72
76
  const { project, held, targetRoot } = input;
73
77
  const registered = registrations(input);
74
78
  const constructed = constructedForParameters(input);
75
- const classes = new Set([...registered.map((one) => one.id), ...constructed]);
79
+ const classes = new Set([...registered.map((one) => one.id), ...constructed.keys()]);
76
80
  if (classes.size === 0) {
77
81
  return [];
78
82
  }
@@ -85,16 +89,15 @@ export function injectionContainer(input) {
85
89
  found.push({ id: member.id, detail: registration.detail, site: at(registration.site) });
86
90
  }
87
91
  }
88
- for (const id of constructed) {
92
+ for (const [id, asks] of constructed) {
89
93
  for (const member of injected(id)) {
90
- const [decorator] = decoratorsOf(member.node);
91
- if (decorator !== undefined) {
92
- found.push({
93
- id: member.id,
94
- detail: `injected by ${spelledDecorator(decorator)}`,
95
- site: at(decorator),
96
- });
97
- }
94
+ // An undecorated parameter property is passed because a sibling parameter asks.
95
+ const [decorator = asks] = decoratorsOf(member.node);
96
+ found.push({
97
+ id: member.id,
98
+ detail: `injected by ${spelledDecorator(decorator)}`,
99
+ site: at(decorator),
100
+ });
98
101
  }
99
102
  }
100
103
  return found;
@@ -4,8 +4,8 @@
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 { isArrayBindingPattern, 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/ast";
8
- import { nodeKey } from "./inventory.js";
7
+ import { isArrayBindingPattern, isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isBindingElement, isCallExpression, isCaseClause, isCatchClause, isClassDeclaration, isClassExpression, isConstructorDeclaration, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
8
+ import { isParameterProperty, nodeKey } from "./inventory.js";
9
9
  import { positionKey, renderPosition } from "./position.js";
10
10
  import { isAnswered, UNANSWERED } from "./query.js";
11
11
  import { signatureOf } from "./signatures.js";
@@ -234,6 +234,39 @@ function hierarchyFixes(declared, extended) {
234
234
  }
235
235
  return owner.name !== undefined && extended.has(owner.name.text);
236
236
  }
237
+ /** Whether a decorator on a constructor's class or on one of its parameters fixes its signature. */
238
+ function decoratorFixes(constructor) {
239
+ const owner = constructor.parent;
240
+ return ((owner.modifiers ?? []).some(isDecorator) ||
241
+ constructor.parameters.some((parameter) => (parameter.modifiers ?? []).some(isDecorator)));
242
+ }
243
+ /** The name an expression spells a class by: an identifier, or the member a property access names. */
244
+ function spelledName(expression) {
245
+ const node = unwrapped(expression);
246
+ if (isIdentifier(node)) {
247
+ return node;
248
+ }
249
+ return isPropertyAccessExpression(node) && isIdentifier(node.name) ? node.name : undefined;
250
+ }
251
+ /**
252
+ * The names one node writes that use a class without using its constructor as a value:
253
+ * the class a `new` expression calls, the class an `extends` or `implements` clause
254
+ * names, and the name a type reference spells.
255
+ */
256
+ function constructionNames(node) {
257
+ if (isNewExpression(node)) {
258
+ return [spelledName(node.expression) ?? []].flat();
259
+ }
260
+ if (isHeritageClause(node) &&
261
+ (isClassDeclaration(node.parent) || isClassExpression(node.parent))) {
262
+ return node.types.flatMap((type) => isTypeReferenceNode(type) ? [] : (spelledName(type.expression) ?? []));
263
+ }
264
+ if (isTypeReferenceNode(node)) {
265
+ const named = isQualifiedName(node.typeName) ? node.typeName.right : node.typeName;
266
+ return isIdentifier(named) ? [named] : [];
267
+ }
268
+ return [];
269
+ }
237
270
  /**
238
271
  * The symbol identifier each of a batch of identifiers resolves to, where the batch
239
272
  * resolves it, and {@link UNANSWERED} where the checker did not answer.
@@ -270,18 +303,16 @@ function readsBinding(use, declared, ids) {
270
303
  export function projectParts(project, held, references, targetRoot) {
271
304
  const files = project.ownSourceFiles();
272
305
  const extended = extendedNames(files);
273
- const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
274
306
  const parts = [];
275
307
  const signatures = [];
276
308
  const calls = new Map();
277
309
  const specifiers = new Set();
310
+ const constructions = new Set();
278
311
  const sites = [];
279
312
  const contextual = [];
280
313
  for (const file of files) {
281
- const declarationAt = (node) => {
282
- const id = held.declarations.get(nodeKey(file, node));
283
- return id === undefined || kinds.get(id) === "file" ? undefined : id;
284
- };
314
+ // A parameter property's own parts stay with the parameter when its member goes.
315
+ const declarationAt = (node) => isParameterProperty(node) ? undefined : held.declarations.get(nodeKey(file, node));
285
316
  // A part no declaration holds belongs to the file, which is judged while the file is live.
286
317
  const fileId = held.declarations.get(nodeKey(file, file));
287
318
  const holderOf = (node) => {
@@ -332,6 +363,17 @@ export function projectParts(project, held, references, targetRoot) {
332
363
  walk.declared.push({ id, node, signature });
333
364
  covered.add(signature);
334
365
  }
366
+ // A constructor is no declaration, so the class it belongs to holds its parts.
367
+ const constructs = isConstructorDeclaration(node) && node.body !== undefined
368
+ ? declarationAt(node.parent)
369
+ : undefined;
370
+ if (constructs !== undefined && isConstructorDeclaration(node)) {
371
+ walk.declared.push({ id: constructs, node, signature: node });
372
+ covered.add(node);
373
+ }
374
+ for (const name of constructionNames(node)) {
375
+ constructions.add(position(name));
376
+ }
335
377
  const value = covered.has(node) ? undefined : valueSignature(node);
336
378
  const valueHolder = id ?? holderOf(node);
337
379
  if (value !== undefined && valueHolder !== undefined && !isStub(value)) {
@@ -413,11 +455,15 @@ export function projectParts(project, held, references, targetRoot) {
413
455
  }
414
456
  const ids = resolvedIds(project, [...new Set(asked)]);
415
457
  for (const one of walk.declared) {
416
- sites.push({ file, declared: one });
458
+ const constructor = isConstructorDeclaration(one.node);
459
+ if (!constructor) {
460
+ sites.push({ file, declared: one });
461
+ }
417
462
  signatures.push({
418
463
  declaration: one.id,
419
464
  stub: isStub(one.signature),
420
- fixed: hierarchyFixes(one.node, extended),
465
+ fixed: constructor ? decoratorFixes(one.node) : hierarchyFixes(one.node, extended),
466
+ constructor,
421
467
  });
422
468
  const reader = readers.get(one.signature);
423
469
  if (reader !== undefined) {
@@ -475,7 +521,13 @@ export function projectParts(project, held, references, targetRoot) {
475
521
  }
476
522
  }
477
523
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
478
- return { parts: withContextualRead(project, parts, contextual), signatures, calls, specifiers };
524
+ return {
525
+ parts: withContextualRead(project, parts, contextual),
526
+ signatures,
527
+ calls,
528
+ specifiers,
529
+ constructions,
530
+ };
479
531
  }
480
532
  /**
481
533
  * A function written in place that no declaration declares: an arrow, a function
@@ -496,16 +548,12 @@ function valueSignature(node) {
496
548
  }
497
549
  /**
498
550
  * Every name one function's parameters bind, a binding pattern's names one by one. A
499
- * `this` parameter receives no argument and a decorated one is read by its decorator, so
500
- * neither binds a subject.
551
+ * `this` parameter receives no argument, a decorated one is read by its decorator, and a
552
+ * parameter property is read by the write to its member, so none binds a subject.
501
553
  */
502
554
  function parameterNames(signature) {
503
555
  const found = [];
504
556
  const patternNames = (pattern, at) => {
505
- if (isIdentifier(pattern)) {
506
- found.push({ name: pattern, at, pattern: true, exempt: false });
507
- return;
508
- }
509
557
  const rest = isObjectBindingPattern(pattern) &&
510
558
  pattern.elements.some((element) => element.dotDotDotToken !== undefined);
511
559
  for (const element of pattern.elements) {
@@ -523,7 +571,8 @@ function parameterNames(signature) {
523
571
  };
524
572
  signature.parameters.forEach((parameter, at) => {
525
573
  const name = parameter.name;
526
- if ((parameter.modifiers ?? []).some(isDecorator)) {
574
+ if ((parameter.modifiers ?? []).some(isDecorator) ||
575
+ isParameterProperty(parameter)) {
527
576
  return;
528
577
  }
529
578
  if (isIdentifier(name)) {
@@ -682,9 +731,7 @@ function deadStores(file, root, block, holder, locals, ids) {
682
731
  for (const write of writes) {
683
732
  // The rest of the write's own statement runs after it: a later declarator may read the local.
684
733
  const own = statements[write.at];
685
- const sameStatement = own !== undefined && write.target === local.name
686
- ? usesIn(own).filter((use) => use !== local.name)
687
- : [];
734
+ const sameStatement = own !== undefined && write.target === local.name ? usesIn(own) : [];
688
735
  if (sameStatement.length > 0) {
689
736
  continue;
690
737
  }
@@ -787,7 +834,7 @@ export function intraFunctionFacts(projects, references, exempted) {
787
834
  for (const [at, value] of one.calls) {
788
835
  calls.set(at, value);
789
836
  }
790
- for (const at of one.specifiers) {
837
+ for (const at of [...one.specifiers, ...one.constructions]) {
791
838
  specifiers.add(at);
792
839
  }
793
840
  }
@@ -815,9 +862,11 @@ export function intraFunctionFacts(projects, references, exempted) {
815
862
  const free = new Set(signatures
816
863
  .map((one) => one.declaration)
817
864
  .filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)));
865
+ // A class used as a value fixes its constructor's whole signature, whatever reads follow.
866
+ const constructors = new Set(signatures.filter((one) => one.constructor).map((one) => one.declaration));
818
867
  const freeButValued = new Set(signatures
819
868
  .map((one) => one.declaration)
820
- .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)));
869
+ .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id) && !constructors.has(id)));
821
870
  // Awaiting a thenable result sequences on it, so a call that awaits one uses it.
822
871
  const sequenced = new Set([...parts.values()]
823
872
  .filter((part) => part.thenable === true && awaited.has(part.declaration))
@@ -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, isStringLiteral, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/ast";
1
+ import { isClassDeclaration, isComputedPropertyName, isConstructorDeclaration, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isParameterDeclaration, isParenthesizedTypeNode, isPrivateIdentifier, isPropertyDeclaration, isPropertySignatureDeclaration, isSetAccessorDeclaration, isStringLiteral, isTypeAliasDeclaration, isTypeLiteralNode, isUnionTypeNode, isVariableStatement, ModifierFlags, } from "@typescript/native/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";
@@ -232,6 +232,20 @@ function unnamedStart(node) {
232
232
  const prefix = DEFAULT_EXPORT.exec(node.getText());
233
233
  return prefix === null ? start : start + prefix[0].length;
234
234
  }
235
+ /**
236
+ * Whether one node is a parameter property: a constructor parameter carrying an
237
+ * accessibility, `readonly` or `override` modifier, which also declares an instance
238
+ * member of the class, with the parameter's node as the member's declaration.
239
+ */
240
+ export function isParameterProperty(node) {
241
+ return (isParameterDeclaration(node) &&
242
+ isConstructorDeclaration(node.parent) &&
243
+ (modifiersOf(node) & ModifierFlags.ParameterPropertyModifier) !== 0);
244
+ }
245
+ /** The parameter properties every constructor of one class declares. */
246
+ function parameterPropertiesOf(node) {
247
+ return node.members.flatMap((member) => isConstructorDeclaration(member) ? member.parameters.filter(isParameterProperty) : []);
248
+ }
235
249
  /** The kind a class element takes: a method, or a property or accessor. */
236
250
  function classMemberKind(node) {
237
251
  return isMethodDeclaration(node) ? "method" : "class-member";
@@ -367,7 +381,7 @@ export function inventory(project, host, targetRoot) {
367
381
  owner: record,
368
382
  declaration: node,
369
383
  nameNode: node.name,
370
- members: node.members,
384
+ members: [...node.members, ...parameterPropertiesOf(node)],
371
385
  memberKind: "class-member",
372
386
  isClass: true,
373
387
  membersAreExports: false,
@@ -677,7 +691,8 @@ export function inventory(project, host, targetRoot) {
677
691
  if (isPropertyDeclaration(node) ||
678
692
  isMethodDeclaration(node) ||
679
693
  isGetAccessorDeclaration(node) ||
680
- isSetAccessorDeclaration(node)) {
694
+ isSetAccessorDeclaration(node) ||
695
+ isParameterProperty(node)) {
681
696
  return classMemberKind(node);
682
697
  }
683
698
  return undefined;
@@ -718,9 +733,7 @@ export function inventory(project, host, targetRoot) {
718
733
  });
719
734
  }
720
735
  const byId = new Map(building.map((record) => [positionKey(record.position), record]));
721
- const symbols = building
722
- .map(render)
723
- .sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
736
+ const symbols = building.map(render).sort((a, b) => byPosition(a.position, b.position));
724
737
  return {
725
738
  symbols,
726
739
  declarations: new Map([...byNode].map(([key, record]) => [key, positionKey(record.position)])),
package/dist/src/kinds.js CHANGED
@@ -153,7 +153,7 @@ const ROWS = [
153
153
  code: "DS1301",
154
154
  name: "write-only-symbol",
155
155
  languages: ["go", "ts"],
156
- 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.",
156
+ 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.",
157
157
  defaultSeverity: "deny",
158
158
  maxClass: "certain",
159
159
  fixability: "deletable",
@@ -289,7 +289,7 @@ const ROWS = [
289
289
  name: "unused-parameter",
290
290
  languages: ["go", "ts"],
291
291
  rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
292
- precondition: "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.",
292
+ precondition: "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.",
293
293
  defaultSeverity: "warn",
294
294
  maxClass: "certain",
295
295
  fixability: "manual",
@@ -7,13 +7,6 @@ import { componentsOf } from "./components.js";
7
7
  import { graphOf, OUTSIDE } from "./graph.js";
8
8
  import { byPosition } from "./position.js";
9
9
  import { reachedWithTheTests, sweep, } from "./sweep.js";
10
- /** Two strings ordered bytewise. */
11
- function compare(a, b) {
12
- if (a === b) {
13
- return 0;
14
- }
15
- return a < b ? -1 : 1;
16
- }
17
10
  /**
18
11
  * What makes one reference one reference, whichever configuration made it. A consumer's
19
12
  * position is rendered against its own root, so the consumer is part of what it names.
@@ -76,7 +69,7 @@ export function matrixOf(per) {
76
69
  references.push(reference);
77
70
  }
78
71
  });
79
- symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
72
+ symbols.sort((a, b) => byPosition(a.position, b.position));
80
73
  return {
81
74
  configurations: per.map((one) => one.configuration),
82
75
  graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles)),