@cplieger/deadset-ts 7.2.0-dev.4 → 8.0.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 (122) hide show
  1. package/dist/src/analysis.js +108 -10
  2. package/dist/src/augmentations.js +62 -18
  3. package/dist/src/automatic-types.js +189 -0
  4. package/dist/src/base-url.js +18 -0
  5. package/dist/src/components.js +7 -6
  6. package/dist/src/config.js +1 -0
  7. package/dist/src/configuration-files.js +50 -12
  8. package/dist/src/conformance-record.js +2 -2
  9. package/dist/src/convention-rows.js +98 -0
  10. package/dist/src/conventions.js +101 -5
  11. package/dist/src/decorator.js +21 -18
  12. package/dist/src/dependencies.js +183 -28
  13. package/dist/src/discover.js +132 -11
  14. package/dist/src/entry-point-gaps.js +7 -10
  15. package/dist/src/entry-points.js +112 -11
  16. package/dist/src/findings/completion.js +172 -40
  17. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  18. package/dist/src/findings/emitters.js +1 -1
  19. package/dist/src/findings/intra-function.js +44 -13
  20. package/dist/src/findings/non-code-artifacts.js +66 -33
  21. package/dist/src/findings/unused-declarations.js +30 -5
  22. package/dist/src/findings/visibility-narrowing.js +29 -5
  23. package/dist/src/generated-file.js +11 -0
  24. package/dist/src/git-ignore.js +86 -0
  25. package/dist/src/graph.js +4 -1
  26. package/dist/src/import-bindings.js +97 -0
  27. package/dist/src/indexed-access.js +130 -28
  28. package/dist/src/interface-satisfaction.js +30 -12
  29. package/dist/src/intra-function-parts.js +297 -45
  30. package/dist/src/inventory.js +52 -8
  31. package/dist/src/kinds.js +13 -13
  32. package/dist/src/matrix.js +57 -10
  33. package/dist/src/mdx-entries.js +197 -0
  34. package/dist/src/module-declarations.js +9 -13
  35. package/dist/src/overrides.js +41 -10
  36. package/dist/src/print.js +1 -0
  37. package/dist/src/ref.js +10 -0
  38. package/dist/src/references.js +38 -4
  39. package/dist/src/report.js +4 -2
  40. package/dist/src/resolve.js +2 -0
  41. package/dist/src/roots.js +110 -38
  42. package/dist/src/schema.js +1 -0
  43. package/dist/src/serialization-contract.js +54 -8
  44. package/dist/src/session.js +7 -0
  45. package/dist/src/source-files.js +39 -14
  46. package/dist/src/suppress-file.js +10 -2
  47. package/dist/src/suppress-inline.js +18 -2
  48. package/dist/src/suppress.js +8 -5
  49. package/dist/src/sweep.js +27 -10
  50. package/dist/src/symbol-keys.js +103 -2
  51. package/dist/src/test-support.js +119 -18
  52. package/dist/src/type-errors.js +34 -7
  53. package/dist/src/type-parameter-reads.js +172 -0
  54. package/dist/src/verbs/analyze.js +16 -0
  55. package/dist/src/verbs/explain.js +4 -2
  56. package/dist/src/version.js +1 -1
  57. package/dist/src/workflow-steps.js +119 -22
  58. package/dist/src/workspace-programs.js +7 -3
  59. package/dist/src/workspace-resolution.js +53 -10
  60. package/jsr.json +1 -1
  61. package/package.json +2 -2
  62. package/src/analysis.ts +146 -11
  63. package/src/augmentations.ts +76 -22
  64. package/src/automatic-types.ts +246 -0
  65. package/src/base-url.ts +24 -0
  66. package/src/components.ts +7 -6
  67. package/src/config.ts +3 -0
  68. package/src/configuration-files.ts +66 -12
  69. package/src/conformance-record.ts +2 -2
  70. package/src/convention-rows.ts +128 -0
  71. package/src/conventions.ts +121 -7
  72. package/src/decorator.ts +28 -18
  73. package/src/dependencies.ts +247 -41
  74. package/src/discover.ts +156 -13
  75. package/src/entry-point-gaps.ts +8 -11
  76. package/src/entry-points.ts +133 -11
  77. package/src/finding.ts +15 -0
  78. package/src/findings/completion.ts +222 -46
  79. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  80. package/src/findings/emitters.ts +1 -1
  81. package/src/findings/intra-function.ts +56 -14
  82. package/src/findings/non-code-artifacts.ts +81 -41
  83. package/src/findings/unused-declarations.ts +36 -5
  84. package/src/findings/visibility-narrowing.ts +40 -5
  85. package/src/generated-file.ts +16 -0
  86. package/src/git-ignore.ts +98 -0
  87. package/src/graph.ts +6 -0
  88. package/src/host.ts +5 -9
  89. package/src/import-bindings.ts +128 -0
  90. package/src/indexed-access.ts +152 -30
  91. package/src/interface-satisfaction.ts +33 -13
  92. package/src/intra-function-parts.ts +394 -59
  93. package/src/inventory.ts +71 -7
  94. package/src/kinds.ts +13 -13
  95. package/src/matrix.ts +104 -15
  96. package/src/mdx-entries.ts +241 -0
  97. package/src/module-declarations.ts +11 -13
  98. package/src/overrides.ts +51 -12
  99. package/src/print.ts +1 -0
  100. package/src/ref.ts +12 -0
  101. package/src/references.ts +48 -3
  102. package/src/report.ts +16 -3
  103. package/src/resolve.ts +3 -0
  104. package/src/roots.ts +163 -45
  105. package/src/schema.ts +1 -0
  106. package/src/serialization-contract.ts +55 -4
  107. package/src/session.ts +7 -0
  108. package/src/source-files.ts +66 -16
  109. package/src/suppress-file.ts +12 -1
  110. package/src/suppress-inline.ts +18 -1
  111. package/src/suppress.ts +10 -5
  112. package/src/sweep.ts +32 -11
  113. package/src/symbol-keys.ts +129 -2
  114. package/src/test-support.ts +155 -18
  115. package/src/type-errors.ts +38 -6
  116. package/src/type-parameter-reads.ts +227 -0
  117. package/src/verbs/analyze.ts +22 -0
  118. package/src/verbs/explain.ts +5 -2
  119. package/src/version.ts +1 -1
  120. package/src/workflow-steps.ts +127 -22
  121. package/src/workspace-programs.ts +7 -3
  122. package/src/workspace-resolution.ts +62 -10
