@cplieger/deadset-ts 4.0.0 → 5.0.0

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 (117) hide show
  1. package/README.md +68 -50
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +230 -37
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +11 -4
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +6 -3
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +4 -2
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +395 -71
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +11 -7
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +6 -3
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +4 -2
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
@@ -1,16 +1,16 @@
1
- import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
1
+ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
2
2
  import { aliasChains } from "./alias-chain.js";
3
3
  import { destructuring, propertyReadsOf } from "./destructuring.js";
4
4
  import { globExpression } from "./glob.js";
5
5
  import { declarationsByName, nodeKey } from "./inventory.js";
6
6
  import { relativePath } from "./paths.js";
7
- import { byPosition, renderPosition } from "./position.js";
7
+ import { byPosition, isComponentFile, renderPosition } from "./position.js";
8
8
  import { DEFAULT_BATCH_CAP, UNANSWERED } from "./query.js";
9
+ import { typeQueryAliases } from "./type-query-alias.js";
9
10
  /**
10
11
  * The operators whose left side one assignment stores into. A compound assignment is
11
- * among them: it reads its target only to write the result back, so nothing else
12
- * receives what it read, and a declaration whose every other reference is one of these
13
- * carries no information out of itself.
12
+ * among them: where its value is discarded it reads its target only to write the result
13
+ * back, so nothing else receives what it read.
14
14
  */
