@cplieger/deadset-ts 6.1.0 → 7.0.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 (47) hide show
  1. package/dist/src/analysis.js +39 -16
  2. package/dist/src/conformance-record.js +2 -2
  3. package/dist/src/contextual-uses.js +27 -3
  4. package/dist/src/convention-rows.js +42 -0
  5. package/dist/src/conventions.js +48 -0
  6. package/dist/src/dependencies.js +109 -38
  7. package/dist/src/findings/completion.js +6 -7
  8. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  9. package/dist/src/html-entries.js +29 -14
  10. package/dist/src/indexed-access.js +371 -81
  11. package/dist/src/intra-function-parts.js +57 -11
  12. package/dist/src/kinds.js +6 -6
  13. package/dist/src/references.js +60 -21
  14. package/dist/src/roots.js +96 -6
  15. package/dist/src/session.js +6 -1
  16. package/dist/src/source-files.js +1 -0
  17. package/dist/src/sweep.js +29 -17
  18. package/dist/src/symbol-keys.js +114 -0
  19. package/dist/src/test-joins.js +101 -0
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/workflow-steps.js +21 -15
  22. package/dist/src/workspace-programs.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis.ts +45 -17
  26. package/src/conformance-record.ts +2 -2
  27. package/src/contextual-uses.ts +36 -4
  28. package/src/convention-rows.ts +57 -0
  29. package/src/conventions.ts +59 -0
  30. package/src/dependencies.ts +134 -46
  31. package/src/finding.ts +2 -0
  32. package/src/findings/completion.ts +7 -7
  33. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  34. package/src/html-entries.ts +33 -15
  35. package/src/indexed-access.ts +468 -102
  36. package/src/intra-function-parts.ts +84 -12
  37. package/src/kinds.ts +7 -6
  38. package/src/references.ts +86 -27
  39. package/src/roots.ts +129 -6
  40. package/src/session.ts +7 -1
  41. package/src/source-files.ts +3 -0
  42. package/src/sweep.ts +29 -17
  43. package/src/symbol-keys.ts +172 -0
  44. package/src/test-joins.ts +120 -0
  45. package/src/version.ts +1 -1
  46. package/src/workflow-steps.ts +23 -16
  47. package/src/workspace-programs.ts +1 -1
@@ -4,7 +4,7 @@
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 { 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/unstable/ast";
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/unstable/ast";
8
8
  import { nodeKey } from "./inventory.js";
9
9
  import { positionKey, renderPosition } from "./position.js";
10
10
  import { isAnswered, UNANSWERED } from "./query.js";
@@ -277,6 +277,7 @@ export function projectParts(project, held, references, targetRoot) {
277
277
  const calls = new Map();
278
278
  const specifiers = new Set();
279
279
  const sites = [];
280
+ const contextual = [];
280
281
  for (const file of files) {
281
282
  const declarationAt = (node) => {
282
283
  const id = held.declarations.get(nodeKey(file, node));
@@ -421,13 +422,17 @@ export function projectParts(project, held, references, targetRoot) {
421
422
  });
422
423
  const reader = readers.get(one.signature);
423
424
  if (reader !== undefined) {
424
- parts.push(...parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false));
425
+ const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false);
426
+ parts.push(...found.parts);
427
+ contextual.push(...found.contextual);
425
428
  }
426
429
  }
427
430
  for (const one of walk.values) {
428
431
  const reader = readers.get(one.signature);
429
432
  if (reader !== undefined) {
430
- parts.push(...parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true));
433
+ const found = parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true);
434
+ parts.push(...found.parts);
435
+ contextual.push(...found.contextual);
431
436
  }
432
437
  }
433
438
  walk.catches.forEach(({ name, holder }, at) => {
@@ -471,7 +476,7 @@ export function projectParts(project, held, references, targetRoot) {
471
476
  }
472
477
  }
473
478
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
474
- return { parts, signatures, calls, specifiers };
479
+ return { parts: withContextualRead(project, parts, contextual), signatures, calls, specifiers };
475
480
  }
