@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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 (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The one reader of every command string the analysis reads. Layer 3, the holding rule
3
- * `roots/root-set.ts` applies through `text/named-files.ts`, reads all of the string's text
4
- * whatever the other two layers make of it; layer 1 parses the string and layer 2 reads each
5
- * simple command a tool's reader understands, and they only add the files and modules it runs.
3
+ * `roots/root-set.ts` applies through `commands/held.ts` and `text/named-files.ts`, reads every
4
+ * word of the string whatever the other two layers make of it; layer 1 parses the string and
5
+ * layer 2 reads each simple command a tool's reader understands, and they only add the files
6
+ * and modules it runs.
6
7
  */
7
8
 
8
9
  import { joinPath, resolvePath } from "../paths.ts";
@@ -11,10 +12,11 @@ import {
11
12
  configurationsOf,
12
13
  preloadTarget,
13
14
  readBuiltin,
14
- readTool,
15
+ handedOn,
16
+ testRun,
15
17
  type Outcome,
16
18
  type Preload,
17
- type Reading,
19
+ type TestRun,
18
20
  } from "./tools.ts";
19
21
  import { APPENDED_EXTENSIONS } from "../extensions.ts";
20
22
 
@@ -41,6 +43,11 @@ interface NamedConfiguration {
41
43
  readonly written: string;
42
44
  }
43
45
 
46
+ /** One test run a command string writes, with the directory it runs in. */
47
+ export interface CommandTests extends TestRun {
48
+ readonly dir: string;
49
+ }
50
+
44
51
  /** What one command string runs. */
45
52
  export interface CommandReading {
46
53
  readonly runs: readonly Run[];
@@ -50,7 +57,9 @@ export interface CommandReading {
50
57
  */
51
58
  readonly commands: readonly CommandWords[];
52
59
  readonly configurations: readonly NamedConfiguration[];
53
- /** The text layer 3 reads: all of the string. */
60
+ /** Each test run of {@link commands}, as `testRun` reads the command a launcher hands on. */
61
+ readonly tests: readonly CommandTests[];
62
+ /** The string's whole text, which the dependency rule reads. */
54
63
  readonly held: string;
55
64
  }
56
65
 
@@ -90,14 +99,17 @@ export function readCommands(text: string, dir: string | undefined): CommandRead
90
99
  const runs: Run[] = [];
91
100
  const commands: CommandWords[] = [];
92
101
  const configurations: NamedConfiguration[] = [];
102
+ const tests: CommandTests[] = [];
103
+ const readTests = (words: readonly Word[], at: string): void => {
104
+ const run = testRun(words);
105
+ if (run !== undefined) {
106
+ tests.push({ dir: at, ...run });
107
+ }
108
+ };
93
109
  const readCommand = (words: readonly Word[], at: string): void => {
94
110
  commands.push({ dir: at, words: words.map((word) => word.value) });
95
- let handed = words;
96
- let reading: Reading | undefined = readTool(handed);
97
- while (reading?.kind === "command") {
98
- handed = reading.words;
99
- reading = readTool(handed);
100
- }
111
+ const [handed, reading] = handedOn(words);
112
+ readTests(handed, at);
101
113
  if (reading?.kind === "line") {
102
114
  readLine(reading.text, at);
103
115
  return;
@@ -147,6 +159,7 @@ export function readCommands(text: string, dir: string | undefined): CommandRead
147
159
  )) {
148
160
  commands.push({ dir, words: words.map((word) => word.value) });
149
161
  configurations.push(...configurationsOf(words).map((written) => ({ dir, written })));
162
+ readTests(handedOn(words)[0], dir);
150
163
  }
151
164
  }
152
165
  const unique = <T>(all: readonly T[]): T[] => [
@@ -156,6 +169,7 @@ export function readCommands(text: string, dir: string | undefined): CommandRead
156
169
  runs,
157
170
  commands: unique(commands),
158
171
  configurations: unique(configurations),
172
+ tests: unique(tests),
159
173
  held: text,
160
174
  };
161
175
  }
