@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
@@ -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());
@@ -54,13 +54,13 @@ function codeOf(judged) {
54
54
  NOT_THIS_FAMILY.has(symbol.kind)) {
55
55
  return undefined;
56
56
  }
57
- if (candidate.unreferencedTest) {
58
- return unreferencedCode(symbol);
59
- }
60
57
  const referenced = candidate.productionRefs + candidate.testRefs > 0;
61
58
  if (symbol.kind === "enum-member" && !referenced) {
62
59
  return undefined;
63
60
  }
61
+ if (candidate.unreferencedTest) {
62
+ return unreferencedCode(symbol);
63
+ }
64
64
  if (candidate.heldByTests === candidate.id) {
65
65
  return "DS1004";
66
66
  }
@@ -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.
@@ -275,12 +308,16 @@ export function projectParts(project, held, references, targetRoot) {
275
308
  const signatures = [];
276
309
  const calls = new Map();
277
310
  const specifiers = new Set();
311
+ const constructions = new Set();
278
312
  const sites = [];
279
313
  const contextual = [];
280
314
  for (const file of files) {
315
+ // A parameter property's own parts stay with the parameter when its member goes.
281
316
  const declarationAt = (node) => {
282
317
  const id = held.declarations.get(nodeKey(file, node));
283
- return id === undefined || kinds.get(id) === "file" ? undefined : id;
318
+ return id === undefined || kinds.get(id) === "file" || isParameterProperty(node)
319
+ ? undefined
320
+ : id;
284
321
  };
285
322
  // A part no declaration holds belongs to the file, which is judged while the file is live.
286
323
  const fileId = held.declarations.get(nodeKey(file, file));
@@ -332,6 +369,17 @@ export function projectParts(project, held, references, targetRoot) {
332
369
  walk.declared.push({ id, node, signature });
333
370
  covered.add(signature);
334
371
  }
372
+ // A constructor is no declaration, so the class it belongs to holds its parts.
373
+ const constructs = isConstructorDeclaration(node) && node.body !== undefined
374
+ ? declarationAt(node.parent)
375
+ : undefined;
376
+ if (constructs !== undefined && isConstructorDeclaration(node)) {
377
+ walk.declared.push({ id: constructs, node, signature: node });
378
+ covered.add(node);
379
+ }
380
+ for (const name of constructionNames(node)) {
381
+ constructions.add(position(name));
382
+ }
335
383
  const value = covered.has(node) ? undefined : valueSignature(node);
336
384
  const valueHolder = id ?? holderOf(node);
337
385
  if (value !== undefined && valueHolder !== undefined && !isStub(value)) {
@@ -413,11 +461,15 @@ export function projectParts(project, held, references, targetRoot) {
413
461
  }
414
462
  const ids = resolvedIds(project, [...new Set(asked)]);
415
463
  for (const one of walk.declared) {
416
- sites.push({ file, declared: one });
464
+ const constructor = isConstructorDeclaration(one.node);
465
+ if (!constructor) {
466
+ sites.push({ file, declared: one });
467
+ }
417
468
  signatures.push({
418
469
  declaration: one.id,
419
470
  stub: isStub(one.signature),
420
- fixed: hierarchyFixes(one.node, extended),
471
+ fixed: constructor ? decoratorFixes(one.node) : hierarchyFixes(one.node, extended),
472
+ constructor,
421
473
  });
422
474
  const reader = readers.get(one.signature);
423
475
  if (reader !== undefined) {
@@ -475,7 +527,13 @@ export function projectParts(project, held, references, targetRoot) {
475
527
  }
476
528
  }
477
529
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
478
- return { parts: withContextualRead(project, parts, contextual), signatures, calls, specifiers };
530
+ return {
531
+ parts: withContextualRead(project, parts, contextual),
532
+ signatures,
533
+ calls,
534
+ specifiers,
535
+ constructions,
536
+ };
479
537
  }
480
538
  /**
481
539
  * A function written in place that no declaration declares: an arrow, a function
@@ -496,8 +554,8 @@ function valueSignature(node) {
496
554
  }
497
555
  /**
498
556
  * 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.
557
+ * `this` parameter receives no argument, a decorated one is read by its decorator, and a
558
+ * parameter property is read by the write to its member, so none binds a subject.
501
559
  */
502
560
  function parameterNames(signature) {
503
561
  const found = [];
@@ -523,7 +581,8 @@ function parameterNames(signature) {
523
581
  };
524
582
  signature.parameters.forEach((parameter, at) => {
525
583
  const name = parameter.name;
526
- if ((parameter.modifiers ?? []).some(isDecorator)) {
584
+ if ((parameter.modifiers ?? []).some(isDecorator) ||
585
+ isParameterProperty(parameter)) {
527
586
  return;
528
587
  }
529
588
  if (isIdentifier(name)) {
@@ -787,7 +846,7 @@ export function intraFunctionFacts(projects, references, exempted) {
787
846
  for (const [at, value] of one.calls) {
788
847
  calls.set(at, value);
789
848
  }
790
- for (const at of one.specifiers) {
849
+ for (const at of [...one.specifiers, ...one.constructions]) {
791
850
  specifiers.add(at);
792
851
  }
793
852
  }
@@ -815,9 +874,11 @@ export function intraFunctionFacts(projects, references, exempted) {
815
874
  const free = new Set(signatures
816
875
  .map((one) => one.declaration)
817
876
  .filter((id) => !fixed.has(id) && !exempted.has(id) && !valued.has(id)));
877
+ // A class used as a value fixes its constructor's whole signature, whatever reads follow.
878
+ const constructors = new Set(signatures.filter((one) => one.constructor).map((one) => one.declaration));
818
879
  const freeButValued = new Set(signatures
819
880
  .map((one) => one.declaration)
820
- .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)));
881
+ .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id) && !constructors.has(id)));
821
882
  // Awaiting a thenable result sequences on it, so a call that awaits one uses it.
822
883
  const sequenced = new Set([...parts.values()]
823
884
  .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;
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",
package/dist/src/ref.js CHANGED
@@ -25,9 +25,10 @@ const TS_COMPUTED = String.raw `\[[^\]\r\n\t ]+\]`;
25
25
  const TS_HEAD = `(?:${TS_IDENT}|${TS_QUOTED})`;
26
26
  const TS_MEMBER = `(?:${TS_IDENT}|#${TS_IDENT}|${TS_QUOTED}|${TS_COMPUTED})`;
27
27
  const TS_PACKAGE = String.raw `(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+`;
28
- const TS_FILE = String.raw `(?:[^/#\r\n]+/)*[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
28
+ const TS_DIRS = String.raw `(?:[^/#\r\n]+/)*`;
29
+ const TS_FILE = String.raw `${TS_DIRS}[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
29
30
  const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
30
- const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
31
+ const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_DIRS}package\\.json`;
31
32
  /**
32
33
  * One expression per form the grammar defines, each anchored and each the whole of
33
34
  * a reference. Several forms share one expression because they differ in what the
@@ -5,7 +5,7 @@ import { destructuring, propertyReadsOf } from "./destructuring.js";
5
5
  import { specifierCandidates } from "./configuration-files.js";
6
6
  import { namespaceUses } from "./namespace-objects.js";
7
7
  import { globExpression } from "./glob.js";
8
- import { declarationsByName, nodeKey } from "./inventory.js";
8
+ import { declarationsByName, isParameterProperty, nodeKey, } from "./inventory.js";
9
9
  import { augmentationReferences } from "./augmentations.js";
10
10
  import { elementAccessReferences } from "./element-access.js";
11
11
  import { indexedAccessReferences } from "./indexed-access.js";
@@ -336,6 +336,7 @@ function sitesOf(file, declarations, fileId, unread) {
336
336
  const wholes = [];
337
337
  const urls = [];
338
338
  const decorated = [];
339
+ const parameterWrites = [];
339
340
  const namespaces = namespaceUses(file);
340
341
  const literals = [];
341
342
  const flows = [];
@@ -419,7 +420,16 @@ function sitesOf(file, declarations, fileId, unread) {
419
420
  }
420
421
  const held = declarations.get(nodeKey(file, node));
421
422
  const outer = enclosing;
422
- if (held !== undefined) {
423
+ // A parameter property's annotation, initializer and decorators stay with the
424
+ // parameter when the member goes, so they are written inside the constructor's class.
425
+ if (held !== undefined && isParameterProperty(node)) {
426
+ parameterWrites.push({
427
+ name: node.name,
428
+ from: outer,
429
+ member: held,
430
+ });
431
+ }
432
+ else if (held !== undefined) {
423
433
  enclosing = held;
424
434
  for (const modifier of node.modifiers ?? []) {
425
435
  if (isDecorator(modifier)) {
@@ -441,6 +451,7 @@ function sitesOf(file, declarations, fileId, unread) {
441
451
  wholes,
442
452
  urls,
443
453
  decorated,
454
+ parameterWrites,
444
455
  keyed,
445
456
  };
446
457
  }
@@ -523,15 +534,12 @@ function importedNames(file) {
523
534
  importedCache.set(file, names);
524
535
  return names;
525
536
  }
526
- /**
527
- * The name node of every import binding one file's top level declares, and whether it
528
- * binds a whole module.
529
- */
537
+ /** The name node of every import binding one file's top level declares. */
530
538
  function importBindings(file) {
531
539
  const found = [];
532
540
  for (const statement of file.statements) {
533
541
  if (isImportEqualsDeclaration(statement)) {
534
- found.push({ node: statement.name, whole: true });
542
+ found.push({ node: statement.name });
535
543
  continue;
536
544
  }
537
545
  const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
@@ -539,17 +547,17 @@ function importBindings(file) {
539
547
  continue;
540
548
  }
541
549
  if (clause.name !== undefined) {
542
- found.push({ node: clause.name, whole: false });
550
+ found.push({ node: clause.name });
543
551
  }
544
552
  const bindings = clause.namedBindings;
545
553
  if (bindings === undefined) {
546
554
  continue;
547
555
  }
548
556
  if (bindings.kind === SyntaxKind.NamespaceImport) {
549
- found.push({ node: bindings.name, whole: true });
557
+ found.push({ node: bindings.name });
550
558
  }
551
559
  else {
552
- found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
560
+ found.push(...bindings.elements.map((element) => ({ node: element.name })));
553
561
  }
554
562
  }
555
563
  return found;
@@ -856,7 +864,7 @@ export function references(project, held, targetRoot, options) {
856
864
  }
857
865
  }
858
866
  const keyNames = computedKeyNames(held.symbols);
859
- for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated, keyed, } of walked) {
867
+ for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated, parameterWrites, keyed, } of walked) {
860
868
  const test = testFile.get(file) === true;
861
869
  // A component file's markup may use any binding its blocks declare at the top level.
862
870
  const markup = isComponentFile(file);
@@ -874,6 +882,17 @@ export function references(project, held, targetRoot, options) {
874
882
  test,
875
883
  });
876
884
  }
885
+ // The constructor stores its argument in the member a parameter property declares.
886
+ for (const { name, from, member } of parameterWrites) {
887
+ found.push({
888
+ from,
889
+ to: member,
890
+ position: renderPosition(file, root, name.getStart()),
891
+ use: "write",
892
+ resolution: "syntax",
893
+ test,
894
+ });
895
+ }
877
896
  // One batch per capped run of the file's name nodes, the re-exports' nodes after
878
897
  // the uses and the evaluated modules' specifiers last, each node once: a bare star
879
898
  // re-export's specifier is a link and an evaluation both. A shorthand is left out
@@ -1011,15 +1030,14 @@ export function references(project, held, targetRoot, options) {
1011
1030
  const chained = symbol === UNANSWERED
1012
1031
  ? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
1013
1032
  : chains.chainOf(symbol);
1014
- // The markup may name any export of a module a binding holds whole.
1015
- const targets = binding.whole
1016
- ? chained.flatMap((target) => [
1017
- target,
1018
- ...declaredIn(target.id)
1019
- .filter((one) => one.exported)
1020
- .map((one) => ({ ...target, id: one.id })),
1021
- ])
1022
- : chained;
1033
+ // The markup may name any export of a module a binding's chain reaches, whether a
1034
+ // namespace import binds it or a named or default import of a namespace re-export.
1035
+ const targets = chained.flatMap((target) => [
1036
+ target,
1037
+ ...declaredIn(target.id)
1038
+ .filter((one) => one.exported)
1039
+ .map((one) => ({ ...target, id: one.id })),
1040
+ ]);
1023
1041
  for (const target of targets) {
1024
1042
  found.push({
1025
1043
  from: fileId,
@@ -4,7 +4,7 @@
4
4
  * renders this document, so no two renderings disagree about which findings exist.
5
5
  */
6
6
  /** The version of the report schema this analyzer writes a report to. */
7
- const SCHEMA_VERSION = "8.1.0";
7
+ const SCHEMA_VERSION = "8.2.0";
8
8
  /** Every report schema version this analyzer reads: the one the Contract it implements admits. */
9
9
  export const SCHEMA_VERSIONS_ACCEPTED = [SCHEMA_VERSION];
10
10
  /** The name this analyzer writes every document under and mints every identifier with. */