@cplieger/deadset-ts 8.0.0-dev.6 → 8.0.0-dev.8

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 (193) hide show
  1. package/dist/bin/mapper-connection.js +2 -1
  2. package/dist/bin/node-host.js +24 -5
  3. package/dist/src/analysis.js +37 -56
  4. package/dist/src/automatic-types.js +10 -29
  5. package/dist/src/build-scripts.js +17 -29
  6. package/dist/src/component-layer.js +4 -2
  7. package/dist/src/components.js +2 -3
  8. package/dist/src/config.js +6 -82
  9. package/dist/src/configuration-files.js +27 -168
  10. package/dist/src/configured-declarations.js +15 -20
  11. package/dist/src/consumers.js +2 -2
  12. package/dist/src/contract/config.generated.js +544 -0
  13. package/dist/src/contract/contract.generated.js +13 -0
  14. package/dist/src/contract/report.generated.js +3 -2
  15. package/dist/src/conventions.js +20 -60
  16. package/dist/src/dependencies.js +25 -43
  17. package/dist/src/discover.js +14 -40
  18. package/dist/src/edges.js +38 -122
  19. package/dist/src/element-access.js +4 -6
  20. package/dist/src/entry-point-gaps.js +2 -8
  21. package/dist/src/entry-points.js +3 -10
  22. package/dist/src/enum-group.js +3 -10
  23. package/dist/src/erased-stores.js +4 -6
  24. package/dist/src/exempt-classes.js +0 -4
  25. package/dist/src/exempt.js +6 -22
  26. package/dist/src/file-facts.js +2 -25
  27. package/dist/src/findings/completion.js +5 -4
  28. package/dist/src/findings/emitters.js +1 -1
  29. package/dist/src/findings/intra-function.js +1 -1
  30. package/dist/src/findings/non-code-artifacts.js +2 -2
  31. package/dist/src/findings/reads-and-writes.js +2 -1
  32. package/dist/src/findings/self-check.js +3 -2
  33. package/dist/src/findings/unused-declarations.js +1 -1
  34. package/dist/src/findings/visibility-narrowing.js +2 -2
  35. package/dist/src/findings-pass.js +7 -12
  36. package/dist/src/framework-lifecycle.js +7 -7
  37. package/dist/src/fs/document.js +59 -0
  38. package/dist/src/fs/manifest.js +43 -0
  39. package/dist/src/fs/tree.js +88 -0
  40. package/dist/src/git-ignore.js +34 -25
  41. package/dist/src/html-entries.js +6 -33
  42. package/dist/src/in-operands.js +2 -1
  43. package/dist/src/injection-container.js +1 -1
  44. package/dist/src/intra-function-parts.js +10 -13
  45. package/dist/src/inventory.js +2 -21
  46. package/dist/src/json-document.js +198 -67
  47. package/dist/src/keyof-comparisons.js +4 -6
  48. package/dist/src/kinds.js +0 -7
  49. package/dist/src/manifest.js +5 -43
  50. package/dist/src/mdx-entries.js +3 -26
  51. package/dist/src/partial.js +2 -1
  52. package/dist/src/paths.js +26 -15
  53. package/dist/src/position.js +2 -1
  54. package/dist/src/print.js +11 -79
  55. package/dist/src/product.js +13 -0
  56. package/dist/src/references.js +14 -27
  57. package/dist/src/report-schema.js +3 -225
  58. package/dist/src/report.js +22 -31
  59. package/dist/src/reporters/sarif.js +4 -2
  60. package/dist/src/reporters/template.js +4 -19
  61. package/dist/src/resolve.js +245 -724
  62. package/dist/src/roots.js +7 -17
  63. package/dist/src/run.js +55 -61
  64. package/dist/src/schema.js +320 -343
  65. package/dist/src/scope.js +34 -103
  66. package/dist/src/serialization-contract.js +5 -9
  67. package/dist/src/session.js +2 -1
  68. package/dist/src/suppress-file.js +21 -25
  69. package/dist/src/suppress.js +3 -3
  70. package/dist/src/symbol-keys.js +3 -5
  71. package/dist/src/template-field.js +9 -19
  72. package/dist/src/test-globals.js +6 -28
  73. package/dist/src/test-joins.js +6 -6
  74. package/dist/src/text/compare.js +26 -0
  75. package/dist/src/text/glob.js +194 -0
  76. package/dist/src/text/shell.js +159 -0
  77. package/dist/src/{yaml-document.js → text/yaml.js} +38 -0
  78. package/dist/src/type-errors.js +14 -38
  79. package/dist/src/unseen-loads.js +2 -9
  80. package/dist/src/unwrap.js +23 -0
  81. package/dist/src/usage.js +10 -0
  82. package/dist/src/verbs/analyze.js +25 -26
  83. package/dist/src/verbs/describe.js +4 -4
  84. package/dist/src/verbs/explain.js +8 -6
  85. package/dist/src/verbs/print-projects.js +4 -3
  86. package/dist/src/verbs/print-retained.js +2 -1
  87. package/dist/src/verbs/print-roots.js +2 -1
  88. package/dist/src/verbs/version.js +2 -1
  89. package/dist/src/workflow-steps.js +9 -46
  90. package/dist/src/workspace-manifests.js +2 -2
  91. package/dist/src/workspace-programs.js +5 -3
  92. package/dist/src/workspace-resolution.js +13 -17
  93. package/dist/src/workspace.js +28 -116
  94. package/jsr.json +1 -1
  95. package/package.json +1 -1
  96. package/src/analysis.ts +55 -76
  97. package/src/automatic-types.ts +11 -29
  98. package/src/build-scripts.ts +17 -38
  99. package/src/component-layer.ts +4 -2
  100. package/src/components.ts +2 -4
  101. package/src/config.ts +31 -296
  102. package/src/configuration-files.ts +27 -170
  103. package/src/configured-declarations.ts +18 -23
  104. package/src/consumers.ts +4 -4
  105. package/src/contract/config.generated.ts +798 -0
  106. package/src/contract/contract.generated.ts +25 -0
  107. package/src/contract/report.generated.ts +3 -2
  108. package/src/conventions.ts +26 -74
  109. package/src/dependencies.ts +32 -50
  110. package/src/discover.ts +15 -40
  111. package/src/edges.ts +41 -140
  112. package/src/element-access.ts +4 -7
  113. package/src/entry-point-gaps.ts +3 -9
  114. package/src/entry-points.ts +2 -13
  115. package/src/enum-group.ts +2 -11
  116. package/src/erased-stores.ts +3 -7
  117. package/src/exempt-classes.ts +0 -5
  118. package/src/exempt.ts +8 -30
  119. package/src/file-facts.ts +6 -24
  120. package/src/findings/completion.ts +5 -6
  121. package/src/findings/emitter.ts +2 -2
  122. package/src/findings/emitters.ts +1 -1
  123. package/src/findings/intra-function.ts +1 -1
  124. package/src/findings/non-code-artifacts.ts +4 -4
  125. package/src/findings/reads-and-writes.ts +2 -1
  126. package/src/findings/self-check.ts +3 -2
  127. package/src/findings/unused-declarations.ts +1 -1
  128. package/src/findings/visibility-narrowing.ts +2 -2
  129. package/src/findings-pass.ts +9 -15
  130. package/src/framework-lifecycle.ts +7 -7
  131. package/src/fs/document.ts +74 -0
  132. package/src/fs/manifest.ts +52 -0
  133. package/src/fs/tree.ts +110 -0
  134. package/src/git-ignore.ts +38 -25
  135. package/src/host.ts +12 -5
  136. package/src/html-entries.ts +6 -33
  137. package/src/in-operands.ts +2 -1
  138. package/src/injection-container.ts +1 -1
  139. package/src/intra-function-parts.ts +10 -14
  140. package/src/inventory.ts +2 -20
  141. package/src/json-document.ts +241 -76
  142. package/src/keyof-comparisons.ts +3 -7
  143. package/src/kinds.ts +0 -8
  144. package/src/manifest.ts +5 -46
  145. package/src/mdx-entries.ts +3 -26
  146. package/src/partial.ts +2 -1
  147. package/src/paths.ts +27 -15
  148. package/src/position.ts +2 -1
  149. package/src/print.ts +12 -92
  150. package/src/product.ts +18 -0
  151. package/src/references.ts +14 -29
  152. package/src/report-schema.ts +3 -301
  153. package/src/report.ts +24 -34
  154. package/src/reporters/sarif.ts +4 -4
  155. package/src/reporters/template.ts +4 -20
  156. package/src/resolve.ts +283 -1114
  157. package/src/roots.ts +7 -20
  158. package/src/run.ts +57 -75
  159. package/src/schema.ts +391 -388
  160. package/src/scope.ts +40 -126
  161. package/src/serialization-contract.ts +5 -10
  162. package/src/session.ts +2 -1
  163. package/src/suppress-file.ts +21 -29
  164. package/src/suppress.ts +3 -3
  165. package/src/symbol-keys.ts +2 -5
  166. package/src/template-field.ts +15 -25
  167. package/src/test-globals.ts +9 -27
  168. package/src/test-joins.ts +6 -8
  169. package/src/text/compare.ts +27 -0
  170. package/src/text/glob.ts +210 -0
  171. package/src/text/shell.ts +161 -0
  172. package/src/{yaml-document.ts → text/yaml.ts} +46 -0
  173. package/src/type-errors.ts +25 -36
  174. package/src/unseen-loads.ts +1 -12
  175. package/src/unwrap.ts +34 -0
  176. package/src/usage.ts +10 -0
  177. package/src/verbs/analyze.ts +32 -42
  178. package/src/verbs/describe.ts +4 -4
  179. package/src/verbs/explain.ts +8 -10
  180. package/src/verbs/print-projects.ts +4 -3
  181. package/src/verbs/print-retained.ts +2 -1
  182. package/src/verbs/print-roots.ts +2 -1
  183. package/src/verbs/verb.ts +2 -2
  184. package/src/verbs/version.ts +2 -1
  185. package/src/workflow-steps.ts +9 -44
  186. package/src/workspace-manifests.ts +2 -2
  187. package/src/workspace-programs.ts +5 -3
  188. package/src/workspace-resolution.ts +21 -17
  189. package/src/workspace.ts +35 -121
  190. package/dist/src/glob.js +0 -65
  191. package/dist/src/pnpm-workspace.js +0 -120
  192. package/src/glob.ts +0 -67
  193. package/src/pnpm-workspace.ts +0 -135