15
15
  const ASSIGNMENTS = new Set([
16
16
  SyntaxKind.EqualsToken,
@@ -30,6 +30,36 @@ const ASSIGNMENTS = new Set([
30
30
  SyntaxKind.QuestionQuestionEqualsToken,
31
31
  SyntaxKind.CaretEqualsToken,
32
32
  ]);
33
+ /**
34
+ * The assignments that read their target to decide whether to store at all, so the read
35
+ * carries information out whatever becomes of the result.
36
+ */
37
+ const LOGICAL_ASSIGNMENTS = new Set([
38
+ SyntaxKind.BarBarEqualsToken,
39
+ SyntaxKind.AmpersandAmpersandEqualsToken,
40
+ SyntaxKind.QuestionQuestionEqualsToken,
41
+ ]);
42
+ /**
43
+ * Whether the value of one store expression is discarded: it is a statement of its own, a
44
+ * clause of a `for` loop, the operand of `void`, or the left operand of a comma.
45
+ */
46
+ function valueDiscarded(node) {
47
+ let child = node;
48
+ let parent = node.parent;
49
+ while (isParenthesizedExpression(parent)) {
50
+ child = parent;
51
+ parent = parent.parent;
52
+ }
53
+ if (isExpressionStatement(parent) || isVoidExpression(parent)) {
54
+ return true;
55
+ }
56
+ if (isForStatement(parent)) {
57
+ return parent.initializer === child || parent.incrementor === child;
58
+ }
59
+ return (isBinaryExpression(parent) &&
60
+ parent.operatorToken.kind === SyntaxKind.CommaToken &&
61
+ parent.left === child);
62
+ }
33
63
  /** Whether one node is an identifier or a private name, the two forms a name takes. */
34
64
  function isName(node) {
35
65
  return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
@@ -97,12 +127,18 @@ function markWrite(target, writes) {
97
127
  * Records every store one node performs: an assignment, a compound assignment, an
98
128
  * increment or a decrement, a `delete`, and an iteration that assigns to a name declared
99
129
  * elsewhere. An iteration that declares its own name stores into nothing the inventory
100
- * holds.
130
+ * holds. A store that reads its target as well records it in `reads`: a logical
131
+ * assignment, and a compound assignment, an increment or a decrement whose value is used.
101
132
  */
102
- function markStores(node, writes) {
133
+ function markStores(node, writes, reads) {
103
134
  if (isBinaryExpression(node)) {
104
- if (ASSIGNMENTS.has(node.operatorToken.kind)) {
135
+ const operator = node.operatorToken.kind;
136
+ if (ASSIGNMENTS.has(operator)) {
105
137
  markWrite(node.left, writes);
138
+ if (LOGICAL_ASSIGNMENTS.has(operator) ||
139
+ (operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))) {
140
+ markWrite(node.left, reads);
141
+ }
106
142
  }
107
143
  return;
108
144
  }
@@ -110,6 +146,9 @@ function markStores(node, writes) {
110
146
  if (node.operator === SyntaxKind.PlusPlusToken ||
111
147
  node.operator === SyntaxKind.MinusMinusToken) {
112
148
  markWrite(node.operand, writes);
149
+ if (!valueDiscarded(node)) {
150
+ markWrite(node.operand, reads);
151
+ }
113
152
  }
114
153
  return;
115
154
  }
@@ -198,15 +237,17 @@ function linksOf(file, node, declarations, enclosing) {
198
237
  * order, each with its enclosing declaration and its use, and every re-export the file
199
238
  * writes. One walk answers all of it, because a second would have to agree with this one
200
239
  * about which nodes are names. A declaration's own declared name is left out, which is
201
- * what makes a declaration referenced only from its own site an unreferenced one.
240
+ * what makes a declaration referenced only from its own site an unreferenced one, and so
241
+ * is every node of `unread`, a subtree that is no use.
202
242
  */
203
- function sitesOf(file, declarations, fileId) {
243
+ function sitesOf(file, declarations, fileId, unread) {
204
244
  const found = [];
205
245
  const reads = [];
206
246
  const links = [];
207
247
  const evaluations = [];
208
248
  const decorated = [];
209
249
  const writes = new Set();
250
+ const alsoRead = new Set();
210
251
  const declared = new Set();
211
252
  const shorthands = new Map();
212
253
  let enclosing = fileId;
@@ -228,19 +269,23 @@ function sitesOf(file, declarations, fileId) {
228
269
  }
229
270
  };
230
271
  const visit = (node) => {
272
+ if (unread.has(node)) {
273
+ return;
274
+ }
231
275
  if (isName(node)) {
232
276
  if (!declared.has(node.pos)) {
233
- found.push({
234
- node,
235
- shorthand: shorthands.get(node.pos),
236
- from: enclosing,
237
- use: writes.has(node.pos) ? "write" : "read",
238
- });
277
+ const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
278
+ if (writes.has(node.pos)) {
279
+ found.push({ ...site, use: "write" });
280
+ }
281
+ if (!writes.has(node.pos) || alsoRead.has(node.pos)) {
282
+ found.push({ ...site, use: "read" });
283
+ }
239
284
  }
240
285
  return;
241
286
  }
242
287
  markNames(node);
243
- markStores(node, writes);
288
+ markStores(node, writes, alsoRead);
244
289
  for (const read of propertyReadsOf(node)) {
245
290
  reads.push({ ...read, from: enclosing });
246
291
  }
@@ -265,6 +310,8 @@ function sitesOf(file, declarations, fileId) {
265
310
  file.forEachChild(visit);
266
311
  return { uses: found, reads, links, evaluations, decorated };
267
312
  }
313
+ /** The rule a file the analysis classified as test-support code is counted under. */
314
+ const TEST_SUPPORT_RULE = "test-support";
268
315
  /**
269
316
  * The name one test-file rule takes. A rule's name is a word list the report's own
270
317
  * vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
@@ -277,17 +324,19 @@ function patternRule(index) {
277
324
  * The rules over the test files of several projects at once, ordered by rule: a file two
278
325
  * projects hold is one file, so each rule counts the distinct paths it matches.
279
326
  */
280
- export function testFileRulesOf(testFiles, paths) {
327
+ export function testFileRulesOf(testFiles, paths, supportFiles = []) {
281
328
  const distinct = [...new Set(paths)];
282
- return testFiles
283
- .map((pattern, index) => {
284
- const expression = globExpression(pattern);
285
- return {
286
- rule: patternRule(index),
287
- matched: distinct.filter((path) => expression.test(path)).length,
288
- };
289
- })
290
- .sort((a, b) => compare(a.rule, b.rule));
329
+ const support = new Set(supportFiles).size;
330
+ return [
331
+ ...testFiles.map((pattern, index) => {
332
+ const expression = globExpression(pattern);
333
+ return {
334
+ rule: patternRule(index),
335
+ matched: distinct.filter((path) => expression.test(path)).length,
336
+ };
337
+ }),
338
+ ...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
339
+ ].sort((a, b) => compare(a.rule, b.rule));
291
340
  }
292
341
  const importedCache = new WeakMap();
293
342
  /**
@@ -332,6 +381,37 @@ function importedNames(file) {
332
381
  importedCache.set(file, names);
333
382
  return names;
334
383
  }
384
+ /**
385
+ * The name node of every import binding one file's top level declares, and whether it
386
+ * binds a whole module.
387
+ */
388
+ function importBindings(file) {
389
+ const found = [];
390
+ for (const statement of file.statements) {
391
+ if (isImportEqualsDeclaration(statement)) {
392
+ found.push({ node: statement.name, whole: true });
393
+ continue;
394
+ }
395
+ const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
396
+ if (clause === undefined) {
397
+ continue;
398
+ }
399
+ if (clause.name !== undefined) {
400
+ found.push({ node: clause.name, whole: false });
401
+ }
402
+ const bindings = clause.namedBindings;
403
+ if (bindings === undefined) {
404
+ continue;
405
+ }
406
+ if (bindings.kind === SyntaxKind.NamespaceImport) {
407
+ found.push({ node: bindings.name, whole: true });
408
+ }
409
+ else {
410
+ found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
411
+ }
412
+ }
413
+ return found;
414
+ }
335
415
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
336
416
  function compare(a, b) {
337
417
  if (a === b) {
@@ -376,6 +456,20 @@ export function references(project, held, targetRoot, options) {
376
456
  .filter((file) => relativePath(consumer.root, file.fileName) !== undefined &&
377
457
  relativePath(targetRoot, file.fileName) === undefined);
378
458
  const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
459
+ let topLevel;
460
+ /** The declarations one file's top level holds. */
461
+ const declaredIn = (fileId) => {
462
+ if (topLevel === undefined) {
463
+ const byParent = new Map();
464
+ for (const symbol of held.symbols) {
465
+ if (fileIds.has(symbol.parent)) {
466
+ byParent.set(symbol.parent, [...(byParent.get(symbol.parent) ?? []), symbol]);
467
+ }
468
+ }
469
+ topLevel = byParent;
470
+ }
471
+ return topLevel.get(fileId) ?? [];
472
+ };
379
473
  /** The identifier of the declaration one node of this program is, where it is one. */
380
474
  const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
381
475
  /**
@@ -405,6 +499,45 @@ export function references(project, held, targetRoot, options) {
405
499
  ]),
406
500
  ];
407
501
  };
502
+ // A type-query alias's own type names its export without using it; a use of the
503
+ // global is the use, so it names every link of the export's chain as well.
504
+ const aliases = project.ownSourceFiles().flatMap(typeQueryAliases);
505
+ const unread = new Set(aliases.map((alias) => alias.type));
506
+ const aliased = new Map();
507
+ if (aliases.length > 0) {
508
+ const exported = project.symbolsAt(aliases.map((alias) => project.handle(alias.exported)));
509
+ aliases.forEach((alias, index) => {
510
+ const id = declaredAt(alias.declaration);
511
+ const symbol = exported[index];
512
+ if (id === undefined || symbol === undefined) {
513
+ return;
514
+ }
515
+ aliased.set(id, symbol === UNANSWERED
516
+ ? (declarationsByName(held).get(alias.name) ?? []).map((one) => ({
517
+ id: one,
518
+ stepped: true,
519
+ guessed: true,
520
+ }))
521
+ : chains.chainOf(symbol).map((target) => ({ ...target, stepped: true })));
522
+ });
523
+ }
524
+ /** One use's targets, then every link an alias global among them stands for, each once. */
525
+ const throughAliases = (targets) => {
526
+ if (aliased.size === 0) {
527
+ return targets;
528
+ }
529
+ const all = [...targets];
530
+ const seen = new Set(targets.map((target) => target.id));
531
+ for (const target of targets) {
532
+ for (const forwarded of aliased.get(target.id) ?? []) {
533
+ if (!seen.has(forwarded.id)) {
534
+ seen.add(forwarded.id);
535
+ all.push(forwarded);
536
+ }
537
+ }
538
+ }
539
+ return all;
540
+ };
408
541
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
409
542
  const symbolFor = (site, answered) => {
410
543
  if (site.shorthand !== undefined) {
@@ -424,7 +557,11 @@ export function references(project, held, targetRoot, options) {
424
557
  if (test) {
425
558
  tests.push(path);
426
559
  }
427
- const { uses, reads, links, evaluations, decorated } = sitesOf(file, declarations, declaredAt(file) ?? "");
560
+ const fileId = declaredAt(file) ?? "";
561
+ const { uses, reads, links, evaluations, decorated } = sitesOf(file, declarations, fileId, unread);
562
+ // A component file's markup may use any binding its blocks declare at the top level.
563
+ const markup = isComponentFile(file);
564
+ const bindings = markup ? importBindings(file) : [];
428
565
  // A decorator is written inside the declaration it is attached to and receives it,
429
566
  // so the declaration is used where the decorator is written, whatever the
430
567
  // decorator does with it.
@@ -448,6 +585,7 @@ export function references(project, held, targetRoot, options) {
448
585
  ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
449
586
  ...links.map((link) => link.node),
450
587
  ...evaluations.map((evaluation) => evaluation.node),
588
+ ...bindings.map((binding) => binding.node),
451
589
  ]),
452
590
  ];
453
591
  const answered = new Map();
@@ -504,6 +642,47 @@ export function references(project, held, targetRoot, options) {
504
642
  }
505
643
  }
506
644
  }
645
+ if (markup) {
646
+ for (const symbol of declaredIn(fileId)) {
647
+ found.push({
648
+ from: fileId,
649
+ to: symbol.id,
650
+ position: symbol.position,
651
+ use: "read",
652
+ resolution: "syntax",
653
+ test,
654
+ });
655
+ }
656
+ for (const binding of bindings) {
657
+ const symbol = answered.get(binding.node);
658
+ if (symbol === undefined) {
659
+ continue;
660
+ }
661
+ const position = renderPosition(file, root, binding.node.getStart());
662
+ const chained = symbol === UNANSWERED
663
+ ? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
664
+ : chains.chainOf(symbol);
665
+ // The markup may name any export of a module a binding holds whole.
666
+ const targets = binding.whole
667
+ ? chained.flatMap((target) => [
668
+ target,
669
+ ...declaredIn(target.id)
670
+ .filter((one) => one.exported)
671
+ .map((one) => ({ ...target, id: one.id })),
672
+ ])
673
+ : chained;
674
+ for (const target of targets) {
675
+ found.push({
676
+ from: fileId,
677
+ to: target.id,
678
+ position,
679
+ use: "read",
680
+ resolution: target.guessed ? "by-name" : target.stepped ? "alias" : "batch",
681
+ test,
682
+ });
683
+ }
684
+ }
685
+ }
507
686
  // A pattern names a property of the value it destructures, which no name node of
508
687
  // the pattern resolves to: a shorthand's name is the local it binds.
509
688
  destructured.resolve(reads).forEach((symbol, index) => {
@@ -543,9 +722,9 @@ export function references(project, held, targetRoot, options) {
543
722
  const position = renderPosition(file, root, site.node.getStart());
544
723
  // A re-export something imports is used and so is the declaration behind it, so
545
724
  // the use names every link of the chain its name stands at the head of.
546
- const targets = symbol === UNANSWERED
725
+ const targets = throughAliases(symbol === UNANSWERED
547
726
  ? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
548
- : chains.chainOf(symbol);
727
+ : chains.chainOf(symbol));
549
728
  for (const target of targets) {
550
729
  found.push({
551
730
  from: site.from,
@@ -4,12 +4,9 @@
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 = "6.1.0";
8
- /**
9
- * Every report schema version this analyzer reads: the schema versions the Contract it
10
- * implements admits, each an instance of the one it writes.
11
- */
12
- export const SCHEMA_VERSIONS_ACCEPTED = ["6.0.0", SCHEMA_VERSION];
7
+ const SCHEMA_VERSION = "7.0.0";
8
+ /** Every report schema version this analyzer reads: the one the Contract it implements admits. */
9
+ export const SCHEMA_VERSIONS_ACCEPTED = [SCHEMA_VERSION];
13
10
  /** The name this analyzer writes every document under and mints every identifier with. */
14
11
  export const ANALYZER_NAME = "deadset-ts";
15
12
  /** The language this analyzer claims. */
@@ -234,6 +231,29 @@ export function buildReport(input) {
234
231
  test_file_rules: [...input.testFileRules]
235
232
  .sort((a, b) => compare(a.rule, b.rule) || a.matched - b.matched)
236
233
  .map((one) => ({ rule: one.rule, matched: one.matched })),
234
+ type_error_skips: [
235
+ ...new Map(input.typeErrorSkips.map((one) => {
236
+ const skip = { path: one.path, line: one.line, message: one.message };
237
+ return [JSON.stringify(skip), skip];
238
+ })),
239
+ ]
240
+ .sort(([a, x], [b, y]) => compare(x.path, y.path) || x.line - y.line || compare(a, b))
241
+ .map(([, skip]) => skip),
242
+ notes: [],
243
+ unanswered_questions: [...input.unanswered]
244
+ .sort((a, b) => compare(a.configuration, b.configuration) ||
245
+ compare(JSON.stringify(a), JSON.stringify(b)))
246
+ .map((one) => ({
247
+ configuration: one.configuration,
248
+ questions: one.questions,
249
+ declarations: one.declarations,
250
+ })),
251
+ conventions_applied: input.conventionsApplied.map((one) => ({
252
+ name: one.name,
253
+ package: one.package,
254
+ version: one.version,
255
+ manifest: one.manifest,
256
+ })),
237
257
  totals: totalsOf(findings, input.result),
238
258
  };
239
259
  }
@@ -22,11 +22,15 @@ function kindName(code) {
22
22
  function command(level, at, endLine, title, message) {
23
23
  return `::${level} file=${escapeProperty(at.path)},line=${String(at.line)},col=${String(at.column)},endLine=${String(endLine)},title=${escapeProperty(title)}::${escapeData(message)}`;
24
24
  }
25
+ /** The title and the message prefix of a type-error skip's annotation. */
26
+ const SKIP_TITLE = "type error skipped";
27
+ const SKIP_MESSAGE = "the analysis did not evaluate the function or statement holding this type error: ";
25
28
  /**
26
29
  * One workflow annotation per finding, then one per stale suppression, each naming its
27
- * file, line, column and last line, titled by its code and kind. A finding at or above the
28
- * failing severity is an error and one below it a warning; a stale suppression is an error,
29
- * because its kind is fixed on at the failing severity.
30
+ * file, line, column and last line, titled by its code and kind, then one warning per
31
+ * type-error skip naming its file and line. A finding at or above the failing severity is
32
+ * an error and one below it a warning; a stale suppression is an error, because its kind
33
+ * is fixed on at the failing severity.
30
34
  */
31
35
  export function annotations(report, options) {
32
36
  return coalesced((function* lines() {
@@ -36,5 +40,8 @@ export function annotations(report, options) {
36
40
  for (const stale of report.stale_suppressions) {
37
41
  yield `${command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message)}\n`;
38
42
  }
43
+ for (const skip of report.type_error_skips) {
44
+ yield `::warning file=${escapeProperty(skip.path)},line=${String(skip.line)},title=${escapeProperty(SKIP_TITLE)}::${escapeData(SKIP_MESSAGE + skip.message)}\n`;
45
+ }
39
46
  })());
40
47
  }
@@ -775,7 +775,7 @@ class Execution {
775
775
  let held = value;
776
776
  for (const name of names) {
777
777
  if (!isObject(held)) {
778
- throw new RenderError(`can't evaluate field ${name} in a ${kindOf(held)}`);
778
+ throw new RenderError(`cannot evaluate field ${name} in a ${kindOf(held)}`);
779
779
  }
780
780
  if (!Object.hasOwn(held, name)) {
781
781
  throw new RenderError(`the document has no member ${JSON.stringify(name)} here`);
@@ -805,9 +805,12 @@ class Execution {
805
805
  if (head === undefined) {
806
806
  throw new RenderError("empty command");
807
807
  }
808
+ if (head.k === "lit" && head.value === null) {
809
+ throw new RenderError("nil is not a command");
810
+ }
808
811
  if (head.k !== "func") {
809
812
  if (rest.length > 0 || piped !== undefined) {
810
- throw new RenderError("can't give an argument to a non-function");
813
+ throw new RenderError("cannot give an argument to a non-function");
811
814
  }
812
815
  return this.arg(head, dot);
813
816
  }
@@ -903,7 +906,7 @@ class Execution {
903
906
  .map((key) => [key, value[key] ?? null]);
904
907
  }
905
908
  else {
906
- throw new RenderError(`range can't iterate over a ${kindOf(value)}`);
909
+ throw new RenderError(`range cannot iterate over a ${kindOf(value)}`);
907
910
  }
908
911
  if (entries.length === 0) {
909
912
  this.run(node.otherwise, dot);
@@ -1,4 +1,5 @@
1
1
  import { ConfigError, defaultConfig, } from "./config.js";
2
+ import { CONVENTION_ROWS } from "./convention-rows.js";
2
3
  import { FIXED_SEVERITY_CODES, LIVE_CODES } from "./kinds.js";
3
4
  import { checkDocument, malformed, nestFlags, SCHEMA_ROOT, SEVERITY_SECTION, } from "./schema.js";
4
5
  /** Names the command-line flags in a refusal, where a document is named by its path. */
@@ -149,6 +150,26 @@ function readStrings(parent, name, path, label, minItems, allowed) {
149
150
  }
150
151
  return value;
151
152
  }
153
+ /** A component file's extension: a full stop followed by letters and digits. */
154
+ const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
155
+ function readComponentExtensions(ts, label) {
156
+ const extensions = readStrings(ts, "component_extensions", "ts.component_extensions", label, 0);
157
+ const wrong = extensions?.find((one) => !COMPONENT_EXTENSION.test(one));
158
+ if (wrong !== undefined) {
159
+ throw malformed(label, "ts.component_extensions", `${JSON.stringify(wrong)} is not a full stop followed by letters and digits`);
160
+ }
161
+ return extensions;
162
+ }
163
+ /** The names of the convention rows this analyzer's table carries. */
164
+ const CONVENTION_NAMES = new Set(CONVENTION_ROWS.map((row) => row.name));
165
+ function readDisabledConventions(ts, label) {
166
+ const names = readStrings(ts, "disabled_conventions", "ts.disabled_conventions", label, 0);
167
+ const unknown = names?.find((one) => !CONVENTION_NAMES.has(one));
168
+ if (unknown !== undefined) {
169
+ throw malformed(label, "ts.disabled_conventions", `${JSON.stringify(unknown)} names no convention row this analyzer carries`);
170
+ }
171
+ return names;
172
+ }
152
173
  function requireMember(value, path, label) {
153
174
  if (value === undefined || value === "") {
154
175
  throw malformed(label, path, "is required and names nothing");
@@ -507,6 +528,8 @@ function readDoc(value, label) {
507
528
  providers: readProviders(providers, label),
508
529
  testFiles: readStrings(ts, "test_files", "ts.test_files", label, 1),
509
530
  entryFiles: readStrings(ts, "entry_files", "ts.entry_files", label, 0),
531
+ componentExtensions: readComponentExtensions(ts, label),
532
+ disabledConventions: readDisabledConventions(ts, label),
510
533
  injectionRegistrations: readDeclarations(ts, "injection_registrations", "ts.injection_registrations", label),
511
534
  lifecycleContracts: readLifecycleContracts(ts, label),
512
535
  serializers: readDeclarations(ts, "serializers", "ts.serializers", label),
@@ -667,13 +690,13 @@ export function resolve(inputs) {
667
690
  return { config, provenance };
668
691
  }
669
692
  /**
670
- * The build matrix the configuration documents resolve to, every document read and
671
- * refused as {@link resolve} refuses it, and nothing else required of them: which
672
- * projects a run analyzes depends on the matrix alone, so a question about the
673
- * projects asks no target kind.
693
+ * The configuration the documents resolve to, every document read and refused as
694
+ * {@link resolve} refuses it, with no target kind required of them: which projects a
695
+ * run analyzes does not depend on the kind, so a question about the projects asks
696
+ * none. The kind is empty where no document names one.
674
697
  */
675
- export function resolveMatrix(inputs) {
676
- return settleAll(inputs).config.analysis.configurations;
698
+ export function resolveUnkinded(inputs) {
699
+ return settleAll(inputs).config;
677
700
  }
678
701
  /**
679
702
  * Every setting the documents supply, each from the highest-ranked source carrying
@@ -714,6 +737,8 @@ function settleAll(inputs) {
714
737
  ts: {
715
738
  testFiles: at("ts.test_files", defaults.ts.testFiles, (doc) => doc.testFiles),
716
739
  entryFiles: at("ts.entry_files", defaults.ts.entryFiles, (doc) => doc.entryFiles),
740
+ componentExtensions: at("ts.component_extensions", defaults.ts.componentExtensions, (doc) => doc.componentExtensions),
741
+ disabledConventions: at("ts.disabled_conventions", defaults.ts.disabledConventions, (doc) => doc.disabledConventions),
717
742
  injectionRegistrations: at("ts.injection_registrations", defaults.ts.injectionRegistrations, (doc) => doc.injectionRegistrations),
718
743
  lifecycleContracts: at("ts.lifecycle_contracts", defaults.ts.lifecycleContracts, (doc) => doc.lifecycleContracts),
719
744
  serializers: at("ts.serializers", defaults.ts.serializers, (doc) => doc.serializers),
package/dist/src/roots.js CHANGED
@@ -8,12 +8,20 @@
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
10
  import { aliasChains } from "./alias-chain.js";
11
+ import { conventionEntries } from "./conventions.js";
11
12
  import { entryPoints } from "./entry-points.js";
12
13
  import { globExpression } from "./glob.js";
13
14
  import { nodeKey } from "./inventory.js";
14
15
  import { byPosition } from "./position.js";
15
16
  import { UNANSWERED } from "./query.js";
16
17
  import { sourceFilesOf } from "./source-files.js";
18
+ import { typeQueryAliases } from "./type-query-alias.js";
19
+ /** The kind of root each role of a manifest entry makes. */
20
+ const ENTRY_KINDS = {
21
+ import: "manifest-entry",
22
+ run: "manifest-binary",
23
+ script: "script",
24
+ };
17
25
  /** Whether a configured string carries either special character. */
18
26
  function isPattern(text) {
19
27
  return text.includes("*") || text.includes("?");
@@ -117,18 +125,23 @@ export function roots(project, held, targetRoot, options) {
117
125
  entered.push({ file, kind, source, exports });
118
126
  };
119
127
  const published = [];
120
- const publishes = (entry) => options.publishedAPI &&
121
- entry.published &&
122
- (options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
123
- for (const entry of options.manifest.entries) {
124
- if (publishes(entry)) {
128
+ const entries = [
129
+ ...options.manifest.entries.map((entry) => ({
130
+ entry,
131
+ publishes: entry.published &&
132
+ (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
133
+ })),
134
+ ...(options.workspaceEntries ?? []).map((entry) => ({ entry, publishes: entry.published })),
135
+ ];
136
+ for (const { entry, publishes } of entries) {
137
+ if (options.publishedAPI && publishes) {
125
138
  published.push(...files.named(entry.path));
126
139
  }
127
140
  }
128
141
  const publishedNames = new Set(published.map((file) => file.fileName));
129
- for (const entry of options.manifest.entries) {
142
+ for (const { entry } of entries) {
130
143
  for (const file of files.named(entry.path)) {
131
- const kind = entry.role === "run" ? "manifest-binary" : "manifest-entry";
144
+ const kind = ENTRY_KINDS[entry.role];
132
145
  enter(file, kind, entry.member, !publishedNames.has(file.fileName));
133
146
  }
134
147
  }
@@ -140,9 +153,25 @@ export function roots(project, held, targetRoot, options) {
140
153
  }
141
154
  }
142
155
  }
143
- for (const point of entryPoints(project, files, options.testFiles)) {
156
+ for (const { file, row } of options.conventions === undefined
157
+ ? []
158
+ : conventionEntries(options.conventions, files)) {
159
+ enter(file, "convention", row, true);
160
+ }
161
+ for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot)) {
144
162
  enter(point.file, point.rule, point.source, point.exports);
145
163
  }
164
+ for (const file of files.byPath.values()) {
165
+ for (const alias of typeQueryAliases(file)) {
166
+ const source = alias.type.getText().replace(/\s+/gu, " ");
167
+ for (const node of [alias.declaration, ...alias.containers]) {
168
+ const id = declared.get(nodeKey(file, node));
169
+ if (id !== undefined) {
170
+ add(id, "type-query-alias", source);
171
+ }
172
+ }
173
+ }
174
+ }
146
175
  const { tables: exportsOf, answered } = exportTables(project, held, [
147
176
  ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
148
177
  ...published,
package/dist/src/run.js CHANGED
@@ -3,6 +3,7 @@ import { DiscoveryError, renderDiagnostic } from "./discover.js";
3
3
  import { EMITTERS } from "./findings/emitters.js";
4
4
  import { joinPath, resolvePath } from "./paths.js";
5
5
  import { readScope, ScopeError, scopeForDir } from "./scope.js";
6
+ import { SetupError, setupLine } from "./setup-failure.js";
6
7
  import { openEngine } from "./session.js";
7
8
  import { printConfigVerb } from "./verbs/print-config.js";
8
9
  import { printProjectsVerb } from "./verbs/print-projects.js";
@@ -158,7 +159,7 @@ function scopeOf(host, options) {
158
159
  * `openClient` opens the compiler client; a caller that watches what a run reads
159
160
  * supplies its own of either.
160
161
  */
161
- export function run(args, out, err, host, openClient = (collectTiming) => openEngine({ collectTiming })) {
162
+ export function run(args, out, err, host, openClient = openEngine) {
162
163
  const editing = sourceEditOption(args);
163
164
  if (editing !== undefined) {
164
165
  err.write(`deadset-ts: ${editing} is not supported: deadset-ts reports and never edits a source file\n`);
@@ -193,8 +194,11 @@ function invoke(verb, own, args, streams, openClient) {
193
194
  scope: () => scopeOf(streams.host, read()),
194
195
  option: (name) => read().named.get(name),
195
196
  repeated: (name) => read().repeated.get(name) ?? [],
196
- openClient: (collectTiming) => {
197
- opened = openClient(collectTiming);
197
+ openClient: (config) => {
198
+ opened = openClient({
199
+ collectTiming: false,
200
+ components: { extensions: config.ts.componentExtensions, host: streams.host },
201
+ });
198
202
  return opened;
199
203
  },
200
204
  emitters: EMITTERS,
@@ -219,6 +223,12 @@ function refuse(error, err) {
219
223
  err.write(`deadset-ts: ${error.message}\n`);
220
224
  return EXIT_FAILURE;
221
225
  }
226
+ if (error instanceof SetupError) {
227
+ for (const failure of error.failures) {
228
+ err.write(`${setupLine(failure)}\n`);
229
+ }
230
+ return EXIT_FAILURE;
231
+ }
222
232
  if (error instanceof ScopeError) {
223
233
  err.write(`deadset-ts: ${error.message}\n`);
224
234
  return EXIT_FAILURE;