pabst-checker 0.12.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/build-spec.d.ts +2 -0
  4. package/dist/build-spec.js +46 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +119 -0
  7. package/dist/codegen.d.ts +6 -0
  8. package/dist/codegen.js +30 -0
  9. package/dist/desugar.d.ts +5 -0
  10. package/dist/desugar.js +136 -0
  11. package/dist/discover.d.ts +16 -0
  12. package/dist/discover.js +113 -0
  13. package/dist/domains.d.ts +47 -0
  14. package/dist/domains.js +130 -0
  15. package/dist/emit.d.ts +2 -0
  16. package/dist/emit.js +74 -0
  17. package/dist/envelope.d.ts +29 -0
  18. package/dist/envelope.js +28 -0
  19. package/dist/equations.d.ts +8 -0
  20. package/dist/equations.js +256 -0
  21. package/dist/errors.d.ts +9 -0
  22. package/dist/errors.js +9 -0
  23. package/dist/extract.d.ts +21 -0
  24. package/dist/extract.js +177 -0
  25. package/dist/formula-ast.d.ts +26 -0
  26. package/dist/formula-ast.js +18 -0
  27. package/dist/formula-lexer.d.ts +31 -0
  28. package/dist/formula-lexer.js +191 -0
  29. package/dist/formula-parser.d.ts +2 -0
  30. package/dist/formula-parser.js +122 -0
  31. package/dist/free-idents.d.ts +6 -0
  32. package/dist/free-idents.js +60 -0
  33. package/dist/ir.d.ts +45 -0
  34. package/dist/ir.js +1 -0
  35. package/dist/issue.d.ts +26 -0
  36. package/dist/issue.js +18 -0
  37. package/dist/lower.d.ts +12 -0
  38. package/dist/lower.js +37 -0
  39. package/dist/parse-formula.d.ts +12 -0
  40. package/dist/parse-formula.js +78 -0
  41. package/dist/prefix-parser.d.ts +7 -0
  42. package/dist/prefix-parser.js +188 -0
  43. package/dist/qualified-name.d.ts +5 -0
  44. package/dist/qualified-name.js +11 -0
  45. package/dist/range.d.ts +5 -0
  46. package/dist/range.js +152 -0
  47. package/dist/regex-guard.d.ts +28 -0
  48. package/dist/regex-guard.js +100 -0
  49. package/dist/run.d.ts +26 -0
  50. package/dist/run.js +50 -0
  51. package/dist/runtime.d.ts +25 -0
  52. package/dist/runtime.js +62 -0
  53. package/dist/seed.d.ts +7 -0
  54. package/dist/seed.js +20 -0
  55. package/package.json +67 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jesse Alama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # Pabst: A blue-ribbon approach to **P**roperty-**B**ased **T**esting
