@intentius/tsad-conformance 1.4.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.
- package/README.md +59 -0
- package/dist/adapter.d.ts +65 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/bundled.d.ts +5 -0
- package/dist/bundled.d.ts.map +1 -0
- package/dist/bundled.js +28 -0
- package/dist/bundled.js.map +1 -0
- package/dist/fixture.d.ts +49 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +95 -0
- package/dist/fixture.js.map +1 -0
- package/dist/host.d.ts +33 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +79 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/rules.d.ts +46 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +135 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +26 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +152 -0
- package/dist/runner.js.map +1 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/expect.json +8 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/caller.ts +5 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/leaky.ts +7 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/pure.ts +5 -0
- package/fixtures/F-Cycle/mutual-imports/expect.json +6 -0
- package/fixtures/F-Cycle/mutual-imports/project/a.ts +3 -0
- package/fixtures/F-Cycle/mutual-imports/project/b.ts +3 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/expect.json +19 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/project/deep.ts +3 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/project/rec.ts +3 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/expect.json +21 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/project/fn.ts +3 -0
- package/fixtures/F-Div-Eager/referenced-not-called/expect.json +8 -0
- package/fixtures/F-Div-Eager/referenced-not-called/project/app.ts +3 -0
- package/fixtures/F-Div-NsNew/namespace-callee/expect.json +18 -0
- package/fixtures/F-Div-NsNew/namespace-callee/input.ts +3 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/expect.json +20 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/app.ts +3 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/local.ts +3 -0
- package/fixtures/F-Div-SameFileNew/no-module-graph/expect.json +8 -0
- package/fixtures/F-Div-SameFileNew/no-module-graph/input.ts +5 -0
- package/fixtures/F-Div-Tag/unregistered-tag/expect.json +15 -0
- package/fixtures/F-Div-Tag/unregistered-tag/input.ts +1 -0
- package/fixtures/F-Eval-Array/spread-string-rejected/expect.json +14 -0
- package/fixtures/F-Eval-Array/spread-string-rejected/input.ts +1 -0
- package/fixtures/F-Eval-Binary/division-and-float/expect.json +19 -0
- package/fixtures/F-Eval-Binary/division-and-float/input.ts +1 -0
- package/fixtures/F-Eval-Binary/logical-return-operand/expect.json +20 -0
- package/fixtures/F-Eval-Binary/logical-return-operand/input.ts +1 -0
- package/fixtures/F-Eval-Binary/plus-dispatch/expect.json +19 -0
- package/fixtures/F-Eval-Binary/plus-dispatch/input.ts +1 -0
- package/fixtures/F-Eval-Binary/relational-on-strings/expect.json +18 -0
- package/fixtures/F-Eval-Binary/relational-on-strings/input.ts +1 -0
- package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/expect.json +8 -0
- package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/project/app.ts +3 -0
- package/fixtures/F-Eval-CallEager/not-a-function/expect.json +22 -0
- package/fixtures/F-Eval-CallEager/not-a-function/project/app.ts +3 -0
- package/fixtures/F-Eval-CallEager/not-a-function/project/local.ts +1 -0
- package/fixtures/F-Eval-CallHelper/helper-folds/expect.json +8 -0
- package/fixtures/F-Eval-CallHelper/helper-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/expect.json +22 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/project/fn.ts +5 -0
- package/fixtures/F-Eval-CallIntrinsic/call-form-folds/expect.json +8 -0
- package/fixtures/F-Eval-CallIntrinsic/call-form-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/expect.json +22 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/fn.ts +5 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/expect.json +7 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/project/pure.ts +3 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/expect.json +17 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/project/rec.ts +3 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/project/shallow.ts +3 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/expect.json +20 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/app.ts +5 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/pure.ts +3 -0
- package/fixtures/F-Eval-CallMethod/method-missing/expect.json +17 -0
- package/fixtures/F-Eval-CallMethod/method-missing/input.ts +1 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-dot/expect.json +13 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-dot/input.ts +2 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-optional/expect.json +2 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-optional/input.ts +2 -0
- package/fixtures/F-Eval-CallMethod/real-receiver/expect.json +2 -0
- package/fixtures/F-Eval-CallMethod/real-receiver/input.ts +1 -0
- package/fixtures/F-Eval-Template/number-formatting/expect.json +11 -0
- package/fixtures/F-Eval-Template/number-formatting/input.ts +1 -0
- package/fixtures/F-Eval-Template/tostring-of-non-numbers/expect.json +10 -0
- package/fixtures/F-Eval-Template/tostring-of-non-numbers/input.ts +1 -0
- package/fixtures/F-Eval-Unary/coercion/expect.json +19 -0
- package/fixtures/F-Eval-Unary/coercion/input.ts +1 -0
- package/fixtures/F-Eval-Undefined/bare-undefined/expect.json +2 -0
- package/fixtures/F-Eval-Undefined/bare-undefined/input.ts +1 -0
- package/fixtures/F-Exc-Registry/call-form-without-registry/expect.json +15 -0
- package/fixtures/F-Exc-Registry/call-form-without-registry/input.ts +1 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/expect.json +9 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/closed.ts +3 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/open.ts +3 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/pure.ts +3 -0
- package/fixtures/F-Host-DataExports/plain-data-export/expect.json +8 -0
- package/fixtures/F-Host-DataExports/plain-data-export/project/app.ts +3 -0
- package/fixtures/F-Host-Registry/tag-called-as-function/expect.json +8 -0
- package/fixtures/F-Host-Registry/tag-called-as-function/project/app.ts +3 -0
- package/fixtures/F-Host-Trust/unowned-package/expect.json +6 -0
- package/fixtures/F-Host-Trust/unowned-package/project/app.ts +3 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/expect.json +7 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/project/use.ts +3 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/project/values.ts +2 -0
- package/fixtures/F-NoExports/nothing-to-produce/expect.json +7 -0
- package/fixtures/F-NoExports/nothing-to-produce/project/defaulted.ts +1 -0
- package/fixtures/F-NoExports/nothing-to-produce/project/empty.ts +3 -0
- package/fixtures/F-Prebuild/same-file-construction-shared/expect.json +19 -0
- package/fixtures/F-Prebuild/same-file-construction-shared/project/shared.ts +6 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/expect.json +39 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/project/defaults.ts +5 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/project/service.ts +10 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/expect.json +8 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/project/app.ts +8 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/project/lone.ts +1 -0
- package/fixtures/F-Succ/backward-along-captures/expect.json +26 -0
- package/fixtures/F-Succ/backward-along-captures/project/breaker.ts +10 -0
- package/fixtures/F-Succ/backward-along-captures/project/shared.ts +1 -0
- package/fixtures/F-Succ/backward-along-captures/project/sibling.ts +3 -0
- package/fixtures/F-Succ/forward-into-imports/expect.json +8 -0
- package/fixtures/F-Succ/forward-into-imports/project/app.ts +10 -0
- package/fixtures/F-Succ/forward-into-imports/project/config.ts +1 -0
- package/fixtures/F-Taint/independent-file-still-folds/expect.json +9 -0
- package/fixtures/F-Taint/independent-file-still-folds/project/alone.ts +1 -0
- package/fixtures/F-Taint/independent-file-still-folds/project/app.ts +8 -0
- package/fixtures/F-Total/one-bad-declarator-fails-the-file/expect.json +7 -0
- package/fixtures/F-Total/one-bad-declarator-fails-the-file/project/mixed.ts +3 -0
- package/fixtures/F-Val-Arity/props-attributes-and-spread/expect.json +14 -0
- package/fixtures/F-Val-Arity/props-attributes-and-spread/project/arities.ts +5 -0
- package/fixtures/F-Val-Callable/host-function-as-value/expect.json +16 -0
- package/fixtures/F-Val-Callable/host-function-as-value/project/app.ts +3 -0
- package/fixtures/F-Val-Fate/revive-every-envelope/expect.json +15 -0
- package/fixtures/F-Val-Fate/revive-every-envelope/project/entities.ts +6 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/expect.json +9 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/breaker.ts +10 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/quiet.ts +3 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/source.ts +5 -0
- package/fixtures/F-Val-Position/attrref-in-host-arguments/expect.json +8 -0
- package/fixtures/F-Val-Position/attrref-in-host-arguments/project/rejected.ts +4 -0
- package/fixtures/F-Val-Undefined/array-element-undefined/expect.json +15 -0
- package/fixtures/F-Val-Undefined/array-element-undefined/input.ts +1 -0
- package/fixtures/F-Val-Undefined/conditional-yields-undefined/expect.json +13 -0
- package/fixtures/F-Val-Undefined/conditional-yields-undefined/input.ts +2 -0
- package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/expect.json +13 -0
- package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/input.ts +2 -0
- package/fixtures/F-Val-Undefined/property-literal-undefined/expect.json +16 -0
- package/fixtures/F-Val-Undefined/property-literal-undefined/input.ts +1 -0
- package/fixtures/F-Val-Undefined/spread-carries-undefined/expect.json +14 -0
- package/fixtures/F-Val-Undefined/spread-carries-undefined/input.ts +2 -0
- package/fixtures/S-Array/spread/expect.json +1 -0
- package/fixtures/S-Array/spread/input.ts +1 -0
- package/fixtures/S-Binary/arithmetic-and-concat/expect.json +1 -0
- package/fixtures/S-Binary/arithmetic-and-concat/input.ts +1 -0
- package/fixtures/S-Binary/lazy-untaken-unfoldable/expect.json +1 -0
- package/fixtures/S-Binary/lazy-untaken-unfoldable/input.ts +1 -0
- package/fixtures/S-Binary/nullish-coalesce/expect.json +1 -0
- package/fixtures/S-Binary/nullish-coalesce/input.ts +1 -0
- package/fixtures/S-CallLocal/imported-callee-is-shape-valid/expect.json +10 -0
- package/fixtures/S-CallLocal/imported-callee-is-shape-valid/input.ts +3 -0
- package/fixtures/S-Conditional/taken-branch-only/expect.json +1 -0
- package/fixtures/S-Conditional/taken-branch-only/input.ts +1 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/expect.json +30 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/project/app.ts +3 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/project/policy.ts +1 -0
- package/fixtures/S-ExportDefault/plain-object/expect.json +21 -0
- package/fixtures/S-ExportDefault/plain-object/input.ts +1 -0
- package/fixtures/S-Ident/const-chain/expect.json +1 -0
- package/fixtures/S-Ident/const-chain/input.ts +1 -0
- package/fixtures/S-Ident/process/expect.json +1 -0
- package/fixtures/S-Ident/process/input.ts +1 -0
- package/fixtures/S-Ident/unresolved/expect.json +1 -0
- package/fixtures/S-Ident/unresolved/input.ts +1 -0
- package/fixtures/S-Index/dynamic-key/expect.json +1 -0
- package/fixtures/S-Index/dynamic-key/input.ts +1 -0
- package/fixtures/S-Index/literal-key/expect.json +1 -0
- package/fixtures/S-Index/literal-key/input.ts +1 -0
- package/fixtures/S-Literal/null-and-booleans/expect.json +1 -0
- package/fixtures/S-Literal/null-and-booleans/input.ts +1 -0
- package/fixtures/S-Literal/numeric/expect.json +1 -0
- package/fixtures/S-Literal/numeric/input.ts +1 -0
- package/fixtures/S-LocalFunction/arrow-const-call/expect.json +21 -0
- package/fixtures/S-LocalFunction/arrow-const-call/project/label.ts +3 -0
- package/fixtures/S-LocalFunction/function-as-value-still-rejects/expect.json +11 -0
- package/fixtures/S-LocalFunction/function-as-value-still-rejects/input.ts +3 -0
- package/fixtures/S-LocalFunction/same-file-declaration-call/expect.json +19 -0
- package/fixtures/S-LocalFunction/same-file-declaration-call/project/twice.ts +5 -0
- package/fixtures/S-Member/nested/expect.json +1 -0
- package/fixtures/S-Member/nested/input.ts +1 -0
- package/fixtures/S-Member/nullish-plain-refuses/expect.json +1 -0
- package/fixtures/S-Member/nullish-plain-refuses/input.ts +1 -0
- package/fixtures/S-Member/optional-chain/expect.json +1 -0
- package/fixtures/S-Member/optional-chain/input.ts +1 -0
- package/fixtures/S-Object/spread-later-wins/expect.json +1 -0
- package/fixtures/S-Object/spread-later-wins/input.ts +1 -0
- package/fixtures/S-Object/spread-non-object/expect.json +1 -0
- package/fixtures/S-Object/spread-non-object/input.ts +1 -0
- package/fixtures/S-Reject/arrow-as-value/expect.json +1 -0
- package/fixtures/S-Reject/arrow-as-value/input.ts +1 -0
- package/fixtures/S-Reject/call-as-value/expect.json +1 -0
- package/fixtures/S-Reject/call-as-value/input.ts +1 -0
- package/fixtures/S-Template/coercion/expect.json +2 -0
- package/fixtures/S-Template/coercion/input.ts +2 -0
- package/fixtures/S-Template/composite-step-span/expect.json +2 -0
- package/fixtures/S-Template/composite-step-span/input.ts +1 -0
- package/fixtures/S-Template/const-span/expect.json +2 -0
- package/fixtures/S-Template/const-span/input.ts +2 -0
- package/fixtures/S-Template/envelope-span/expect.json +2 -0
- package/fixtures/S-Template/envelope-span/input.ts +2 -0
- package/fixtures/S-Template/nested-construction-span/expect.json +2 -0
- package/fixtures/S-Template/nested-construction-span/input.ts +1 -0
- package/fixtures/S-Template/no-substitution/expect.json +2 -0
- package/fixtures/S-Template/no-substitution/input.ts +1 -0
- package/fixtures/S-Template/unfoldable-span/expect.json +16 -0
- package/fixtures/S-Template/unfoldable-span/input.ts +1 -0
- package/fixtures/S-Unary/not-and-negate/expect.json +1 -0
- package/fixtures/S-Unary/not-and-negate/input.ts +1 -0
- package/fixtures/S-Unclaimed/composite-step/expect.json +3 -0
- package/fixtures/S-Unclaimed/composite-step/input.ts +1 -0
- package/fixtures/S-Unwrap/as-and-parens/expect.json +1 -0
- package/fixtures/S-Unwrap/as-and-parens/input.ts +1 -0
- package/fixtures/UNCOVERED.md +37 -0
- package/package.json +35 -0
- package/spec/VERSION +1 -0
- package/spec/divergence.md +129 -0
- package/spec/grammar.md +277 -0
- package/spec/hosts.md +175 -0
- package/spec/judgments.md +1049 -0
- package/spec/rules.md +102 -0
- package/spec/values.md +247 -0
- package/src/adapter.ts +52 -0
- package/src/adapters/chant.ts +125 -0
- package/src/bundled.ts +27 -0
- package/src/chant-agreement.test.ts +67 -0
- package/src/corpus.test.ts +66 -0
- package/src/corpus.ts +540 -0
- package/src/fixture.ts +117 -0
- package/src/host.ts +105 -0
- package/src/index.ts +6 -0
- package/src/rules.test.ts +50 -0
- package/src/rules.ts +140 -0
- package/src/runner.test.ts +46 -0
- package/src/runner.ts +119 -0
- package/src/version.test.ts +31 -0
package/src/fixture.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture format (#7, extended for projects by #24). A directory per fixture
|
|
3
|
+
* under spec/fixtures/<Rule>/<name>/. Two kinds, told apart by what the
|
|
4
|
+
* directory contains.
|
|
5
|
+
*
|
|
6
|
+
* An *expression* fixture judges one export of one file:
|
|
7
|
+
* input.ts — the module under test
|
|
8
|
+
* expect.json — { "rules": ["S-Template", "F-Eval-Template"], // identifiers this fixture exercises (#8 gate)
|
|
9
|
+
* "export": "x", // which export is judged
|
|
10
|
+
* "shape": "accept" | "reject", // S-* verdict
|
|
11
|
+
* "fold": "fold" | "run", // F-* verdict
|
|
12
|
+
* "value": <json> | "$undefined", // required when fold = "fold"; the sentinel, at the top or nested, means the fold yields undefined there, which JSON cannot write (#82)
|
|
13
|
+
* "rejectAt": { "line": n, "column": n }, // optional, when fold = "run": where the rejection must point
|
|
14
|
+
* "note": "why this fixture exists" }
|
|
15
|
+
*
|
|
16
|
+
* A *project* fixture judges every file of a small build, which is what J3
|
|
17
|
+
* needs: a taint edge cannot be seen in one file.
|
|
18
|
+
* project/ — the build's source, one or more .ts files, nested allowed
|
|
19
|
+
* expect.json — { "rules": ["F-Seed", "F-Succ"],
|
|
20
|
+
* "project": true,
|
|
21
|
+
* "verdicts": { "app.ts": "run", "config.ts": "run" }, // final, after J3
|
|
22
|
+
* "tentative": { "config.ts": "fold" }, // optional, J2 before J3 disposed
|
|
23
|
+
* "taintedBy": { "config.ts": "app.ts" }, // optional, the file whose taint reached it
|
|
24
|
+
* "exports": { "config.ts": { "port": 8080 } }, // optional, for files that finally fold
|
|
25
|
+
* "rejectRule": { "app.ts": "F-Eval-CallLocal" }, // optional, the rule a run verdict must name; checked when the adapter reports one
|
|
26
|
+
* "host": "shapes", // optional, a named host from host.ts; required if the sources import one
|
|
27
|
+
* "profiles": ["full"], // optional; see profilesOf for the default
|
|
28
|
+
* "note": "why this fixture exists" }
|
|
29
|
+
* `tentative` and `taintedBy` are what separate "folds because nothing
|
|
30
|
+
* reached it" from "would have folded, and an edge killed it" — without them
|
|
31
|
+
* a project fixture cannot tell F-Seed from F-Taint.
|
|
32
|
+
*/
|
|
33
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
34
|
+
import { join, relative } from "node:path";
|
|
35
|
+
|
|
36
|
+
export interface ExpressionFixture {
|
|
37
|
+
kind: "expression";
|
|
38
|
+
profiles: Profile[];
|
|
39
|
+
id: string; dir: string; input: string;
|
|
40
|
+
rules: string[]; exportName: string;
|
|
41
|
+
shape: "accept" | "reject"; fold: "fold" | "run";
|
|
42
|
+
value?: unknown; rejectAt?: { line: number; column: number }; note?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ProjectFixture {
|
|
45
|
+
kind: "project";
|
|
46
|
+
profiles: Profile[];
|
|
47
|
+
id: string; dir: string; rules: string[];
|
|
48
|
+
/** Path relative to project/, to source. Paths use "/" on every platform. */
|
|
49
|
+
files: Map<string, string>;
|
|
50
|
+
verdicts: Record<string, "fold" | "run">;
|
|
51
|
+
tentative?: Record<string, "fold" | "run">;
|
|
52
|
+
taintedBy?: Record<string, string>;
|
|
53
|
+
exports?: Record<string, Record<string, unknown>>;
|
|
54
|
+
/** The rule a `run` verdict must name, per file. An adapter that reports no rule is not held to it. */
|
|
55
|
+
rejectRule?: Record<string, string>;
|
|
56
|
+
/** A named host from host.ts. Required for any fixture whose sources import one. */
|
|
57
|
+
host?: string;
|
|
58
|
+
note?: string;
|
|
59
|
+
}
|
|
60
|
+
export type Fixture = ExpressionFixture | ProjectFixture;
|
|
61
|
+
|
|
62
|
+
export type Profile = "full" | "data-host";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The profiles a fixture is judged in (F-Profile, #78). Explicit
|
|
66
|
+
* `"profiles"` wins. Otherwise: a fixture that names a host, asserts J3's
|
|
67
|
+
* tentative verdicts or taint edges, or constructs with `new` needs the
|
|
68
|
+
* runtime, so it is `full` only; anything else is data and belongs to both.
|
|
69
|
+
*/
|
|
70
|
+
export function profilesOf(f: Fixture, explicit?: string[]): Profile[] {
|
|
71
|
+
if (explicit) return explicit as Profile[];
|
|
72
|
+
const sources = f.kind === "expression" ? [f.input] : [...f.files.values()];
|
|
73
|
+
const usesNew = sources.some((s) => /\bnew\s+[A-Za-z_$]/.test(s));
|
|
74
|
+
if (f.kind === "project" && (f.host || f.tentative || f.taintedBy)) return ["full"];
|
|
75
|
+
return usesNew ? ["full"] : ["full", "data-host"];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Kept as the name the expression-only callers import; `kind` narrows. */
|
|
79
|
+
export const expressionFixtures = (all: Fixture[]): ExpressionFixture[] => all.filter((f): f is ExpressionFixture => f.kind === "expression");
|
|
80
|
+
export const projectFixtures = (all: Fixture[]): ProjectFixture[] => all.filter((f): f is ProjectFixture => f.kind === "project");
|
|
81
|
+
|
|
82
|
+
function readProject(dir: string): Map<string, string> {
|
|
83
|
+
const files = new Map<string, string>();
|
|
84
|
+
const walk = (d: string) => {
|
|
85
|
+
for (const name of readdirSync(d)) {
|
|
86
|
+
const p = join(d, name);
|
|
87
|
+
if (statSync(p).isDirectory()) walk(p);
|
|
88
|
+
else if (name.endsWith(".ts")) files.set(relative(dir, p).split(/[\\/]/).join("/"), readFileSync(p, "utf8"));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
walk(dir);
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function loadFixtures(root: string): Fixture[] {
|
|
96
|
+
const out: Fixture[] = [];
|
|
97
|
+
for (const rule of readdirSync(root)) {
|
|
98
|
+
const rd = join(root, rule); if (!statSync(rd).isDirectory()) continue;
|
|
99
|
+
for (const name of readdirSync(rd)) {
|
|
100
|
+
const d = join(rd, name); if (!statSync(d).isDirectory()) continue;
|
|
101
|
+
const e = JSON.parse(readFileSync(join(d, "expect.json"), "utf8"));
|
|
102
|
+
const id = `${rule}/${name}`;
|
|
103
|
+
if (e.project) {
|
|
104
|
+
const fx: ProjectFixture = { kind: "project", id, dir: d, rules: e.rules, files: readProject(join(d, "project")), profiles: [],
|
|
105
|
+
verdicts: e.verdicts, tentative: e.tentative, taintedBy: e.taintedBy, exports: e.exports, rejectRule: e.rejectRule, host: e.host, note: e.note };
|
|
106
|
+
fx.profiles = profilesOf(fx, e.profiles);
|
|
107
|
+
out.push(fx);
|
|
108
|
+
} else {
|
|
109
|
+
const fx: ExpressionFixture = { kind: "expression", id, dir: d, input: readFileSync(join(d, "input.ts"), "utf8"), profiles: [],
|
|
110
|
+
rules: e.rules, exportName: e.export, shape: e.shape, fold: e.fold, value: e.value, rejectAt: e.rejectAt, note: e.note };
|
|
111
|
+
fx.profiles = profilesOf(fx, e.profiles);
|
|
112
|
+
out.push(fx);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named hosts a fixture may ask for (#61). A fixture is data — `.ts` sources
|
|
3
|
+
* and a JSON expectation — and F-Host-Interface item 1 requires real classes
|
|
4
|
+
* that revival can construct, so the classes live here, in code, and a fixture
|
|
5
|
+
* names the host it wants.
|
|
6
|
+
*
|
|
7
|
+
* Neutral by construction: nothing here imports an implementation. An adapter
|
|
8
|
+
* translates a `ConformanceHost` into whatever its own host interface is.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** An intrinsic registration, F-Host-Registry's shape. */
|
|
12
|
+
export interface HostIntrinsic {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly isTag: boolean;
|
|
15
|
+
readonly foldsAsCall?: boolean;
|
|
16
|
+
readonly foldsEagerly?: boolean;
|
|
17
|
+
readonly outputKey?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ConformanceHost {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** F-Host-Trust arm 1: the specifiers this host owns. */
|
|
23
|
+
readonly ownedSpecifierPrefixes: readonly string[];
|
|
24
|
+
readonly intrinsics: readonly HostIntrinsic[];
|
|
25
|
+
readonly helpers: readonly { name: string; module: string; note: string }[];
|
|
26
|
+
/** Specifier, then export name, to the real value. Revival calls these (F-Val-Fate). */
|
|
27
|
+
readonly values: ReadonlyMap<string, ReadonlyMap<string, unknown>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DECLARABLE = Symbol.for("tsad.conformance.declarable");
|
|
31
|
+
|
|
32
|
+
/** F-Host-Interface item 1's shape: `new (props, attributes?)`, carrying the markers an entity carries. */
|
|
33
|
+
class Bucket {
|
|
34
|
+
readonly entityType = "Bucket";
|
|
35
|
+
readonly lexicon = "shapes";
|
|
36
|
+
readonly props: Record<string, unknown>;
|
|
37
|
+
readonly attributes: Record<string, unknown>;
|
|
38
|
+
constructor(props: Record<string, unknown> = {}, attributes: Record<string, unknown> = {}) {
|
|
39
|
+
this.props = props;
|
|
40
|
+
this.attributes = attributes;
|
|
41
|
+
Object.defineProperty(this, DECLARABLE, { value: true, enumerable: false });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Spread-`args` arity, the other arm of F-Val-Arity. */
|
|
46
|
+
class Pair {
|
|
47
|
+
readonly entityType = "Pair";
|
|
48
|
+
readonly lexicon = "shapes";
|
|
49
|
+
constructor(readonly left: unknown, readonly right: unknown) {
|
|
50
|
+
Object.defineProperty(this, DECLARABLE, { value: true, enumerable: false });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A live object the host owns outright, for the F-CallLeak case: no construction involved. */
|
|
55
|
+
const registry = new Map<string, string>([["one", "1"]]);
|
|
56
|
+
|
|
57
|
+
/** An intrinsic in tag form: revival invokes it as `Name(strings, ...values)`. */
|
|
58
|
+
const join = (strings: readonly string[], ...values: unknown[]): string =>
|
|
59
|
+
strings.reduce((acc, s, i) => acc + s + (i < values.length ? String(values[i]) : ""), "");
|
|
60
|
+
|
|
61
|
+
/** An authoring helper: pure, deterministic, the same at fold time as at run time. */
|
|
62
|
+
const upper = (s: string): string => s.toUpperCase();
|
|
63
|
+
|
|
64
|
+
/** An intrinsic in call form (F-Host-Registry `foldsAsCall`): revival invokes it with the folded arguments. */
|
|
65
|
+
const ref = (name: string): { Ref: string } => ({ Ref: name });
|
|
66
|
+
|
|
67
|
+
/** An eager intrinsic (`foldsEagerly`): evaluated at fold time, and F-Host-Admission's third clause holds because it is a pure function of its arguments. */
|
|
68
|
+
const count = (items: readonly unknown[]): number => items.length;
|
|
69
|
+
|
|
70
|
+
/** A plain-data export (F-Host-DataExports): folds as a value through a named import. */
|
|
71
|
+
const SIZES = { small: 1, large: 3 } as const;
|
|
72
|
+
|
|
73
|
+
const SHAPES: ConformanceHost = {
|
|
74
|
+
name: "shapes",
|
|
75
|
+
ownedSpecifierPrefixes: ["@tsad/shapes"],
|
|
76
|
+
intrinsics: [
|
|
77
|
+
{ name: "join", isTag: true },
|
|
78
|
+
{ name: "ref", isTag: false, foldsAsCall: true },
|
|
79
|
+
{ name: "count", isTag: false, foldsEagerly: true },
|
|
80
|
+
],
|
|
81
|
+
helpers: [{ name: "upper", module: "@tsad/shapes", note: "pure string transform, no environment read" }],
|
|
82
|
+
values: new Map([
|
|
83
|
+
[
|
|
84
|
+
"@tsad/shapes",
|
|
85
|
+
new Map<string, unknown>([
|
|
86
|
+
["Bucket", Bucket],
|
|
87
|
+
["Pair", Pair],
|
|
88
|
+
["join", join],
|
|
89
|
+
["upper", upper],
|
|
90
|
+
["ref", ref],
|
|
91
|
+
["count", count],
|
|
92
|
+
["SIZES", SIZES],
|
|
93
|
+
["registry", registry],
|
|
94
|
+
]),
|
|
95
|
+
],
|
|
96
|
+
]),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const HOSTS: ReadonlyMap<string, ConformanceHost> = new Map([[SHAPES.name, SHAPES]]);
|
|
100
|
+
|
|
101
|
+
export function requireHost(name: string): ConformanceHost {
|
|
102
|
+
const h = HOSTS.get(name);
|
|
103
|
+
if (!h) throw new Error(`no such conformance host: ${name}. Known: ${[...HOSTS.keys()].join(", ")}`);
|
|
104
|
+
return h;
|
|
105
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** #34 — the rule index, the citation check, and chant's docs against both when a checkout is at hand. */
|
|
2
|
+
import { describe, test, expect } from "vitest";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { loadRules, checkCitations, describeFinding, bundledSpecDir } from "./index";
|
|
6
|
+
import { chantAdapter } from "./adapters/chant";
|
|
7
|
+
|
|
8
|
+
const index = loadRules(bundledSpecDir());
|
|
9
|
+
|
|
10
|
+
describe("the rule index (#34)", () => {
|
|
11
|
+
test("indexes every rule the fixture gate knows, with text", () => {
|
|
12
|
+
expect(index.version).toMatch(/^\d+\.\d+$/);
|
|
13
|
+
expect(index.rules.size).toBeGreaterThanOrEqual(120);
|
|
14
|
+
for (const id of ["S-Ident", "F-Eval-Ident", "F-Prebuild", "F-Depth", "F-Val-Fate", "F-Div-Nullish", "F-Host-Trust"]) {
|
|
15
|
+
expect(index.rules.has(id), id).toBe(true);
|
|
16
|
+
expect(index.rules.get(id)!.text.length, `${id} has text`).toBeGreaterThan(20);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("the citation check (#34)", () => {
|
|
22
|
+
test("an identifier that is not a rule is reported with its line", () => {
|
|
23
|
+
const f = checkCitations("chant implements F-Eval-Ident.\nAnd also F-Made-Up here.\n", index);
|
|
24
|
+
expect(f.map(describeFinding)).toEqual(["line 2: F-Made-Up is not a rule of this specification"]);
|
|
25
|
+
});
|
|
26
|
+
test("a verbatim quote passes and a paraphrase does not", () => {
|
|
27
|
+
const depth = index.rules.get("F-Depth")!.text;
|
|
28
|
+
const words = depth.split(" ").slice(0, 8).join(" ");
|
|
29
|
+
expect(checkCitations(`{/* rule: F-Depth */}\n> ${words}\n`, index)).toEqual([]);
|
|
30
|
+
const f = checkCitations(`<!-- rule: F-Depth -->\n> The bounds are whatever an implementation likes.\n`, index);
|
|
31
|
+
expect(f.map((x) => x.kind)).toEqual(["quote-not-in-rule"]);
|
|
32
|
+
});
|
|
33
|
+
test("a marker with no quote, and a declared version that is not the index's", () => {
|
|
34
|
+
const f = checkCitations(`{/* rule: F-Depth */}\nprose instead of a quote\n`, index, "2.0");
|
|
35
|
+
expect(f.map((x) => x.kind).sort()).toEqual(["marker-without-quote", "version-mismatch"]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const chant = process.env.TSAD_CHANT_REPO ? resolve(process.env.TSAD_CHANT_REPO) : undefined;
|
|
40
|
+
const DOCS = ["docs/src/content/docs/concepts/typescript-as-data.mdx", "docs/src/content/docs/architecture/sandbox.mdx"];
|
|
41
|
+
|
|
42
|
+
describe.skipIf(!chant || !DOCS.every((d) => existsSync(join(chant, d))))("chant's docs cite only rules that exist (#34)", () => {
|
|
43
|
+
for (const doc of DOCS) {
|
|
44
|
+
test(doc, () => {
|
|
45
|
+
const declared = chantAdapter.specVersion === "undeclared" ? undefined : chantAdapter.specVersion;
|
|
46
|
+
const findings = checkCitations(readFileSync(join(chant!, doc), "utf8"), index, declared);
|
|
47
|
+
expect(findings.map(describeFinding), findings.map(describeFinding).join("\n")).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
package/src/rules.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #34 — the rule index, and the check a downstream document runs against it.
|
|
3
|
+
*
|
|
4
|
+
* chant's documentation describes the mechanism this specification owns. A
|
|
5
|
+
* subset change that lands here can leave that prose describing the old
|
|
6
|
+
* subset, and chant#2306 was three such claims, one in a shape chant's own
|
|
7
|
+
* parity gate could not see because it only looked at fenced blocks under
|
|
8
|
+
* headings. This gate looks at rule identifiers instead: every `S-*` or
|
|
9
|
+
* `F-*` a document cites must exist at the specification version the
|
|
10
|
+
* document's implementation declares, and where the document quotes a rule,
|
|
11
|
+
* the quote must be the rule's own text.
|
|
12
|
+
*
|
|
13
|
+
* Two things a document can do, and what each is held to:
|
|
14
|
+
*
|
|
15
|
+
* - Mention an identifier in prose, `F-Depth`. It must be defined. Nothing
|
|
16
|
+
* about the surrounding claim is checked; the identifier is a link, and a
|
|
17
|
+
* link to nothing is the failure.
|
|
18
|
+
* - Quote a rule, as a blockquote directly under a marker line. The marker
|
|
19
|
+
* is a comment on a line of its own whose content is `rule: F-Depth`, in
|
|
20
|
+
* MDX's comment form or HTML's, and the blockquote follows it:
|
|
21
|
+
*
|
|
22
|
+
* > The rule's text, or its opening, verbatim.
|
|
23
|
+
*
|
|
24
|
+
* The quoted text must occur verbatim in the rule's normative text. A
|
|
25
|
+
* paraphrase is a claim the gate cannot check, so it is not accepted where a
|
|
26
|
+
* quote was promised.
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
/** The five files that define rules, in reading order. The same list `spec/fixtures.test.ts` gates on. */
|
|
32
|
+
export const RULE_FILES = ["grammar.md", "judgments.md", "values.md", "divergence.md", "hosts.md", "rules.md"] as const;
|
|
33
|
+
|
|
34
|
+
export interface Rule {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly file: string;
|
|
37
|
+
/** The rule's normative text: from its definition to the next definition or heading, whitespace collapsed. */
|
|
38
|
+
readonly text: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFINITION = [
|
|
42
|
+
/^#{1,6}\s+([SF]-[A-Za-z0-9-]+)\b/,
|
|
43
|
+
/^\*\*([SF]-[A-Za-z0-9-]+)[.\s(]/,
|
|
44
|
+
/^\|\s*([SF]-[A-Za-z0-9-]+)\s*\|/,
|
|
45
|
+
/^\s*([SF]-[A-Za-z0-9-]+)\s*::=/,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const collapse = (s: string) => s.replace(/\s+/g, " ").trim();
|
|
49
|
+
|
|
50
|
+
/** Every rule the specification defines, with its text. `specDir` holds the rule files and `VERSION`. */
|
|
51
|
+
export function loadRules(specDir: string): { version: string; rules: Map<string, Rule> } {
|
|
52
|
+
const version = readFileSync(join(specDir, "VERSION"), "utf8").trim();
|
|
53
|
+
const rules = new Map<string, Rule>();
|
|
54
|
+
for (const file of RULE_FILES) {
|
|
55
|
+
const lines = readFileSync(join(specDir, file), "utf8").split("\n");
|
|
56
|
+
let current: { id: string; body: string[] } | undefined;
|
|
57
|
+
const flush = () => {
|
|
58
|
+
if (!current) return;
|
|
59
|
+
const text = collapse(current.body.join("\n"));
|
|
60
|
+
// A rule may be defined in more than one place (a table row and a
|
|
61
|
+
// rationale paragraph). The first definition wins; later ones append.
|
|
62
|
+
const prior = rules.get(current.id);
|
|
63
|
+
rules.set(current.id, { id: current.id, file, text: prior ? `${prior.text} ${text}` : text });
|
|
64
|
+
current = undefined;
|
|
65
|
+
};
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const id = DEFINITION.map((re) => re.exec(line)?.[1]).find(Boolean);
|
|
68
|
+
if (id) { flush(); current = { id, body: [line] }; continue; }
|
|
69
|
+
if (/^#{1,6}\s/.test(line) || /^---\s*$/.test(line)) { flush(); continue; }
|
|
70
|
+
current?.body.push(line);
|
|
71
|
+
}
|
|
72
|
+
flush();
|
|
73
|
+
}
|
|
74
|
+
return { version, rules };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type CitationFinding =
|
|
78
|
+
| { kind: "unknown-identifier"; id: string; line: number }
|
|
79
|
+
| { kind: "quote-not-in-rule"; id: string; line: number; quote: string }
|
|
80
|
+
| { kind: "marker-without-quote"; id: string; line: number }
|
|
81
|
+
| { kind: "version-mismatch"; declared: string; spec: string };
|
|
82
|
+
|
|
83
|
+
const IDENTIFIER = /\b([SF]-[A-Z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)\b/g;
|
|
84
|
+
const MARKER = /^\s*(?:\{\/\*|<!--)\s*rule:\s*([SF]-[A-Za-z0-9-]+)\s*(?:\*\/\}|-->)\s*$/;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check one document's citations against the rule index.
|
|
88
|
+
*
|
|
89
|
+
* @param declared the specification version the document's implementation
|
|
90
|
+
* declares, compared against the index's; `undefined` skips the comparison
|
|
91
|
+
* and is reported as nothing, since an undeclared version is #18's concern
|
|
92
|
+
* and not this gate's.
|
|
93
|
+
*/
|
|
94
|
+
export function checkCitations(
|
|
95
|
+
document: string,
|
|
96
|
+
index: { version: string; rules: ReadonlyMap<string, Rule> },
|
|
97
|
+
declared?: string,
|
|
98
|
+
): CitationFinding[] {
|
|
99
|
+
const findings: CitationFinding[] = [];
|
|
100
|
+
// Same major, minor not ahead of the index: the policy says an addition
|
|
101
|
+
// leaves an older minor's rules unchanged, so a document written against
|
|
102
|
+
// 1.0 is held to 1.1's index. A different major, or a declaration ahead of
|
|
103
|
+
// the index, is a mismatch.
|
|
104
|
+
if (declared !== undefined) {
|
|
105
|
+
const [dm, dn] = declared.split(".").map(Number);
|
|
106
|
+
const [im, in_] = index.version.split(".").map(Number);
|
|
107
|
+
if (!(dm === im && dn <= in_)) findings.push({ kind: "version-mismatch", declared, spec: index.version });
|
|
108
|
+
}
|
|
109
|
+
const lines = document.split("\n");
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
const marker = MARKER.exec(line);
|
|
113
|
+
if (marker) {
|
|
114
|
+
const id = marker[1];
|
|
115
|
+
const rule = index.rules.get(id);
|
|
116
|
+
if (!rule) { findings.push({ kind: "unknown-identifier", id, line: i + 1 }); continue; }
|
|
117
|
+
const quoted: string[] = [];
|
|
118
|
+
let j = i + 1;
|
|
119
|
+
while (j < lines.length && /^\s*>/.test(lines[j])) quoted.push(lines[j].replace(/^\s*>\s?/, "")), j++;
|
|
120
|
+
if (quoted.length === 0) { findings.push({ kind: "marker-without-quote", id, line: i + 1 }); continue; }
|
|
121
|
+
const quote = collapse(quoted.join(" "));
|
|
122
|
+
if (!rule.text.includes(quote)) findings.push({ kind: "quote-not-in-rule", id, line: i + 1, quote });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const m of line.matchAll(IDENTIFIER)) {
|
|
126
|
+
if (!index.rules.has(m[1])) findings.push({ kind: "unknown-identifier", id: m[1], line: i + 1 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return findings;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One line per finding, for a test's failure message. */
|
|
133
|
+
export function describeFinding(f: CitationFinding): string {
|
|
134
|
+
switch (f.kind) {
|
|
135
|
+
case "unknown-identifier": return `line ${f.line}: ${f.id} is not a rule of this specification`;
|
|
136
|
+
case "quote-not-in-rule": return `line ${f.line}: the quote under ${f.id} is not the rule's text: "${f.quote}"`;
|
|
137
|
+
case "marker-without-quote": return `line ${f.line}: rule marker ${f.id} is not followed by a blockquote`;
|
|
138
|
+
case "version-mismatch": return `the document's implementation declares spec ${f.declared}, the rules are at ${f.spec}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { loadFixtures, runFixtures, projectFixtures } from "./index";
|
|
5
|
+
import { referenceAdapter, referenceDataHostAdapter } from "@intentius/tsad-reference";
|
|
6
|
+
|
|
7
|
+
const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "spec", "fixtures");
|
|
8
|
+
|
|
9
|
+
describe("conformance runner (#7) over the reference implementation (#10)", () => {
|
|
10
|
+
const fixtures = loadFixtures(fixturesDir);
|
|
11
|
+
test("fixtures load", async () => { expect(fixtures.length).toBeGreaterThan(0); for (const f of fixtures) expect(f.rules.length).toBeGreaterThan(0); });
|
|
12
|
+
test("the reference implementation passes every fixture of the full profile", async () => {
|
|
13
|
+
const reports = await runFixtures(referenceAdapter, fixtures.filter((f) => f.profiles.includes("full")));
|
|
14
|
+
const failed = reports.filter((r) => !r.pass).map((r) => `${r.fixture}: ${r.failures.join("; ")}`);
|
|
15
|
+
expect(failed, failed.join("\n")).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
test("the data-host profile: the reference with an empty host passes every fixture tagged for it (#78)", async () => {
|
|
18
|
+
const tagged = fixtures.filter((f) => f.profiles.includes("data-host"));
|
|
19
|
+
expect(tagged.length).toBeGreaterThan(40);
|
|
20
|
+
expect(tagged.every((f) => f.kind === "expression" || !f.host)).toBe(true);
|
|
21
|
+
const failed = (await runFixtures(referenceDataHostAdapter, tagged)).filter((r) => !r.pass).map((r) => `${r.fixture}: ${r.failures.join("; ")}`);
|
|
22
|
+
expect(failed, failed.join("\n")).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
test("the project fixtures are actually exercised, not all skipped (#24)", async () => {
|
|
25
|
+
const reports = (await runFixtures(referenceAdapter, fixtures)).filter((r) => projectFixtures(fixtures).some((p) => p.id === r.fixture));
|
|
26
|
+
expect(reports.length).toBe(projectFixtures(fixtures).length);
|
|
27
|
+
expect(reports.filter((r) => r.skipped)).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
test("a stub adapter that runs every file fails the project fixtures that expect a fold", async () => {
|
|
30
|
+
// The control the fixtures themselves describe: falling back everywhere is
|
|
31
|
+
// sound and useless, and must not pass. Without this, F-Taint's "least set"
|
|
32
|
+
// would be untested.
|
|
33
|
+
const stub = {
|
|
34
|
+
name: "always-runs", specVersion: "1.1", shape: () => "unavailable" as const,
|
|
35
|
+
foldExport: () => ({ ok: false as const, line: 1, column: 1, message: "runs" }),
|
|
36
|
+
foldProject: (files: Map<string, string>) => ({ verdicts: Object.fromEntries([...files.keys()].map((p) => [p, { kind: "run" as const, reason: "runs" }])) }),
|
|
37
|
+
};
|
|
38
|
+
const failed = (await runFixtures(stub, fixtures)).filter((r) => !r.pass && projectFixtures(fixtures).some((p) => p.id === r.fixture));
|
|
39
|
+
expect(failed.length).toBeGreaterThan(0);
|
|
40
|
+
});
|
|
41
|
+
test("a stub adapter that folds everything to null fails the reject fixture and the value fixtures", async () => {
|
|
42
|
+
const stub = { name: "stub", specVersion: "1.1", shape: () => ({ accepted: true } as const), foldExport: () => ({ ok: true as const, value: null }) };
|
|
43
|
+
const reports = await runFixtures(stub, fixtures);
|
|
44
|
+
expect(reports.some((r) => !r.pass)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/runner.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ConformanceAdapter } from "./adapter.js";
|
|
2
|
+
import type { ExpressionFixture, Fixture, ProjectFixture } from "./fixture.js";
|
|
3
|
+
import { expressionFixtures, projectFixtures } from "./fixture.js";
|
|
4
|
+
import { requireHost } from "./host.js";
|
|
5
|
+
|
|
6
|
+
export interface FixtureReport { fixture: string; adapter: string; pass: boolean; skipped?: string; failures: string[] }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The comparison form of a folded value. `JSON.stringify` drops an
|
|
10
|
+
* `undefined`-valued key and turns an `undefined` element into `null`, which
|
|
11
|
+
* is exactly the distinction F-Val-Undefined draws (#82): a fixture that wants
|
|
12
|
+
* to see it writes the sentinel `"$undefined"` where the fold must yield
|
|
13
|
+
* `undefined`, and this encoding maps the fold's `undefined` to the same
|
|
14
|
+
* sentinel before the two are compared. Non-finite numbers, which JSON cannot
|
|
15
|
+
* write either, encode as ToString would print them.
|
|
16
|
+
*/
|
|
17
|
+
export function encodeValue(v: unknown): string {
|
|
18
|
+
const walk = (x: unknown): unknown => {
|
|
19
|
+
if (x === undefined) return "$undefined";
|
|
20
|
+
if (typeof x === "number" && !Number.isFinite(x)) return String(x);
|
|
21
|
+
if (x === null || typeof x !== "object") return x;
|
|
22
|
+
if (Array.isArray(x)) return x.map(walk);
|
|
23
|
+
return Object.fromEntries(Object.entries(x).map(([k, e]) => [k, walk(e)]));
|
|
24
|
+
};
|
|
25
|
+
return JSON.stringify(walk(v));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function runFixture(adapter: ConformanceAdapter, f: ExpressionFixture): FixtureReport {
|
|
29
|
+
const failures: string[] = []; let skipped: string | undefined;
|
|
30
|
+
const s = adapter.shape(f.input, f.exportName);
|
|
31
|
+
if (s === "unavailable") skipped = "shape classifier unavailable";
|
|
32
|
+
else if (s.accepted !== (f.shape === "accept")) failures.push(`shape: expected ${f.shape}, got ${s.accepted ? "accept" : `reject (${s.message})`}`);
|
|
33
|
+
const r = adapter.foldExport(f.input, f.exportName);
|
|
34
|
+
if (f.fold === "fold") {
|
|
35
|
+
if (!r.ok) failures.push(`fold: expected fold, got run (${r.message})`);
|
|
36
|
+
else if (encodeValue(r.value) !== encodeValue(f.value)) failures.push(`fold: value ${encodeValue(r.value)} ≠ expected ${encodeValue(f.value)}`);
|
|
37
|
+
} else {
|
|
38
|
+
if (r.ok) failures.push(`fold: expected run, got fold with ${JSON.stringify(r.value)}`);
|
|
39
|
+
else if (f.rejectAt && (r.line !== f.rejectAt.line || r.column !== f.rejectAt.column))
|
|
40
|
+
failures.push(`fold: rejection at ${r.line}:${r.column}, expected ${f.rejectAt.line}:${f.rejectAt.column}`);
|
|
41
|
+
}
|
|
42
|
+
return { fixture: f.id, adapter: adapter.name, pass: failures.length === 0, skipped, failures };
|
|
43
|
+
}
|
|
44
|
+
export async function runFixtures(adapter: ConformanceAdapter, fixtures: Fixture[]): Promise<FixtureReport[]> {
|
|
45
|
+
return [
|
|
46
|
+
...expressionFixtures(fixtures).map((f) => runFixture(adapter, f)),
|
|
47
|
+
...(await Promise.all(projectFixtures(fixtures).map((f) => runProjectFixture(adapter, f)))),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** #24 — a whole-build fixture. J3's edges are invisible in any single file, so this is the only shape that can test them. */
|
|
52
|
+
export async function runProjectFixture(adapter: ConformanceAdapter, f: ProjectFixture): Promise<FixtureReport> {
|
|
53
|
+
const base = { fixture: f.id, adapter: adapter.name };
|
|
54
|
+
if (!adapter.foldProject) return { ...base, pass: true, skipped: "no project entry", failures: [] };
|
|
55
|
+
const r = await adapter.foldProject(f.files, f.host ? requireHost(f.host) : undefined);
|
|
56
|
+
if (r === "unavailable") return { ...base, pass: true, skipped: "project entry unavailable", failures: [] };
|
|
57
|
+
const failures: string[] = [];
|
|
58
|
+
for (const [path, want] of Object.entries(f.verdicts)) {
|
|
59
|
+
const got = r.verdicts[path];
|
|
60
|
+
if (!got) { failures.push(`${path}: no verdict reported`); continue; }
|
|
61
|
+
if (got.kind !== want) failures.push(`${path}: expected ${want}, got ${got.kind}${got.kind === "run" ? ` (${got.reason})` : ""}`);
|
|
62
|
+
}
|
|
63
|
+
for (const path of Object.keys(r.verdicts)) if (!(path in f.verdicts)) failures.push(`${path}: verdict reported but not expected`);
|
|
64
|
+
for (const [path, want] of Object.entries(f.tentative ?? {})) {
|
|
65
|
+
if (!r.tentative) { failures.push(`${path}: fixture expects a tentative verdict, adapter reports none`); continue; }
|
|
66
|
+
if (r.tentative[path] !== want) failures.push(`${path}: tentative expected ${want}, got ${r.tentative[path] ?? "none"}`);
|
|
67
|
+
}
|
|
68
|
+
for (const [path, want] of Object.entries(f.taintedBy ?? {})) {
|
|
69
|
+
if (!r.taintedBy) { failures.push(`${path}: fixture expects a taint source, adapter reports none`); continue; }
|
|
70
|
+
if (r.taintedBy[path] !== want) failures.push(`${path}: tainted by ${r.taintedBy[path] ?? "nothing"}, expected ${want}`);
|
|
71
|
+
}
|
|
72
|
+
for (const [path, want] of Object.entries(f.rejectRule ?? {})) {
|
|
73
|
+
const got = r.verdicts[path];
|
|
74
|
+
if (got?.kind !== "run") { failures.push(`${path}: expected a ${want} rejection, but the file folded`); continue; }
|
|
75
|
+
if (got.rule !== undefined && got.rule !== want) failures.push(`${path}: rejected under ${got.rule}, expected ${want} (${got.reason})`);
|
|
76
|
+
}
|
|
77
|
+
for (const [path, want] of Object.entries(f.exports ?? {})) {
|
|
78
|
+
const got = r.verdicts[path];
|
|
79
|
+
if (got?.kind !== "fold") { failures.push(`${path}: expected exports, but the file did not fold`); continue; }
|
|
80
|
+
for (const [name, value] of Object.entries(want)) {
|
|
81
|
+
if (encodeValue(got.exports[name]) !== encodeValue(value)) {
|
|
82
|
+
failures.push(`${path}: export ${name} = ${encodeValue(got.exports[name])} ≠ expected ${encodeValue(value)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { ...base, pass: failures.length === 0, failures };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** #11 — two implementations must agree on every fixture, independently of what the fixture expects. */
|
|
90
|
+
export async function compareAdapters(a: ConformanceAdapter, b: ConformanceAdapter, fixtures: Fixture[]): Promise<string[]> {
|
|
91
|
+
const dis: string[] = [];
|
|
92
|
+
for (const f of projectFixtures(fixtures)) {
|
|
93
|
+
if (!a.foldProject || !b.foldProject) continue;
|
|
94
|
+
const host = f.host ? requireHost(f.host) : undefined;
|
|
95
|
+
const [ra, rb] = await Promise.all([a.foldProject(f.files, host), b.foldProject(f.files, host)]);
|
|
96
|
+
if (ra === "unavailable" || rb === "unavailable") continue;
|
|
97
|
+
for (const path of new Set([...Object.keys(ra.verdicts), ...Object.keys(rb.verdicts)])) {
|
|
98
|
+
const va = ra.verdicts[path]?.kind ?? "absent", vb = rb.verdicts[path]?.kind ?? "absent";
|
|
99
|
+
if (va !== vb) dis.push(`${f.id} ${path}: ${a.name} ${va}, ${b.name} ${vb}`);
|
|
100
|
+
// The verdict alone cannot tell a seed from a taint casualty, and both of
|
|
101
|
+
// those are "run". Where both implementations report the tentative
|
|
102
|
+
// verdict and the edge, compare them: that is where J3 actually lives.
|
|
103
|
+
if (ra.tentative && rb.tentative && ra.tentative[path] !== rb.tentative[path]) {
|
|
104
|
+
dis.push(`${f.id} ${path}: tentative — ${a.name} ${ra.tentative[path] ?? "none"}, ${b.name} ${rb.tentative[path] ?? "none"}`);
|
|
105
|
+
}
|
|
106
|
+
if (ra.taintedBy && rb.taintedBy && ra.taintedBy[path] !== rb.taintedBy[path]) {
|
|
107
|
+
dis.push(`${f.id} ${path}: tainted by — ${a.name} ${ra.taintedBy[path] ?? "nothing"}, ${b.name} ${rb.taintedBy[path] ?? "nothing"}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const f of expressionFixtures(fixtures)) {
|
|
112
|
+
const ra = a.foldExport(f.input, f.exportName), rb = b.foldExport(f.input, f.exportName);
|
|
113
|
+
if (ra.ok !== rb.ok) dis.push(`${f.id}: ${a.name} ${ra.ok ? "folds" : "runs"}, ${b.name} ${rb.ok ? "folds" : "runs"}`);
|
|
114
|
+
else if (ra.ok && rb.ok && encodeValue(ra.value) !== encodeValue(rb.value)) dis.push(`${f.id}: values differ — ${a.name} ${encodeValue(ra.value)} vs ${b.name} ${encodeValue(rb.value)}`);
|
|
115
|
+
const sa = a.shape(f.input, f.exportName), sb = b.shape(f.input, f.exportName);
|
|
116
|
+
if (sa !== "unavailable" && sb !== "unavailable" && sa.accepted !== sb.accepted) dis.push(`${f.id}: shape — ${a.name} ${sa.accepted ? "accepts" : "rejects"}, ${b.name} ${sb.accepted ? "accepts" : "rejects"}`);
|
|
117
|
+
}
|
|
118
|
+
return dis;
|
|
119
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** #18 — the reference implementation declares the specification version it implements, and it is the current one. */
|
|
2
|
+
import { describe, test, expect } from "vitest";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { referenceAdapter } from "@intentius/tsad-reference";
|
|
7
|
+
import { chantAdapter } from "./adapters/chant";
|
|
8
|
+
|
|
9
|
+
const VERSION = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "spec", "VERSION"), "utf8").trim();
|
|
10
|
+
|
|
11
|
+
describe("specification version (#18)", () => {
|
|
12
|
+
test("spec/VERSION is a major.minor", () => {
|
|
13
|
+
expect(VERSION).toMatch(/^\d+\.\d+$/);
|
|
14
|
+
});
|
|
15
|
+
test("the changelog's newest entry is the current version", () => {
|
|
16
|
+
const changelog = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "spec", "CHANGELOG.md"), "utf8");
|
|
17
|
+
const newest = /^## (\d+\.\d+)\b/m.exec(changelog)?.[1];
|
|
18
|
+
expect(newest, "CHANGELOG.md has no versioned entry").toBe(VERSION);
|
|
19
|
+
});
|
|
20
|
+
test("the reference implementation declares the current version", () => {
|
|
21
|
+
expect(referenceAdapter.specVersion).toBe(VERSION);
|
|
22
|
+
});
|
|
23
|
+
test("chant declares a version of this specification's major (chant#2424)", () => {
|
|
24
|
+
// chant-v0.71.0 exports SPEC_VERSION. It may lag the minor: the policy says
|
|
25
|
+
// an implementation of 1.0 implements 1.1's full profile unchanged, so what
|
|
26
|
+
// is asserted is a real declaration on the same major, never "undeclared".
|
|
27
|
+
expect(chantAdapter.specVersion).toMatch(/^\d+\.\d+$/);
|
|
28
|
+
expect(chantAdapter.specVersion.split(".")[0]).toBe(VERSION.split(".")[0]);
|
|
29
|
+
expect(Number(chantAdapter.specVersion.split(".")[1])).toBeLessThanOrEqual(Number(VERSION.split(".")[1]));
|
|
30
|
+
});
|
|
31
|
+
});
|