476
481
  /**
477
482
  * A function written in place that no declaration declares: an arrow, a function
@@ -533,8 +538,26 @@ function parameterNames(signature) {
533
538
  return found;
534
539
  }
535
540
  /**
536
- * Each name one function's parameters bind and whether the function reads it. A binding
537
- * pattern counts as read where its function reads any name it binds.
541
+ * The parts with each name of `contextual` read where the compiler gives its function a
542
+ * contextual type: the inference of the call holding the function may read a parameter's
543
+ * annotation, and the code handing the argument may act on what an object pattern reads.
544
+ * A question the checker leaves unanswered keeps the name read.
545
+ */
546
+ function withContextualRead(project, parts, contextual) {
547
+ if (contextual.length === 0) {
548
+ return parts;
549
+ }
550
+ const signatures = [...new Set(contextual.map((one) => one.signature))];
551
+ const answers = project.queries.contextualTypes(signatures);
552
+ const typed = new Set(signatures.filter((_signature, at) => answers[at] !== undefined));
553
+ const read = new Set(contextual.filter((one) => typed.has(one.signature)).map((one) => one.part));
554
+ return parts.map((part) => (read.has(part) ? { ...part, used: true } : part));
555
+ }
556
+ /**
557
+ * Each name one function's parameters bind and whether the function reads it, and the
558
+ * unread names a contextual type of a function expression may keep: those of a parameter
559
+ * that carries a type annotation or is an object binding pattern. A binding pattern counts
560
+ * as read where its function reads any name it binds.
538
561
  */