@@ -5,6 +5,7 @@ import type { MatrixCandidate } from "../matrix.ts";
5
5
  import { positionKey } from "../position.ts";
6
6
  import { declarationKey } from "../ref.ts";
7
7
  import type { Emitter, EmitterInput } from "./emitter.ts";
8
+ import { bytewise } from "../text/compare.ts";
8
9
 
9
10
  /** The codes this emitter reports. */
10
11
  type ReadsAndWritesCode =
@@ -123,7 +124,7 @@ function writeOnlyMessage(
123
124
  : "never read";
124
125
  const cascade = [
125
126
  ...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
126
- ...[...held.alone].sort().map((name) => `import ${name}`),
127
+ ...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
127
128
  ];
128
129
  const word = SUBJECT_WORDS[symbol.kind] ?? "member";
129
130
  const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
@@ -12,6 +12,7 @@ import {
12
12
  type SuppressionRecord,
13
13
  } from "../suppress.ts";
14
14
  import type { Emitter } from "./emitter.ts";
15
+ import { bytewise } from "../text/compare.ts";
15
16
 
16
17
  /** The subject kind of a finding about a configured declaration. */
17
18
  const DECLARATION_SUBJECT = "configured-declaration";
@@ -171,10 +172,10 @@ export function staleSuppressions(
171
172
  bySite.set(key, held);
172
173
  }
173
174
  return [...bySite.values()].map(({ first, records }) => {
174
- const codes = [...new Set(records.map((one) => one.record.code))].sort();
175
+ const codes = [...new Set(records.map((one) => one.record.code))].sort(bytewise);
175
176
  const reports = [...new Set(records.flatMap((one) => one.reports))]
176
177
  .filter((code) => !codes.includes(code))
177
- .sort();
178
+ .sort(bytewise);
178
179
  const rows = codes.filter(isUnsuppressibleCode);
179
180
  const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
180
181
  const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
@@ -218,7 +218,7 @@ export const unusedDeclarations: Emitter = (input) => [
218
218
  */
219
219
  function unusedImports(input: EmitterInput): Finding[] {
220
220
  const generated =
221
- input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
221
+ input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
222
222
  return input.intraFunction.imports.flatMap((binding) =>
223
223
  binding.used || generated?.has(binding.position.path) === true
224
224
  ? []
@@ -266,13 +266,13 @@ function unreachable(
266
266
  export function narrowings(input: EmitterInput): readonly Finding[] {
267
267
  const facts = factsOf(input.swept.matrix.union);
268
268
  const { declared, loaded } = input.boundary.consumers;
269
- const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
269
+ const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
270
270
  const world = { closed, writeOnly: writeOnlyDeclarations(input) };
271
271
  const candidates = new Map(
272
272
  input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]),
273
273
  );
274
274
  const generated =
275
- input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
275
+ input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
276
276
 
277
277
  const findings: Finding[] = [];
278
278
  facts.graph.symbols.forEach((symbol, at) => {
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { EXIT_CLEAN, EXIT_FINDINGS, EXIT_PENDING } from "./contract/contract.generated.ts";
8
8
  import type { AnalysisInputs } from "./analysis.ts";
9
- import { fails, type Config } from "./config.ts";
9
+ import { fails, type ResolvedConfig } from "./config.ts";
10
10
  import type { EdgeEvaluation } from "./edges.ts";
11
11
  import type { CompletedFinding, Finding } from "./finding.ts";
12
12
  import { decidedFindings, type Withheld } from "./findings/emitters.ts";
@@ -24,6 +24,8 @@ import {
24
24
  type Ledger,
25
25
  } from "./suppress.ts";
26
26
  import type { Provenance, Recorded } from "./suppress-file.ts";
27
+ import { bytewise } from "./text/compare.ts";
28
+ import { PRODUCT } from "./product.ts";
27
29
 
28
30
  /** The counts a report's totals carry beside its findings. */
29
31
  interface PassTotals {
@@ -49,22 +51,14 @@ export interface PassResult {
49
51
  readonly ledger: Ledger;
50
52
  }
51
53
 
52
- /** Two strings ordered bytewise. */
53
- function compare(a: string, b: string): number {
54
- if (a === b) {
55
- return 0;
56
- }
57
- return a < b ? -1 : 1;
58
- }
59
-
60
54
  /** The canonical order: path, line, column, code, then the symbol reference. */
61
55
  function canonical(a: Finding, b: Finding): number {
62
56
  return (
63
- compare(a.position.path, b.position.path) ||
57
+ bytewise(a.position.path, b.position.path) ||
64
58
  a.position.line - b.position.line ||
65
59
  a.position.column - b.position.column ||
66
- compare(a.code, b.code) ||
67
- compare(a.symbol.ref, b.symbol.ref)
60
+ bytewise(a.code, b.code) ||
61
+ bytewise(a.symbol.ref, b.symbol.ref)
68
62
  );
69
63
  }
70
64
 
@@ -115,7 +109,7 @@ export function findingsPass(inputs: AnalysisInputs): PassResult {
115
109
  * suppression is stale or a finding's severity reaches the configured failing severity;
116
110
  * clean otherwise.
117
111
  */
118
- export function verdictOf(result: PassResult, config: Config): number {
112
+ export function verdictOf(result: PassResult, config: ResolvedConfig): number {
119
113
  if (result.totals.pending > 0) {
120
114
  return EXIT_PENDING;
121
115
  }
@@ -123,7 +117,7 @@ export function verdictOf(result: PassResult, config: Config): number {
123
117
  return EXIT_FINDINGS;
124
118
  }
125
119
  const failing = result.findings.some((finding) =>
126
- fails(finding.severity, config.reporters.failOn),
120
+ fails(finding.severity, config.reporters.fail_on),
127
121
  );
128
122
  return failing ? EXIT_FINDINGS : EXIT_CLEAN;
129
123
  }
@@ -161,5 +155,5 @@ export function rowKey(row: Recorded): string {
161
155
 
162
156
  /** The analyzer identity a baseline this analyzer writes names as its rows' provenance. */
163
157
  export function analyzerProvenance(version: string): Provenance {
164
- return { analyzer: "deadset-ts", version };
158
+ return { analyzer: PRODUCT, version };
165
159
  }
@@ -55,8 +55,8 @@ function resolveContracts<Brand>(
55
55
  contracts: readonly LifecycleContract[],
56
56
  ): readonly ResolvedContract[] {
57
57
  const entries: DeclarationEntry[] = contracts.flatMap((contract) => [
58
- ...contract.components,
59
- ...contract.bases,
58
+ ...(contract.components ?? []),
59
+ ...(contract.bases ?? []),
60
60
  ]);
61
61
  const resolved = resolveEntries(project, held, entries);
62
62
  let next = 0;
@@ -67,8 +67,8 @@ function resolveContracts<Brand>(
67
67
  };
68
68
  return contracts.map((contract) => ({
69
69
  contract,
70
- components: take(contract.components.length),
71
- bases: take(contract.bases.length),
70
+ components: take(contract.components?.length ?? 0),
71
+ bases: take(contract.bases?.length ?? 0),
72
72
  }));
73
73
  }
74
74
 
@@ -243,11 +243,11 @@ function byBases<Brand>(
243
243
  */
244
244
  export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
245
245
  const { project, held, targetRoot } = input;
246
- if (input.ts.lifecycleContracts.length === 0) {
246
+ if (input.ts.lifecycle_contracts.length === 0) {
247
247
  return [];
248
248
  }
249
249
  const chains = aliasChains(project, held);
250
- const contracts = resolveContracts(project, held, input.ts.lifecycleContracts);
250
+ const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
251
251
  const components = [
252
252
  ...byComponents(project, held, chains, contracts),
253
253
  ...byBases(project, held, chains, contracts),
@@ -256,7 +256,7 @@ export function frameworkLifecycle<Brand>(input: DetectorInput<Brand>): readonly
256
256
  return [];
257
257
  }
258
258
  const ids = new Set(components.map((one) => one.id));
259
- const sides: readonly ClassSide[] = input.ts.lifecycleContracts.some((contract) =>
259
+ const sides: readonly ClassSide[] = input.ts.lifecycle_contracts.some((contract) =>
260
260
  contract.members.some((name) => name.endsWith(":static")),
261
261
  )
262
262
  ? ["instance", "static"]
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The one read of a document the target holds at its root, which a checkout under analysis
3
+ * controls: the repository configuration, the ignore file, the baseline and the edges
4
+ * document. The read stays inside the target root, takes a regular file alone, and holds no
5
+ * more of it than the Contract's document bound.
6
+ */
7
+
8
+ import { CONFIG_MAX_BYTES } from "../contract/config.generated.ts";
9
+ import type { Host } from "../host.ts";
10
+ import { joinPath, relativePath } from "../paths.ts";
11
+ import { message } from "../product.ts";
12
+ import { utf8Bytes } from "../reporters/sha256.ts";
13
+ import { UsageError } from "../usage.ts";
14
+
15
+ /**
16
+ * A document at the target root that is not one to read: its name resolves outside the
17
+ * target root or to anything but a regular file, or the read fails. Nothing is read from
18
+ * it, and the run ends with the failure code.
19
+ */
20
+ export class TargetDocumentError extends Error {
21
+ constructor(label: string, reason: string) {
22
+ super(`${label}: ${reason}`);
23
+ this.name = "TargetDocumentError";
24
+ }
25
+ }
26
+
27
+ /** Refuses a document past the Contract's bound with the usage code, naming it `label`. */
28
+ export function refuseOverBound(label: string, text: string): void {
29
+ if (utf8Bytes(text).length > CONFIG_MAX_BYTES) {
30
+ throw new UsageError(`${label}: holds more than ${String(CONFIG_MAX_BYTES)} bytes`);
31
+ }
32
+ }
33
+
34
+ /** What `read` returns, or its throw as the document's {@link TargetDocumentError}. */
35
+ function attempt<T>(label: string, read: () => T): T {
36
+ try {
37
+ return read();
38
+ } catch (error: unknown) {
39
+ throw new TargetDocumentError(label, message(error));
40
+ }
41
+ }
42
+
43
+ /**
44
+ * The text of the document `name` at the absolute `targetRoot`, undefined where nothing is
45
+ * there, each refusal naming it `label`. A document past the bound is read one byte past
46
+ * it and refused as {@link refuseOverBound} refuses it.
47
+ */
48
+ export function readTargetDocument(
49
+ host: Host,
50
+ targetRoot: string,
51
+ name: string,
52
+ label = name,
53
+ ): string | undefined {
54
+ const path = joinPath(targetRoot, name);
55
+ const kind = attempt(label, () => host.kindOf(path));
56
+ if (kind === "absent") {
57
+ return undefined;
58
+ }
59
+ const real = attempt(label, () => host.realPath(path));
60
+ if (
61
+ relativePath(
62
+ attempt(label, () => host.realPath(targetRoot)),
63
+ real,
64
+ ) === undefined
65
+ ) {
66
+ throw new TargetDocumentError(label, `resolves to ${real}, outside the target root`);
67
+ }
68
+ if (kind !== "file") {
69
+ throw new TargetDocumentError(label, "is not a regular file");
70
+ }
71
+ const text = attempt(label, () => host.readFile(real, CONFIG_MAX_BYTES + 1));
72
+ refuseOverBound(label, text);
73
+ return text;
74
+ }
@@ -0,0 +1,52 @@
1
+ import type { Host } from "../host.ts";
2
+ import { isRecord } from "../json-document.ts";
3
+ import { joinPath } from "../paths.ts";
4
+
5
+ /** The file a package's manifest is read from. */
6
+ export const MANIFEST = "package.json";
7
+
8
+ /** The directory a package manager installs packages into, whose files are no project's own. */
9
+ export const INSTALLED_PACKAGES = "node_modules";
10
+
11
+ /** Whether one path passes through an installed-packages directory. */
12
+ export function inInstalledPackages(path: string): boolean {
13
+ return path.split("/").includes(INSTALLED_PACKAGES);
14
+ }
15
+
16
+ /** One JSON file's text and decoded value, or undefined where it cannot be read or parsed. */
17
+ export function readDocument(
18
+ host: Host,
19
+ path: string,
20
+ ): { text: string; value: unknown } | undefined {
21
+ let text: string;
22
+ try {
23
+ text = host.readFile(path);
24
+ } catch {
25
+ return undefined;
26
+ }
27
+ try {
28
+ return { text, value: JSON.parse(text) as unknown };
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ /** One JSON file's decoded value, or undefined where it cannot be read or parsed. */
35
+ export function readJSON(host: Host, path: string): unknown {
36
+ return readDocument(host, path)?.value;
37
+ }
38
+
39
+ /**
40
+ * The manifest in one directory, decoded, or undefined where none reads as an object. A
41
+ * manifest this program cannot read declares nothing, the same answer as an absent one:
42
+ * the package manager owns the refusal of a manifest it rejects.
43
+ */
44
+ export function manifestAt(host: Host, dir: string): Record<string, unknown> | undefined {
45
+ const value = readJSON(host, joinPath(dir, MANIFEST));
46
+ return isRecord(value) ? value : undefined;
47
+ }
48
+
49
+ /** Whether one directory holds a manifest file. */
50
+ export function holdsManifest(host: Host, dir: string): boolean {
51
+ return host.kindOf(joinPath(dir, MANIFEST)) === "file";
52
+ }
package/src/fs/tree.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { DirectoryEntry, Host } from "../host.ts";
2
+ import { ancestors, joinPath, resolvePath } from "../paths.ts";
3
+ import { LANGUAGE_ROWS } from "../contract/contract.generated.ts";
4
+ import { LANGUAGE } from "../product.ts";
5
+ import { bytewise } from "../text/compare.ts";
6
+ import { globExpression } from "../text/glob.ts";
7
+
8
+ /** The directories no walk of the tree enters, by the patterns the language's row names. */
9
+ const SKIPPED_DIRECTORIES = LANGUAGE_ROWS[LANGUAGE].skippedDirectories.map(globExpression);
10
+
11
+ function skipped(name: string): boolean {
12
+ return SKIPPED_DIRECTORIES.some((pattern) => pattern.test(name));
13
+ }
14
+
15
+ /** A git repository's working tree root, and the `info/exclude` file git reads for it. */
16
+ interface Repository {
17
+ readonly root: string;
18
+ /** Undefined where a `.git` file names no repository directory. */
19
+ readonly exclude: string | undefined;
20
+ }
21
+
22
+ /** One file's text without its line ending, or undefined where it cannot be read. */
23
+ function lineOf(host: Host, path: string): string | undefined {
24
+ try {
25
+ return host.readFile(path).replace(/\r?\n$/u, "");
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * The repository whose working tree holds `dir`: the nearest directory at or above it with a
33
+ * `.git`. A `.git` file, which a linked worktree or a submodule holds, names the repository's
34
+ * directory on its `gitdir:` line; that directory's `commondir` file, where it has one, names
35
+ * the directory shared with the main worktree, which holds `info/exclude`:
36
+ * https://git-scm.com/docs/gitrepository-layout
37
+ */
38
+ export function repositoryOf(host: Host, dir: string): Repository | undefined {
39
+ for (const root of ancestors(dir)) {
40
+ const dotGit = joinPath(root, ".git");
41
+ const kind = host.kindOf(dotGit);
42
+ if (kind === "directory") {
43
+ return { root, exclude: joinPath(dotGit, "info/exclude") };
44
+ }
45
+ if (kind === "file") {
46
+ const named = /^gitdir: (.+)$/u.exec(lineOf(host, dotGit) ?? "")?.[1];
47
+ const gitDir = named === undefined ? undefined : resolvePath(root, named);
48
+ const common = gitDir === undefined ? undefined : lineOf(host, joinPath(gitDir, "commondir"));
49
+ const shared =
50
+ gitDir === undefined || common === undefined ? gitDir : resolvePath(gitDir, common);
51
+ return { root, exclude: shared === undefined ? undefined : joinPath(shared, "info/exclude") };
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ /** The entries of one directory, none where it cannot be read. */
58
+ export function entriesOf(host: Host, dir: string): readonly DirectoryEntry[] {
59
+ try {
60
+ return host.readDirectory(dir);
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /** The names of the files directly in `dir` whose name `accepts`, in bytewise order. */
67
+ export function filesIn(
68
+ host: Host,
69
+ dir: string,
70
+ accepts: (name: string) => boolean,
71
+ ): readonly string[] {
72
+ return entriesOf(host, dir)
73
+ .filter((entry) => !entry.directory && accepts(entry.name))
74
+ .map((entry) => entry.name)
75
+ .sort(bytewise);
76
+ }
77
+
78
+ /** The names of the directories directly in `dir` a walk enters, in bytewise order. */
79
+ export function directoriesIn(host: Host, dir: string): readonly string[] {
80
+ return entriesOf(host, dir)
81
+ .filter((entry) => entry.directory && !skipped(entry.name))
82
+ .map((entry) => entry.name)
83
+ .sort(bytewise);
84
+ }
85
+
86
+ /**
87
+ * Every file below `root` whose name `accepts`, by its path relative to `root` in bytewise
88
+ * order, read in no skipped directory.
89
+ */
90
+ export function filesBelow(
91
+ host: Host,
92
+ root: string,
93
+ accepts: (name: string) => boolean,
94
+ ): readonly string[] {
95
+ const found: string[] = [];
96
+ const pending = [""];
97
+ for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
98
+ for (const entry of entriesOf(host, joinPath(root, dir))) {
99
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
100
+ if (!entry.directory) {
101
+ if (accepts(entry.name)) {
102
+ found.push(path);
103
+ }
104
+ } else if (!skipped(entry.name)) {
105
+ pending.push(path);
106
+ }
107
+ }
108
+ }
109
+ return found.sort(bytewise);
110
+ }
package/src/git-ignore.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Which files the `.gitignore` files of a tree ignore, read as git reads them:
2
+ * Which files git ignores below a tree's root, read as git reads its ignore files:
3
3
  * https://git-scm.com/docs/gitignore#_pattern_format
4
4
  */
5
5
 
6
- import { globExpression } from "./glob.ts";
7
6
  import type { Host } from "./host.ts";
8
- import { dirnamePath, joinPath, relativePath } from "./paths.ts";
7
+ import { ancestors, dirnamePath, joinPath, relativePath } from "./paths.ts";
8
+ import { gitignoreExpression } from "./text/glob.ts";
9
+ import { repositoryOf } from "./fs/tree.ts";
9
10
 
10
- /** One pattern line of a `.gitignore` file. */
11
+ /** One pattern line of an ignore file. */
11
12
  interface Rule {
12
13
  readonly negated: boolean;
13
14
  /** Whether the pattern ends with `/`, so it matches a directory alone. */
@@ -16,7 +17,7 @@ interface Rule {
16
17
  readonly expression: RegExp;
17
18
  }
18
19
 
19
- /** The rules one `.gitignore` text writes, in order. */
20
+ /** The rules one ignore file's text writes, in order. */
20
21
  function rulesOf(text: string): readonly Rule[] {
21
22
  const rules: Rule[] = [];
22
23
  for (const raw of text.split(/\r?\n/u)) {
@@ -25,7 +26,7 @@ function rulesOf(text: string): readonly Rule[] {
25
26
  continue;
26
27
  }
27
28
  const negated = line.startsWith("!");
28
- line = negated || line.startsWith("\\") ? line.slice(1) : line;
29
+ line = negated ? line.slice(1) : line;
29
30
  const directoryOnly = line.endsWith("/");
30
31
  line = line.replace(/\/+$/u, "");
31
32
  if (line === "") {
@@ -38,43 +39,55 @@ function rulesOf(text: string): readonly Rule[] {
38
39
  rules.push({
39
40
  negated,
40
41
  directoryOnly,
41
- expression: globExpression(anchored ? pattern : `**/${pattern}`),
42
+ expression: gitignoreExpression(anchored ? pattern : `**/${pattern}`),
42
43
  });
43
44
  }
44
45
  return rules;
45
46
  }
46
47
 
48
+ function rulesIn(host: Host, path: string): readonly Rule[] {
49
+ try {
50
+ return rulesOf(host.readFile(path));
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
47
56
  /**
48
- * Whether the `.gitignore` files at and below `root` ignore one absolute path. A path
49
- * outside `root` is not ignored, and a file below an ignored directory stays ignored
50
- * whatever a later pattern says, as git cannot re-include it.
57
+ * Whether git ignores one absolute path below `root`: by the repository's `info/exclude`,
58
+ * then the `.gitignore` files from the repository's root down to the path's directory, a
59
+ * later pattern overriding an earlier one. A path outside `root` is not ignored, and a file
60
+ * below an ignored directory stays ignored whatever a later pattern says, as git cannot
61
+ * re-include it. With no repository at or above `root`, the files at and below it are read;
62
+ * with one, the directories above `root` give patterns but are not themselves tested.
51
63
  */
52
64
  export function gitIgnored(host: Host, root: string): (path: string) => boolean {
65
+ const found = repositoryOf(host, root);
66
+ const repository = found?.root;
67
+ const above =
68
+ repository === undefined ? [] : [...ancestors(dirnamePath(root), repository)].reverse();
69
+ const excluded = found?.exclude === undefined ? [] : rulesIn(host, found.exclude);
53
70
  const read = new Map<string, readonly Rule[]>();
54
71
  const rulesAt = (dir: string): readonly Rule[] => {
55
72
  let rules = read.get(dir);
56
73
  if (rules === undefined) {
57
- try {
58
- rules = rulesOf(host.readFile(joinPath(dir, ".gitignore")));
59
- } catch {
60
- rules = [];
61
- }
74
+ rules = rulesIn(host, joinPath(dir, ".gitignore"));
62
75
  read.set(dir, rules);
63
76
  }
64
77
  return rules;
65
78
  };
66
- /** Whether the rules of `root` and of each directory down to one path's own decide it ignored. */
67
79
  const decided = (path: string, directory: boolean): boolean => {
68
- const dirs: string[] = [];
69
- for (let at = dirnamePath(path); relativePath(root, at) !== undefined; at = dirnamePath(at)) {
70
- dirs.unshift(at);
71
- }
72
- dirs.unshift(root);
80
+ const sources: [string, readonly Rule[]][] = [
81
+ ...(repository === undefined ? [] : [[repository, excluded] as [string, readonly Rule[]]]),
82
+ ...[...above, ...[...ancestors(dirnamePath(path), root)].reverse()].map(
83
+ (dir): [string, readonly Rule[]] => [dir, rulesAt(dir)],
84
+ ),
85
+ ];
73
86
  let ignored = false;
74
- for (const dir of dirs) {
75
- const below = relativePath(dir, path);
76
- for (const rule of below === undefined ? [] : rulesAt(dir)) {
77
- if ((directory || !rule.directoryOnly) && rule.expression.test(below ?? "")) {
87
+ for (const [dir, rules] of sources) {
88
+ const below = relativePath(dir, path) ?? "";
89
+ for (const rule of rules) {
90
+ if ((directory || !rule.directoryOnly) && rule.expression.test(below)) {
78
91
  ignored = !rule.negated;
79
92
  }
80
93
  }
package/src/host.ts CHANGED
@@ -1,5 +1,5 @@
1
- /** What one path names. */
2
- export type PathKind = "file" | "directory" | "absent";
1
+ /** What one path names: `"other"` is anything else there, a named pipe, a device or a socket. */
2
+ export type PathKind = "file" | "directory" | "other" | "absent";
3
3
 
4
4
  /** One entry of a directory. */
5
5
  export interface DirectoryEntry {
@@ -17,11 +17,18 @@ export interface DirectoryEntry {
17
17
  export interface Host {
18
18
  /** The directory a relative path resolves against. */
19
19
  workingDirectory(): string;
20
- /** The text of one file, or a throw naming why it cannot be read. */
21
- readFile(path: string): string;
20
+ /**
21
+ * The text of one file, or a throw naming why it cannot be read. Given `limit`, the
22
+ * text of no more than the file's first `limit` bytes, so a caller bounding a document
23
+ * asks for one byte past its bound and never holds a larger file whole.
24
+ */
25
+ readFile(path: string, limit?: number): string;
22
26
  /** The entries of one directory, or a throw naming why it cannot be read. */
23
27
  readDirectory(path: string): readonly DirectoryEntry[];
24
- /** What one path names, `"absent"` where nothing is there. */
28
+ /**
29
+ * What one path names once every symbolic link along it is followed, `"absent"` where
30
+ * nothing is there, or a throw naming why it cannot be known, as for a link loop.
31
+ */
25
32
  kindOf(path: string): PathKind;
26
33
  /**
27
34
  * The path one path names once every symbolic link along it is followed, or a throw
@@ -12,8 +12,10 @@ import {
12
12
  } from "@typescript/native/ast";
13
13
  import { resolveRelativeSpecifier, type ParseFile } from "./configuration-files.ts";
14
14
  import type { Host } from "./host.ts";
15
- import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
15
+ import { ancestors, dirnamePath, joinPath, resolvePath } from "./paths.ts";
16
16
  import type { SourceFiles } from "./source-files.ts";
17
+ import { filesBelow } from "./fs/tree.ts";
18
+ import { holdsManifest } from "./fs/manifest.ts";
17
19
 
18
20
  /** One file a script loads, and the value or specifier that named it. */
19
21
  interface HTMLEntry {
@@ -21,9 +23,6 @@ interface HTMLEntry {
21
23
  readonly source: string;
22
24
  }
23
25
 
24
- /** The directory no HTML file is read below. */
25
- const SKIPPED = "node_modules";
26
-
27
26
  /** A `<script` start tag, its attribute text captured. */
28
27
  const SCRIPT_TAG = /<script(?=[\s>/])([^>]*)>/giu;
29
28
 
@@ -42,28 +41,6 @@ const CLASSIC_TYPES: ReadonlySet<string> = new Set(["text/javascript", "applicat
42
41
  /** The prefixes an inline script's specifier names a file of the target with. */
43
42
  const FILE_PREFIXES = ["/", "./", "../"] as const;
44
43
 
45
- /** Every HTML file below `dir`, outside every `node_modules` directory, by absolute path. */
46
- function htmlFiles(host: Host, dir: string): readonly string[] {
47
- let entries;
48
- try {
49
- entries = host.readDirectory(dir);
50
- } catch {
51
- return [];
52
- }
53
- const found: string[] = [];
54
- for (const entry of entries) {
55
- const path = joinPath(dir, entry.name);
56
- if (entry.directory) {
57
- if (entry.name !== SKIPPED) {
58
- found.push(...htmlFiles(host, path));
59
- }
60
- } else if (entry.name.endsWith(".html")) {
61
- found.push(path);
62
- }
63
- }
64
- return found.sort();
65
- }
66
-
67
44
  /** Each attribute of one start tag's attribute text, by its lowercase name. */
68
45
  function attributes(text: string): ReadonlyMap<string, string> {
69
46
  const found = new Map<string, string>();
@@ -127,12 +104,7 @@ function scriptValues(parse: ParseFile | undefined, path: string, text: string):
127
104
 
128
105
  /** The directory of the nearest `package.json` at or above `dir`, inside `root`, else `root`. */
129
106
  function manifestDirectory(host: Host, root: string, dir: string): string {
130
- for (let at = dir; relativePath(root, at) !== undefined; at = dirnamePath(at)) {
131
- if (host.kindOf(joinPath(at, "package.json")) === "file") {
132
- return at;
133
- }
134
- }
135
- return root;
107
+ return ancestors(dir, root).find((at) => holdsManifest(host, at)) ?? root;
136
108
  }
137
109
 
138
110
  /**
@@ -150,7 +122,8 @@ export function htmlEntries(
150
122
  ): readonly HTMLEntry[] {
151
123
  const root = resolvePath(host.workingDirectory(), targetRoot);
152
124
  const found: HTMLEntry[] = [];
153
- for (const path of htmlFiles(host, root)) {
125
+ for (const below of filesBelow(host, root, (name) => name.endsWith(".html"))) {
126
+ const path = joinPath(root, below);
154
127
  let text: string;
155
128
  try {
156
129
  text = host.readFile(path);
@@ -13,6 +13,7 @@ import {
13
13
  } from "@typescript/native/ast";
14
14
  import type { ProjectView } from "./session.ts";
15
15
  import { sourceFilesOf } from "./source-files.ts";
16
+ import { bytewise } from "./text/compare.ts";
16
17
 
17
18
  /**
18
19
  * The text of each string literal, or template literal with no substitution, written as
@@ -41,5 +42,5 @@ export function inOperandNames<Brand>(
41
42
  file.forEachChild(visit);
42
43
  }
43
44
  }
44
- return [...found].sort();
45
+ return [...found].sort(bytewise);
45
46
  }
@@ -53,7 +53,7 @@ function isInjected(node: Node): boolean {
53
53
  /** The classes of the target a call whose callee an entry names passes, with that call's entry. */
54
54
  function registrations<Brand>(input: DetectorInput<Brand>): readonly Registration[] {
55
55
  const { project, held } = input;
56
- const resolved = resolveEntries(project, held, input.ts.injectionRegistrations);
56
+ const resolved = resolveEntries(project, held, input.ts.injection_registrations);
57
57
  if (!resolved.some(namesADeclaration)) {
58
58
  return [];
59
59
  }