@@ -1,22 +1,49 @@
1
1
  /**
2
2
  * Layer 1 of the command reader: one command line, parsed by the vendored shell parser, as
3
- * its and-or lists, their pipelines and the words of each simple command. Nothing here
4
- * tokenizes, unquotes or splits text.
3
+ * its and-or lists, their pipelines and the words of each simple command, and the one place
4
+ * that decides which parts of a word the shell computes and which words a brace expansion
5
+ * stands for, for layer 3 too.
5
6
  */
6
7
 
7
8
  import {
8
9
  parse,
9
10
  type CommandNode,
11
+ type ParsedScript,
10
12
  type PipelineNode,
11
13
  type Word as ShellWord,
12
14
  type WordPart,
13
15
  } from "../vendor/unbash/parser.ts";
14
16
 
17
+ /**
18
+ * The character that stands, in a word's text as layer 3 reads it, for a part only the run
19
+ * knows. It is a private-use character, so no path holds it, and the parser reads it as
20
+ * literal text.
21
+ */
22
+ export const PART = "\uE000";
23
+
24
+ /** A GitHub Actions expression, which the runner substitutes before any shell reads the line. */
25
+ const EXPRESSION = /\$\{\{[\s\S]*?\}\}/gu;
26
+
27
+ /** `text` with each `${{ }}` expression in it read as a part only the run knows. */
28
+ export function expressionsAsParts(text: string): string {
29
+ return text.replace(EXPRESSION, PART);
30
+ }
31
+
32
+ /** One command line parsed, each `${{ }}` expression in it read as a part only the run knows. */
33
+ export function parseLine(source: string): ParsedScript {
34
+ return parse(expressionsAsParts(source));
35
+ }
36
+
37
+ /** The words a word's brace expansions stand for, at most this many. */
38
+ const ALTERNATIVES = 64;
39
+
15
40
  /** One word of a simple command: its value, or `undefined` where the shell computes it. */
16
41
  export interface Word {
17
42
  readonly value: string | undefined;
18
43
  /** The word as the line writes it. */
19
44
  readonly text: string;
45
+ /** Where the shell expands it to one text, that text with each part it computes as {@link PART}. */
46
+ readonly written?: string;
20
47
  }
21
48
 
22
49
  /** One pipeline of an and-or list. */
@@ -45,29 +72,104 @@ interface List {
45
72
  readonly pipelines: readonly Pipeline[];
46
73
  }
47
74
 
