@cplieger/deadset-ts 5.2.3 → 6.0.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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +28 -5
  3. package/dist/src/augmentations.js +50 -0
  4. package/dist/src/components.js +15 -15
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/convention-rows.js +17 -2
  7. package/dist/src/conventions.js +82 -1
  8. package/dist/src/dependencies.js +42 -26
  9. package/dist/src/entry-points.js +28 -14
  10. package/dist/src/exempt.js +1 -0
  11. package/dist/src/findings/completion.js +9 -4
  12. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  13. package/dist/src/findings/emitters.js +64 -25
  14. package/dist/src/findings/interfaces.js +4 -3
  15. package/dist/src/findings/intra-function.js +15 -2
  16. package/dist/src/findings/reads-and-writes.js +3 -2
  17. package/dist/src/findings/unused-declarations.js +13 -1
  18. package/dist/src/findings/visibility-narrowing.js +2 -1
  19. package/dist/src/findings-pass.js +1 -0
  20. package/dist/src/graph.js +6 -6
  21. package/dist/src/implementations.js +1 -1
  22. package/dist/src/indexed-access.js +215 -0
  23. package/dist/src/interface-satisfaction.js +85 -23
  24. package/dist/src/intra-function-parts.js +139 -30
  25. package/dist/src/inventory.js +6 -1
  26. package/dist/src/kinds.js +13 -12
  27. package/dist/src/matrix.js +34 -9
  28. package/dist/src/module-declarations.js +180 -0
  29. package/dist/src/outside-tests.js +17 -15
  30. package/dist/src/overrides.js +70 -9
  31. package/dist/src/query.js +7 -0
  32. package/dist/src/ref.js +9 -0
  33. package/dist/src/references.js +6 -0
  34. package/dist/src/report.js +6 -1
  35. package/dist/src/reporters/sarif.js +6 -2
  36. package/dist/src/reporters/text.js +23 -3
  37. package/dist/src/roots.js +50 -10
  38. package/dist/src/suppress-file.js +3 -5
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/workflow-steps.js +82 -0
  41. package/dist/src/workspace-programs.js +6 -31
  42. package/jsr.json +1 -1
  43. package/package.json +8 -2
  44. package/src/analysis.ts +40 -4
  45. package/src/augmentations.ts +72 -0
  46. package/src/components.ts +15 -13
  47. package/src/conformance-record.ts +2 -2
  48. package/src/convention-rows.ts +25 -3
  49. package/src/conventions.ts +106 -2
  50. package/src/dependencies.ts +51 -31
  51. package/src/entry-points.ts +32 -19
  52. package/src/exempt.ts +4 -1
  53. package/src/findings/completion.ts +11 -4
  54. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  55. package/src/findings/emitters.ts +89 -27
  56. package/src/findings/interfaces.ts +7 -6
  57. package/src/findings/intra-function.ts +20 -2
  58. package/src/findings/reads-and-writes.ts +7 -2
  59. package/src/findings/unused-declarations.ts +16 -1
  60. package/src/findings/visibility-narrowing.ts +2 -1
  61. package/src/findings-pass.ts +4 -1
  62. package/src/graph.ts +6 -6
  63. package/src/implementations.ts +1 -1
  64. package/src/indexed-access.ts +295 -0
  65. package/src/interface-satisfaction.ts +118 -27
  66. package/src/intra-function-parts.ts +196 -35
  67. package/src/inventory.ts +8 -0
  68. package/src/kinds.ts +14 -12
  69. package/src/matrix.ts +51 -8
  70. package/src/module-declarations.ts +241 -0
  71. package/src/outside-tests.ts +18 -16
  72. package/src/overrides.ts +80 -14
  73. package/src/query.ts +28 -0
  74. package/src/ref.ts +10 -0
  75. package/src/references.ts +6 -0
  76. package/src/report.ts +11 -1
  77. package/src/reporters/sarif.ts +9 -2
  78. package/src/reporters/text.ts +25 -3
  79. package/src/roots.ts +56 -7
  80. package/src/suppress-file.ts +3 -6
  81. package/src/sweep.ts +12 -0
  82. package/src/version.ts +1 -1
  83. package/src/workflow-steps.ts +104 -0
  84. package/src/workspace-programs.ts +6 -35
