@systemfsoftware/oxlint-plugin-test-discipline 3.8.1 → 3.9.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/CHANGELOG.md +6 -0
- package/README.md +16 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +291 -130
- package/dist/oxlint-plugin-test-discipline-untrimmed.d.ts +2 -0
- package/dist/oxlint-plugin-test-discipline.d.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @systemfsoftware/oxlint-plugin-test-discipline
|
|
2
2
|
|
|
3
|
+
## 3.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `expect-boolean-predicate`, which refuses an `expect` on a predicate call, comparison, negation or `&&`/`||` asserted with `toBe(true|false)` and names the `toSatisfy` or `toEqual` rewrite, and `vitest-from-effect-vitest`, which refuses any value import from `vitest` — named, namespace, default, side-effect or dynamic — and names `@effect/vitest` as the rewrite; a type-only import stays legal. `prop-arbitrary-schema-origin` also checks the `of` entries of the object form `it.prop(name, { of, subject, runs }, holds)`.
|
|
8
|
+
|
|
3
9
|
## 3.8.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -4,19 +4,21 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
|
|
|
4
4
|
|
|
5
5
|
## Rules
|
|
6
6
|
|
|
7
|
-
| Rule | What it enforces
|
|
8
|
-
| ------------------------------------ |
|
|
9
|
-
| `no-test-file-in-src` | Under `src/`, the only sanctioned test file is a single-segment `<stem>.workflow.property.test.ts` inside a sanctioned test directory, plus the generated `schema-laws.test.ts` entry point. Every other test file is banned: a kernel, policy, or schema suite becomes an in-source `import.meta.vitest` block, and a public-surface test moves outside `src/` as an integration test.
|
|
10
|
-
| `src-property-test-cell` | A property test under `src/` must be a single-segment `<stem>.workflow.property.test.ts` beside the `<stem>.workflow.ts` it covers. A source file whose suffix names a cell listed in `cellsRequiringTest` must also carry an in-source vitest block; that list is empty by default.
|
|
11
|
-
| `test-file-outside-tests-dir` | A test file outside `src/` must live under `tests/`.
|
|
12
|
-
| `test-suffix-outside-src` | Outside `src/`, a test file must end `.integration.test.ts`, `.differential.test.ts`, or `.trace.test.ts` — the three behaviour suffixes.
|
|
13
|
-
| `tests-dir-helpers-in-fixtures` | Under `tests/`, the only non-test modules are helpers and fixtures, and they live inside `tests/__fixtures__/`.
|
|
14
|
-
| `no-io-module-in-source-test` | An in-source `import.meta.vitest` test block is forbidden in a module that performs I/O — decided from the module's own syntax (a non-type import from a filesystem, process or network module, called at least once), never from its filename. Only the in-source-test idiom is judged: a module whose tests live in separate files is a no-op for this rule.
|
|
15
|
-
| `behaviour-test-requires-gherkin` | A `.integration.test.ts` must import `makeFeature` from `@systemfsoftware/effect-gherkin-spec` and must not import test runners directly from `vitest` or `@effect/vitest`.
|
|
16
|
-
| `behaviour-exercises-use-case` | A `.integration.test.ts` must reach at least one shell entry — an executor/handler/adapter/store, the package `main`/`mod`/`index`, or a non-foundation package — so it drives a use case.
|
|
17
|
-
| `behaviour-one-feature-per-file` | A `.integration.test.ts` must contain exactly one `Feature(...)` call; zero or two-or-more is the junk-drawer signal.
|
|
18
|
-
| `differential-test-requires-harness` | A `.differential.test.ts` must import `@systemfsoftware/differential-spec` and invoke its harness; direct `vitest`/`@effect/vitest` imports and raw runner calls (`it`, `test`, `describe`, `it.effect`, and aliases) are forbidden, and importing the harness without invoking it is equally non-compliant.
|
|
19
|
-
| `tests-import-public-api` | Every file under the package-root `tests/` or `__tests__/` trees, regardless of basename, may not relative-import `src` or climb into an `internal` folder. Import the published package name or a sibling helper.
|
|
7
|
+
| Rule | What it enforces |
|
|
8
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `no-test-file-in-src` | Under `src/`, the only sanctioned test file is a single-segment `<stem>.workflow.property.test.ts` inside a sanctioned test directory, plus the generated `schema-laws.test.ts` entry point. Every other test file is banned: a kernel, policy, or schema suite becomes an in-source `import.meta.vitest` block, and a public-surface test moves outside `src/` as an integration test. |
|
|
10
|
+
| `src-property-test-cell` | A property test under `src/` must be a single-segment `<stem>.workflow.property.test.ts` beside the `<stem>.workflow.ts` it covers. A source file whose suffix names a cell listed in `cellsRequiringTest` must also carry an in-source vitest block; that list is empty by default. |
|
|
11
|
+
| `test-file-outside-tests-dir` | A test file outside `src/` must live under `tests/`. |
|
|
12
|
+
| `test-suffix-outside-src` | Outside `src/`, a test file must end `.integration.test.ts`, `.differential.test.ts`, or `.trace.test.ts` — the three behaviour suffixes. |
|
|
13
|
+
| `tests-dir-helpers-in-fixtures` | Under `tests/`, the only non-test modules are helpers and fixtures, and they live inside `tests/__fixtures__/`. |
|
|
14
|
+
| `no-io-module-in-source-test` | An in-source `import.meta.vitest` test block is forbidden in a module that performs I/O — decided from the module's own syntax (a non-type import from a filesystem, process or network module, called at least once), never from its filename. Only the in-source-test idiom is judged: a module whose tests live in separate files is a no-op for this rule. |
|
|
15
|
+
| `behaviour-test-requires-gherkin` | A `.integration.test.ts` must import `makeFeature` from `@systemfsoftware/effect-gherkin-spec` and must not import test runners directly from `vitest` or `@effect/vitest`. |
|
|
16
|
+
| `behaviour-exercises-use-case` | A `.integration.test.ts` must reach at least one shell entry — an executor/handler/adapter/store, the package `main`/`mod`/`index`, or a non-foundation package — so it drives a use case. |
|
|
17
|
+
| `behaviour-one-feature-per-file` | A `.integration.test.ts` must contain exactly one `Feature(...)` call; zero or two-or-more is the junk-drawer signal. |
|
|
18
|
+
| `differential-test-requires-harness` | A `.differential.test.ts` must import `@systemfsoftware/differential-spec` and invoke its harness; direct `vitest`/`@effect/vitest` imports and raw runner calls (`it`, `test`, `describe`, `it.effect`, and aliases) are forbidden, and importing the harness without invoking it is equally non-compliant. |
|
|
19
|
+
| `tests-import-public-api` | Every file under the package-root `tests/` or `__tests__/` trees, regardless of basename, may not relative-import `src` or climb into an `internal` folder. Import the published package name or a sibling helper. |
|
|
20
|
+
| `expect-boolean-predicate` | An `expect(...)` asserted with `toBe(true)`, `toBe(false)`, `toEqual(true)` or `toEqual(false)` must wrap a boolean the code under test returned. A predicate call, comparison, negation or `&&` / `\|\|` evaluated in the test is refused, with the `toSatisfy` or `toEqual` rewrite as the fix. |
|
|
21
|
+
| `vitest-from-effect-vitest` | Every value a test uses from Vitest is imported from `@effect/vitest`, which re-exports all of Vitest. Any value import from `vitest` — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal. Exempt: the packages that drive Vitest's own runner instead of the fork (`vitest` itself, `vitest-conformance`, and `effect-spec-runtime`, whose `Register` calls Vitest's option-taking collector). |
|
|
20
22
|
|
|
21
23
|
| suffix | layer | doubles | location |
|
|
22
24
|
| ----------------------------- | ------------ | ------------------------ | ------------------ |
|
|
@@ -27,7 +29,7 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
|
|
|
27
29
|
|
|
28
30
|
## Enrollment
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
`@systemfsoftware/oxlint-config-recommended` loads this plugin in `jsPlugins` and turns on `expect-boolean-predicate` and `vitest-from-effect-vitest` at `error` in its default config. The rest of `configs.recommended` is exported from that preset as `rules`, but the default config does not enable it, so those rules fire only in a package that spreads it into its own `rules`.
|
|
31
33
|
|
|
32
34
|
## Testing
|
|
33
35
|
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ declare const _default: {
|
|
|
25
25
|
'behaviour-one-feature-per-file': import("@oxlint/plugins").Rule;
|
|
26
26
|
'no-pseudo-gherkin-unit-tests': import("@oxlint/plugins").Rule;
|
|
27
27
|
'tests-dir-helpers-in-fixtures': import("@oxlint/plugins").Rule;
|
|
28
|
+
'expect-boolean-predicate': import("@oxlint/plugins").Rule;
|
|
29
|
+
'vitest-from-effect-vitest': import("@oxlint/plugins").Rule;
|
|
28
30
|
'no-io-module-in-source-test': import("@oxlint/plugins").Rule;
|
|
29
31
|
'tests-import-public-api': import("@oxlint/plugins").Rule;
|
|
30
32
|
'differential-test-requires-harness': import("@oxlint/plugins").Rule;
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const EMIT_CALLEES = [
|
|
|
8
8
|
];
|
|
9
9
|
const EXPECTED$3 = "a span declared once with Span.declare({ id, name, attrs }) and started through the start(attrs) it returns";
|
|
10
10
|
const FIX$3 = "declare the span with Span.declare({ id, name, attrs }) and emit it with SomeSpan.start(attrs)";
|
|
11
|
-
const meta$
|
|
11
|
+
const meta$27 = {
|
|
12
12
|
type: "problem",
|
|
13
13
|
docs: { description: "Ban a raw span name passed to an emit API (startSpan, spanBuilder, startActiveSpan). Span names enter through Span.declare, never as a literal at the emit site." },
|
|
14
14
|
schema: [],
|
|
@@ -49,7 +49,7 @@ const inspect = (node) => {
|
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
51
|
const banRawSpanNameEmit = defineRule({
|
|
52
|
-
meta: meta$
|
|
52
|
+
meta: meta$27,
|
|
53
53
|
create(context) {
|
|
54
54
|
return { CallExpression(node) {
|
|
55
55
|
const emitCall = inspect(node);
|
|
@@ -99,12 +99,17 @@ const GHERKIN_PACKAGE = "@systemfsoftware/effect-gherkin-spec";
|
|
|
99
99
|
const DIFFERENTIAL_PACKAGE = "@systemfsoftware/differential-spec";
|
|
100
100
|
const TRACE_SPEC_PACKAGE = "@systemfsoftware/trace-spec";
|
|
101
101
|
const FOREIGN_RUNNERS = /* @__PURE__ */ new Set(["vitest", "@effect/vitest"]);
|
|
102
|
+
const RAW_VITEST_PACKAGES = {
|
|
103
|
+
"effect-spec-runtime": true,
|
|
104
|
+
"vitest": true,
|
|
105
|
+
"vitest-conformance": true
|
|
106
|
+
};
|
|
102
107
|
const RUNNER_NAMES = /* @__PURE__ */ new Set([
|
|
103
108
|
"it",
|
|
104
109
|
"test",
|
|
105
110
|
"describe"
|
|
106
111
|
]);
|
|
107
|
-
const MESSAGE$
|
|
112
|
+
const MESSAGE$9 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
108
113
|
const ABSENCE_MESSAGE = "{{name}} is untested. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
109
114
|
//#endregion
|
|
110
115
|
//#region src/rules/behaviour-exercises-use-case.config.ts
|
|
@@ -112,11 +117,11 @@ const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package
|
|
|
112
117
|
const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
|
|
113
118
|
const NO_SUBJECT_IMPORT_ACTUAL = "a behaviour file whose every runtime import is vitest, @effect/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
|
|
114
119
|
const NO_SUBJECT_IMPORT_FIX = "a behaviour test exercises a use case, so it has to reach the package. A file that imports nothing but its runner and effect is asserting over values it built in the same file. Ask whether the assertion tests anything at all: if it restates a literal, delete the scenario; if it states an invariant that holds over generated inputs, move it to a *.property.test.ts beside the cell that decides it. Type-only imports never count - they are erased before anything runs - while a side-effect import (import \"../src/x.js\", import {} from \"../src/x.js\") does count, because it executes that module.";
|
|
115
|
-
const meta$
|
|
120
|
+
const meta$26 = {
|
|
116
121
|
type: "problem",
|
|
117
122
|
docs: { description: "A *.integration.test.ts must import the package under test, not only its runner and effect, so the scenario exercises code that ships rather than values the test built itself." },
|
|
118
123
|
schema: [],
|
|
119
|
-
messages: { noSubjectImport: MESSAGE$
|
|
124
|
+
messages: { noSubjectImport: MESSAGE$9 }
|
|
120
125
|
};
|
|
121
126
|
//#endregion
|
|
122
127
|
//#region src/rules/path.ts
|
|
@@ -126,6 +131,7 @@ const basenameOf = (filename) => Array$1.lastNonEmpty(segmentsOf(filename));
|
|
|
126
131
|
/** Directory segments only — the basename never counts as a directory. */
|
|
127
132
|
const directoriesOf = (filename) => Array$1.initNonEmpty(segmentsOf(filename));
|
|
128
133
|
const isUnderSrc = (filename) => directoriesOf(filename).includes("src");
|
|
134
|
+
const isRawVitestPackage = (filename) => directoriesOf(filename).some((segment) => RAW_VITEST_PACKAGES[segment] === true);
|
|
129
135
|
const isInSanctionedTestDir = (filename) => directoriesOf(filename).some((segment) => SANCTIONED_TEST_DIRS.has(segment));
|
|
130
136
|
const isInTestsImportScope = (filename) => {
|
|
131
137
|
const segments = segmentsOf(filename);
|
|
@@ -225,7 +231,7 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
|
|
|
225
231
|
* path names nothing satisfies the rule the same way a real one does.
|
|
226
232
|
*/
|
|
227
233
|
const behaviourExercisesUseCase = defineRule({
|
|
228
|
-
meta: meta$
|
|
234
|
+
meta: meta$26,
|
|
229
235
|
create(context) {
|
|
230
236
|
let reached = false;
|
|
231
237
|
return {
|
|
@@ -275,13 +281,13 @@ const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Fe
|
|
|
275
281
|
const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
|
|
276
282
|
const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
|
|
277
283
|
const TOO_MANY_FEATURES_FIX = "splitting a junk drawer into several smaller junk drawers is not an improvement. When separating scenarios surfaces assertions that restate a pure function return value — change detectors against a lookup table or constant — those get deleted, not rehoused. Each surviving capability keeps its own file with exactly one Feature.";
|
|
278
|
-
const meta$
|
|
284
|
+
const meta$25 = {
|
|
279
285
|
type: "problem",
|
|
280
286
|
docs: { description: "A *.integration.test.ts must contain exactly one Feature(...) call. Zero or two-or-more is the junk-drawer signal that produced 41 scenarios of pure-function assertions in a single file." },
|
|
281
287
|
schema: [],
|
|
282
288
|
messages: {
|
|
283
|
-
tooFewFeatures: MESSAGE$
|
|
284
|
-
tooManyFeatures: MESSAGE$
|
|
289
|
+
tooFewFeatures: MESSAGE$9,
|
|
290
|
+
tooManyFeatures: MESSAGE$9
|
|
285
291
|
}
|
|
286
292
|
};
|
|
287
293
|
//#endregion
|
|
@@ -320,7 +326,7 @@ const hasAnyFeatureCall = (program) => {
|
|
|
320
326
|
return false;
|
|
321
327
|
};
|
|
322
328
|
const behaviourOneFeaturePerFile = defineRule({
|
|
323
|
-
meta: meta$
|
|
329
|
+
meta: meta$25,
|
|
324
330
|
create(context) {
|
|
325
331
|
return { "Program:exit"(node) {
|
|
326
332
|
if (!isBehaviourTest$2(basenameOf(context.filename))) return;
|
|
@@ -361,13 +367,13 @@ const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
|
|
|
361
367
|
const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
|
|
362
368
|
const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
|
|
363
369
|
const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
|
|
364
|
-
const meta$
|
|
370
|
+
const meta$24 = {
|
|
365
371
|
type: "problem",
|
|
366
372
|
docs: { description: "A *.integration.test.ts must drive its suite through makeFeature from @systemfsoftware/effect-gherkin-spec and must not import test runners from vitest or @effect/vitest." },
|
|
367
373
|
schema: [],
|
|
368
374
|
messages: {
|
|
369
|
-
foreignRunner: MESSAGE$
|
|
370
|
-
missingMakeFeature: MESSAGE$
|
|
375
|
+
foreignRunner: MESSAGE$9,
|
|
376
|
+
missingMakeFeature: MESSAGE$9
|
|
371
377
|
}
|
|
372
378
|
};
|
|
373
379
|
//#endregion
|
|
@@ -389,7 +395,7 @@ const foreignRunnerNameOf = (specifier) => {
|
|
|
389
395
|
};
|
|
390
396
|
const isBehaviourTest$1 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
|
|
391
397
|
const behaviourTestRequiresGherkin = defineRule({
|
|
392
|
-
meta: meta$
|
|
398
|
+
meta: meta$24,
|
|
393
399
|
create(context) {
|
|
394
400
|
const basename = basenameOf(context.filename);
|
|
395
401
|
return { Program(node) {
|
|
@@ -452,7 +458,7 @@ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwEr
|
|
|
452
458
|
const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
|
|
453
459
|
const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
|
|
454
460
|
const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
|
|
455
|
-
const meta$
|
|
461
|
+
const meta$23 = {
|
|
456
462
|
type: "suggestion",
|
|
457
463
|
docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
|
|
458
464
|
schema: [],
|
|
@@ -532,7 +538,7 @@ const isTestFunctionCall = (node) => {
|
|
|
532
538
|
return false;
|
|
533
539
|
};
|
|
534
540
|
const dampTestNaming = defineRule({
|
|
535
|
-
meta: meta$
|
|
541
|
+
meta: meta$23,
|
|
536
542
|
create(context) {
|
|
537
543
|
return { CallExpression(node) {
|
|
538
544
|
if (!isTestFunctionCall(node)) return;
|
|
@@ -555,7 +561,7 @@ const dampTestNaming = defineRule({
|
|
|
555
561
|
});
|
|
556
562
|
//#endregion
|
|
557
563
|
//#region src/rules/differential-test-requires-harness.config.ts
|
|
558
|
-
const meta$
|
|
564
|
+
const meta$22 = {
|
|
559
565
|
type: "problem",
|
|
560
566
|
docs: { description: "Differential tests must use @systemfsoftware/differential-spec harness. Raw runner calls (it, test, describe, and member forms like it.effect) and direct runner imports are forbidden in *.differential.test.ts files; importing the harness without invoking it is equally non-compliant." },
|
|
561
567
|
schema: [],
|
|
@@ -577,7 +583,7 @@ const recordHarnessBindings = (node, bindings) => {
|
|
|
577
583
|
};
|
|
578
584
|
const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS.has(node.source.value);
|
|
579
585
|
const differentialTestRequiresHarness = defineRule({
|
|
580
|
-
meta: meta$
|
|
586
|
+
meta: meta$22,
|
|
581
587
|
create(context) {
|
|
582
588
|
if (!context.filename.endsWith(".differential.test.ts")) return {};
|
|
583
589
|
const harnessBindings = /* @__PURE__ */ new Set();
|
|
@@ -648,16 +654,97 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
648
654
|
}
|
|
649
655
|
});
|
|
650
656
|
//#endregion
|
|
657
|
+
//#region src/rules/expect-boolean-predicate.config.ts
|
|
658
|
+
const MESSAGE$8 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
659
|
+
const BOOLEAN_MATCHERS = {
|
|
660
|
+
toBe: true,
|
|
661
|
+
toEqual: true
|
|
662
|
+
};
|
|
663
|
+
const COMPARISON_OPERATORS = {
|
|
664
|
+
"===": true,
|
|
665
|
+
"!==": true,
|
|
666
|
+
"==": true,
|
|
667
|
+
"!=": true,
|
|
668
|
+
"<": true,
|
|
669
|
+
"<=": true,
|
|
670
|
+
">": true,
|
|
671
|
+
">=": true,
|
|
672
|
+
instanceof: true,
|
|
673
|
+
in: true
|
|
674
|
+
};
|
|
675
|
+
const VIOLATION_NAME$4 = "a predicate evaluated in the test asserted against true or false";
|
|
676
|
+
const VIOLATION_EXPECTED$1 = "expect(...) around a boolean the code under test returned";
|
|
677
|
+
const VIOLATION_ACTUAL$1 = "expect(...) wraps a call, a comparison, a `!` or an `&&` / `||` evaluated in the test, so a failure reports `expected false to be true` instead of the values that disagreed";
|
|
678
|
+
const VIOLATION_FIX$1 = "rewrite a predicate call as expect(x).toSatisfy(P) — e.g. expect(s.reading).toSatisfy(Result.isFailure) — and a comparison as expect(a).toEqual(b)";
|
|
679
|
+
const meta$21 = {
|
|
680
|
+
type: "problem",
|
|
681
|
+
docs: { description: "An expect(...) asserted with toBe(true|false) or toEqual(true|false) must wrap a boolean the code under test returned. A predicate call, comparison, negation or && / || evaluated in the test is refused, with the toSatisfy or toEqual rewrite as the fix." },
|
|
682
|
+
schema: [],
|
|
683
|
+
messages: { booleanPredicate: MESSAGE$8 }
|
|
684
|
+
};
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/rules/expect-call.ts
|
|
687
|
+
const EXPECT = "expect";
|
|
688
|
+
const isExpectCallee = (callee) => callee.type === "Identifier" && callee.name === EXPECT;
|
|
689
|
+
const expectCallOf = (node, acceptsMember) => {
|
|
690
|
+
if (node.callee.type !== "MemberExpression") return void 0;
|
|
691
|
+
let target = node.callee.object;
|
|
692
|
+
while (target.type === "MemberExpression") {
|
|
693
|
+
if (acceptsMember !== void 0 && acceptsMember(target) === false) return void 0;
|
|
694
|
+
target = target.object;
|
|
695
|
+
}
|
|
696
|
+
if (target.type !== "CallExpression" || isExpectCallee(target.callee) === false) return void 0;
|
|
697
|
+
return target;
|
|
698
|
+
};
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region src/rules/expect-boolean-predicate.ts
|
|
701
|
+
const isNotMember = (member) => member.property.type === "Identifier" && member.property.name === "not";
|
|
702
|
+
const isBooleanLiteral = (node) => node !== void 0 && node.type === "Literal" && typeof node.value === "boolean";
|
|
703
|
+
/** The ARG positions `expect` exposes that no subject can fill by accident. */
|
|
704
|
+
const isEvaluatedInTest = (arg) => {
|
|
705
|
+
switch (arg.type) {
|
|
706
|
+
case "CallExpression": return true;
|
|
707
|
+
case "UnaryExpression": return arg.operator === "!";
|
|
708
|
+
case "LogicalExpression": return arg.operator === "&&" || arg.operator === "||";
|
|
709
|
+
case "BinaryExpression": return COMPARISON_OPERATORS[arg.operator] === true;
|
|
710
|
+
default: return false;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
const expectBooleanPredicate = defineRule({
|
|
714
|
+
meta: meta$21,
|
|
715
|
+
create(context) {
|
|
716
|
+
return { CallExpression(node) {
|
|
717
|
+
if (node.callee.type !== "MemberExpression" || node.callee.property.type !== "Identifier") return;
|
|
718
|
+
if (BOOLEAN_MATCHERS[node.callee.property.name] !== true) return;
|
|
719
|
+
if (!isBooleanLiteral(node.arguments[0])) return;
|
|
720
|
+
const receiver = expectCallOf(node, isNotMember);
|
|
721
|
+
if (receiver === void 0) return;
|
|
722
|
+
const arg = receiver.arguments[0];
|
|
723
|
+
if (arg === void 0 || !isEvaluatedInTest(arg)) return;
|
|
724
|
+
context.report({
|
|
725
|
+
node,
|
|
726
|
+
messageId: "booleanPredicate",
|
|
727
|
+
data: {
|
|
728
|
+
name: VIOLATION_NAME$4,
|
|
729
|
+
expected: VIOLATION_EXPECTED$1,
|
|
730
|
+
actual: VIOLATION_ACTUAL$1,
|
|
731
|
+
fix: VIOLATION_FIX$1
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
} };
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
//#endregion
|
|
651
738
|
//#region src/rules/in-source-test-prop-only.config.ts
|
|
652
739
|
const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vitest` block";
|
|
653
740
|
const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
|
|
654
741
|
const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
|
|
655
742
|
const NON_PROP_CALL_FIX = "delete the block — non-property in-source tests belong nowhere in `src/`; re-home a meaningful example through the cell public export as `*.integration.test.ts`, or rewrite a real invariant as `it.prop` over a schema-derived arbitrary";
|
|
656
|
-
const meta$
|
|
743
|
+
const meta$20 = {
|
|
657
744
|
type: "problem",
|
|
658
745
|
docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must contain only `it.prop` or `it.effect.prop` calls; every other test call fails — delete the block or rewrite the invariant as a property." },
|
|
659
746
|
schema: [],
|
|
660
|
-
messages: { nonPropCall: MESSAGE$
|
|
747
|
+
messages: { nonPropCall: MESSAGE$9 }
|
|
661
748
|
};
|
|
662
749
|
//#endregion
|
|
663
750
|
//#region src/rules/vitest-guard.ts
|
|
@@ -717,7 +804,7 @@ const rootNameOf = (node) => {
|
|
|
717
804
|
if (node.type === "CallExpression") return rootNameOf(node.callee);
|
|
718
805
|
};
|
|
719
806
|
const inSourceTestPropOnly = defineRule({
|
|
720
|
-
meta: meta$
|
|
807
|
+
meta: meta$20,
|
|
721
808
|
create(context) {
|
|
722
809
|
const filename = context.filename;
|
|
723
810
|
const basename = basenameOf(filename);
|
|
@@ -762,13 +849,13 @@ const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no privat
|
|
|
762
849
|
const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising a non-exported module-level binding";
|
|
763
850
|
const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported or imported names";
|
|
764
851
|
const NO_PRIVATE_TARGET_FIX = "test the public surface from tests/ as *.integration.test.ts; in-source blocks exist for private helpers only — if the public behaviour you meant to cover is a pure function, delete the assertion: the type system already proves it";
|
|
765
|
-
const meta$
|
|
852
|
+
const meta$19 = {
|
|
766
853
|
type: "problem",
|
|
767
854
|
docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise at least one non-exported binding; other tests belong in tests/." },
|
|
768
855
|
schema: [],
|
|
769
856
|
messages: {
|
|
770
|
-
notModuleLevel: MESSAGE$
|
|
771
|
-
noPrivateTarget: MESSAGE$
|
|
857
|
+
notModuleLevel: MESSAGE$9,
|
|
858
|
+
noPrivateTarget: MESSAGE$9
|
|
772
859
|
}
|
|
773
860
|
};
|
|
774
861
|
//#endregion
|
|
@@ -796,7 +883,7 @@ const collectPrivateNames = (body, out) => {
|
|
|
796
883
|
}
|
|
797
884
|
};
|
|
798
885
|
const inSourceTestTargetsPrivate = defineRule({
|
|
799
|
-
meta: meta$
|
|
886
|
+
meta: meta$19,
|
|
800
887
|
create(context) {
|
|
801
888
|
const filename = context.filename;
|
|
802
889
|
const basename = basenameOf(filename);
|
|
@@ -854,15 +941,15 @@ const inSourceTestTargetsPrivate = defineRule({
|
|
|
854
941
|
});
|
|
855
942
|
//#endregion
|
|
856
943
|
//#region src/rules/no-assert-in-property.config.ts
|
|
857
|
-
const MESSAGE$
|
|
858
|
-
const meta$
|
|
944
|
+
const MESSAGE$7 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
945
|
+
const meta$18 = {
|
|
859
946
|
type: "problem",
|
|
860
947
|
docs: { description: "Property predicates (it.prop / it.effect.prop) must never call expect(...), assert*(...), or raw fc.assert/fc.check. The boolean return IS the verdict — assertions fork the failure channel. assert* remains correct in normal (non-property) tests." },
|
|
861
948
|
schema: [],
|
|
862
949
|
messages: {
|
|
863
|
-
expectCall: MESSAGE$
|
|
864
|
-
assertCall: MESSAGE$
|
|
865
|
-
rawFcRun: MESSAGE$
|
|
950
|
+
expectCall: MESSAGE$7,
|
|
951
|
+
assertCall: MESSAGE$7,
|
|
952
|
+
rawFcRun: MESSAGE$7
|
|
866
953
|
}
|
|
867
954
|
};
|
|
868
955
|
//#endregion
|
|
@@ -893,7 +980,7 @@ const isInsidePropPredicate = (node) => {
|
|
|
893
980
|
return isInsidePropPredicate(parent);
|
|
894
981
|
};
|
|
895
982
|
const noAssertInProperty = defineRule({
|
|
896
|
-
meta: meta$
|
|
983
|
+
meta: meta$18,
|
|
897
984
|
create(context) {
|
|
898
985
|
return { CallExpression(node) {
|
|
899
986
|
const callee = node.callee;
|
|
@@ -946,7 +1033,7 @@ const SKIP_WALK_KEYS = /* @__PURE__ */ new Set([
|
|
|
946
1033
|
"range",
|
|
947
1034
|
"parent"
|
|
948
1035
|
]);
|
|
949
|
-
const meta$
|
|
1036
|
+
const meta$17 = {
|
|
950
1037
|
type: "problem",
|
|
951
1038
|
docs: { description: "Flag an assertion whose subject and expectation are both built only from imported declarations and literals. Such an assertion invokes nothing, so no change to the behaviour under test can make it fail." },
|
|
952
1039
|
schema: [],
|
|
@@ -996,15 +1083,6 @@ const dependsOnBehaviour = (node, imported) => {
|
|
|
996
1083
|
}
|
|
997
1084
|
return false;
|
|
998
1085
|
};
|
|
999
|
-
const expectCallOf = (node) => {
|
|
1000
|
-
if (node.callee.type !== "MemberExpression") return void 0;
|
|
1001
|
-
let target = node.callee.object;
|
|
1002
|
-
while (target.type === "MemberExpression") target = target.object;
|
|
1003
|
-
if (target.type !== "CallExpression") return void 0;
|
|
1004
|
-
const callee = target.callee;
|
|
1005
|
-
if (!("name" in callee && callee.name === "expect")) return void 0;
|
|
1006
|
-
return target;
|
|
1007
|
-
};
|
|
1008
1086
|
const importedNames = (program) => {
|
|
1009
1087
|
const names = /* @__PURE__ */ new Set();
|
|
1010
1088
|
for (const statement of program.body) {
|
|
@@ -1014,7 +1092,7 @@ const importedNames = (program) => {
|
|
|
1014
1092
|
return names;
|
|
1015
1093
|
};
|
|
1016
1094
|
const noBehaviourlessAssertion = defineRule({
|
|
1017
|
-
meta: meta$
|
|
1095
|
+
meta: meta$17,
|
|
1018
1096
|
create(context) {
|
|
1019
1097
|
if (!TEST_FILE.test(context.filename)) return {};
|
|
1020
1098
|
const imported = importedNames(context.sourceCode.ast);
|
|
@@ -1084,7 +1162,7 @@ const IO_SOURCE_TEST_NAME = "An in-source `import.meta.vitest` test block";
|
|
|
1084
1162
|
const IO_SOURCE_TEST_EXPECTED = "the tests of a module whose own source calls an I/O binding to live outside it — a separate test file, or a composition test with a double at the port";
|
|
1085
1163
|
const IO_SOURCE_TEST_ACTUAL = "this module calls a binding imported from a filesystem, process or network module and guards tests in-source with `import.meta.vitest`";
|
|
1086
1164
|
const IO_SOURCE_TEST_FIX = "test the module from outside its own source — a separate test file or a composition test doubling the boundary — or, when an assertion merely restates a literal the module already computes, it is a change detector: delete it. The verdict here is the file's own imports and calls, never its name";
|
|
1087
|
-
const meta$
|
|
1165
|
+
const meta$16 = {
|
|
1088
1166
|
type: "problem",
|
|
1089
1167
|
docs: { description: "Reports an in-source `import.meta.vitest` test block in a module whose own syntax shows a called, non-type import from a filesystem, process or network module. Judges only the in-source-test idiom — a module whose tests live in separate files is a no-op for this rule, whatever it imports." },
|
|
1090
1168
|
schema: [],
|
|
@@ -1105,7 +1183,7 @@ const bindingBase = (callee) => {
|
|
|
1105
1183
|
if (callee.type === "Identifier") return callee.name;
|
|
1106
1184
|
};
|
|
1107
1185
|
const noIoModuleInSourceTest = defineRule({
|
|
1108
|
-
meta: meta$
|
|
1186
|
+
meta: meta$16,
|
|
1109
1187
|
create(context) {
|
|
1110
1188
|
/** Local binding name -> specifier it was (non-type) imported from. */
|
|
1111
1189
|
const ioBindings = {};
|
|
@@ -1147,7 +1225,7 @@ const noIoModuleInSourceTest = defineRule({
|
|
|
1147
1225
|
});
|
|
1148
1226
|
//#endregion
|
|
1149
1227
|
//#region src/rules/no-nested-quantification.config.ts
|
|
1150
|
-
const MESSAGE$
|
|
1228
|
+
const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1151
1229
|
const Options$2 = Schema.Struct({ exempt: Schema.Array(Schema.String).pipe(Schema.annotate({ description: "File basenames this rule stays silent on. An owner who has measured the cost and accepted it names the file here; the property still runs." }), Schema.withDecodingDefaultType(Effect.succeed([]))) });
|
|
1152
1230
|
const ITERATOR_METHODS = /* @__PURE__ */ new Set([
|
|
1153
1231
|
"every",
|
|
@@ -1166,15 +1244,15 @@ const ITERATOR_METHODS = /* @__PURE__ */ new Set([
|
|
|
1166
1244
|
]);
|
|
1167
1245
|
const CONSTANT_POOL_ARBITRARIES = /* @__PURE__ */ new Set(["constant", "constantFrom"]);
|
|
1168
1246
|
const FASTCHECK_NAMESPACES = /* @__PURE__ */ new Set(["fc"]);
|
|
1169
|
-
const VIOLATION_NAME$
|
|
1247
|
+
const VIOLATION_NAME$3 = "quantification nested inside a property predicate";
|
|
1170
1248
|
const EXPECTED$2 = "per-case cost bounded by the draw, not by the draw times a second traversal — inspect a generated value with a fold whose body calls nothing, or move the inner quantifier into the generator so the shrinker can see it";
|
|
1171
1249
|
const ACTUAL$2 = "the predicate iterates a value derived from a generated parameter and calls a free function inside that loop, so cost scales with the drawn size times whatever that call costs";
|
|
1172
1250
|
const FIX$2 = "hoist the inner call out of the loop when its result does not vary per element; otherwise assert one drawn element per case and let numRuns supply the quantifier, or generate the pair and compare directly. If the cost is understood and accepted, add this file's basename to the rule's exempt option";
|
|
1173
|
-
const meta$
|
|
1251
|
+
const meta$15 = {
|
|
1174
1252
|
type: "problem",
|
|
1175
1253
|
docs: { description: "A property predicate must not quantify over its own generated value and call out again inside that loop. Per-case cost then scales with the drawn size rather than with the draw count, which is the shape Hypothesis reports as nested_given: the suite slows superlinearly as the generator widens, and a CI budget tuned on small draws times out on large ones. Iteration over a bound the generator does not control, and a fold whose body calls nothing, are both fine." },
|
|
1176
1254
|
schema: [Schema.toJsonSchemaDocument(Options$2).schema],
|
|
1177
|
-
messages: { nestedQuantification: MESSAGE$
|
|
1255
|
+
messages: { nestedQuantification: MESSAGE$6 }
|
|
1178
1256
|
};
|
|
1179
1257
|
//#endregion
|
|
1180
1258
|
//#region src/rules/no-nested-quantification.ts
|
|
@@ -1229,11 +1307,17 @@ const isConstantPool = (element) => {
|
|
|
1229
1307
|
};
|
|
1230
1308
|
const boundedPoolIndices = (call) => {
|
|
1231
1309
|
const bounded = /* @__PURE__ */ new Set();
|
|
1232
|
-
const
|
|
1233
|
-
if (
|
|
1234
|
-
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1310
|
+
const options = call.arguments[1];
|
|
1311
|
+
if (options === void 0 || options.type !== "ObjectExpression") return bounded;
|
|
1312
|
+
for (const property of options.properties) {
|
|
1313
|
+
if (property.type !== "Property" || property.computed) continue;
|
|
1314
|
+
if (property.key.type !== "Identifier" || property.key.name !== "of") continue;
|
|
1315
|
+
if (property.value.type !== "ArrayExpression") return bounded;
|
|
1316
|
+
property.value.elements.forEach((element, index) => {
|
|
1317
|
+
if (isConstantPool(element)) bounded.add(index);
|
|
1318
|
+
});
|
|
1319
|
+
return bounded;
|
|
1320
|
+
}
|
|
1237
1321
|
return bounded;
|
|
1238
1322
|
};
|
|
1239
1323
|
const drawnNamesIn = (predicate, bounded) => {
|
|
@@ -1241,12 +1325,12 @@ const drawnNamesIn = (predicate, bounded) => {
|
|
|
1241
1325
|
const addFrom = (value) => {
|
|
1242
1326
|
for (const name of identifiersIn(value)) drawn.add(name);
|
|
1243
1327
|
};
|
|
1244
|
-
const [
|
|
1245
|
-
if (isNode$3(
|
|
1328
|
+
const [, values, ...rest] = predicate.params;
|
|
1329
|
+
if (isNode$3(values) && values.type === "ArrayPattern") values.elements.forEach((element, index) => {
|
|
1246
1330
|
if (bounded.has(index)) return;
|
|
1247
1331
|
addFrom(element);
|
|
1248
1332
|
});
|
|
1249
|
-
else addFrom(
|
|
1333
|
+
else addFrom(values);
|
|
1250
1334
|
addFrom(rest);
|
|
1251
1335
|
visit(predicate.body, (node) => {
|
|
1252
1336
|
if (node.type !== "VariableDeclarator") return;
|
|
@@ -1306,7 +1390,7 @@ const check = (context, call, predicate) => {
|
|
|
1306
1390
|
node: iteration.node,
|
|
1307
1391
|
messageId: "nestedQuantification",
|
|
1308
1392
|
data: {
|
|
1309
|
-
name: VIOLATION_NAME$
|
|
1393
|
+
name: VIOLATION_NAME$3,
|
|
1310
1394
|
expected: EXPECTED$2,
|
|
1311
1395
|
actual: ACTUAL$2,
|
|
1312
1396
|
fix: FIX$2
|
|
@@ -1321,7 +1405,7 @@ const check = (context, call, predicate) => {
|
|
|
1321
1405
|
* and excluded; DISCHARGED_BY in the suite pins the unbounded recipe shape.
|
|
1322
1406
|
*/
|
|
1323
1407
|
const noNestedQuantification = defineRule({
|
|
1324
|
-
meta: meta$
|
|
1408
|
+
meta: meta$15,
|
|
1325
1409
|
create(context) {
|
|
1326
1410
|
const options = Schema.decodeUnknownSync(Options$2)(context.options[0] ?? {});
|
|
1327
1411
|
const exempt = new Set(options.exempt);
|
|
@@ -1342,11 +1426,11 @@ const NO_LAYER_IN_FEATURE_NAME = "a *.integration.test.ts feature with no enviro
|
|
|
1342
1426
|
const NO_LAYER_IN_FEATURE_EXPECTED = "a Feature builder chained with .withLayer(layer) or .withScenarioLayer(layer)";
|
|
1343
1427
|
const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call without .withLayer or .withScenarioLayer";
|
|
1344
1428
|
const NO_LAYER_IN_FEATURE_FIX = "an integration test under WGI-CLS1 must declare its collaborator environment via .withLayer(...) or .withScenarioLayer(...). Chain .withLayer(Layer.empty) (or .withScenarioLayer(Layer.empty)) if the feature exercises in-memory collaborators without custom services, or provide the external boundary Layer it exercises (e.g. .withLayer(MyService.Live)).";
|
|
1345
|
-
const meta$
|
|
1429
|
+
const meta$14 = {
|
|
1346
1430
|
type: "problem",
|
|
1347
1431
|
docs: { description: "A *.integration.test.ts Feature builder must configure at least one environment Layer (.withLayer or .withScenarioLayer) to enforce real integration boundaries under WGI-CLS1." },
|
|
1348
1432
|
schema: [],
|
|
1349
|
-
messages: { noLayerInFeature: MESSAGE$
|
|
1433
|
+
messages: { noLayerInFeature: MESSAGE$9 }
|
|
1350
1434
|
};
|
|
1351
1435
|
//#endregion
|
|
1352
1436
|
//#region src/rules/no-pseudo-gherkin-unit-tests.ts
|
|
@@ -1377,7 +1461,7 @@ const findRootFeatureCall = (callNode) => {
|
|
|
1377
1461
|
return null;
|
|
1378
1462
|
};
|
|
1379
1463
|
const noPseudoGherkinUnitTests = defineRule({
|
|
1380
|
-
meta: meta$
|
|
1464
|
+
meta: meta$14,
|
|
1381
1465
|
create(context) {
|
|
1382
1466
|
if (!isBehaviourTest(basenameOf(context.filename))) return {};
|
|
1383
1467
|
return { CallExpression(node) {
|
|
@@ -1401,16 +1485,16 @@ const noPseudoGherkinUnitTests = defineRule({
|
|
|
1401
1485
|
});
|
|
1402
1486
|
//#endregion
|
|
1403
1487
|
//#region src/rules/no-silent-return.config.ts
|
|
1404
|
-
const MESSAGE$
|
|
1405
|
-
const meta$
|
|
1488
|
+
const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1489
|
+
const meta$13 = {
|
|
1406
1490
|
type: "problem",
|
|
1407
1491
|
docs: { description: "Property predicates (it.prop / it.effect.prop from @effect/vitest) must return a boolean verdict on every code path. fast-check counts undefined as success, so a bare return, a non-boolean return, or falling off the end of the body is a silent pass. Opaque values (identifiers, member expressions, calls) are trusted to be boolean; literals and operators are checked." },
|
|
1408
1492
|
schema: [],
|
|
1409
1493
|
messages: {
|
|
1410
|
-
bareReturn: MESSAGE$
|
|
1411
|
-
nonBooleanReturn: MESSAGE$
|
|
1412
|
-
missingReturn: MESSAGE$
|
|
1413
|
-
nonBooleanBody: MESSAGE$
|
|
1494
|
+
bareReturn: MESSAGE$5,
|
|
1495
|
+
nonBooleanReturn: MESSAGE$5,
|
|
1496
|
+
missingReturn: MESSAGE$5,
|
|
1497
|
+
nonBooleanBody: MESSAGE$5
|
|
1414
1498
|
}
|
|
1415
1499
|
};
|
|
1416
1500
|
//#endregion
|
|
@@ -1550,7 +1634,7 @@ const checkFn = (context, fn) => {
|
|
|
1550
1634
|
if (!Array$1.last(body.body).pipe(Option.exists(pathExits))) report(context, fn, "missingReturn", "the predicate can fall off the end without returning — undefined is a silent pass");
|
|
1551
1635
|
};
|
|
1552
1636
|
const noSilentReturn = defineRule({
|
|
1553
|
-
meta: meta$
|
|
1637
|
+
meta: meta$13,
|
|
1554
1638
|
create(context) {
|
|
1555
1639
|
return { CallExpression(node) {
|
|
1556
1640
|
if (!isPropCallee(node.callee)) return;
|
|
@@ -1579,21 +1663,21 @@ const SCHEMA_TEST_DETAIL = {
|
|
|
1579
1663
|
actual: "an authored *.schema.test.ts restating generated coverage",
|
|
1580
1664
|
fix: "delete it. The generated laws already state round-trip identity and encode stability. What they cannot state is rejection — every input they draw comes from the arbitrary the schema itself supplies — so a refusal belongs in an in-source if (import.meta.vitest) block in the schema file, never here"
|
|
1581
1665
|
};
|
|
1582
|
-
const meta$
|
|
1666
|
+
const meta$12 = {
|
|
1583
1667
|
type: "problem",
|
|
1584
1668
|
docs: { description: "Under src/, the only sanctioned test file is a single-segment <stem>.workflow.property.test.ts inside a sanctioned test directory, plus the generated schema-laws.test.ts entry point. Every other test file is banned: a kernel, policy, or schema suite becomes an in-source import.meta.vitest block, and a public-surface test moves outside src/ as an integration test. The sanctioned directory list is the sanctionedDirs option, defaulting to the one directory this repo runs." },
|
|
1585
1669
|
schema: [Schema.toJsonSchemaDocument(Options$1).schema],
|
|
1586
1670
|
messages: {
|
|
1587
|
-
testFileInSrc: MESSAGE$
|
|
1588
|
-
schemaTestInSrc: MESSAGE$
|
|
1589
|
-
propertyTestOutsideTestsDir: MESSAGE$
|
|
1671
|
+
testFileInSrc: MESSAGE$9,
|
|
1672
|
+
schemaTestInSrc: MESSAGE$9,
|
|
1673
|
+
propertyTestOutsideTestsDir: MESSAGE$9
|
|
1590
1674
|
}
|
|
1591
1675
|
};
|
|
1592
1676
|
//#endregion
|
|
1593
1677
|
//#region src/rules/no-test-file-in-src.ts
|
|
1594
1678
|
const violationOf = (basename, isPropertyTest, dir) => basename.endsWith(".schema.test.ts") ? ["schemaTestInSrc", SCHEMA_TEST_DETAIL] : isPropertyTest ? ["propertyTestOutsideTestsDir", propertyTestLocationDetail(dir)] : ["testFileInSrc", testFileInSrcDetail(dir)];
|
|
1595
1679
|
const noTestFileInSrc = defineRule({
|
|
1596
|
-
meta: meta$
|
|
1680
|
+
meta: meta$12,
|
|
1597
1681
|
create(context) {
|
|
1598
1682
|
const { sanctionedDirs } = Schema.decodeUnknownSync(Options$1)(context.options[0] ?? {});
|
|
1599
1683
|
const basename = basenameOf(context.filename);
|
|
@@ -1646,7 +1730,7 @@ const PREDICATE_SYMBOLS = /* @__PURE__ */ new Set([
|
|
|
1646
1730
|
const NULLARY_PREDICATE_SYMBOLS = /* @__PURE__ */ new Set(["⊥"]);
|
|
1647
1731
|
const PASCAL_CASE = /^[A-Z][a-z][a-zA-Z0-9]*$/;
|
|
1648
1732
|
const DAMP_WORDS = /When|Should|Given|Then|Otherwise|After|Before/;
|
|
1649
|
-
const meta$
|
|
1733
|
+
const meta$11 = {
|
|
1650
1734
|
type: "suggestion",
|
|
1651
1735
|
docs: { description: "Enforce a complete formal-specification name for property-based tests (it.prop / it.effect.prop). Format: [ScopeSymbol][binder]_[Domain]_[PredicateSymbol][operand] (e.g., ∀x_DecodeEncode_=x, ∀l_Filter_⊆Input, →Shipped_Cancel_⊥Allowed). Both the quantifier and the predicate must carry an operand — a bare symbol specifies nothing." },
|
|
1652
1736
|
schema: [],
|
|
@@ -1690,7 +1774,7 @@ const parseSegments = (name) => {
|
|
|
1690
1774
|
};
|
|
1691
1775
|
};
|
|
1692
1776
|
const pbtNaming = defineRule({
|
|
1693
|
-
meta: meta$
|
|
1777
|
+
meta: meta$11,
|
|
1694
1778
|
create(context) {
|
|
1695
1779
|
return { CallExpression(node) {
|
|
1696
1780
|
if (!isPropCall(node)) return;
|
|
@@ -1782,8 +1866,8 @@ const pbtNaming = defineRule({
|
|
|
1782
1866
|
});
|
|
1783
1867
|
//#endregion
|
|
1784
1868
|
//#region src/rules/prop-arbitrary-schema-origin.config.ts
|
|
1785
|
-
const MESSAGE$
|
|
1786
|
-
const VIOLATION_NAME$
|
|
1869
|
+
const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1870
|
+
const VIOLATION_NAME$2 = "a hand-built fast-check arbitrary with no schema underneath";
|
|
1787
1871
|
const EXPECTED$1 = "every in-source it.prop arbitrary derives from an Effect Schema — a schema reference, a schema-attached arbitrary annotation, or a pipe/map/oneof chain rooted in one; an input with no schema grows one first";
|
|
1788
1872
|
const ACTUAL$1 = "the arbitrary is assembled from fast-check constructors alone, so the generator can emit shapes the schema never declared and the suite stays green while exercising almost nothing";
|
|
1789
1873
|
const FIX$1 = "delete the block, or grow the schema the input needs and derive the arbitrary from it — Arbitrary.schema(schema) is the sanctioned derivation; nothing rewrites a hand-built generator into a property";
|
|
@@ -1798,13 +1882,13 @@ const SCHEMA_NAMESPACE_NAMES = {
|
|
|
1798
1882
|
};
|
|
1799
1883
|
const FASTCHECK_NAMESPACE_NAMES = { FastCheck: true };
|
|
1800
1884
|
const COMBINATOR_CALLEES = { pipe: true };
|
|
1801
|
-
const meta$
|
|
1885
|
+
const meta$10 = {
|
|
1802
1886
|
type: "problem",
|
|
1803
1887
|
docs: { description: "Inside an import.meta.vitest in-source block, every it.prop / it.effect.prop arbitrary must derive from an Effect Schema — a schema reference, a schema-attached arbitrary annotation, or a chain rooted in one. A hand-built fast-check construction with no schema underneath reports; statically opaque arbitraries (unresolved or foreign bindings) fail open into the runtime domain audit." },
|
|
1804
1888
|
schema: [],
|
|
1805
1889
|
messages: {
|
|
1806
|
-
handBuiltArbitrary: MESSAGE$
|
|
1807
|
-
stockDerivedArbitrary: MESSAGE$
|
|
1890
|
+
handBuiltArbitrary: MESSAGE$4,
|
|
1891
|
+
stockDerivedArbitrary: MESSAGE$4
|
|
1808
1892
|
}
|
|
1809
1893
|
};
|
|
1810
1894
|
//#endregion
|
|
@@ -2062,26 +2146,49 @@ const checkArbitraryFactory = (provenance, context, call) => {
|
|
|
2062
2146
|
}
|
|
2063
2147
|
});
|
|
2064
2148
|
};
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2149
|
+
const reportHandBuilt = (provenance, context, node) => {
|
|
2150
|
+
if (provenance.verdictOf(node, 0) !== "handBuilt") return;
|
|
2151
|
+
context.report({
|
|
2152
|
+
node,
|
|
2153
|
+
messageId: "handBuiltArbitrary",
|
|
2154
|
+
data: {
|
|
2155
|
+
name: VIOLATION_NAME$2,
|
|
2156
|
+
expected: EXPECTED$1,
|
|
2157
|
+
actual: ACTUAL$1,
|
|
2158
|
+
fix: FIX$1
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
};
|
|
2162
|
+
const checkArrayOf = (provenance, context, of) => {
|
|
2163
|
+
for (const element of of.elements) {
|
|
2069
2164
|
if (element === null) continue;
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2165
|
+
reportHandBuilt(provenance, context, element);
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
const checkRecordOf = (provenance, context, of) => {
|
|
2169
|
+
for (const member of of.properties) {
|
|
2170
|
+
if (member.type !== "Property" || member.computed) continue;
|
|
2171
|
+
reportHandBuilt(provenance, context, member.value);
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
const checkPropCall$1 = (provenance, context, call) => {
|
|
2175
|
+
const options = call.arguments[1];
|
|
2176
|
+
if (options === void 0 || options.type !== "ObjectExpression") return;
|
|
2177
|
+
for (const property of options.properties) {
|
|
2178
|
+
if (property.type !== "Property" || property.computed) continue;
|
|
2179
|
+
if (property.key.type !== "Identifier" || property.key.name !== "of") continue;
|
|
2180
|
+
if (property.value.type === "ArrayExpression") {
|
|
2181
|
+
checkArrayOf(provenance, context, property.value);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (property.value.type === "ObjectExpression") {
|
|
2185
|
+
checkRecordOf(provenance, context, property.value);
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2081
2188
|
}
|
|
2082
2189
|
};
|
|
2083
2190
|
const propArbitrarySchemaOrigin = defineRule({
|
|
2084
|
-
meta: meta$
|
|
2191
|
+
meta: meta$10,
|
|
2085
2192
|
create(context) {
|
|
2086
2193
|
const provenance = new Provenance(context.sourceCode.getScope);
|
|
2087
2194
|
const collect = (value, out) => {
|
|
@@ -2140,16 +2247,16 @@ const propArbitrarySchemaOrigin = defineRule({
|
|
|
2140
2247
|
});
|
|
2141
2248
|
//#endregion
|
|
2142
2249
|
//#region src/rules/prop-fixture-schema-origin.config.ts
|
|
2143
|
-
const MESSAGE$
|
|
2144
|
-
const VIOLATION_NAME = "a hand-rolled recursive schema fixture inside a test block";
|
|
2250
|
+
const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2251
|
+
const VIOLATION_NAME$1 = "a hand-rolled recursive schema fixture inside a test block";
|
|
2145
2252
|
const EXPECTED = "a recursive fixture schema enters the block through a named local builder call, or its recursion point declares its generation with a visible toCodecArbitrary derivation or a recursionBudget ceiling";
|
|
2146
2253
|
const ACTUAL = "the union and its recursion cycle are assembled inline, so the suite exercises a surrogate universe the shipped schema contract never declared and no budget gate grades";
|
|
2147
2254
|
const FIX = "hoist the members into a named builder the annotated and counterfactual fixtures share, or declare the recursion point's generation in its annotation";
|
|
2148
|
-
const meta$
|
|
2255
|
+
const meta$9 = {
|
|
2149
2256
|
type: "problem",
|
|
2150
2257
|
docs: { description: "Inside an import.meta.vitest in-source block or a *.test.ts file, a recursive schema union must enter through a named local builder or an imported helper, or its recursion point must declare its generation with a visible toCodecArbitrary derivation or recursionBudget ceiling. An inline hand-rolled recursive union reports; outside test scope the rule is silent, because production schemas are the derivation-cost rule domain." },
|
|
2151
2258
|
schema: [],
|
|
2152
|
-
messages: { handRolledRecursiveFixture: MESSAGE$
|
|
2259
|
+
messages: { handRolledRecursiveFixture: MESSAGE$3 }
|
|
2153
2260
|
};
|
|
2154
2261
|
//#endregion
|
|
2155
2262
|
//#region src/rules/prop-fixture-schema-origin.ts
|
|
@@ -2323,7 +2430,7 @@ const isNamedBuilderOrigin = (union, getScope) => {
|
|
|
2323
2430
|
return false;
|
|
2324
2431
|
};
|
|
2325
2432
|
const propFixtureSchemaOrigin = defineRule({
|
|
2326
|
-
meta: meta$
|
|
2433
|
+
meta: meta$9,
|
|
2327
2434
|
create(context) {
|
|
2328
2435
|
const getScope = context.sourceCode.getScope;
|
|
2329
2436
|
return { CallExpression(node) {
|
|
@@ -2340,7 +2447,7 @@ const propFixtureSchemaOrigin = defineRule({
|
|
|
2340
2447
|
node,
|
|
2341
2448
|
messageId: "handRolledRecursiveFixture",
|
|
2342
2449
|
data: {
|
|
2343
|
-
name: VIOLATION_NAME,
|
|
2450
|
+
name: VIOLATION_NAME$1,
|
|
2344
2451
|
expected: EXPECTED,
|
|
2345
2452
|
actual: ACTUAL,
|
|
2346
2453
|
fix: FIX
|
|
@@ -2351,7 +2458,7 @@ const propFixtureSchemaOrigin = defineRule({
|
|
|
2351
2458
|
});
|
|
2352
2459
|
//#endregion
|
|
2353
2460
|
//#region src/rules/prop-generated-law-duplicate.config.ts
|
|
2354
|
-
const MESSAGE$
|
|
2461
|
+
const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2355
2462
|
const COMPILER_NAME = "a runtime probe of a compile-time guarantee";
|
|
2356
2463
|
const COMPILER_EXPECTED = "the type checker owns brand and nominal-identity contracts — a dropped brand is a compile error at the consumer brand gate, not a failing property";
|
|
2357
2464
|
const COMPILER_ACTUAL = "the predicate reflects over brand symbols at runtime, so it re-asserts what tsc already enforces and passes whenever the type system passes";
|
|
@@ -2360,13 +2467,13 @@ const NO_FUNCTION_NAME = "a property that tests no function";
|
|
|
2360
2467
|
const NO_FUNCTION_EXPECTED = "every in-source property exercises domain logic — at least one call to a module-local function that is not a schema codec accessor — because a predicate that only feeds values through encode or decode tests the schema declaration, not code, and decode acceptance and refusal alike are generated or declared elsewhere";
|
|
2361
2468
|
const NO_FUNCTION_ACTUAL = "no call in the predicate reaches module code — every call is a codec accessor, a schema wrapper, or an iteration combinator — so the property cannot fail unless the declaration it restates changes meaning";
|
|
2362
2469
|
const NO_FUNCTION_FIX = "delete the prop; test the function that owns the decision — the workflow or a private helper in this module — with its input derived from a domain schema";
|
|
2363
|
-
const meta$
|
|
2470
|
+
const meta$8 = {
|
|
2364
2471
|
type: "problem",
|
|
2365
2472
|
docs: { description: "Inside an import.meta.vitest in-source block: a predicate that reflects over brand symbols re-asserts a compile-time guarantee, and a predicate containing no module-local non-codec function call tests a schema declaration instead of code. Both report." },
|
|
2366
2473
|
schema: [],
|
|
2367
2474
|
messages: {
|
|
2368
|
-
compilerDuplicate: MESSAGE$
|
|
2369
|
-
noDomainFunction: MESSAGE$
|
|
2475
|
+
compilerDuplicate: MESSAGE$2,
|
|
2476
|
+
noDomainFunction: MESSAGE$2
|
|
2370
2477
|
}
|
|
2371
2478
|
};
|
|
2372
2479
|
//#endregion
|
|
@@ -2506,7 +2613,7 @@ const checkPropCall = (provenance, context, call) => {
|
|
|
2506
2613
|
});
|
|
2507
2614
|
};
|
|
2508
2615
|
const propGeneratedLawDuplicate = defineRule({
|
|
2509
|
-
meta: meta$
|
|
2616
|
+
meta: meta$8,
|
|
2510
2617
|
create(context) {
|
|
2511
2618
|
const provenance = new Provenance(context.sourceCode.getScope);
|
|
2512
2619
|
const collect = (value, out) => {
|
|
@@ -2549,17 +2656,17 @@ const propGeneratedLawDuplicate = defineRule({
|
|
|
2549
2656
|
//#endregion
|
|
2550
2657
|
//#region src/rules/property-file-purity.config.ts
|
|
2551
2658
|
const PROPERTY_TEST_SUFFIX = ".property.test.ts";
|
|
2552
|
-
const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2553
|
-
const meta$
|
|
2659
|
+
const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2660
|
+
const meta$7 = {
|
|
2554
2661
|
type: "problem",
|
|
2555
2662
|
docs: { description: "Property tests live ONLY in *.property.test.ts files, and those files contain ONLY property tests. In a property file: no plain it()/test()/it.effect(), no raw fc.assert/fc.check/fc.property/fc.asyncProperty. In any other test file: no FastCheck import and no it.prop/it.effect.prop — move the property to a *.property.test.ts file." },
|
|
2556
2663
|
schema: [],
|
|
2557
2664
|
messages: {
|
|
2558
|
-
plainIt: MESSAGE,
|
|
2559
|
-
plainEffectIt: MESSAGE,
|
|
2560
|
-
rawFastCheck: MESSAGE,
|
|
2561
|
-
fastCheckImport: MESSAGE,
|
|
2562
|
-
propCall: MESSAGE
|
|
2665
|
+
plainIt: MESSAGE$1,
|
|
2666
|
+
plainEffectIt: MESSAGE$1,
|
|
2667
|
+
rawFastCheck: MESSAGE$1,
|
|
2668
|
+
fastCheckImport: MESSAGE$1,
|
|
2669
|
+
propCall: MESSAGE$1
|
|
2563
2670
|
}
|
|
2564
2671
|
};
|
|
2565
2672
|
//#endregion
|
|
@@ -2576,7 +2683,7 @@ const PROPERTY_FILE_KIND = {
|
|
|
2576
2683
|
plainFix: "move the scenario test to a plain *.test.ts file, or rewrite it as a property with arbitraries and a boolean-returning predicate",
|
|
2577
2684
|
rawFcExpected: "it.prop(...) or it.effect.prop(...) from @effect/vitest",
|
|
2578
2685
|
rawFcActualSuffix: "bypasses the vitest/Effect integration",
|
|
2579
|
-
rawFcFix: "rewrite as it.prop(name,
|
|
2686
|
+
rawFcFix: "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
|
|
2580
2687
|
};
|
|
2581
2688
|
const DIFFERENTIAL_FILE_KIND = {
|
|
2582
2689
|
suffix: DIFFERENTIAL_SUFFIX,
|
|
@@ -2666,7 +2773,7 @@ const createScenarioFileVisitors = (context) => ({
|
|
|
2666
2773
|
});
|
|
2667
2774
|
const kindOf = (filename) => filename.endsWith(".differential.test.ts") ? DIFFERENTIAL_FILE_KIND : PROPERTY_FILE_KIND;
|
|
2668
2775
|
const propertyFilePurity = defineRule({
|
|
2669
|
-
meta: meta$
|
|
2776
|
+
meta: meta$7,
|
|
2670
2777
|
create(context) {
|
|
2671
2778
|
if (!isTestFile(basenameOf(context.filename))) return {};
|
|
2672
2779
|
if (context.filename.endsWith(".property.test.ts") || context.filename.endsWith(".differential.test.ts")) return createPropertyFileVisitors(context, kindOf(context.filename));
|
|
@@ -2682,12 +2789,12 @@ const UNSANCTIONED_CELL_FIX = "rename it <stem>.workflow.property.test.ts beside
|
|
|
2682
2789
|
const MISSING_CELL_TEST_EXPECTED = "a test for every cell suffix the consumer lists in cellsRequiringTest";
|
|
2683
2790
|
const MISSING_CELL_TEST_ACTUAL = "a declared cell whose own module carries no `if (import.meta.vitest)` block";
|
|
2684
2791
|
const MISSING_CELL_TEST_FIX = "add an `if (import.meta.vitest)` block to this module, or drop this cell from cellsRequiringTest and cover it with a colocated test in a sanctioned test directory — the rule reads the file it is given, so a sibling test file is invisible to it and a declared cell must satisfy the requirement from its own source";
|
|
2685
|
-
const meta$
|
|
2792
|
+
const meta$6 = {
|
|
2686
2793
|
type: "problem",
|
|
2687
2794
|
docs: { description: "A property test under src/ must be a single-segment <stem>.workflow.property.test.ts beside the <stem>.workflow.ts it covers; every other property-test basename is unsanctioned. A source file whose suffix names a cell listed in the cellsRequiringTest option must additionally carry an in-source vitest block; that list is empty by default, so the presence arm is opt-in per consumer." },
|
|
2688
2795
|
schema: [Schema.toJsonSchemaDocument(Options).schema],
|
|
2689
2796
|
messages: {
|
|
2690
|
-
unsanctionedCell: MESSAGE$
|
|
2797
|
+
unsanctionedCell: MESSAGE$9,
|
|
2691
2798
|
missingCellTest: ABSENCE_MESSAGE
|
|
2692
2799
|
}
|
|
2693
2800
|
};
|
|
@@ -2695,7 +2802,7 @@ const meta$5 = {
|
|
|
2695
2802
|
//#region src/rules/src-property-test-cell.ts
|
|
2696
2803
|
const carriesInSourceBlock = (body) => body.some((statement) => statement.type === "IfStatement" && isVitestGuard(statement.test));
|
|
2697
2804
|
const srcPropertyTestCell = defineRule({
|
|
2698
|
-
meta: meta$
|
|
2805
|
+
meta: meta$6,
|
|
2699
2806
|
create(context) {
|
|
2700
2807
|
const { cellsRequiringTest } = Schema.decodeUnknownSync(Options)(context.options[0] ?? {});
|
|
2701
2808
|
const filename = context.filename;
|
|
@@ -2748,7 +2855,7 @@ const testFileOutsideTestsDir = defineRule({
|
|
|
2748
2855
|
type: "problem",
|
|
2749
2856
|
docs: { description: "Test files outside src/ must live under a tests/ directory; free-standing test files have no sanctioned home." },
|
|
2750
2857
|
schema: [],
|
|
2751
|
-
messages: { strayTestFile: MESSAGE$
|
|
2858
|
+
messages: { strayTestFile: MESSAGE$9 }
|
|
2752
2859
|
},
|
|
2753
2860
|
create(context) {
|
|
2754
2861
|
if (isUnderSrc(context.filename)) return {};
|
|
@@ -2780,7 +2887,7 @@ const testSuffixOutsideSrc = defineRule({
|
|
|
2780
2887
|
type: "problem",
|
|
2781
2888
|
docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, or .trace.test.ts." },
|
|
2782
2889
|
schema: [],
|
|
2783
|
-
messages: { unsanctionedSuffix: MESSAGE$
|
|
2890
|
+
messages: { unsanctionedSuffix: MESSAGE$9 }
|
|
2784
2891
|
},
|
|
2785
2892
|
create(context) {
|
|
2786
2893
|
const filename = context.filename;
|
|
@@ -2814,7 +2921,7 @@ const testsDirHelpersInFixtures = defineRule({
|
|
|
2814
2921
|
type: "problem",
|
|
2815
2922
|
docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/." },
|
|
2816
2923
|
schema: [],
|
|
2817
|
-
messages: { helperOutsideFixtures: MESSAGE$
|
|
2924
|
+
messages: { helperOutsideFixtures: MESSAGE$9 }
|
|
2818
2925
|
},
|
|
2819
2926
|
create(context) {
|
|
2820
2927
|
const filename = context.filename;
|
|
@@ -2843,11 +2950,11 @@ const testsDirHelpersInFixtures = defineRule({
|
|
|
2843
2950
|
const REACH_IN_EXPECTED = "a package name or subpath, or a sibling helper under the test tree";
|
|
2844
2951
|
const REACH_IN_ACTUAL = "a relative import that reaches src or climbs into an internal folder";
|
|
2845
2952
|
const REACH_IN_FIX = "rewrite onto the published package name when the binding is public. Delete the test when the subject is an internal";
|
|
2846
|
-
const meta$
|
|
2953
|
+
const meta$2 = {
|
|
2847
2954
|
type: "problem",
|
|
2848
2955
|
docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
|
|
2849
2956
|
schema: [],
|
|
2850
|
-
messages: { sourceReachIn: MESSAGE$
|
|
2957
|
+
messages: { sourceReachIn: MESSAGE$9 }
|
|
2851
2958
|
};
|
|
2852
2959
|
//#endregion
|
|
2853
2960
|
//#region src/rules/tests-import-public-api.ts
|
|
@@ -2865,7 +2972,7 @@ const specifierOf = (node) => {
|
|
|
2865
2972
|
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
2866
2973
|
};
|
|
2867
2974
|
const testsImportPublicApi = defineRule({
|
|
2868
|
-
meta: meta$
|
|
2975
|
+
meta: meta$2,
|
|
2869
2976
|
create(context) {
|
|
2870
2977
|
if (!isInTestsImportScope(context.filename)) return {};
|
|
2871
2978
|
const reportIfForbidden = (sourceNode) => {
|
|
@@ -2923,14 +3030,14 @@ const HTTP_TERMINATION_FIX = "replace the HTTP assertion with a relation over th
|
|
|
2923
3030
|
const RAW_EMIT_EXPECTED = "spans declared once with Span.declare({ id, name, attrs }) inside the traced cell; the spec observes the decoded graph";
|
|
2924
3031
|
const RAW_EMIT_ACTUAL = "a raw span emit call inside a trace spec";
|
|
2925
3032
|
const RAW_EMIT_FIX = "delete the emit; declare the span with Span.declare in the system under test and assert on the graph the spec receives";
|
|
2926
|
-
const meta = {
|
|
3033
|
+
const meta$1 = {
|
|
2927
3034
|
type: "problem",
|
|
2928
3035
|
docs: { description: "A *.trace.test.ts must import its harness from @systemfsoftware/trace-spec and terminate cases on relation holds over the declared span graph. Raw emit calls (startSpan, spanBuilder, startActiveSpan) and HTTP status, statusText, or body assertions (expect(res.status).toBe(200), expect(res).toHaveProperty(\"status\", 200), expect(res).resolves.toMatchObject({ status: 200 }), expect(res).toEqual(expect.objectContaining({ status: 200 }))) are forbidden — replacing that altitude is what a trace spec exists for." },
|
|
2929
3036
|
schema: [],
|
|
2930
3037
|
messages: {
|
|
2931
|
-
missingHarnessImport: MESSAGE$
|
|
2932
|
-
httpTermination: MESSAGE$
|
|
2933
|
-
rawEmitCall: MESSAGE$
|
|
3038
|
+
missingHarnessImport: MESSAGE$9,
|
|
3039
|
+
httpTermination: MESSAGE$9,
|
|
3040
|
+
rawEmitCall: MESSAGE$9
|
|
2934
3041
|
}
|
|
2935
3042
|
};
|
|
2936
3043
|
//#endregion
|
|
@@ -3097,7 +3204,7 @@ const reportHttpTermination = (context, node) => {
|
|
|
3097
3204
|
});
|
|
3098
3205
|
};
|
|
3099
3206
|
const traceTestRequiresTaxonomy = defineRule({
|
|
3100
|
-
meta,
|
|
3207
|
+
meta: meta$1,
|
|
3101
3208
|
create(context) {
|
|
3102
3209
|
if (!context.filename.endsWith(".trace.test.ts")) return {};
|
|
3103
3210
|
let hasHarnessImport = false;
|
|
@@ -3126,6 +3233,56 @@ const traceTestRequiresTaxonomy = defineRule({
|
|
|
3126
3233
|
}
|
|
3127
3234
|
});
|
|
3128
3235
|
//#endregion
|
|
3236
|
+
//#region src/rules/vitest-from-effect-vitest.config.ts
|
|
3237
|
+
const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
3238
|
+
const VIOLATION_NAME = "a value import from vitest";
|
|
3239
|
+
const VIOLATION_EXPECTED = "every test value imported from @effect/vitest, which re-exports all of Vitest";
|
|
3240
|
+
const VIOLATION_ACTUAL = "a named, namespace, default, side-effect or dynamic value import from vitest";
|
|
3241
|
+
const VIOLATION_FIX = "import it from @effect/vitest instead of vitest";
|
|
3242
|
+
const meta = {
|
|
3243
|
+
type: "problem",
|
|
3244
|
+
docs: { description: "Every value a test uses from Vitest is imported from @effect/vitest, which re-exports all of Vitest. Any value import from vitest — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal." },
|
|
3245
|
+
schema: [],
|
|
3246
|
+
messages: { vitestImport: MESSAGE }
|
|
3247
|
+
};
|
|
3248
|
+
//#endregion
|
|
3249
|
+
//#region src/rules/vitest-from-effect-vitest.ts
|
|
3250
|
+
const isTypeOnly = (node) => {
|
|
3251
|
+
if (node.importKind === "type") return true;
|
|
3252
|
+
if (node.specifiers.length === 0) return false;
|
|
3253
|
+
return node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
|
|
3254
|
+
};
|
|
3255
|
+
const isVitestLiteral = (node) => node.type === "Literal" && typeof node.value === "string" && node.value === "vitest";
|
|
3256
|
+
const vitestFromEffectVitest = defineRule({
|
|
3257
|
+
meta,
|
|
3258
|
+
create(context) {
|
|
3259
|
+
if (isRawVitestPackage(context.filename)) return {};
|
|
3260
|
+
const report = (node) => {
|
|
3261
|
+
context.report({
|
|
3262
|
+
node,
|
|
3263
|
+
messageId: "vitestImport",
|
|
3264
|
+
data: {
|
|
3265
|
+
name: VIOLATION_NAME,
|
|
3266
|
+
expected: VIOLATION_EXPECTED,
|
|
3267
|
+
actual: VIOLATION_ACTUAL,
|
|
3268
|
+
fix: VIOLATION_FIX
|
|
3269
|
+
}
|
|
3270
|
+
});
|
|
3271
|
+
};
|
|
3272
|
+
return {
|
|
3273
|
+
ImportDeclaration(node) {
|
|
3274
|
+
if (node.source.value !== "vitest") return;
|
|
3275
|
+
if (isTypeOnly(node)) return;
|
|
3276
|
+
report(node);
|
|
3277
|
+
},
|
|
3278
|
+
ImportExpression(node) {
|
|
3279
|
+
if (!isVitestLiteral(node.source)) return;
|
|
3280
|
+
report(node);
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
});
|
|
3285
|
+
//#endregion
|
|
3129
3286
|
//#region src/index.ts
|
|
3130
3287
|
const PLUGIN_NAME = "@systemfsoftware/oxlint-plugin-test-discipline";
|
|
3131
3288
|
const rule = (name) => `${PLUGIN_NAME}/${name}`;
|
|
@@ -3150,6 +3307,8 @@ const recommendedRules = {
|
|
|
3150
3307
|
[rule("behaviour-exercises-use-case")]: "error",
|
|
3151
3308
|
[rule("behaviour-one-feature-per-file")]: "error",
|
|
3152
3309
|
[rule("tests-dir-helpers-in-fixtures")]: "error",
|
|
3310
|
+
[rule("expect-boolean-predicate")]: "error",
|
|
3311
|
+
[rule("vitest-from-effect-vitest")]: "error",
|
|
3153
3312
|
[rule("no-io-module-in-source-test")]: "error",
|
|
3154
3313
|
[rule("tests-import-public-api")]: "error",
|
|
3155
3314
|
[rule("differential-test-requires-harness")]: "error",
|
|
@@ -3181,6 +3340,8 @@ var src_default = {
|
|
|
3181
3340
|
"behaviour-one-feature-per-file": behaviourOneFeaturePerFile,
|
|
3182
3341
|
"no-pseudo-gherkin-unit-tests": noPseudoGherkinUnitTests,
|
|
3183
3342
|
"tests-dir-helpers-in-fixtures": testsDirHelpersInFixtures,
|
|
3343
|
+
"expect-boolean-predicate": expectBooleanPredicate,
|
|
3344
|
+
"vitest-from-effect-vitest": vitestFromEffectVitest,
|
|
3184
3345
|
"no-io-module-in-source-test": noIoModuleInSourceTest,
|
|
3185
3346
|
"tests-import-public-api": testsImportPublicApi,
|
|
3186
3347
|
"differential-test-requires-harness": differentialTestRequiresHarness,
|
|
@@ -26,6 +26,8 @@ declare const _default: {
|
|
|
26
26
|
'behaviour-one-feature-per-file': Rule;
|
|
27
27
|
'no-pseudo-gherkin-unit-tests': Rule;
|
|
28
28
|
'tests-dir-helpers-in-fixtures': Rule;
|
|
29
|
+
'expect-boolean-predicate': Rule;
|
|
30
|
+
'vitest-from-effect-vitest': Rule;
|
|
29
31
|
'no-io-module-in-source-test': Rule;
|
|
30
32
|
'tests-import-public-api': Rule;
|
|
31
33
|
'differential-test-requires-harness': Rule;
|
|
@@ -26,6 +26,8 @@ declare const _default: {
|
|
|
26
26
|
'behaviour-one-feature-per-file': Rule;
|
|
27
27
|
'no-pseudo-gherkin-unit-tests': Rule;
|
|
28
28
|
'tests-dir-helpers-in-fixtures': Rule;
|
|
29
|
+
'expect-boolean-predicate': Rule;
|
|
30
|
+
'vitest-from-effect-vitest': Rule;
|
|
29
31
|
'no-io-module-in-source-test': Rule;
|
|
30
32
|
'tests-import-public-api': Rule;
|
|
31
33
|
'differential-test-requires-harness': Rule;
|