@cplieger/deadset-ts 4.0.1 → 5.1.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 (138) hide show
  1. package/README.md +4 -4
  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 +301 -41
  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 +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
@@ -1,4 +1,11 @@
1
- import { runAnalysis, runBaselineWrite, type RunAnalysis, type RunProject } from "../analysis.ts";
1
+ import {
2
+ runAnalysis,
3
+ runBaselineWrite,
4
+ unansweredCounts,
5
+ type ComponentWarning,
6
+ type RunAnalysis,
7
+ type RunProject,
8
+ } from "../analysis.ts";
2
9
  import { ConfigError, type Format } from "../config.ts";
3
10
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
4
11
  import { moduleIdentity } from "../consumers.ts";
@@ -220,17 +227,19 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
220
227
  let recorded: readonly Recorded[];
221
228
  let verdict: number;
222
229
  let partial: readonly string[];
230
+ let warnings: readonly ComponentWarning[];
231
+ let unread: readonly string[];
223
232
  try {
224
233
  const root = runRelative(invoked, targetRoot);
225
234
  const mode = { production: true };
226
235
  let analysis: RunAnalysis;
227
236
  if (documents.baseline === undefined) {
228
- analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
237
+ analysis = runAnalysis(openClient(config), host, scoped, config, provenance, mode);
229
238
  recorded = [];
230
239
  } else {
231
240
  const reason = rowReason(analyzerProvenance(version));
232
241
  const write = runBaselineWrite(
233
- openClient(false),
242
+ openClient(config),
234
243
  host,
235
244
  scoped,
236
245
  config,
@@ -242,7 +251,16 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
242
251
  recorded = write.rows;
243
252
  }
244
253
  const { result, run } = analysis;
245
- partial = partialNotes({ notBuilt: run.notBuilt, unanswered: run.unanswered });
254
+ warnings = run.componentWarnings;
255
+ unread = run.componentsUnread.map(
256
+ ({ configFile, reason }) =>
257
+ `${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`,
258
+ );
259
+ partial = partialNotes({
260
+ notBuilt: run.notBuilt,
261
+ unanswered: run.unanswered,
262
+ targetRoot: resolvePath(invoked, targetRoot),
263
+ });
246
264
  const built = buildReport({
247
265
  contractVersion: CONTRACT_VERSION,
248
266
  version,
@@ -258,6 +276,9 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
258
276
  unavailable: [],
259
277
  result,
260
278
  testFileRules: run.testFileRules,
279
+ typeErrorSkips: run.typeErrorSkips,
280
+ unanswered: unansweredCounts(analysis),
281
+ conventionsApplied: run.conventionsApplied,
261
282
  });
262
283
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
263
284
  verdict = verdictOf(result, config);
@@ -293,6 +314,19 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
293
314
  for (const note of partial) {
294
315
  err.write(`deadset-ts: ${note}\n`);
295
316
  }
317
+ for (const line of unread) {
318
+ err.write(`deadset-ts: ${line}\n`);
319
+ }
320
+ for (const warning of warnings) {
321
+ err.write(
322
+ `deadset-ts: ${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}\n`,
323
+ );
324
+ }
325
+ for (const skip of report.type_error_skips) {
326
+ err.write(
327
+ `deadset-ts: ${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated\n`,
328
+ );
329
+ }
296
330
  if (report.totals.pending > 0) {
297
331
  err.write(
298
332
  `deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`,
@@ -7,6 +7,7 @@ import { OUTSIDE, swept as judged } from "../graph.ts";
7
7
  import type { InventorySymbol } from "../inventory.ts";
8
8
  import { referenceKey, type Configured } from "../matrix.ts";
9
9
  import { partialNotes } from "../partial.ts";
10
+ import { resolvePath } from "../paths.ts";
10
11
  import { positionKey } from "../position.ts";
11
12
  import type { Reference } from "../references.ts";
12
13
  import { resolve } from "../resolve.ts";
@@ -365,13 +366,15 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
365
366
  export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
366
367
  const named = namedSymbol(option);
367
368
  const { config, provenance } = resolve(inputs());
368
- const analysis = runAnalysis(openClient(false), host, scope(), config, provenance, {
369
+ const scoped = scope();
370
+ const analysis = runAnalysis(openClient(config), host, scoped, config, provenance, {
369
371
  production: true,
370
372
  });
371
373
  const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
372
374
  const notes = partialNotes({
373
375
  notBuilt: analysis.run.notBuilt,
374
376
  unanswered: analysis.run.unanswered,
377
+ targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
375
378
  });
376
379
  if (subject === undefined) {
377
380
  err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
@@ -1,80 +1,28 @@
1
- import {
2
- discoverProjects,
3
- DiscoveryError,
4
- isGuess,
5
- projectErrors,
6
- renderDiagnostic,
7
- targetRelative,
8
- type NotBuilt,
9
- } from "../discover.ts";
1
+ import { runProjects } from "../analysis.ts";
10
2
  import { partialNotes } from "../partial.ts";
11
- import { relativePath, resolvePath } from "../paths.ts";
12
- import { resolveMatrix } from "../resolve.ts";
13
- import { diagnosticsOf, runSession } from "../session.ts";
14
- import { EXIT_CLEAN, EXIT_FAILURE, type Verb } from "./verb.ts";
3
+ import { resolvePath } from "../paths.ts";
4
+ import { resolveUnkinded } from "../resolve.ts";
5
+ import { EXIT_CLEAN, type Verb } from "./verb.ts";
15
6
 
16
7
  /**
17
- * Writes the name of every project one run analyzes, one per line, having read each
18
- * project's diagnostics first: the identifier the build matrix declares for it, or,
19
- * where discovery derived the project, its configuration file's path below the
20
- * target root. A derived project that does not load is not one the run analyzes, so it
21
- * is named on the error stream instead, as the analysis drops it.
8
+ * Writes the name of every project one run analyzes, one per line: the identifier the
9
+ * build matrix declares for it, or, where discovery derived the project, its
10
+ * configuration file's path below the target root. The projects are read as the
11
+ * analysis reads them, so a project the analysis drops is named on the error stream
12
+ * instead, and one that fails the analysis fails this command the same way.
22
13
  */
23
14
  export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
24
- const matrix = resolveMatrix(inputs());
15
+ const config = resolveUnkinded(inputs());
25
16
  const scoped = scope();
26
- const engine = openClient(false);
27
- const discovered = discoverProjects(engine, host, scoped, matrix);
28
- const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
29
- const root = resolvePath(host.workingDirectory(), scoped.target.path);
30
- const notBuilt: NotBuilt[] = [...discovered.notBuilt];
31
- const failures: string[] = [];
32
- const session = runSession(
33
- engine,
34
- discovered.configFiles,
35
- (project) => {
36
- const id = ids.get(project.configFile) ?? project.configFile;
37
- const errors = projectErrors(diagnosticsOf(project));
38
- const [unloaded] = errors.load;
39
- if (unloaded !== undefined && isGuess(discovered, project.configFile)) {
40
- const error = renderDiagnostic(unloaded, (path) => relativePath(root, path) ?? path);
41
- notBuilt.push({ id, configFile: project.configFile, error: targetRelative(error, root) });
42
- return undefined;
43
- }
44
- failures.push(...[...errors.load, ...errors.check].map((one) => renderDiagnostic(one)));
45
- return id;
46
- },
47
- discovered.derived ? "record" : "refuse",
48
- );
49
- for (const configFile of session.unopened) {
50
- if (!isGuess(discovered, configFile)) {
51
- throw new DiscoveryError(`${configFile} opened no project`, []);
52
- }
53
- notBuilt.push({
54
- id: ids.get(configFile) ?? configFile,
55
- configFile,
56
- error: "the compiler opened no project for the configuration",
57
- });
58
- }
59
- if (failures.length > 0) {
60
- for (const line of failures) {
61
- err.write(`${line}\n`);
62
- }
63
- err.write(`deadset-ts: ${String(failures.length)} error(s), so no answer was produced\n`);
64
- return EXIT_FAILURE;
65
- }
66
- const projects = session.projects.filter((id): id is string => id !== undefined);
67
- if (projects.length === 0 && notBuilt.length > 0) {
68
- for (const note of partialNotes({ notBuilt, unanswered: [] })) {
69
- err.write(`deadset-ts: ${note}\n`);
70
- }
71
- err.write("deadset-ts: no configuration discovery derived could be built\n");
72
- return EXIT_FAILURE;
73
- }
74
- for (const id of projects) {
17
+ const answer = runProjects(openClient(config), host, scoped, config);
18
+ for (const id of answer.configurations) {
75
19
  out.write(`${id}\n`);
76
20
  }
77
- for (const note of partialNotes({ notBuilt, unanswered: [] })) {
21
+ for (const note of partialNotes({
22
+ notBuilt: answer.notBuilt,
23
+ unanswered: answer.unanswered,
24
+ targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
25
+ })) {
78
26
  err.write(`deadset-ts: ${note}\n`);
79
27
  }
80
28
  return EXIT_CLEAN;
@@ -1,6 +1,7 @@
1
1
  import { runSweep } from "../analysis.ts";
2
2
  import { retainedLines } from "../exempt.ts";
3
3
  import { partialNotes } from "../partial.ts";
4
+ import { resolvePath } from "../paths.ts";
4
5
  import { resolve } from "../resolve.ts";
5
6
  import { EXIT_CLEAN, type Verb } from "./verb.ts";
6
7
 
@@ -14,14 +15,18 @@ import { EXIT_CLEAN, type Verb } from "./verb.ts";
14
15
  export const printRetainedVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
15
16
  const { config } = resolve(inputs());
16
17
  const scoped = scope();
17
- const swept = runSweep(openClient(false), host, scoped, config, {
18
+ const swept = runSweep(openClient(config), host, scoped, config, {
18
19
  marked: [],
19
20
  mode: { production: true },
20
21
  });
21
22
  for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
22
23
  out.write(`${line}\n`);
23
24
  }
24
- for (const note of partialNotes({ notBuilt: swept.notBuilt, unanswered: swept.unanswered })) {
25
+ for (const note of partialNotes({
26
+ notBuilt: swept.notBuilt,
27
+ unanswered: swept.unanswered,
28
+ targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
29
+ })) {
25
30
  err.write(`deadset-ts: ${note}\n`);
26
31
  }
27
32
  return EXIT_CLEAN;
@@ -1,5 +1,6 @@
1
1
  import { runRoots, type RunRoot } from "../analysis.ts";
2
2
  import { partialNotes } from "../partial.ts";
3
+ import { resolvePath } from "../paths.ts";
3
4
  import { resolve } from "../resolve.ts";
4
5
  import { EXIT_CLEAN, EXIT_FINDINGS, type Verb } from "./verb.ts";
5
6
 
@@ -27,7 +28,7 @@ function rootLine(root: RunRoot, several: boolean): string {
27
28
  export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
28
29
  const { config, provenance } = resolve(inputs());
29
30
  const scoped = scope();
30
- const answer = runRoots(openClient(false), host, scoped, config, provenance);
31
+ const answer = runRoots(openClient(config), host, scoped, config, provenance);
31
32
  const several = answer.configurations.length > 1;
32
33
  for (const root of answer.roots) {
33
34
  out.write(`${rootLine(root, several)}\n`);
@@ -35,7 +36,11 @@ export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient
35
36
  for (const finding of answer.findings) {
36
37
  err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
37
38
  }
38
- for (const note of partialNotes({ notBuilt: answer.notBuilt, unanswered: answer.unanswered })) {
39
+ for (const note of partialNotes({
40
+ notBuilt: answer.notBuilt,
41
+ unanswered: answer.unanswered,
42
+ targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
43
+ })) {
39
44
  err.write(`deadset-ts: ${note}\n`);
40
45
  }
41
46
  return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
package/src/verbs/verb.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Inputs } from "../config.ts";
1
+ import type { Config, Inputs } from "../config.ts";
2
2
  import type { Emitters } from "../findings/emitter.ts";
3
3
  import type { Host } from "../host.ts";
4
4
  import type { Writer } from "../run.ts";
@@ -24,8 +24,11 @@ export interface Invocation {
24
24
  readonly inputs: () => Inputs;
25
25
  /** The scope the options name: the document `--scope` names, or the target. */
26
26
  readonly scope: () => Scope;
27
- /** Opens the compiler client. A client the verb opened is closed if the verb throws. */
28
- readonly openClient: (collectTiming: boolean) => Engine;
27
+ /**
28
+ * Opens the compiler client for a run under `config`. A client the verb opened is
29
+ * closed if the verb throws.
30
+ */
31
+ readonly openClient: (config: Config) => Engine;
29
32
  /** The emitter of every kind family, which a verb that reports findings runs. */
30
33
  readonly emitters: Emitters;
31
34
  /** The arguments after the verb's name, as written. */
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Whether a version satisfies a dependency range, under the grammar npm, pnpm and yarn
3
+ * share: `||` alternatives of hyphen ranges or comparator runs, `x` and missing parts
4
+ * standing for any value, and a prerelease admitted only where a comparator names one
5
+ * of the same version. A range this grammar does not read is satisfied by nothing.
6
+ */
7
+
8
+ /** A version: three numbers and the prerelease identifiers, build metadata dropped. */
9
+ interface Version {
10
+ readonly major: number;
11
+ readonly minor: number;
12
+ readonly patch: number;
13
+ readonly prerelease: readonly string[];
14
+ }
15
+
16
+ /** One comparison a version must pass. */
17
+ interface Comparator {
18
+ readonly operator: "<" | "<=" | ">" | ">=" | "=";
19
+ readonly version: Version;
20
+ }
21
+
22
+ /** A version whose parts may be missing or a wildcard, as a range writes one. */
23
+ interface Partial {
24
+ readonly major?: number | undefined;
25
+ readonly minor?: number | undefined;
26
+ readonly patch?: number | undefined;
27
+ readonly prerelease: readonly string[];
28
+ }
29
+
30
+ const NUMBER = /^(?:0|[1-9]\d*)$/u;
31
+ const IDENTIFIER = /^[0-9A-Za-z-]+$/u;
32
+ const WILDCARD = new Set(["x", "X", "*"]);
33
+
34
+ /** The prerelease identifiers after a hyphen, or `undefined` where one is malformed. */
35
+ function prereleaseOf(text: string | undefined): readonly string[] | undefined {
36
+ if (text === undefined) {
37
+ return [];
38
+ }
39
+ const parts = text.split(".");
40
+ return parts.every((part) => IDENTIFIER.test(part)) ? parts : undefined;
41
+ }
42
+
43
+ /**
44
+ * One version as a range writes it, a leading `v` or `=` allowed, or `undefined`
45
+ * where it is not one.
46
+ */
47
+ function partialOf(text: string): Partial | undefined {
48
+ const match =
49
+ /^[v=]?([^.+-]+)(?:\.([^.+-]+)(?:\.([^.+-]+)(?:-([^+]+))?)?)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(
50
+ text,
51
+ );
52
+ if (match === null) {
53
+ return undefined;
54
+ }
55
+ const parts: (number | undefined)[] = [];
56
+ let wild = false;
57
+ for (const part of [match[1], match[2], match[3]]) {
58
+ if (part === undefined || WILDCARD.has(part)) {
59
+ wild = true;
60
+ parts.push(undefined);
61
+ continue;
62
+ }
63
+ if (wild || !NUMBER.test(part)) {
64
+ return undefined;
65
+ }
66
+ parts.push(Number(part));
67
+ }
68
+ const prerelease = prereleaseOf(match[4]);
69
+ if (prerelease === undefined || (prerelease.length > 0 && wild)) {
70
+ return undefined;
71
+ }
72
+ return { major: parts[0], minor: parts[1], patch: parts[2], prerelease };
73
+ }
74
+
75
+ /** One exact version, or `undefined` where the text is not one. */
76
+ function versionOf(text: string): Version | undefined {
77
+ const partial = partialOf(text.trim());
78
+ if (partial?.major === undefined || partial.minor === undefined || partial.patch === undefined) {
79
+ return undefined;
80
+ }
81
+ return {
82
+ major: partial.major,
83
+ minor: partial.minor,
84
+ patch: partial.patch,
85
+ prerelease: partial.prerelease,
86
+ };
87
+ }
88
+
89
+ function exact(
90
+ major: number,
91
+ minor: number,
92
+ patch: number,
93
+ prerelease: readonly string[] = [],
94
+ ): Version {
95
+ return { major, minor, patch, prerelease };
96
+ }
97
+
98
+ /** The lowest prerelease of one version, which bounds a range from above. */
99
+ function floorOf(major: number, minor: number, patch: number): Version {
100
+ return exact(major, minor, patch, ["0"]);
101
+ }
102
+
103
+ function compareIdentifiers(a: string, b: string): number {
104
+ const numericA = NUMBER.test(a);
105
+ const numericB = NUMBER.test(b);
106
+ if (numericA && numericB) {
107
+ return Math.sign(Number(a) - Number(b));
108
+ }
109
+ if (numericA !== numericB) {
110
+ return numericA ? -1 : 1;
111
+ }
112
+ return a === b ? 0 : a < b ? -1 : 1;
113
+ }
114
+
115
+ /** Two versions ordered by precedence. */
116
+ function compare(a: Version, b: Version): number {
117
+ const core =
118
+ Math.sign(a.major - b.major) || Math.sign(a.minor - b.minor) || Math.sign(a.patch - b.patch);
119
+ if (core !== 0) {
120
+ return core;
121
+ }
122
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
123
+ return Math.sign(b.prerelease.length - a.prerelease.length);
124
+ }
125
+ for (let at = 0; at < Math.max(a.prerelease.length, b.prerelease.length); at += 1) {
126
+ const left = a.prerelease[at];
127
+ const right = b.prerelease[at];
128
+ if (left === undefined || right === undefined) {
129
+ return left === undefined ? -1 : 1;
130
+ }
131
+ const order = compareIdentifiers(left, right);
132
+ if (order !== 0) {
133
+ return order;
134
+ }
135
+ }
136
+ return 0;
137
+ }
138
+
139
+ function passes(version: Version, comparator: Comparator): boolean {
140
+ const order = compare(version, comparator.version);
141
+ switch (comparator.operator) {
142
+ case "<":
143
+ return order < 0;
144
+ case "<=":
145
+ return order <= 0;
146
+ case ">":
147
+ return order > 0;
148
+ case ">=":
149
+ return order >= 0;
150
+ case "=":
151
+ return order === 0;
152
+ }
153
+ }
154
+
155
+ /** The comparators a version with missing parts stands for: any value of each. */
156
+ function anyOf(partial: Partial): Comparator[] {
157
+ const { major, minor } = partial;
158
+ if (major === undefined) {
159
+ return [{ operator: ">=", version: exact(0, 0, 0) }];
160
+ }
161
+ if (minor === undefined) {
162
+ return [
163
+ { operator: ">=", version: exact(major, 0, 0) },
164
+ { operator: "<", version: floorOf(major + 1, 0, 0) },
165
+ ];
166
+ }
167
+ if (partial.patch === undefined) {
168
+ return [
169
+ { operator: ">=", version: exact(major, minor, 0) },
170
+ { operator: "<", version: floorOf(major, minor + 1, 0) },
171
+ ];
172
+ }
173
+ return [{ operator: "=", version: exact(major, minor, partial.patch, partial.prerelease) }];
174
+ }
175
+
176
+ /** The lowest version a partial version stands for. */
177
+ function lowest(partial: Partial): Version {
178
+ return exact(partial.major ?? 0, partial.minor ?? 0, partial.patch ?? 0, partial.prerelease);
179
+ }
180
+
181
+ function tilde(partial: Partial): Comparator[] {
182
+ const { major, minor } = partial;
183
+ if (major === undefined || minor === undefined) {
184
+ return anyOf(partial);
185
+ }
186
+ return [
187
+ { operator: ">=", version: lowest(partial) },
188
+ { operator: "<", version: floorOf(major, minor + 1, 0) },
189
+ ];
190
+ }
191
+
192
+ function caret(partial: Partial): Comparator[] {
193
+ const { major, minor, patch } = partial;
194
+ if (major === undefined) {
195
+ return anyOf(partial);
196
+ }
197
+ const from = { operator: ">=" as const, version: lowest(partial) };
198
+ if (major > 0 || minor === undefined) {
199
+ return [from, { operator: "<", version: floorOf(major + 1, 0, 0) }];
200
+ }
201
+ if (minor > 0 || patch === undefined) {
202
+ return [from, { operator: "<", version: floorOf(0, minor + 1, 0) }];
203
+ }
204
+ return [from, { operator: "<", version: floorOf(0, 0, patch + 1) }];
205
+ }
206
+
207
+ /** The comparators one primitive comparison stands for, a partial version filled in. */
208
+ function primitive(operator: "<" | "<=" | ">" | ">=", partial: Partial): Comparator[] {
209
+ const { major, minor, patch } = partial;
210
+ if (major === undefined) {
211
+ return operator === "<" || operator === ">"
212
+ ? [{ operator: "<", version: floorOf(0, 0, 0) }]
213
+ : [{ operator: ">=", version: exact(0, 0, 0) }];
214
+ }
215
+ if (minor !== undefined && patch !== undefined) {
216
+ return [{ operator, version: lowest(partial) }];
217
+ }
218
+ const next = minor === undefined ? exact(major + 1, 0, 0) : exact(major, minor + 1, 0);
219
+ switch (operator) {
220
+ case ">":
221
+ return [{ operator: ">=", version: next }];
222
+ case ">=":
223
+ return [{ operator: ">=", version: lowest(partial) }];
224
+ case "<":
225
+ return [{ operator: "<", version: floorOf(major, minor ?? 0, 0) }];
226
+ case "<=":
227
+ return [{ operator: "<", version: floorOf(next.major, next.minor, 0) }];
228
+ }
229
+ }
230
+
231
+ /** The comparators one written comparator stands for, or `undefined` where it is none. */
232
+ function comparatorsOf(token: string): Comparator[] | undefined {
233
+ const match = /^(<=|>=|<|>|=|~>|~|\^)?(.*)$/u.exec(token);
234
+ const operator = match?.[1] ?? "";
235
+ const partial = partialOf(match?.[2] ?? "");
236
+ if (partial === undefined) {
237
+ return undefined;
238
+ }
239
+ switch (operator) {
240
+ case "~":
241
+ case "~>":
242
+ return tilde(partial);
243
+ case "^":
244
+ return caret(partial);
245
+ case "<":
246
+ case "<=":
247
+ case ">":
248
+ case ">=":
249
+ return primitive(operator, partial);
250
+ default:
251
+ return anyOf(partial);
252
+ }
253
+ }
254
+
255
+ /** The comparators of one alternative, or `undefined` where it does not read. */
256
+ function alternativeOf(text: string): Comparator[] | undefined {
257
+ const spaced = text.trim().replaceAll(/(<=|>=|<|>|=|~>|~|\^)\s+/gu, "$1");
258
+ if (spaced === "") {
259
+ return anyOf({ prerelease: [] });
260
+ }
261
+ const hyphen = /^(\S+)\s+-\s+(\S+)$/u.exec(spaced);
262
+ if (hyphen !== null) {
263
+ const from = partialOf(hyphen[1] ?? "");
264
+ const to = partialOf(hyphen[2] ?? "");
265
+ if (from === undefined || to === undefined) {
266
+ return undefined;
267
+ }
268
+ return [...primitive(">=", from), ...(to.major === undefined ? [] : primitive("<=", to))];
269
+ }
270
+ const found: Comparator[] = [];
271
+ for (const token of spaced.split(/\s+/u)) {
272
+ const comparators = comparatorsOf(token);
273
+ if (comparators === undefined) {
274
+ return undefined;
275
+ }
276
+ found.push(...comparators);
277
+ }
278
+ return found;
279
+ }
280
+
281
+ /** Whether one alternative admits one version, its prerelease rule included. */
282
+ function admits(alternative: readonly Comparator[], version: Version): boolean {
283
+ if (!alternative.every((comparator) => passes(version, comparator))) {
284
+ return false;
285
+ }
286
+ if (version.prerelease.length === 0) {
287
+ return true;
288
+ }
289
+ return alternative.some(
290
+ ({ version: bound }) =>
291
+ bound.prerelease.length > 0 &&
292
+ bound.major === version.major &&
293
+ bound.minor === version.minor &&
294
+ bound.patch === version.patch,
295
+ );
296
+ }
297
+
298
+ /** Whether `version` satisfies `range`; `false` where either does not read. */
299
+ export function satisfies(version: string, range: string): boolean {
300
+ const parsed = versionOf(version);
301
+ if (parsed === undefined) {
302
+ return false;
303
+ }
304
+ const alternatives = range.split("||").map(alternativeOf);
305
+ return (
306
+ alternatives.every((alternative) => alternative !== undefined) &&
307
+ alternatives.some((alternative) => admits(alternative, parsed))
308
+ );
309
+ }
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 = "4.0.0";
7
+ export const CONTRACT_VERSION = "5.2.0";