package/src/roots.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  isQualifiedName,
16
16
  isTypeQueryNode,
17
17
  isTypeReferenceNode,
18
+ isVariableStatement,
18
19
  type Node,
19
20
  type SourceFile,
20
21
  } from "@typescript/native/unstable/ast";
@@ -297,6 +298,7 @@ export function roots<Brand>(
297
298
  options.host,
298
299
  targetRoot,
299
300
  options.parse,
301
+ options.conventions?.selected,
300
302
  );
301
303
  const published: SourceFile[] = [];
302
304
  const entries = [
@@ -379,7 +381,7 @@ export function roots<Brand>(
379
381
  }
380
382
 
381
383
  const api = options.publishedAPI
382
- ? publishedAPI(project, published, exportsOf, held, files.byPath)
384
+ ? publishedAPI(project, published, exportsOf, held, files.byPath, releaseTagged(held, files))
383
385
  : { ids: [], guessed: [] };
384
386
  for (const id of api.ids) {
385
387
  add(id, "published-api", "");
@@ -453,16 +455,59 @@ function exportTables<Brand>(
453
455
  return { tables, answered };
454
456
  }
455
457
 
458
+ /** A release tag of a documentation comment: `@` and its name, between white space or the ends. */
459
+ const RELEASE_TAG = /(?:^|\s)@(?:public|beta|alpha)(?=\s|$)/u;
460
+
461
+ /**
462
+ * Whether the documentation comment of the statement that starts at `start`, the last
463
+ * `/** *\/` comment before it with only white space between the two, holds a release tag.
464
+ */
465
+ function releaseTagIn(text: string, from: number, start: number): boolean {
466
+ const leading = text.slice(from, start);
467
+ const open = leading.lastIndexOf("/**");
468
+ const close = open < 0 ? -1 : leading.indexOf("*/", open + 3);
469
+ if (close < 0 || leading.slice(close + 2).trim() !== "") {
470
+ return false;
471
+ }
472
+ return RELEASE_TAG.test(leading.slice(open + 3, close));
473
+ }
474
+
475
+ /** The exported top-level declarations of the program's files a release tag marks. */
476
+ function releaseTagged(
477
+ held: Inventory,
478
+ files: { readonly byPath: ReadonlyMap<string, SourceFile> },
479
+ ): readonly string[] {
480
+ const exported = new Set(held.symbols.filter((symbol) => symbol.exported).map((one) => one.id));
481
+ const found: string[] = [];
482
+ for (const file of files.byPath.values()) {
483
+ for (const statement of file.statements) {
484
+ if (!releaseTagIn(file.text, statement.pos, statement.getStart())) {
485
+ continue;
486
+ }
487
+ const nodes: readonly Node[] = isVariableStatement(statement)
488
+ ? statement.declarationList.declarations
489
+ : [statement];
490
+ for (const node of nodes) {
491
+ const id = held.declarations.get(nodeKey(file, node));
492
+ if (id !== undefined && exported.has(id)) {
493
+ found.push(id);
494
+ }
495
+ }
496
+ }
497
+ }
498
+ return found;
499
+ }
500
+
456
501
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
457
502
  const EXPORTING_CONTAINERS: ReadonlySet<string> = new Set(["file", "namespace"]);
458
503
 
459
504
  /**
460
- * Every declaration a consumer of the library can name: what the published files export
461
- * and, below each, the members a consumer writes: a namespace's exported ones, any other
462
- * declaration's public and protected ones, never a private one or a type parameter. A
463
- * declaration a published one's type names is published too, exported or not. A type name
464
- * the checker leaves unanswered publishes every declaration spelled as it is, and those are
465
- * `guessed`. A published file is not itself published.
505
+ * Every declaration a consumer of the library can name: what the published files export,
506
+ * and below each the members a consumer writes, a namespace's exported ones and any other
507
+ * declaration's public and protected ones. A declaration a published one's type names is
508
+ * published too, and so is an exported declaration a release tag marks. A type name the
509
+ * checker leaves unanswered publishes every declaration spelled as it is, as `guessed`. A
510
+ * published file is not itself published.
466
511
  */
467
512
  function publishedAPI<Brand>(
468
513
  project: ProjectView<Brand>,
@@ -470,6 +515,7 @@ function publishedAPI<Brand>(
470
515
  exportsOf: ReadonlyMap<string, readonly string[]>,
471
516
  held: Inventory,
472
517
  byPath: ReadonlyMap<string, SourceFile>,
518
+ tagged: readonly string[],
473
519
  ): { readonly ids: readonly string[]; readonly guessed: readonly string[] } {
474
520
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
475
521
  const children = childrenOf(held);
@@ -484,6 +530,9 @@ function publishedAPI<Brand>(
484
530
  pending.push(id);
485
531
  }
486
532
  };
533
+ for (const id of tagged) {
534
+ reach(id);
535
+ }
487
536
  for (const file of published) {
488
537
  for (const id of exportsOf.get(file.fileName) ?? []) {
489
538
  if (byId.get(id)?.kind === "file") {
@@ -11,6 +11,7 @@ import { jsonChunks } from "./json-chunks.ts";
11
11
  import { joinPath } from "./paths.ts";
12
12
  import { positionAt, walkDocument } from "./json-document.ts";
13
13
  import type { Position } from "./position.ts";
14
+ import { declarationKey } from "./ref.ts";
14
15
  import {
15
16
  CODE_FORM,
16
17
  SUPPRESSION_WITHOUT_REASON,
@@ -84,10 +85,6 @@ interface Read {
84
85
  /** The declarations of the run by reference and file, which is what an entry names. */
85
86
  type ByReference = ReadonlyMap<string, readonly InventorySymbol[]>;
86
87
 
87
- function referenceKey(ref: string, path: string): string {
88
- return `${ref}\u0000${path}`;
89
- }
90
-
91
88
  /**
92
89
  * Reads one entry or row: the records it binds, the refusals it carries, or the
93
90
  * malformed value that ends the run.
@@ -148,7 +145,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
148
145
  if (refusals.length > 0) {
149
146
  return { records: [], refusals };
150
147
  }
151
- const named = byReference.get(referenceKey(symbol, written.path)) ?? [];
148
+ const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
152
149
  if (named.length === 0) {
153
150
  return { records: [{ ...written, bound: "" }], refusals: [] };
154
151
  }
@@ -158,7 +155,7 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
158
155
  function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
159
156
  const byReference = new Map<string, InventorySymbol[]>();
160
157
  for (const symbol of symbols) {
161
- const key = referenceKey(symbol.ref, symbol.position.path);
158
+ const key = declarationKey(symbol.ref, symbol.position.path);
162
159
  byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
163
160
  }
164
161
  return byReference;
package/src/sweep.ts CHANGED
@@ -27,6 +27,18 @@ export interface Exemption {
27
27
  * record holds only while that file is live.
28
28
  */
29
29
  readonly whileLive?: string;
30
+ /**
31
+ * The interface methods the retained member implements, and whether its body is empty.
32
+ * The record holds only while one of them is called through its interface.
33
+ */
34
+ readonly implementing?: Implementing;
35
+ }
36
+
37
+ /** The interface methods one retained class member implements, and whether its body is empty. */
38
+ export interface Implementing {
39
+ /** The declarations of the interface's methods the member answers, by identifier. */
40
+ readonly methods: readonly string[];
41
+ readonly empty: boolean;
30
42
  }
31
43
 
32
44
  /**
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "5.3.0";
7
+ export const CONTRACT_VERSION = "6.0.0";
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The source files a CI workflow's `run:` steps name by their path from the target root.
3
+ * A workflow file is read as text, line by line, with no YAML parsed.
4
+ */
5
+
6
+ import type { SourceFile } from "@typescript/native/unstable/ast";
7
+ import type { Host } from "./host.ts";
8
+ import { scriptTokens } from "./manifest.ts";
9
+ import { joinPath, resolvePath } from "./paths.ts";
10
+ import type { SourceFiles } from "./source-files.ts";
11
+
12
+ /** One file a workflow step names. */
13
+ interface WorkflowEntry {
14
+ readonly file: SourceFile;
15
+ /** The token that named it, as the command writes it. */
16
+ readonly source: string;
17
+ }
18
+
19
+ const WORKFLOWS = ".github/workflows";
20
+
21
+ /** A line holding a run command: indentation and one sequence marker before `run:`. */
22
+ const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
23
+
24
+ const BLOCK_SCALAR = /^[|>][+-]?$/u;
25
+
26
+ const QUOTED = /^(["'])(.*)\1$/su;
27
+
28
+ /** A token holding one of these is computed or a pattern, so it names no file. */
29
+ const COMPUTED = /[${*?[]/u;
30
+
31
+ const INDENT = /^\s*/u;
32
+
33
+ function workflowFiles(host: Host, dir: string): readonly string[] {
34
+ let entries;
35
+ try {
36
+ entries = host.readDirectory(dir);
37
+ } catch {
38
+ return [];
39
+ }
40
+ return entries
41
+ .filter((entry) => !entry.directory && /\.ya?ml$/u.test(entry.name))
42
+ .map((entry) => joinPath(dir, entry.name))
43
+ .sort();
44
+ }
45
+
46
+ /** Every command line one workflow file's `run:` steps hold. */
47
+ export function runCommands(text: string): readonly string[] {
48
+ const lines = text.split(/\r?\n/u);
49
+ const found: string[] = [];
50
+ for (let at = 0; at < lines.length; at += 1) {
51
+ const match = RUN_LINE.exec(lines[at] ?? "");
52
+ if (match === null) {
53
+ continue;
54
+ }
55
+ const column = (match[1] ?? "").length;
56
+ const value = (match[2] ?? "").trim().replace(QUOTED, "$2");
57
+ if (!BLOCK_SCALAR.test(value)) {
58
+ found.push(value);
59
+ continue;
60
+ }
61
+ for (let next = at + 1; next < lines.length; next += 1) {
62
+ const line = lines[next] ?? "";
63
+ if (line.trim() !== "" && (INDENT.exec(line)?.[0].length ?? 0) <= column) {
64
+ break;
65
+ }
66
+ found.push(line);
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+
72
+ /**
73
+ * The own files the `run:` steps of every workflow file in the target root's
74
+ * `.github/workflows` name: a token equal to a file's path below the target root, with or
75
+ * without a leading `./`, whatever directory the step runs in.
76
+ */
77
+ export function workflowEntries(
78
+ host: Host,
79
+ targetRoot: string,
80
+ files: SourceFiles,
81
+ ): readonly WorkflowEntry[] {
82
+ const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
83
+ const found: WorkflowEntry[] = [];
84
+ for (const path of workflowFiles(host, dir)) {
85
+ let text: string;
86
+ try {
87
+ text = host.readFile(path);
88
+ } catch {
89
+ continue;
90
+ }
91
+ for (const command of runCommands(text)) {
92
+ for (const token of scriptTokens(command)) {
93
+ if (COMPUTED.test(token)) {
94
+ continue;
95
+ }
96
+ const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
97
+ if (file !== undefined) {
98
+ found.push({ file, source: token });
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return found;
104
+ }
@@ -17,7 +17,7 @@ import type {
17
17
  import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
18
18
  import type { StringLiteralLikeNode } from "@typescript/native/unstable/ast";
19
19
  import type { Host } from "./host.ts";
20
- import { dirnamePath, joinPath, resolvePath } from "./paths.ts";
20
+ import { dirnamePath, resolvePath } from "./paths.ts";
21
21
  import type { Engine } from "./session.ts";
22
22
  import type { WorkspaceResolver } from "./workspace-resolution.ts";
23
23
 
@@ -82,34 +82,6 @@ function analysisOptions(parsed: Parsed): CreateSnapshotProgramParams["compilerO
82
82
  return composite === true ? { ...kept, declaration: true } : kept;
83
83
  }
84
84
 
85
- /**
86
- * The references a built program can carry: only where every referenced
87
- * configuration reads and is composite with output, because the compiler reports a
88
- * broken reference at syntax a built program does not have.
89
- */
90
- function carriedReferences(
91
- engine: Engine,
92
- parsed: Parsed,
93
- ): NonNullable<Parsed["projectReferences"]> {
94
- const references = parsed.projectReferences ?? [];
95
- for (const reference of references) {
96
- // A reference that names a directory names the `tsconfig.json` in it.
97
- const configFile = reference.path.endsWith(".json")
98
- ? reference.path
99
- : joinPath(reference.path, "tsconfig.json");
100
- let options: Parsed["options"];
101
- try {
102
- options = engine.parseConfigFile(configFile).options;
103
- } catch {
104
- return [];
105
- }
106
- if (options.composite !== true || options.noEmit === true) {
107
- return [];
108
- }
109
- }
110
- return references;
111
- }
112
-
113
85
  /** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
114
86
  function conditionsOf(parsed: Parsed, mode: ResolutionMode | undefined): ReadonlySet<string> {
115
87
  const resolution = parsed.options.moduleResolution;
@@ -132,7 +104,6 @@ function resolutionMode(kind: ModuleKind): ResolutionMode | undefined {
132
104
  interface Reading {
133
105
  readonly configFile: string;
134
106
  readonly parsed: Parsed;
135
- readonly references: NonNullable<Parsed["projectReferences"]>;
136
107
  readonly entries: ModuleResolutionEntry[];
137
108
  readonly seen: Set<string>;
138
109
  /** Per member specifier, every file an import of it is read as. */
@@ -181,7 +152,7 @@ function configurationOf(
181
152
  if (reason !== undefined) {
182
153
  return { kind: "unread", reason };
183
154
  }
184
- const { options } = reading.parsed;
155
+ const { options, projectReferences: references = [] } = reading.parsed;
185
156
  const raw: unknown = (options as Record<string, unknown>)["pathsBasePath"];
186
157
  const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
187
158
  const paths: Record<string, string[]> = {};
@@ -204,8 +175,8 @@ function configurationOf(
204
175
  ...(options.composite === true ? { composite: false, declaration: true } : {}),
205
176
  paths,
206
177
  },
207
- ...(reading.references.length > 0
208
- ? { references: reading.references.map((reference) => ({ path: reference.path })) }
178
+ ...(references.length > 0
179
+ ? { references: references.map((reference) => ({ path: reference.path })) }
209
180
  : {}),
210
181
  ...(exclude === undefined ? {} : { exclude }),
211
182
  });
@@ -240,6 +211,7 @@ function programOf(
240
211
  reading: Reading,
241
212
  ): { readonly params: CreateSnapshotProgramParams; readonly resolver: ModuleResolver } {
242
213
  const compilerOptions = analysisOptions(reading.parsed);
214
+ const references = reading.parsed.projectReferences ?? [];
243
215
  const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
244
216
  return {
245
217
  resolver,
@@ -249,7 +221,7 @@ function programOf(
249
221
  options: {
250
222
  moduleResolver: resolver,
251
223
  configFileParsingDiagnostics: reading.parsed.errors,
252
- ...(reading.references.length > 0 ? { projectReferences: reading.references } : {}),
224
+ ...(references.length > 0 ? { projectReferences: references } : {}),
253
225
  },
254
226
  },
255
227
  };
@@ -436,7 +408,6 @@ export function workspacePrograms(
436
408
  {
437
409
  configFile,
438
410
  parsed,
439
- references: carriedReferences(engine, parsed),
440
411
  entries: [],
441
412
  seen: new Set<string>(),
442
413
  bound: new Map<string, Set<string>>(),