2
+
3
+ Annotate your functions with properties they're supposed to have, then try to invalidate them with [fast-check](https://fast-check.dev/).
4
+
5
+ Put the properties your functions should have in a JSDoc comment, run Pabst,
6
+ and get either "cases passed" or a counterexample that shows the property doesn't hold.
7
+
8
+ _Example_ Look at this code. We're trying to assert that the value of the function is
9
+ non-zero provided the second argument is an integer. Can you spot the error?
10
+
11
+ ```ts
12
+ /**
13
+ * @ensures{nonzero} forall (x: bigint) (y: number),
14
+ * Number.isInteger(y) ==> foo(x, y) !== 0
15
+ */
16
+ export function foo(x: bigint, y: number): number {
17
+ return Number(x % 2n) + (y % 2) + 1;
18
+ }
19
+ ```
20
+
21
+ Each remainder looks like it should be 0 or 1, so the sum looks like it's at
22
+ least 1. But JavaScript's `%` returns _negative_ remainders for negative
23
+ operands: `foo(-1n, 0)` is `-1 + 0 + 1 === 0`. You don't have to spot that —
24
+ `pabst test` falsifies the property and reports a counterexample.
25
+
26
+ ## Philosophy
27
+
28
+ Pabst is a property-based testing tool. Instead of checking
29
+ your function against a handful of hand-picked examples,
30
+ pabst (delegating to `fast-check`) generates many random
31
+ inputs and works hard to **refute** the property you
32
+ attached; when it succeeds, it shrinks the failure to a
33
+ small, readable counterexample. One thing should be
34
+ understood, though: failing to invalidate a property — even
35
+ across many runs — is no _proof_ that the property holds. It
36
+ is evidence that it holds, but the property might still be
37
+ false on inputs the generator never tried. If your goal is
38
+ to prove the absence of counterexamples, you need
39
+ proof-based tools such as
40
+ [Thales](https://github.com/jessealama/thales). That said,
41
+ property-based testing is a powerful technique that exposes
42
+ a lot of bugs for very little effort, and it sits
43
+ comfortably alongside proof-based approaches.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ npm install --save-dev pabst-checker
49
+ ```
50
+
51
+ The package is `pabst-checker`; the command it installs is `pabst`.
52
+
53
+ Requires Node 24+. Pabst bundles its own [vitest](https://vitest.dev/) and
54
+ declares [fast-check](https://fast-check.dev/) as a peer dependency (npm
55
+ installs it for you), so nothing else is needed. The peer relationship
56
+ means pabst validates your annotations against the same fast-check copy
57
+ the generated tests run with — if your project pins an incompatible
58
+ fast-check, npm says so at install time instead of your tests failing
59
+ mysteriously.
60
+
61
+ ## Usage
62
+
63
+ ```bash
64
+ pabst test # discover sources, test, print a JSON report
65
+ pabst test <files-or-globs> # same, on an explicit file list
66
+ pabst test --seed <n> <files-or-globs> # reproduce a prior run's generation
67
+ pabst gen [files-or-globs] # generate only; run your own vitest against .pabst/
68
+ ```
69
+
70
+ With no file arguments, pabst discovers your sources: if `tsconfig.json`
71
+ exists, it scans exactly the files `tsc` would compile; otherwise it falls
72
+ back to `src/**`. If neither yields anything, it exits with an error asking
73
+ for an explicit glob. Discovery stays inside the current directory — a
74
+ tsconfig reaching outside it (say, a monorepo `include` of `../shared`) has
75
+ those files skipped; run pabst in the package that owns them.
76
+
77
+ Declaration files (`.d.ts`) are skipped by default — tsc copies JSDoc into
78
+ them, so scanning both a declaration and its source would extract every
79
+ property twice. A pattern that explicitly names declarations
80
+ (`pabst gen "index.d.ts"`) is honored, for packages whose hand-written types
81
+ are the source.
82
+
83
+ Pabst writes the test files it generates to a `.pabst/` directory in your
84
+ project. Those files are regenerated on every run, so there is no reason to
85
+ commit them — add `.pabst/` to your `.gitignore`:
86
+
87
+ ```gitignore
88
+ .pabst/
89
+ ```
90
+
91
+ ## Output
92
+
93
+ `pabst test` prints a single JSON object to **stdout**; **stderr** carries only
94
+ progress and crashes. The envelope is always present — a clean run just has an
95
+ empty `issues` array:
96
+
97
+ ```json
98
+ {
99
+ "version": "0.7.0",
100
+ "startedAt": "2026-06-26T17:42:03.000Z",
101
+ "cwd": "/path/to/project",
102
+ "seed": 1834592013,
103
+ "generated": 5,
104
+ "passed": 5,
105
+ "failed": 0,
106
+ "issues": []
107
+ }
108
+ ```
109
+
110
+ Each issue records where the property lived and why it failed:
111
+
112
+ ```json
113
+ {
114
+ "file": "src/math.ts",
115
+ "function": "add",
116
+ "property": "commutes",
117
+ "kind": "falsified",
118
+ "counterexample": { "x": 1, "y": 2 }
119
+ }
120
+ ```
121
+
122
+ - `kind` is `"falsified"` (returned `false`), `"threw"` (raised an exception —
123
+ see `error`), or `"exhausted"` (too many precondition skips — `error` explains,
124
+ and there is no `counterexample`).
125
+ - Counterexample values are JSON-native where they round-trip; bigints and
126
+ non-finite numbers appear as fast-check strings (e.g. `"1n"`).
127
+ - The `seed` is generated per run and echoed back; pass it to `--seed` to
128
+ reproduce a failing run exactly.
129
+
130
+ The process exits `0` when `issues` is empty, `1` when there is at least one
131
+ issue, and `2` on usage errors — including annotation errors such as a
132
+ malformed formula, an unsupported domain, or a reference to an unexported
133
+ symbol, which are reported as a one-line message on stderr.
134
+
135
+ ## Grammar
136
+
137
+ The normative grammar lives in [`docs/grammar.ebnf`](docs/grammar.ebnf);
138
+ this section is the guided tour.
139
+
140
+ A property is a universally quantified formula in Pabst's **logic surface**.
141
+ Non-ASCII symbols are the canonical form; most have ASCII fallbacks
142
+ (negation `¬` and the equation glyphs `≡`/`≢` are glyph-only — the ASCII
143
+ spelling of an equation is a plain `Object.is` call).
144
+
145
+ ```ts
146
+ /**
147
+ * @ensures{guarded} forall (x: int),
148
+ * isPrime(x) ∧ x > 2 → isOdd(x)
149
+ */
150
+ ```
151
+
152
+ - **Quantifier:** `forall` / `∀`, one-or-more binder groups, then a comma, then
153
+ the body. Lean-style grouping `(x y: int)` is supported. Existential `∃` /
154
+ `exists` is intentionally rejected (PBT cannot soundly confirm existence).
155
+ - **Domains:** `int`, `nat`, `number`, `boolean`, `string`, `bigint`.
156
+ A numeric domain (`int`, `nat`, `number`, `bigint`) may be constrained to
157
+ an interval: `forall (x: int ∈ [1, 30])` (ASCII fallback: `in`). Each
158
+ bound is independently inclusive (`[`/`]`) or exclusive (`(`/`)`), so
159
+ `(0, 1]`, `[0, 30)`, and `(0, 30)` all work — for `int`/`nat`/`bigint`
160
+ an exclusive bound is a ±1 adjustment. An endpoint may be unbounded:
161
+ `-∞`/`∞` (ASCII: `Infinity`), so `(x: number ∈ (0, ∞))` is a strictly
162
+ positive number (excluding `-0` — and `Infinity`, since the bound is
163
+ exclusive; `[0, ∞]` may generate `Infinity` itself). For `int`, `nat`,
164
+ and `bigint` an ∞ endpoint must be exclusive; for `int`/`nat` it means
165
+ the safe integer limit (±2^53 − 1), and a finite endpoint beyond that
166
+ limit clamps to it with a warning. A `nat` interval reaching below 0
167
+ clamps to 0 (`(-2, 5]` and `(-∞, 5]` denote the same naturals).
168
+ `number` intervals follow fast-check's double ordering, in which every
169
+ double is distinct: `-0` sits below `0`, and an exclusive bound removes
170
+ exactly one adjacent double — so `[-1, 0)` can generate `-0` (which
171
+ `== 0`), and `(-0, 0]` is the singleton `{0}`. A bounded `number` never
172
+ generates `NaN`.
173
+ - **Regex guards** constrain a string binder to strings matching a JS
174
+ regular expression: `forall (s: string ∈ /[a-z]+/)` (ASCII fallback:
175
+ `in`). Membership means the _whole_ string matches — pabst anchors the
176
+ pattern for you (lowering to `fc.stringMatching(/^(?:[a-z]+)$/)`), so
177
+ `/[a-z]+/` never generates `"3fk!"`. Flags `s` and `u` are allowed (`u`
178
+ enables `\p{...}` escapes); everything else is rejected — `m` because it
179
+ would reintroduce substring matching, `i`/`v` because fast-check's
180
+ generator lacks them, `g`/`y`/`d` because they don't affect generation.
181
+ Patterns outside fast-check's supported subset (lookarounds,
182
+ backreferences, `\b`) are compile-time errors. Careful inside JSDoc: a
183
+ `*/` in a pattern (e.g. the trailing star in a pattern matching
184
+ zero-or-more) ends the comment early — write `{0,}` instead of a
185
+ trailing `*`, or wrap it in `(?:...)`.
186
+ - **Connectives** (tightest→loosest): `¬` > `∧` > `∨` > `→` > `↔`.
187
+ Fallbacks: `∧`=`/\`, `∨`=`\/`, `→`=`->`/`==>`, `↔`=`<->`/`iff`.
188
+ Negation `¬` is glyph-only.
189
+ - **Equations:** `A ≡ B` means identity — sugar for `Object.is(A, B)`;
190
+ `A ≢ B` is its negation. Both are glyph-only, like `¬`: in plain ASCII,
191
+ call `Object.is(A, B)` directly (negate at an atom's top level with `≢` or
192
+ `¬(Object.is(A, B))`; nested `!Object.is(A, B)` is fine). This is
193
+ SameValue, not mathematical equality: `NaN ≡ NaN` holds and `-0 ≡ 0` does
194
+ not, so `x + 0 ≡ x` is refutable at `x = -0` (guard with `x ≢ -0 →` if
195
+ that is intended).
196
+ An equation lives at the **top level of an atom** — it splits the atom
197
+ into two JS sides. In nested positions (callbacks, call arguments,
198
+ template substitutions), call `Object.is` directly:
199
+ `xs.every(x => Object.is(x, 0))`, not `xs.every(x => x ≡ 0)`.
200
+ An unparenthesized `??` or ternary beside `≡` is an error — parenthesize
201
+ the intended grouping, e.g. `a ≡ (b ?? c)` or `a ≡ (b ? c : d)`.
202
+ Chains like `a ≡ b ≡ c` are errors — write
203
+ `a ≡ b ∧ b ≡ c`. Loose `==`/`!=` are errors (use `≡`/`≢` or `===`/`!==`);
204
+ `===`/`!==` keep their exact JS meaning; assignments — plain `=` and
205
+ compound forms like `+=` — cannot appear in a formula (default-parameter
206
+ initializers in callbacks are fine); `≠` is rejected with a hint to
207
+ write `≢`.
208
+ - **Atoms are JavaScript** and must be genuine booleans — every atom is checked
209
+ at runtime (`5 ∧ true` is an error, not a coercion). You may **not** use JS
210
+ `&&`/`||`/`!` at an atom's top level — use the glyphs. They remain legal
211
+ _inside_ a leaf (e.g. a callback `xs.every(x => x > 0 && x < 10)`).
212
+ - **Implication discard:** a **top-level** `→`'s antecedents become `fc.pre(...)`
213
+ (QuickCheck-style discarded cases, reported as `exhausted` if too many skip);
214
+ a **parenthesised** `→` is ordinary material implication `¬P ∨ Q`.
215
+ - **Biconditional** `↔` is non-associative (parenthesise chains) and is _not_ a
216
+ discard — it lowers to boolean equality.
217
+ - **Scoping:** every symbol an atom references must be `export`ed from its module.
218
+
219
+ Each `@ensures{name}` becomes one issue (keyed by file, function, and property
220
+ name) if it fails. Generated files land in the `.pabst/` directory (see
221
+ [Usage](#usage)) mirroring the source tree; they are regenerated every run and
222
+ must never be hand-edited.
223
+
224
+ ## Development
225
+
226
+ ```bash
227
+ npm install
228
+ npm test # vitest
229
+ npm run build # tsc -> dist/
230
+ ```
231
+
232
+ Requires Node 24+.
@@ -0,0 +1,2 @@
1
+ import type { PropertySpec } from "./ir.js";
2
+ export declare function buildSpecs(file: string): PropertySpec[];
@@ -0,0 +1,46 @@
1
+ import { extract } from "./extract.js";
2
+ import { parsePrefix } from "./prefix-parser.js";
3
+ import { parseBody } from "./formula-parser.js";
4
+ import { lowerTop } from "./lower.js";
5
+ import { collectAtoms } from "./formula-ast.js";
6
+ import { freeIdentifiers, classify } from "./free-idents.js";
7
+ import { PabstError } from "./errors.js";
8
+ export function buildSpecs(file) {
9
+ const { exports, annotations } = extract(file);
10
+ const specs = [];
11
+ for (const a of annotations) {
12
+ try {
13
+ specs.push(buildSpec(a, exports, file));
14
+ }
15
+ catch (e) {
16
+ if (e instanceof PabstError) {
17
+ throw new PabstError(`${file}:${a.line}: @ensures{${a.propertyName}}: ${e.message}`, { cause: e });
18
+ }
19
+ throw e;
20
+ }
21
+ }
22
+ return specs;
23
+ }
24
+ function buildSpec(a, exports, file) {
25
+ const { binders, body } = parsePrefix(a.formula);
26
+ const ast = parseBody(body);
27
+ const { preconditions, body: loweredBody } = lowerTop(ast);
28
+ const boundVars = new Set(binders.map((b) => b.varName));
29
+ const idents = new Set();
30
+ for (const atom of collectAtoms(ast)) {
31
+ for (const id of freeIdentifiers(atom))
32
+ idents.add(id);
33
+ }
34
+ const { freeExports } = classify(idents, boundVars, exports);
35
+ return {
36
+ name: a.propertyName,
37
+ functionName: a.functionName,
38
+ className: a.className,
39
+ isStatic: a.isStatic,
40
+ binders,
41
+ body: loweredBody,
42
+ preconditions,
43
+ freeExports,
44
+ location: { file, line: a.line },
45
+ };
46
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv?: string[]): number;
package/dist/cli.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { readFileSync, realpathSync } from "node:fs";
4
+ import { pathToFileURL } from "node:url";
5
+ import { generate } from "./codegen.js";
6
+ import { resolveFiles } from "./discover.js";
7
+ import { PabstError } from "./errors.js";
8
+ import { runTests } from "./run.js";
9
+ import { randomSeed, parseSeed } from "./seed.js";
10
+ function readVersion() {
11
+ const url = new URL("../package.json", import.meta.url);
12
+ return JSON.parse(readFileSync(url, "utf8")).version;
13
+ }
14
+ const USAGE = "usage: pabst <test|gen> [--seed <n>] [files-or-globs...]";
15
+ const HELP = `${USAGE}
16
+
17
+ commands:
18
+ test generate property tests from @ensures annotations, run them, and
19
+ print a JSON report to stdout
20
+ gen generate only; run your own vitest against .pabst/
21
+
22
+ when no files are given, pabst discovers your sources: the files that
23
+ tsconfig.json would compile or, failing that, src/**. declaration files
24
+ (.d.ts) are skipped unless a pattern names them (e.g. pabst gen index.d.ts).
25
+
26
+ options:
27
+ --seed <n> reproduce a prior run's generation (n is echoed in the report)
28
+ -h, --help show this help`;
29
+ export function main(argv = process.argv.slice(2)) {
30
+ // parseArgs throws on unknown options and the like — usage errors, which
31
+ // map to the documented exit-2 mode; anything else crashes loudly.
32
+ let positionals;
33
+ let values;
34
+ try {
35
+ ({ positionals, values } = parseArgs({
36
+ args: argv,
37
+ allowPositionals: true,
38
+ options: {
39
+ seed: { type: "string" },
40
+ help: { type: "boolean", short: "h" },
41
+ },
42
+ }));
43
+ }
44
+ catch (e) {
45
+ if (e instanceof TypeError &&
46
+ "code" in e &&
47
+ typeof e.code === "string" &&
48
+ e.code.startsWith("ERR_PARSE_ARGS_")) {
49
+ console.error(USAGE);
50
+ return 2;
51
+ }
52
+ throw e;
53
+ }
54
+ if (values.help) {
55
+ console.log(HELP);
56
+ return 0;
57
+ }
58
+ const command = positionals[0];
59
+ const patterns = positionals.slice(1);
60
+ if (command !== "test" && command !== "gen") {
61
+ console.error(USAGE);
62
+ return 2;
63
+ }
64
+ // User-facing errors anywhere below — a bad --seed, file resolution coming
65
+ // up empty, a malformed tsconfig, compile errors — are PabstErrors and map
66
+ // to the documented exit-2 error mode; anything else is an internal bug
67
+ // and crashes loudly.
68
+ try {
69
+ return run(command, patterns, values.seed);
70
+ }
71
+ catch (e) {
72
+ if (e instanceof PabstError) {
73
+ console.error(`error: ${e.message}`);
74
+ return 2;
75
+ }
76
+ throw e;
77
+ }
78
+ }
79
+ function run(command, patterns, seedArg) {
80
+ const seed = seedArg !== undefined ? parseSeed(seedArg) : randomSeed();
81
+ const { files, source } = resolveFiles(patterns);
82
+ if (source !== "arguments") {
83
+ console.error(`pabst: no files given; discovered ${files.length} file(s) via ${source}`);
84
+ }
85
+ const startedAt = new Date().toISOString();
86
+ const cwd = process.cwd();
87
+ const version = readVersion();
88
+ const results = generate(files, ".pabst", seed);
89
+ const generated = results.reduce((n, r) => n + r.propertyCount, 0);
90
+ console.error(`pabst: generated ${generated} propert${generated === 1 ? "y" : "ies"} across ${results.length} file(s) into .pabst/`);
91
+ if (command === "gen")
92
+ return 0;
93
+ const result = runTests(".pabst", {
94
+ version,
95
+ startedAt,
96
+ cwd,
97
+ seed,
98
+ generated,
99
+ });
100
+ if (result.kind === "no-results") {
101
+ process.stderr.write(result.stdout);
102
+ process.stderr.write(result.stderr);
103
+ return result.status;
104
+ }
105
+ if (result.kind === "broken-run") {
106
+ for (const m of result.messages)
107
+ console.error(`error: ${m}`);
108
+ return result.status;
109
+ }
110
+ console.log(JSON.stringify(result.envelope, null, 2));
111
+ return result.envelope.failed > 0 ? 1 : 0;
112
+ }
113
+ // npm installs the bin as a symlink (node_modules/.bin/pabst -> this file).
114
+ // Node resolves the main module to its realpath, but argv[1] keeps the
115
+ // symlink path, so argv[1] must be realpath'd before comparing.
116
+ if (process.argv[1] &&
117
+ import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
118
+ process.exit(main());
119
+ }
@@ -0,0 +1,6 @@
1
+ export interface GenResult {
2
+ sourceFile: string;
3
+ outFile: string;
4
+ propertyCount: number;
5
+ }
6
+ export declare function generate(files: string[], outRoot?: string, seed?: number): GenResult[];
@@ -0,0 +1,30 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { buildSpecs } from "./build-spec.js";
4
+ import { emit } from "./emit.js";
5
+ import { PabstError } from "./errors.js";
6
+ import { randomSeed } from "./seed.js";
7
+ const SRC_EXT = /\.(ts|tsx|mts|cts|js|mjs|cjs)$/;
8
+ export function generate(files, outRoot = ".pabst", seed = randomSeed()) {
9
+ const results = [];
10
+ for (const file of files) {
11
+ const rel = path.relative(process.cwd(), path.resolve(file));
12
+ // Out files mirror rel under outRoot, so a ".."-led (or, across Windows
13
+ // drives, absolute) rel would place them outside it — where the runner
14
+ // never looks. Refuse rather than silently skip what looks generated.
15
+ if (rel === ".." ||
16
+ rel.startsWith(`..${path.sep}`) ||
17
+ path.isAbsolute(rel)) {
18
+ throw new PabstError(`${file} is outside the current directory; run pabst from the directory containing it`);
19
+ }
20
+ const specs = buildSpecs(file);
21
+ if (specs.length === 0)
22
+ continue;
23
+ const noExt = rel.replace(SRC_EXT, "");
24
+ const outFile = path.join(outRoot, noExt + ".pabst.test.ts");
25
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
26
+ fs.writeFileSync(outFile, emit(specs, file, outFile, seed), "utf8");
27
+ results.push({ sourceFile: file, outFile, propertyCount: specs.length });
28
+ }
29
+ return results;
30
+ }
@@ -0,0 +1,5 @@
1
+ export interface Desugared {
2
+ preconditions: string[];
3
+ body: string;
4
+ }
5
+ export declare function desugar(body: string): Desugared;
@@ -0,0 +1,136 @@
1
+ /** Split `s` at depth-0 implication arrows (==>, ->, →). Returns trimmed segments. */
2
+ function splitTopArrows(s) {
3
+ const segs = [];
4
+ let depth = 0;
5
+ let start = 0;
6
+ let str = null;
7
+ for (let i = 0; i < s.length; i++) {
8
+ const c = s[i];
9
+ if (str) {
10
+ if (c === "\\") {
11
+ i++;
12
+ continue;
13
+ }
14
+ if (c === str)
15
+ str = null;
16
+ continue;
17
+ }
18
+ if (c === "'" || c === '"' || c === "`") {
19
+ str = c;
20
+ continue;
21
+ }
22
+ if (c === "(" || c === "[" || c === "{") {
23
+ depth++;
24
+ continue;
25
+ }
26
+ if (c === ")" || c === "]" || c === "}") {
27
+ depth--;
28
+ continue;
29
+ }
30
+ if (depth !== 0)
31
+ continue;
32
+ if (s.startsWith("==>", i)) {
33
+ segs.push(s.slice(start, i).trim());
34
+ i += 2;
35
+ start = i + 1;
36
+ continue;
37
+ }
38
+ if (s.startsWith("->", i)) {
39
+ segs.push(s.slice(start, i).trim());
40
+ i += 1;
41
+ start = i + 1;
42
+ continue;
43
+ }
44
+ if (c === "→") {
45
+ segs.push(s.slice(start, i).trim());
46
+ start = i + 1;
47
+ continue;
48
+ }
49
+ }
50
+ segs.push(s.slice(start).trim());
51
+ return segs;
52
+ }
53
+ /** Index of the `)` matching the `(` at `open`. */
54
+ function matchParen(s, open) {
55
+ let depth = 0;
56
+ let str = null;
57
+ for (let i = open; i < s.length; i++) {
58
+ const c = s[i];
59
+ if (str) {
60
+ if (c === "\\") {
61
+ i++;
62
+ continue;
63
+ }
64
+ if (c === str)
65
+ str = null;
66
+ continue;
67
+ }
68
+ if (c === "'" || c === '"' || c === "`") {
69
+ str = c;
70
+ continue;
71
+ }
72
+ if (c === "(")
73
+ depth++;
74
+ else if (c === ")") {
75
+ depth--;
76
+ if (depth === 0)
77
+ return i;
78
+ }
79
+ }
80
+ throw new Error(`unbalanced parentheses in property body: ${s}`);
81
+ }
82
+ /** Desugar arrows that appear inside parentheses; pass everything else through. */
83
+ function desugarNested(s) {
84
+ let out = "";
85
+ let i = 0;
86
+ let str = null;
87
+ while (i < s.length) {
88
+ const c = s[i];
89
+ if (str) {
90
+ out += c;
91
+ if (c === "\\" && i + 1 < s.length) {
92
+ out += s[i + 1];
93
+ i += 2;
94
+ continue;
95
+ }
96
+ if (c === str)
97
+ str = null;
98
+ i++;
99
+ continue;
100
+ }
101
+ if (c === "'" || c === '"' || c === "`") {
102
+ str = c;
103
+ out += c;
104
+ i++;
105
+ continue;
106
+ }
107
+ if (c === "(") {
108
+ const close = matchParen(s, i);
109
+ out += "(" + desugarExpr(s.slice(i + 1, close)) + ")";
110
+ i = close + 1;
111
+ continue;
112
+ }
113
+ out += c;
114
+ i++;
115
+ }
116
+ return out;
117
+ }
118
+ /** Fully desugar an expression: arrows at this level → !(P)||(Q), then recurse into parens. */
119
+ function desugarExpr(s) {
120
+ return foldImplication(splitTopArrows(s));
121
+ }
122
+ function foldImplication(segs) {
123
+ if (segs.length === 1)
124
+ return desugarNested(segs[0]);
125
+ return `!(${desugarNested(segs[0])}) || (${foldImplication(segs.slice(1))})`;
126
+ }
127
+ export function desugar(body) {
128
+ const segs = splitTopArrows(body);
129
+ if (segs.length === 1) {
130
+ return { preconditions: [], body: desugarNested(segs[0]) };
131
+ }
132
+ return {
133
+ preconditions: segs.slice(0, -1).map(desugarNested),
134
+ body: desugarNested(segs[segs.length - 1]),
135
+ };
136
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * True for TypeScript source files pabst should scan: .ts/.tsx/.mts/.cts,
3
+ * excluding declaration files — tsc copies JSDoc into declarations, so
4
+ * scanning them alongside their sources extracts every property twice.
5
+ */
6
+ export declare function isTsSource(file: string): boolean;
7
+ export interface Discovery {
8
+ files: string[];
9
+ source: "arguments" | "tsconfig.json" | "src/";
10
+ }
11
+ /**
12
+ * The files pabst should scan: matches of the given patterns or, with no
13
+ * patterns, zero-argument discovery (the tsconfig.json file list, then the
14
+ * src/ convention). Throws PabstError when nothing usable is found.
15
+ */
16
+ export declare function resolveFiles(patterns: string[]): Discovery;