@@ -4,9 +4,12 @@
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, isConstructorDeclaration, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isFunctionLikeDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isSourceFile, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
7
+ import { isArrowFunction, isAwaitExpression, isBigIntLiteral, isBinaryExpression, isBlock, isBreakStatement, isBindingElement, isCallExpression, isCaseClause, isCatchClause, isClassDeclaration, isClassExpression, isConstructorDeclaration, isContinueStatement, isDecorator, isDefaultClause, isEmptyStatement, isExportAssignment, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isForInStatement, isForOfStatement, isFunctionLikeDeclaration, isHeritageClause, isIdentifier, isIfStatement, isImportClause, isImportSpecifier, isInterfaceDeclaration, isMethodDeclaration, isModuleDeclaration, isNamespaceImport, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPrefixUnaryExpression, isPropertyAssignment, isPropertyAccessExpression, isQualifiedName, isReturnStatement, isShorthandPropertyAssignment, isSourceFile, isStringLiteral, isSwitchStatement, isThrowStatement, isTypeAliasDeclaration, isTypeReferenceNode, isVariableDeclarationList, isVariableStatement, isVoidExpression, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
8
+ import { SignatureKind } from "@typescript/native/sync";
9
+ import { exportsLocally, implicitReads, importBindings, } from "./import-bindings.js";
8
10
  import { isParameterProperty, nodeKey } from "./inventory.js";
9
11
  import { positionKey, renderPosition } from "./position.js";
12
+ import { importRef } from "./ref.js";
10
13
  import { isAnswered, UNANSWERED } from "./query.js";
11
14
  import { signatureOf } from "./signatures.js";
12
15
  import { spellsNoBinding } from "./spelled-names.js";
@@ -304,6 +307,33 @@ function readsBinding(use, declared, ids) {
304
307
  const own = ids.get(declared);
305
308
  return at === undefined || at === UNANSWERED || own === UNANSWERED || at === own;
306
309
  }
310
+ /** Whether a name is spelled with a leading underscore, which declares it unread. */
311
+ function declaredUnread(name) {
312
+ return name.text.startsWith("_");
313
+ }
314
+ /** A file's leading comments, where the compiler reads its `@ts-check` and `@ts-nocheck` pragmas. */
315
+ const LEADING_COMMENTS = /^(?:#![^\n]*)?(?:\s|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/u;
316
+ /** A comment of the leading comments, a line comment's text after its slashes captured. */
317
+ const LEADING_COMMENT = /\/\*[\s\S]*?\*\/|\/\/\/?([^\n]*)/gu;
318
+ /**
319
+ * Whether a file's leading comments hold one pragma: as the compiler reads it, a line
320
+ * comment that starts with the pragma's name, in any case, followed by nothing or a space.
321
+ */
322
+ function hasPragma(leading, pragma) {
323
+ return [...leading.matchAll(LEADING_COMMENT)].some(([, text]) => text !== undefined && pragma.test(text));
324
+ }
325
+ /**
326
+ * Whether the compiler checks a file, so its rules report what they find there: a
327
+ * JavaScript file only under `checkJs` or a `@ts-check` pragma, and no file with a
328
+ * `@ts-nocheck` pragma.
329
+ */
330
+ function checkedByTheCompiler(file, checkJs) {
331
+ const leading = LEADING_COMMENTS.exec(file.text)?.[0] ?? "";
332
+ if (hasPragma(leading, /^\s*@ts-nocheck(?:\s|$)/iu)) {
333
+ return false;
334
+ }
335
+ return (!/\.[cm]?jsx?$/u.test(file.fileName) || checkJs || hasPragma(leading, /^\s*@ts-check(?:\s|$)/iu));
336
+ }
307
337
  /**
308
338
  * The parts of one project's own declarations. `references` are the project's own
309
339
  * references, which say which declarations a discarded call reaches, so the checker is
@@ -319,7 +349,12 @@ export function projectParts(project, held, references, targetRoot) {
319
349
  const constructions = new Set();
320
350
  const sites = [];
321
351
  const contextual = [];
352
+ const imports = [];
353
+ const refs = new Map(held.symbols.map((symbol) => [symbol.id, symbol.ref]));
354
+ const options = project.program.getCompilerOptions();
355
+ const jsx = options;
322
356
  for (const file of files) {
357
+ const checked = checkedByTheCompiler(file, options.checkJs === true);
323
358
  // A parameter property's own parts stay with the parameter when its member goes.
324
359
  const declarationAt = (node) => isParameterProperty(node) ? undefined : held.declarations.get(nodeKey(file, node));
325
360
  // A part no declaration holds belongs to the file, which is judged while the file is live.
@@ -338,6 +373,7 @@ export function projectParts(project, held, references, targetRoot) {
338
373
  values: [],
339
374
  catches: [],
340
375
  blocks: [],
376
+ loops: [],
341
377
  unreachable: [],
342
378
  cases: [],
343
379
  };
@@ -433,6 +469,15 @@ export function projectParts(project, held, references, targetRoot) {
433
469
  if (isBlock(node) && silenced === 0 && holderOf(node) !== undefined) {
434
470
  walk.blocks.push(node);
435
471
  }
472
+ const loopHolder = (isForOfStatement(node) || isForInStatement(node)) && silenced === 0
473
+ ? holderOf(node)
474
+ : undefined;
475
+ if (loopHolder !== undefined &&
476
+ (isForOfStatement(node) || isForInStatement(node)) &&
477
+ isVariableDeclarationList(node.initializer) &&
478
+ (node.initializer.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0) {
479
+ walk.loops.push({ loop: node, holder: loopHolder });
480
+ }
436
481
  if (isBlock(node) || isCaseClause(node) || isDefaultClause(node)) {
437
482
  statementsOf(node.statements);
438
483
  return;
@@ -462,7 +507,37 @@ export function projectParts(project, held, references, targetRoot) {
462
507
  for (const store of stores) {
463
508
  asked.push(...store.flatMap((local) => [local.name, ...local.uses]));
464
509
  }
510
+ const loopVariables = walk.loops.map(({ loop, holder }) => {
511
+ const list = loop.initializer;
512
+ // A loop must bind a name, so one spelled declared unread is no subject.
513
+ const names = list.declarations
514
+ .flatMap((declaration) => boundNames(declaration.name))
515
+ .filter((name) => !declaredUnread(name));
516
+ const uses = identifiersNaming(loop.statement, new Set(names.map((one) => one.text)));
517
+ asked.push(...names, ...uses);
518
+ return { names, uses, holder };
519
+ });
520
+ const bindings = fileId === undefined ? [] : importBindings(file);
521
+ const bound = new Set(bindings);
522
+ const bindingUses = bindings.length === 0
523
+ ? []
524
+ : identifiersNaming(file, new Set(bindings.map((one) => one.text))).filter((use) => !bound.has(use));
525
+ asked.push(...bindings, ...bindingUses);
465
526
  const ids = resolvedIds(project, [...new Set(asked)]);
527
+ const implicit = bindings.length === 0 ? new Set() : implicitReads(file, jsx);
528
+ for (const binding of bindings) {
529
+ const fileRef = fileId === undefined ? undefined : refs.get(fileId);
530
+ if (fileId === undefined || fileRef === undefined) {
531
+ continue;
532
+ }
533
+ imports.push({
534
+ ref: importRef(fileRef, binding.text),
535
+ name: binding.text,
536
+ position: spanOf(file, targetRoot, binding),
537
+ used: implicit.has(binding.text) ||
538
+ bindingUses.some((use) => use.text === binding.text && (exportsLocally(use) || readsBinding(use, binding, ids))),
539
+ });
540
+ }
466
541
  for (const one of walk.declared) {
467
542
  const constructor = isConstructorDeclaration(one.node);
468
543
  if (!constructor) {
@@ -476,7 +551,10 @@ export function projectParts(project, held, references, targetRoot) {
476
551
  });
477
552
  const reader = readers.get(one.signature);
478
553
  if (reader !== undefined) {
479
- const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false);
554
+ const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, {
555
+ inValue: false,
556
+ checked,
557
+ });
480
558
  parts.push(...found.parts);
481
559
  contextual.push(...found.contextual);
482
560
  }
@@ -484,7 +562,10 @@ export function projectParts(project, held, references, targetRoot) {
484
562
  for (const one of walk.values) {
485
563
  const reader = readers.get(one.signature);
486
564
  if (reader !== undefined) {
487
- const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true);
565
+ const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, {
566
+ inValue: true,
567
+ checked,
568
+ });
488
569
  parts.push(...found.parts);
489
570
  contextual.push(...found.contextual);
490
571
  }
@@ -500,6 +581,19 @@ export function projectParts(project, held, references, targetRoot) {
500
581
  used: (caught[at] ?? []).some((use) => readsBinding(use, name, ids)),
501
582
  });
502
583
  });
584
+ for (const { names, uses, holder } of loopVariables) {
585
+ for (const name of names) {
586
+ parts.push({
587
+ code: DEAD_STORE,
588
+ declaration: holder,
589
+ kind: "store",
590
+ name: name.text,
591
+ position: spanOf(file, targetRoot, name),
592
+ message: `value written to ${name.text} is never read`,
593
+ used: uses.some((use) => use.text === name.text && readsBinding(use, name, ids)),
594
+ });
595
+ }
596
+ }
503
597
  walk.blocks.forEach((block, at) => {
504
598
  const holder = holderOf(block);
505
599
  if (holder !== undefined) {
@@ -532,6 +626,7 @@ export function projectParts(project, held, references, targetRoot) {
532
626
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
533
627
  return {
534
628
  parts: withContextualRead(project, parts, contextual),
629
+ imports,
535
630
  signatures,
536
631
  calls,
537
632
  specifiers,
@@ -558,7 +653,9 @@ function valueSignature(node) {
558
653
  /**
559
654
  * Every name one function's parameters bind, a binding pattern's names one by one. A
560
655
  * `this` parameter receives no argument, a decorated one is read by its decorator, and a
561
- * parameter property is read by the write to its member, so none binds a subject.
656
+ * parameter property is read by the write to its member, so none binds a subject. A name
657
+ * spelled with a leading underscore is declared unread, so it is listed for its reads and
658
+ * reported never.
562
659
  */
563
660
  function parameterNames(signature) {
564
661
  const found = [];
@@ -570,7 +667,7 @@ function parameterNames(signature) {
570
667
  continue;
571
668
  }
572
669
  if (isIdentifier(element.name)) {
573
- const exempt = rest && element.dotDotDotToken === undefined;
670
+ const exempt = (rest && element.dotDotDotToken === undefined) || declaredUnread(element.name);
574
671
  found.push({ name: element.name, at, pattern: true, exempt });
575
672
  }
576
673
  else {
@@ -586,7 +683,7 @@ function parameterNames(signature) {
586
683
  }
587
684
  if (isIdentifier(name)) {
588
685
  if (name.text !== "this") {
589
- found.push({ name, at, pattern: false, exempt: false });
686
+ found.push({ name, at, pattern: false, exempt: declaredUnread(name) });
590
687
  }
591
688
  return;
592
689
  }
@@ -594,11 +691,91 @@ function parameterNames(signature) {
594
691
  });
595
692
  return found;
596
693
  }
694
+ /** How deep a type is searched for a type parameter: a callback of a callback is two levels. */
695
+ const TYPE_PARAMETER_DEPTH = 3;
696
+ /** Whether one type is or holds a type parameter, through constituents, type arguments and signatures. */
697
+ function namesTypeParameter(project, type, depth = 0) {
698
+ if (type.isTypeParameter()) {
699
+ return true;
700
+ }
701
+ if (depth >= TYPE_PARAMETER_DEPTH) {
702
+ return false;
703
+ }
704
+ const { queries } = project;
705
+ const parts = type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [];
706
+ const args = queries.typeArguments(type);
707
+ const signatures = queries.signaturesOf(type, SignatureKind.Call);
708
+ if (!isAnswered(parts) || !isAnswered(args) || !isAnswered(signatures)) {
709
+ return true;
710
+ }
711
+ const inner = [
712
+ ...parts,
713
+ ...args,
714
+ ...signatures.flatMap((signature) => signature.parameters.map((_parameter, at) => queries.typeAtPosition(signature, at))),
715
+ ];
716
+ return inner.some((one) => !isAnswered(one) || namesTypeParameter(project, one, depth + 1));
717
+ }
597
718
  /**
598
- * The parts with each name of `contextual` read where the compiler gives its function a
599
- * contextual type: the inference of the call holding the function may read a parameter's
600
- * annotation, and the code handing the argument may act on what an object pattern reads.
601
- * A question the checker leaves unanswered keeps the name read.
719
+ * Whether the call a function expression is written in may read the annotation of its
720
+ * parameter at `at`: the callee is generic, and its parameter type at the function's place
721
+ * reads a type parameter there. A function written inside an object literal a generic
722
+ * call is passed is read so too, and a question the checker leaves unanswered reads it.
723
+ */
724
+ function calleeSteers(project, signature, at) {
725
+ const { queries } = project;
726
+ let value = signature;
727
+ while (isParenthesizedExpression(value.parent)) {
728
+ value = value.parent;
729
+ }
730
+ let nested = false;
731
+ while ((isPropertyAssignment(value.parent) && isObjectLiteralExpression(value.parent.parent)) ||
732
+ isObjectLiteralExpression(value.parent)) {
733
+ value = isObjectLiteralExpression(value.parent) ? value.parent : value.parent.parent;
734
+ nested = true;
735
+ }
736
+ const call = value.parent;
737
+ if (!isCallExpression(call) && !isNewExpression(call)) {
738
+ return false;
739
+ }
740
+ const argument = (call.arguments ?? []).findIndex((one) => one === value);
741
+ const resolved = argument < 0 ? undefined : queries.resolvedSignature(call);
742
+ const generic = resolved === undefined || !isAnswered(resolved) ? resolved : queries.genericOf(resolved);
743
+ if (argument < 0 || generic === undefined) {
744
+ return false;
745
+ }
746
+ if (!isAnswered(generic) || nested) {
747
+ return true;
748
+ }
749
+ const position = queries.typeAtPosition(generic, argument);
750
+ if (!isAnswered(position) || position.isTypeParameter()) {
751
+ return true;
752
+ }
753
+ const parts = position.isUnionType() || position.isIntersectionType()
754
+ ? queries.constituents(position)
755
+ : [position];
756
+ if (!isAnswered(parts)) {
757
+ return true;
758
+ }
759
+ return parts.some((part) => {
760
+ if (part.isTypeParameter()) {
761
+ return true;
762
+ }
763
+ const calls = queries.signaturesOf(part, SignatureKind.Call);
764
+ if (!isAnswered(calls)) {
765
+ return true;
766
+ }
767
+ return calls.some((one) => {
768
+ const type = at < one.parameters.length ? queries.typeAtPosition(one, at) : undefined;
769
+ return type !== undefined && (!isAnswered(type) || namesTypeParameter(project, type));
770
+ });
771
+ });
772
+ }
773
+ /**
774
+ * The parts with each name of `contextual` judged where the compiler gives its function a
775
+ * contextual type: the code handing the argument may act on what an object pattern reads,
776
+ * so each such name is read, and an annotated parameter is marked with whether the call's
777
+ * inference may read its annotation. A question the checker leaves unanswered keeps the
778
+ * name read.
602
779
  */
603
780
  function withContextualRead(project, parts, contextual) {
604
781
  if (contextual.length === 0) {
@@ -606,9 +783,25 @@ function withContextualRead(project, parts, contextual) {
606
783
  }
607
784
  const signatures = [...new Set(contextual.map((one) => one.signature))];
608
785
  const answers = project.queries.contextualTypes(signatures);
609
- const typed = new Set(signatures.filter((_signature, at) => answers[at] !== undefined));
610
- const read = new Set(contextual.filter((one) => typed.has(one.signature)).map((one) => one.part));
611
- return parts.map((part) => (read.has(part) ? { ...part, used: true } : part));
786
+ const typedBy = new Map(signatures.map((signature, at) => [signature, answers[at]]));
787
+ const judged = new Map();
788
+ for (const one of contextual) {
789
+ const answer = typedBy.get(one.signature);
790
+ if (answer === undefined) {
791
+ continue;
792
+ }
793
+ const place = one.part.parameter;
794
+ judged.set(one.part, one.pattern || answer === UNANSWERED || place === undefined
795
+ ? { ...one.part, used: true }
796
+ : {
797
+ ...one.part,
798
+ parameter: {
799
+ ...place,
800
+ annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
801
+ },
802
+ });
803
+ }
804
+ return parts.map((part) => judged.get(part) ?? part);
612
805
  }
613
806
  /**
614
807
  * Each name one function's parameters bind and whether the function reads it, and the
@@ -616,7 +809,7 @@ function withContextualRead(project, parts, contextual) {
616
809
  * that carries a type annotation or is an object binding pattern. A binding pattern counts
617
810
  * as read where its function reads any name it binds.
618
811
  */
619
- function parameterParts(file, root, declaration, signature, reader, ids, inValue) {
812
+ function parameterParts(file, root, declaration, signature, reader, ids, place) {
620
813
  // A function that reads its `arguments` reads every parameter by position. A nested
621
814
  // function other than an arrow has an `arguments` object of its own.
622
815
  const readsArguments = !isArrowFunction(signature) &&
@@ -639,7 +832,7 @@ function parameterParts(file, root, declaration, signature, reader, ids, inValue
639
832
  position: spanOf(file, root, name),
640
833
  message: `parameter ${name.text} is never read in the body`,
641
834
  used: read[index] === true,
642
- parameter: { beforeARead: !pattern && at < lastRead, inValue },
835
+ parameter: { beforeARead: !pattern && at < lastRead, ...place },
643
836
  };
644
837
  found.push(part);
645
838
  const parameter = signature.parameters[at];
@@ -647,42 +840,79 @@ function parameterParts(file, root, declaration, signature, reader, ids, inValue
647
840
  !part.used &&
648
841
  parameter !== undefined &&
649
842
  (parameter.type !== undefined || isObjectBindingPattern(parameter.name))) {
650
- contextual.push({ part, signature: expression });
843
+ contextual.push({
844
+ part,
845
+ signature: expression,
846
+ at,
847
+ pattern: isObjectBindingPattern(parameter.name),
848
+ });
651
849
  }
652
850
  });
653
851
  return { parts: found, contextual };
654
852
  }
655
853
  /**
656
854
  * The names one declaration binds that a write of its initializer stores each on its own:
657
- * the declared name, or each element name of an array binding pattern at any depth.
855
+ * the declared name, or each element name of a binding pattern at any depth. A name with a
856
+ * default is left out, because removing it skips the default's evaluation, and so is a name
857
+ * beside a rest element of its object pattern, whose removal changes what the rest holds.
658
858
  */
659
- function arrayPatternNames(name) {
859
+ function boundNames(name) {
660
860
  if (isIdentifier(name)) {
661
861
  return [name];
662
862
  }
663
- if (!isArrayBindingPattern(name)) {
664
- return [];
863
+ const rest = isObjectBindingPattern(name) &&
864
+ name.elements.some((element) => element.dotDotDotToken !== undefined);
865
+ return name.elements.flatMap((element) => {
866
+ if (!isBindingElement(element) || element.name === undefined) {
867
+ return [];
868
+ }
869
+ if (element.initializer !== undefined || (rest && element.dotDotDotToken === undefined)) {
870
+ return [];
871
+ }
872
+ return boundNames(element.name);
873
+ });
874
+ }
875
+ /** The function whose scope a `var` of one block belongs to, or none at a module's top level. */
876
+ function functionOf(block) {
877
+ for (let at = block.parent;; at = at.parent) {
878
+ if (isFunctionLikeDeclaration(at)) {
879
+ return at;
880
+ }
881
+ if (isSourceFile(at)) {
882
+ return undefined;
883
+ }
665
884
  }
666
- return name.elements.flatMap((element) => isBindingElement(element) && element.name !== undefined ? arrayPatternNames(element.name) : []);
667
885
  }
668
- /** The locals one block declares with `let` or `const`, each with the names that spell it. */
886
+ /**
887
+ * The locals one block declares with `let`, `const` or `var`, each with the names of the
888
+ * block that spell it. A `var` of a nested block is one where no nested function of its
889
+ * function names it, since the function scope holds it past the block's end.
890
+ */
669
891
  function localsOf(block) {
670
892
  const declared = [];
893
+ const scope = functionOf(block);
894
+ const outlives = scope?.body !== block;
671
895
  block.statements.forEach((statement, at) => {
672
896
  if (!isVariableStatement(statement)) {
673
897
  return;
674
898
  }
675
899
  const list = statement.declarationList;
676
- if ((list.flags & (NodeFlags.Let | NodeFlags.Const)) === 0 ||
677
- (list.flags & NodeFlags.Using) !== 0) {
900
+ const blockScoped = (list.flags & (NodeFlags.Let | NodeFlags.Const)) !== 0;
901
+ if ((!blockScoped && scope === undefined) || (list.flags & NodeFlags.Using) !== 0) {
678
902
  return;
679
903
  }
680
904
  for (const declaration of list.declarations) {
681
- for (const name of arrayPatternNames(declaration.name)) {
905
+ for (const name of boundNames(declaration.name)) {
906
+ if (!blockScoped &&
907
+ outlives &&
908
+ identifiersNaming(scope?.body ?? block, new Set([name.text])).some((use) => inNestedFunction(use, scope ?? block))) {
909
+ continue;
910
+ }
682
911
  declared.push({
683
912
  name,
684
913
  statement: at,
685
914
  initialized: declaration.initializer !== undefined,
915
+ outlives: !blockScoped && outlives,
686
916
  });
687
917
  }
688
918
  }
@@ -696,19 +926,39 @@ function localsOf(block) {
696
926
  uses: spelled.filter((use) => use !== one.name && use.text === one.name.text),
697
927
  }));
698
928
  }
929
+ /** Whether one name is the whole target of a plain assignment, which writes it and reads nothing. */
930
+ function isPlainWrite(use) {
931
+ const parent = outerParent(use);
932
+ return (isBinaryExpression(parent) &&
933
+ parent.operatorToken.kind === SyntaxKind.EqualsToken &&
934
+ unwrapped(parent.left) === use);
935
+ }
699
936
  /**
700
937
  * The writes to the locals of one block that no read reaches. A write is a declaration's
701
- * initializer or a plain assignment written as a statement of the block itself; it is
702
- * dead when the statements after it in the block read the local nowhere before the next
703
- * such write to it, or before the block ends, which ends the local's scope. A statement
704
- * that writes the local inside a nested construct ends the search with the write kept,
705
- * and a local a nested function names is no subject, because the function may read it
706
- * at any time.
938
+ * initializer or a plain assignment written as a statement of the block; it is dead when
939
+ * the block's later statements read the local nowhere before the next such write or the
940
+ * block's end. A statement writing it inside a nested construct ends the search with the
941
+ * write kept. A local a nested function reads is no subject, since the function may run at
942
+ * any time, and every write to a local nothing reads is dead, a nested function's included.
707
943
  */
708
944
  function deadStores(file, root, block, holder, locals, ids) {
709
945
  const found = [];
946
+ const deadAt = (local, target) => ({
947
+ code: DEAD_STORE,
948
+ declaration: holder,
949
+ kind: "store",
950
+ name: local.name.text,
951
+ position: spanOf(file, root, target),
952
+ message: `value written to ${local.name.text} is never read`,
953
+ used: false,
954
+ });
710
955
  for (const local of locals) {
711
956
  const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
957
+ if (!local.outlives && uses.every(isPlainWrite)) {
958
+ const targets = [...(local.initialized ? [local.name] : []), ...uses];
959
+ found.push(...targets.map((target) => deadAt(local, target)));
960
+ continue;
961
+ }
712
962
  if (uses.some((use) => inNestedFunction(use, block))) {
713
963
  continue;
714
964
  }
@@ -746,7 +996,7 @@ function deadStores(file, root, block, holder, locals, ids) {
746
996
  if (sameStatement.length > 0) {
747
997
  continue;
748
998
  }
749
- let dead = true;
999
+ let dead = !local.outlives;
750
1000
  for (let at = write.at + 1; at < statements.length; at += 1) {
751
1001
  const statement = statements[at];
752
1002
  if (statement === undefined) {
@@ -763,19 +1013,9 @@ function deadStores(file, root, block, holder, locals, ids) {
763
1013
  break;
764
1014
  }
765
1015
  }
766
- if (!dead) {
767
- continue;
1016
+ if (dead) {
1017
+ found.push(deadAt(local, write.target));
768
1018
  }
769
- const position = spanOf(file, root, write.target);
770
- found.push({
771
- code: DEAD_STORE,
772
- declaration: holder,
773
- kind: "store",
774
- name: local.name.text,
775
- position,
776
- message: `value written to ${local.name.text} is never read`,
777
- used: false,
778
- });
779
1019
  }
780
1020
  }
781
1021
  return found;
@@ -839,6 +1079,12 @@ export function intraFunctionFacts(projects, references, exempted) {
839
1079
  const known = parts.get(key);
840
1080
  parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
841
1081
  }
1082
+ const imports = new Map();
1083
+ for (const binding of projects.flatMap((one) => one.imports)) {
1084
+ const key = positionKey(binding.position);
1085
+ const known = imports.get(key);
1086
+ imports.set(key, known === undefined ? binding : { ...known, used: known.used || binding.used });
1087
+ }
842
1088
  const calls = new Map();
843
1089
  const specifiers = new Set();
844
1090
  for (const one of projects) {
@@ -885,5 +1131,11 @@ export function intraFunctionFacts(projects, references, exempted) {
885
1131
  const discardedEverywhere = new Set([...called]
886
1132
  .filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
887
1133
  .map(([id]) => id));
888
- return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
1134
+ return {
1135
+ parts: [...parts.values()],
1136
+ imports: [...imports.values()],
1137
+ free,
1138
+ freeButValued,
1139
+ discardedEverywhere,
1140
+ };
889
1141
  }
@@ -1,4 +1,4 @@
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";
1
+ import { isArrayBindingPattern, isBindingElement, isClassDeclaration, isComputedPropertyName, isConstructorDeclaration, isEnumDeclaration, isEnumMember, isExportAssignment, isExportDeclaration, isFunctionDeclaration, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isIntersectionTypeNode, isMethodDeclaration, isMethodSignatureDeclaration, isModuleBlock, isModuleDeclaration, isNamespaceExport, isObjectBindingPattern, 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";
@@ -65,6 +65,18 @@ function parentOf(path) {
65
65
  function modifiersOf(node) {
66
66
  return node.modifierFlags ?? 0;
67
67
  }
68
+ /** Whether a declaration is ambient: in a declaration file, or under a `declare` modifier. */
69
+ function isAmbient(file, node) {
70
+ if (file.isDeclarationFile) {
71
+ return true;
72
+ }
73
+ for (let at = node; at !== undefined; at = at.parent) {
74
+ if ((modifiersOf(at) & ModifierFlags.Ambient) !== 0) {
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ }
68
80
  /** The type parameters one node declares, and none for a node that declares none. */
69
81
  function typeParametersOf(node) {
70
82
  if (isFunctionDeclaration(node) ||
@@ -200,6 +212,31 @@ function visibilityOf(node, name) {
200
212
  }
201
213
  return "public";
202
214
  }
215
+ /**
216
+ * The binding elements of a destructuring declaration that each declare a name of their
217
+ * own, at any depth, but one with a default, whose removal skips the default's
218
+ * evaluation, and one beside a rest element of its object pattern, whose removal
219
+ * changes what the rest holds.
220
+ */
221
+ function patternElements(name) {
222
+ if (!isObjectBindingPattern(name) && !isArrayBindingPattern(name)) {
223
+ return [];
224
+ }
225
+ const rest = isObjectBindingPattern(name) &&
226
+ name.elements.some((element) => element.dotDotDotToken !== undefined);
227
+ return name.elements.flatMap((element) => {
228
+ if (!isBindingElement(element) ||
229
+ element.initializer !== undefined ||
230
+ (rest && element.dotDotDotToken === undefined)) {
231
+ return [];
232
+ }
233
+ const bound = element.name;
234
+ if (bound === undefined) {
235
+ return [];
236
+ }
237
+ return isIdentifier(bound) ? [{ element, name: bound }] : patternElements(bound);
238
+ });
239
+ }
203
240
  /** The name node of one declaration, for the kinds that carry one. */
204
241
  function nameNodeOf(node) {
205
242
  return node.name;
@@ -491,15 +528,19 @@ export function inventory(project, host, targetRoot) {
491
528
  if (isVariableStatement(node)) {
492
529
  for (const declaration of node.declarationList.declarations) {
493
530
  const component = componentOf(declaration.name);
494
- if (component === undefined) {
495
- // A destructuring pattern declares binding elements, which the reference
496
- // grammar does not represent; the statement stands for them.
531
+ if (component !== undefined) {
532
+ declare(declaration, owner, "variable", declaration.name, {
533
+ component,
534
+ localName: undefined,
535
+ });
497
536
  continue;
498
537
  }
499
- declare(declaration, owner, "variable", declaration.name, {
500
- component,
501
- localName: undefined,
502
- });
538
+ for (const { element, name } of patternElements(declaration.name)) {
539
+ declare(element, owner, "variable", name, {
540
+ component: { text: name.text, computed: false },
541
+ localName: undefined,
542
+ });
543
+ }
503
544
  }
504
545
  return;
505
546
  }
@@ -754,6 +795,9 @@ export function inventory(project, host, targetRoot) {
754
795
  .map((symbol) => symbol.id),
755
796
  byName: namesOf(building),
756
797
  modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
798
+ ambient: new Set(building
799
+ .filter((record) => isAmbient(record.file, record.node))
800
+ .map((record) => positionKey(record.position))),
757
801
  dependents,
758
802
  };
759
803
  }