48
- /** Whether one part of a word has a value the line states: a literal, quoted or escaped text. */
49
- function isStatic(part: WordPart): boolean {
75
+ /**
76
+ * The text one part of a word stands for: a literal's, a quoted text's or an escape's value,
77
+ * and {@link PART} for every part the shell computes, a glob among them.
78
+ */
79
+ function partText(part: WordPart): string {
50
80
  switch (part.type) {
51
81
  case "Literal":
52
82
  case "SingleQuoted":
53
83
  case "AnsiCQuoted":
54
- return true;
84
+ return part.value;
55
85
  case "DoubleQuoted":
56
- return part.parts.every((child) => child.type === "Literal");
86
+ return part.parts.map((child) => (child.type === "Literal" ? child.value : PART)).join("");
57
87
  default:
58
- return false;
88
+ return PART;
59
89
  }
60
90
  }
61
91
 
92
+ /** The texts the word `piece` of a brace expansion stands for, or a part where it is no one word. */
93
+ function alternativeTexts(piece: string): readonly string[] {
94
+ if (piece === "") {
95
+ return [""];
96
+ }
97
+ const script = parse(piece);
98
+ const [statement, ...more] = script.commands;
99
+ const command = statement?.command;
100
+ return (script.errors ?? []).length === 0 &&
101
+ more.length === 0 &&
102
+ command?.type === "Command" &&
103
+ command.name !== undefined &&
104
+ command.prefix.length === 0 &&
105
+ command.suffix.length === 0
106
+ ? heldTextsOf(command.name)
107
+ : [PART];
108
+ }
109
+
110
+ /**
111
+ * The texts a brace expansion stands for, its comma-separated alternatives, or a part where it
112
+ * has none, a sequence such as `{1..3}` among them.
113
+ */
114
+ function braceTexts(text: string): readonly string[] {
115
+ const inner = text.slice(1, -1);
116
+ const pieces: string[] = [];
117
+ let depth = 0;
118
+ let from = 0;
119
+ let quote = "";
120
+ for (let at = 0; at < inner.length; at += 1) {
121
+ const char = inner.charAt(at);
122
+ if (char === "\\") {
123
+ at += 1;
124
+ } else if (quote !== "") {
125
+ quote = char === quote ? "" : quote;
126
+ } else if (char === "'" || char === '"') {
127
+ quote = char;
128
+ } else if (char === "{" || char === "}") {
129
+ depth += char === "{" ? 1 : -1;
130
+ } else if (char === "," && depth === 0) {
131
+ pieces.push(inner.slice(from, at));
132
+ from = at + 1;
133
+ }
134
+ }
135
+ if (pieces.length === 0) {
136
+ return [PART];
137
+ }
138
+ pieces.push(inner.slice(from));
139
+ return pieces.flatMap(alternativeTexts);
140
+ }
141
+
62
142
  /**
63
- * One word as layer 1 reads it. A tilde that starts the word unquoted is the parser's literal
64
- * text but the shell's home directory, so it makes the word dynamic too.
143
+ * Each word the shell may expand `word` to, as layer 3 reads it: its quotes removed, each brace
144
+ * expansion read as its alternatives, past {@link ALTERNATIVES} words as a part, and each part
145
+ * it computes, a tilde that starts it unquoted among them, as {@link PART}.
65
146
  */
66
- function wordOf(word: ShellWord): Word {
67
- const [first] = word.parts ?? [];
68
- const lead = word.parts === undefined ? word.text : first?.type === "Literal" ? first.text : "";
69
- const known = (word.parts ?? []).every(isStatic) && !lead.startsWith("~");
70
- return { value: known ? word.value : undefined, text: word.text };
147
+ export function heldTextsOf(word: ShellWord): readonly string[] {
148
+ const { parts } = word;
149
+ const [first] = parts ?? [];
150
+ const tilde =
151
+ parts === undefined
152
+ ? word.text.startsWith("~")
153
+ : first?.type === "Literal" && first.text.startsWith("~");
154
+ let texts: readonly string[] = [parts === undefined ? word.value : ""];
155
+ for (const part of parts ?? []) {
156
+ const options = part.type === "BraceExpansion" ? braceTexts(part.text) : [partText(part)];
157
+ const next = texts.flatMap((text) => options.map((option) => `${text}${option}`));
158
+ texts = next.length > ALTERNATIVES ? texts.map((text) => `${text}${PART}`) : next;
159
+ }
160
+ return tilde ? texts.map((text) => `${PART}${text.slice(1)}`) : texts;
161
+ }
162
+
163
+ /** One word as layer 1 reads it: static only where the shell expands it to one text with no part. */
164
+ export function wordOf(word: ShellWord): Word {
165
+ const [only, ...more] = heldTextsOf(word);
166
+ const one = more.length === 0 ? only : undefined;
167
+ const known = one !== undefined && !one.includes(PART);
168
+ return {
169
+ value: known ? word.value : undefined,
170
+ text: word.text,
171
+ ...(one === undefined ? {} : { written: one }),
172
+ };
71
173
  }
72
174
 
73
175
  /**
@@ -109,7 +211,7 @@ function pipelineOf(node: PipelineNode, operator: Pipeline["operator"]): Pipelin
109
211
  * gives no words, and marks its pipeline where it may change the line's directory.
110
212
  */
111
213
  export function shellLine(source: string): readonly List[] {
112
- const script = parse(source);
214
+ const script = parseLine(source);
113
215
  if ((script.errors ?? []).length > 0) {
114
216
  return [];
115
217
  }