539
562
  function parameterParts(file, root, declaration, signature, reader, ids, inValue) {
540
563
  // A function that reads `arguments` reads every parameter by position.
@@ -543,11 +566,13 @@ function parameterParts(file, root, declaration, signature, reader, ids, inValue
543
566
  reader.uses.some((use) => use.text === name.text && readsBinding(use, name, ids)));
544
567
  const lastRead = Math.max(-1, ...reader.names.filter((_, at) => read[at]).map((one) => one.at));
545
568
  const found = [];
569
+ const contextual = [];
570
+ const expression = isArrowFunction(signature) || isFunctionExpression(signature) ? signature : undefined;
546
571
  reader.names.forEach(({ name, at, pattern, exempt }, index) => {
547
572
  if (exempt) {
548
573
  return;
549
574
  }
550
- found.push({
575
+ const part = {
551
576
  code: UNUSED_PARAMETER,
552
577
  declaration,
553
578
  kind: "parameter",
@@ -556,9 +581,30 @@ function parameterParts(file, root, declaration, signature, reader, ids, inValue
556
581
  message: `parameter ${name.text} is never read in the body`,
557
582
  used: read[index] === true,
558
583
  parameter: { beforeARead: !pattern && at < lastRead, inValue },
559
- });
584
+ };
585
+ found.push(part);
586
+ const parameter = signature.parameters[at];
587
+ if (expression !== undefined &&
588
+ !part.used &&
589
+ parameter !== undefined &&
590
+ (parameter.type !== undefined || isObjectBindingPattern(parameter.name))) {
591
+ contextual.push({ part, signature: expression });
592
+ }
560
593
  });
561
- return found;
594
+ return { parts: found, contextual };
595
+ }
596
+ /**
597
+ * The names one declaration binds that a write of its initializer stores each on its own:
598
+ * the declared name, or each element name of an array binding pattern at any depth.
599
+ */
600
+ function arrayPatternNames(name) {
601
+ if (isIdentifier(name)) {
602
+ return [name];
603
+ }
604
+ if (!isArrayBindingPattern(name)) {
605
+ return [];
606
+ }
607
+ return name.elements.flatMap((element) => isBindingElement(element) && element.name !== undefined ? arrayPatternNames(element.name) : []);
562
608
  }
563
609
  /** The locals one block declares with `let` or `const`, each with the names that spell it. */
564
610
  function localsOf(block) {
@@ -573,9 +619,9 @@ function localsOf(block) {
573
619
  return;
574
620
  }
575
621
  for (const declaration of list.declarations) {
576
- if (isIdentifier(declaration.name)) {
622
+ for (const name of arrayPatternNames(declaration.name)) {
577
623
  declared.push({
578
- name: declaration.name,
624
+ name,
579
625
  statement: at,
580
626
  initialized: declaration.initializer !== undefined,
581
627
  });
package/dist/src/kinds.js CHANGED
@@ -70,7 +70,7 @@ const ROWS = [
70
70
  code: "DS1101",
71
71
  name: "unnecessary-export",
72
72
  languages: ["go", "ts"],
73
- rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
73
+ rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is 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, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
74
74
  precondition: "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
75
75
  defaultSeverity: "warn",
76
76
  maxClass: "certain",
@@ -81,7 +81,7 @@ const ROWS = [
81
81
  code: "DS1102",
82
82
  name: "unnecessary-exposure",
83
83
  languages: ["go"],
84
- rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
84
+ rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is 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, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
85
85
  precondition: "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
86
86
  defaultSeverity: "warn",
87
87
  maxClass: "certain",
@@ -197,7 +197,7 @@ const ROWS = [
197
197
  name: "unused-dependency",
198
198
  languages: ["go", "ts"],
199
199
  rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
200
- precondition: "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and four more dependencies are needed. The first is one whose installed manifest declares a command, because a command is run by name rather than imported. The second is one that the installed manifest of a needed dependency declares as a peer and does not mark optional. The third is every required peer of a dependency so held, to a fixpoint. The fourth is one that a string names under a manifest key of an applied convention row, as grammar/analysis.md states. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
200
+ precondition: "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and a call of `import.meta.resolve` or of `require.resolve` in them, whatever `require` names, whose first argument is a string literal or a template literal with no substitution needs each dependency an import of that specifier needs. Three more dependencies are needed: one whose command a script entry or a workflow step runs, one that a string of a configuration file names, and one that a string names under a manifest key or a package key of an applied convention row, each as grammar/analysis.md states. A command no script entry and no workflow step runs needs nothing, and a peer the installed manifest of a needed dependency requires is needed only as any other dependency is, because the package manager installs a required peer itself. A dependency of a manifest whose directory holds source only in configurations the run lists in configurations_not_built is reported at the reachability class possible, because the source that may import it was not analyzed. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
201
201
  defaultSeverity: "deny",
202
202
  maxClass: "certain",
203
203
  fixability: "manual",
@@ -279,7 +279,7 @@ const ROWS = [
279
279
  name: "unused-parameter",
280
280
  languages: ["go", "ts"],
281
281
  rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
282
- precondition: "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, 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 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 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.",
282
+ 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.",
283
283
  defaultSeverity: "warn",
284
284
  maxClass: "certain",
285
285
  fixability: "manual",
@@ -290,8 +290,8 @@ const ROWS = [
290
290
  code: "DS1802",
291
291
  name: "unused-receiver",
292
292
  languages: ["go"],
293
- rule: "A named method receiver with no reference inside the method body, on a method whose signature is free to change. Go permits a method with no receiver name, so the fix deletes an identifier and changes no signature.",
294
- precondition: "The same free-signature rule as unused-parameter, applied to the receiver.",
293
+ rule: "A named method receiver with no reference inside the method body. Go permits a method with no receiver name, so the fix deletes an identifier and changes no signature.",
294
+ precondition: "None beyond the rule. Deleting a receiver name changes no signature, so a method retained by interface satisfaction, a method used as a value and a stub are reported as any other method is.",
295
295
  defaultSeverity: "warn",
296
296
  maxClass: "certain",
297
297
  fixability: "deletable",
@@ -14,6 +14,7 @@ import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./pat
14
14
  import { byPosition, isComponentFile, renderPosition } from "./position.js";
15
15
  import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED } from "./query.js";
16
16
  import { typeQueryAliases } from "./type-query-alias.js";
17
+ import { computedKeyNames, keyNameOf, keyedUses } from "./symbol-keys.js";
17
18
  import { handedOn, valuesOf } from "./value-flow.js";
18
19
  /**
19
20
  * The operators whose left side one assignment stores into. A compound assignment is
@@ -84,12 +85,11 @@ function unparenthesized(node) {
84
85
  * Records the name one store writes into.
85
86
  *
86
87
  * A property access writes the member it names. An element access writes the collection
87
- * it stores into, because a collection a module only ever stores into holds nothing
88
- * anything reads, and the key is no declaration of the target. A destructuring target
89
- * writes each name it binds, through the spreads and the nested patterns it binds them
90
- * in.
88
+ * it stores into, since a collection a module only stores into holds nothing anything
89
+ * reads, and joins `accesses` for the member a unique-symbol key selects. A destructuring
90
+ * target writes each name it binds, through its spreads and nested patterns.
91
91
  */
92
- function markWrite(target, writes) {
92
+ function markWrite(target, writes, accesses) {
93
93
  const held = unparenthesized(target);
94
94
  if (isName(held)) {
95
95
  writes.add(held.pos);
@@ -102,37 +102,38 @@ function markWrite(target, writes) {
102
102
  return;
103
103
  }
104
104
  if (isElementAccessExpression(held)) {
105
+ accesses.add(held);
105
106
  // A store through a member's value lands in storage every holder of the value
106
107
  // reads, so it reads the member; a variable only ever stored into stays written.
107
108
  if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
108
- markWrite(held.expression, writes);
109
+ markWrite(held.expression, writes, accesses);
109
110
  }
110
111
  return;
111
112
  }
112
113
  if (isArrayLiteralExpression(held)) {
113
114
  for (const element of held.elements) {
114
- markWrite(element, writes);
115
+ markWrite(element, writes, accesses);
115
116
  }
116
117
  return;
117
118
  }
118
119
  if (isObjectLiteralExpression(held)) {
119
120
  for (const property of held.properties) {
120
121
  if (isPropertyAssignment(property)) {
121
- markWrite(property.initializer, writes);
122
+ markWrite(property.initializer, writes, accesses);
122
123
  continue;
123
124
  }
124
125
  if (isShorthandPropertyAssignment(property)) {
125
- markWrite(property.name, writes);
126
+ markWrite(property.name, writes, accesses);
126
127
  continue;
127
128
  }
128
129
  if (isSpreadAssignment(property)) {
129
- markWrite(property.expression, writes);
130
+ markWrite(property.expression, writes, accesses);
130
131
  }
131
132
  }
132
133
  return;
133
134
  }
134
135
  if (isSpreadElement(held)) {
135
- markWrite(held.expression, writes);
136
+ markWrite(held.expression, writes, accesses);
136
137
  }
137
138
  }
138
139
  /**
@@ -142,14 +143,14 @@ function markWrite(target, writes) {
142
143
  * holds. A store that reads its target as well records it in `reads`: a logical
143
144
  * assignment, and a compound assignment, an increment or a decrement whose value is used.
144
145
  */
145
- function markStores(node, writes, reads) {
146
+ function markStores(node, writes, reads, accesses) {
146
147
  if (isBinaryExpression(node)) {
147
148
  const operator = node.operatorToken.kind;
148
149
  if (ASSIGNMENTS.has(operator)) {
149
- markWrite(node.left, writes);
150
+ markWrite(node.left, writes, accesses.written);
150
151
  if (LOGICAL_ASSIGNMENTS.has(operator) ||
151
152
  (operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))) {
152
- markWrite(node.left, reads);
153
+ markWrite(node.left, reads, accesses.read);
153
154
  }
154
155
  }
155
156
  return;
@@ -157,20 +158,20 @@ function markStores(node, writes, reads) {
157
158
  if (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) {
158
159
  if (node.operator === SyntaxKind.PlusPlusToken ||
159
160
  node.operator === SyntaxKind.MinusMinusToken) {
160
- markWrite(node.operand, writes);
161
+ markWrite(node.operand, writes, accesses.written);
161
162
  if (!valueDiscarded(node)) {
162
- markWrite(node.operand, reads);
163
+ markWrite(node.operand, reads, accesses.read);
163
164
  }
164
165
  }
165
166
  return;
166
167
  }
167
168
  if (isDeleteExpression(node)) {
168
- markWrite(node.expression, writes);
169
+ markWrite(node.expression, writes, accesses.written);
169
170
  return;
170
171
  }
171
172
  if (isForInStatement(node) || isForOfStatement(node)) {
172
173
  if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) {
173
- markWrite(node.initializer, writes);
174
+ markWrite(node.initializer, writes, accesses.written);
174
175
  }
175
176
  }
176
177
  }
@@ -337,6 +338,8 @@ function sitesOf(file, declarations, fileId, unread) {
337
338
  const flows = [];
338
339
  const writes = new Set();
339
340
  const alsoRead = new Set();
341
+ const accesses = { written: new Set(), read: new Set() };
342
+ const keyed = [];
340
343
  const declared = new Set();
341
344
  const shorthands = new Map();
342
345
  let enclosing = fileId;
@@ -381,7 +384,17 @@ function sitesOf(file, declarations, fileId, unread) {
381
384
  return;
382
385
  }
383
386
  markNames(node);
384
- markStores(node, writes, alsoRead);
387
+ markStores(node, writes, alsoRead, accesses);
388
+ const key = isElementAccessExpression(node) ? keyNameOf(node.argumentExpression) : undefined;
389
+ if (key !== undefined && isElementAccessExpression(node)) {
390
+ const written = accesses.written.has(node);
391
+ keyed.push({
392
+ access: node,
393
+ key,
394
+ from: enclosing,
395
+ use: written ? (accesses.read.has(node) ? "both" : "write") : "read",
396
+ });
397
+ }
385
398
  if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
386
399
  literals.push({ literal: node, from: enclosing });
387
400
  }
@@ -415,7 +428,18 @@ function sitesOf(file, declarations, fileId, unread) {
415
428
  enclosing = outer;
416
429
  };
417
430
  file.forEachChild(visit);
418
- return { uses: found, literals, flows, reads, links, evaluations, wholes, urls, decorated };
431
+ return {
432
+ uses: found,
433
+ literals,
434
+ flows,
435
+ reads,
436
+ links,
437
+ evaluations,
438
+ wholes,
439
+ urls,
440
+ decorated,
441
+ keyed,
442
+ };
419
443
  }
420
444
  /** The declaration kinds of a property an object literal declares. */
421
445
  const LITERAL_PROPERTY_KINDS = new Set([
@@ -827,7 +851,8 @@ export function references(project, held, targetRoot, options) {
827
851
  }
828
852
  }
829
853
  }
830
- for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated } of walked) {
854
+ const keyNames = computedKeyNames(held.symbols);
855
+ for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated, keyed, } of walked) {
831
856
  const test = testFile.get(file) === true;
832
857
  // A component file's markup may use any binding its blocks declare at the top level.
833
858
  const markup = isComponentFile(file);
@@ -1029,6 +1054,20 @@ export function references(project, held, targetRoot, options) {
1029
1054
  });
1030
1055
  }
1031
1056
  });
1057
+ const resolvedKeys = keyed.flatMap((access) => {
1058
+ const symbol = isIdentifier(access.key) && keyNames.has(access.key.text)
1059
+ ? answered.get(access.key)
1060
+ : undefined;
1061
+ return typeof symbol === "object" ? [{ access, symbol }] : [];
1062
+ });
1063
+ for (const { access, ids } of keyedUses(project, chains, resolvedKeys, tables, cap)) {
1064
+ const position = renderPosition(file, root, access.access.argumentExpression.getStart());
1065
+ for (const id of ids) {
1066
+ for (const use of access.use === "both" ? ["read", "write"] : [access.use]) {
1067
+ found.push({ from: access.from, to: id, position, use, resolution: "keyed", test });
1068
+ }
1069
+ }
1070
+ }
1032
1071
  for (const site of uses) {
1033
1072
  const direct = site.shorthand !== undefined
1034
1073
  ? "shorthand"
package/dist/src/roots.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * decides; the published API only supposes a consumer, so a published export is live
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
- import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, isVariableStatement, } from "@typescript/native/unstable/ast";
10
+ import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, isPropertyDeclaration, isVariableDeclaration, isVariableStatement, } from "@typescript/native/unstable/ast";
11
11
  import { aliasChains } from "./alias-chain.js";
12
12
  import { conventionEntries } from "./conventions.js";
13
13
  import { entryPoints } from "./entry-points.js";
@@ -18,6 +18,15 @@ import { byPosition } from "./position.js";
18
18
  import { UNANSWERED } from "./query.js";
19
19
  import { sourceFilesOf } from "./source-files.js";
20
20
  import { typeQueryAliases } from "./type-query-alias.js";
21
+ /** Whether one declaration is written below the directory of a published workspace member. */
22
+ function inPublishedMember(held, id, root, members) {
23
+ const path = held.symbols.find((symbol) => symbol.id === id)?.position.path;
24
+ if (path === undefined || members === undefined) {
25
+ return false;
26
+ }
27
+ const file = resolvePath(root, path);
28
+ return [...members].some((dir) => file.startsWith(`${dir}/`));
29
+ }
21
30
  /** The kind of root each role of a manifest entry makes. */
22
31
  const ENTRY_KINDS = {
23
32
  import: "manifest-entry",
@@ -167,10 +176,15 @@ export function roots(project, held, targetRoot, options) {
167
176
  const entries = [
168
177
  ...options.manifest.entries.map((entry) => ({
169
178
  entry,
170
- publishes: entry.published &&
179
+ publishes: options.publishedAPI &&
180
+ entry.published &&
171
181
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
172
182
  })),
173
- ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
183
+ ...(options.otherPackages ?? []).map((entry) => ({
184
+ entry,
185
+ publishes: entry.published &&
186
+ (options.publishedAPI || options.publishedMembers?.has(entry.dir) === true),
187
+ })),
174
188
  ];
175
189
  const scripted = entries.flatMap(({ entry }) => entry.role === "script" ? files.named(entry.path) : []);
176
190
  const readBackTo = [
@@ -184,7 +198,7 @@ export function roots(project, held, targetRoot, options) {
184
198
  : readBackByName(options.host, entry, readBackTo);
185
199
  };
186
200
  for (const { entry, publishes } of entries) {
187
- if (options.publishedAPI && publishes) {
201
+ if (publishes) {
188
202
  published.push(...entryFiles(entry));
189
203
  }
190
204
  }
@@ -238,8 +252,9 @@ export function roots(project, held, targetRoot, options) {
238
252
  }
239
253
  }
240
254
  }
241
- const api = options.publishedAPI
242
- ? publishedAPI(project, published, exportsOf, held, files.byPath, releaseTagged(held, files))
255
+ const tagged = releaseTagged(held, files).filter((id) => options.publishedAPI || inPublishedMember(held, id, targetRoot, options.publishedMembers));
256
+ const api = published.length > 0 || tagged.length > 0
257
+ ? publishedAPI(project, published, exportsOf, held, files.byPath, tagged)
243
258
  : { ids: [], guessed: [] };
244
259
  for (const id of api.ids) {
245
260
  add(id, "published-api", "");
@@ -395,6 +410,15 @@ function publishedAPI(project, published, exportsOf, held, byPath, tagged) {
395
410
  const symbol = byId.get(id);
396
411
  return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
397
412
  });
413
+ for (const symbol of inferredTypeSymbols(project, round.flatMap((id) => {
414
+ const symbol = byId.get(id);
415
+ const node = symbol === undefined ? undefined : nodes.nodeOf(symbol);
416
+ return node === undefined ? [] : [node];
417
+ }))) {
418
+ for (const target of chains.chainOf(symbol)) {
419
+ reach(target.id);
420
+ }
421
+ }
398
422
  project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
399
423
  if (symbol === UNANSWERED) {
400
424
  for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
@@ -443,8 +467,74 @@ function declarationNodes(held, byPath) {
443
467
  const declaration = byId.get(symbol.id);
444
468
  return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
445
469
  },
470
+ nodeOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id),
446
471
  };
447
472
  }
473
+ /** How deep the type arguments and constituents of an inferred type are followed. */
474
+ const INFERRED_DEPTH = 4;
475
+ /**
476
+ * The declarations of the types the compiler infers where a declaration writes none: the
477
+ * type of a variable or a property with no annotation, and the result of a function or a
478
+ * method with no result annotation, each with the types its type arguments and its union
479
+ * or intersection constituents name, to a fixed depth. A question the checker leaves
480
+ * unanswered names nothing.
481
+ */
482
+ function inferredTypeSymbols(project, declarations) {
483
+ const { queries } = project;
484
+ const valued = declarations.flatMap((node) => (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
485
+ node.type === undefined &&
486
+ !isFunctionLikeDeclaration(node.initializer ?? node)
487
+ ? [node.name]
488
+ : []);
489
+ const types = [];
490
+ queries.typesAt(valued).forEach((type) => {
491
+ if (type !== UNANSWERED && type !== undefined) {
492
+ types.push(type);
493
+ }
494
+ });
495
+ for (const node of declarations) {
496
+ const signature = isFunctionLikeDeclaration(node)
497
+ ? node
498
+ : isVariableDeclaration(node) &&
499
+ node.type === undefined &&
500
+ node.initializer !== undefined &&
501
+ isFunctionLikeDeclaration(node.initializer)
502
+ ? node.initializer
503
+ : undefined;
504
+ if (signature === undefined || signature.type !== undefined || signature.body === undefined) {
505
+ continue;
506
+ }
507
+ const typed = queries.signatureOf(signature);
508
+ const result = typed === UNANSWERED || typed === undefined ? undefined : queries.returnTypeOf(typed);
509
+ if (result !== UNANSWERED && result !== undefined) {
510
+ types.push(result);
511
+ }
512
+ }
513
+ const found = new Map();
514
+ const seen = new Set();
515
+ const visit = (type, depth) => {
516
+ if (depth > INFERRED_DEPTH || seen.has(type)) {
517
+ return;
518
+ }
519
+ seen.add(type);
520
+ const symbol = queries.symbolOfType(type);
521
+ if (symbol !== UNANSWERED && symbol !== undefined) {
522
+ found.set(symbol.id, symbol);
523
+ }
524
+ const parts = queries.constituents(type);
525
+ const args = queries.typeArguments(type);
526
+ for (const next of [
527
+ ...(parts === UNANSWERED || (parts.length === 1 && parts[0] === type) ? [] : parts),
528
+ ...(args === UNANSWERED ? [] : args),
529
+ ]) {
530
+ visit(next, depth + 1);
531
+ }
532
+ };
533
+ types.forEach((type) => {
534
+ visit(type, 0);
535
+ });
536
+ return [...found.values()];
537
+ }
448
538
  /**
449
539
  * The names of types one declaration's own syntax writes: in a type reference, a
450
540
  * heritage clause or a type query. A function body is not read, and neither is a
@@ -129,7 +129,12 @@ function viewOf(project, configFile, engine, log) {
129
129
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
130
130
  return undefined;
131
131
  }
132
- const held = queries.immediateAliased(symbol);
132
+ // An alias the checker synthesizes declares nothing, the default a module that assigns
133
+ // its export gets under esModuleInterop, and the immediate step fails on it while the
134
+ // whole resolution answers.
135
+ const held = symbol.declarations.length === 0
136
+ ? queries.aliased(symbol)
137
+ : queries.immediateAliased(symbol);
133
138
  // An alias that resolves to nothing answers with the checker's own unknown
134
139
  // symbol, which declares nothing; a symbol that is no alias answers with
135
140
  // nothing at all.
@@ -48,6 +48,7 @@ export function sourceFilesOf(project, targetRoot) {
48
48
  return {
49
49
  byPath,
50
50
  byName,
51
+ outputDirs: [...new Set([outDir, declarationDir])].filter((dir) => dir !== undefined),
51
52
  named: (path) => {
52
53
  if (!path.includes(SUBPATH_WILDCARD)) {
53
54
  for (const candidate of candidates(path)) {
package/dist/src/sweep.js CHANGED
@@ -163,30 +163,42 @@ function reachable(graph, held, mode) {
163
163
  /**
164
164
  * Admits every test declaration whose set of referenced production declarations is
165
165
  * non-empty and wholly dead, and returns which it admitted. The set is read from every
166
- * reference the test makes, not the ones the mode counts: a production sweep counts none,
167
- * which is what makes the targets dead. A test referencing one live target is never
168
- * admitted, and neither is a marked or exempt test.
166
+ * reference the test makes, which a production sweep does not count. A test-support
167
+ * declaration that a test referencing a live target also references is a live target of
168
+ * every test referencing it. A marked or exempt test is never admitted.
169
169
  */
170
170
  function admitTestsOfDeadCode(graph, dead, held) {
171
- const admitted = graph.symbols.map(() => false);
172
- graph.symbols.forEach((_symbol, at) => {
173
- const testCode = graph.test[at] === true || graph.support[at] === true;
174
- if (!testCode || graph.subject[at] !== true || held[at] === true) {
175
- return;
176
- }
171
+ const testCode = (at) => graph.test[at] === true || graph.support[at] === true;
172
+ const judged = graph.symbols.map((_symbol, at) => {
177
173
  let targets = 0;
178
174
  let live = 0;
179
- for (const edge of graph.out[at] ?? []) {
180
- const target = graph.test[edge.to] === true || graph.support[edge.to] === true;
181
- if (target || graph.subject[edge.to] !== true) {
182
- continue;
175
+ if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
176
+ for (const edge of graph.out[at] ?? []) {
177
+ if (testCode(edge.to) || graph.subject[edge.to] !== true) {
178
+ continue;
179
+ }
180
+ targets += 1;
181
+ if (dead[edge.to] !== true) {
182
+ live += 1;
183
+ }
183
184
  }
184
- targets += 1;
185
- if (dead[edge.to] !== true) {
186
- live += 1;
185
+ }
186
+ return { targets, live };
187
+ });
188
+ const shared = new Set();
189
+ judged.forEach(({ live }, at) => {
190
+ if (live > 0) {
191
+ for (const edge of graph.out[at] ?? []) {
192
+ if (graph.support[edge.to] === true) {
193
+ shared.add(edge.to);
194
+ }
187
195
  }
188
196
  }
189
- if (targets > 0 && live === 0) {
197
+ });
198
+ const admitted = graph.symbols.map(() => false);
199
+ judged.forEach(({ targets, live }, at) => {
200
+ const sharing = (graph.out[at] ?? []).some((edge) => shared.has(edge.to) && edge.to !== at);
201
+ if (targets > 0 && live === 0 && !sharing) {
190
202
  dead[at] = true;
191
203
  admitted[at] = true;
192
204
  }