@systemfsoftware/oxlint-plugin-test-discipline 3.5.1 → 3.6.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 +11 -0
- package/README.md +19 -17
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +157 -39
- package/dist/oxlint-plugin-test-discipline-untrimmed.d.ts +1 -0
- package/dist/oxlint-plugin-test-discipline.d.ts +1 -0
- package/package.json +3 -3
- package/dist/oxlint-plugin-test-placement-untrimmed.d.ts +0 -32
- package/dist/oxlint-plugin-test-placement.d.ts +0 -32
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @systemfsoftware/oxlint-plugin-test-discipline
|
|
2
|
+
|
|
3
|
+
## 3.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- New rule `differential-test-requires-harness` (recommended, error): a `.differential.test.ts` file must import and invoke `@systemfsoftware/differential-spec` (`Differential.compare` / `Metamorphic.on`) and must not call raw test runners. `test-suffix-outside-src` now sanctions the `.differential.test.ts` altitude alongside `.integration.test.ts`, and `property-file-purity` treats differential files as property files.
|
|
8
|
+
|
|
9
|
+
- Publish standalone domain-aligned oxlint plugins and shareable configurations for Effect Schema, DMMF workflows, Effect Platform runtime entrypoints, test discipline, and Cell Architecture.
|
|
10
|
+
|
|
11
|
+
Consolidates and supersedes the 12 retired legacy packages (`@systemfsoftware/all`, `@systemfsoftware/oxlint-config`, `@systemfsoftware/oxlint-plugin`, `@systemfsoftware/oxlint-plugin-recommended`, `@systemfsoftware/oxlint-plugin-effect-dmmf`, `@systemfsoftware/oxlint-plugin-effect-native`, `@systemfsoftware/oxlint-plugin-effect-entrypoint`, `@systemfsoftware/oxlint-plugin-effect-workflow`, `@systemfsoftware/oxlint-plugin-property-testing`, `@systemfsoftware/oxlint-plugin-structure`, `@systemfsoftware/oxlint-plugin-tag-discipline`, `@systemfsoftware/oxlint-plugin-test-hygiene`, `@systemfsoftware/oxlint-plugin-test-placement`).
|
package/README.md
CHANGED
|
@@ -4,23 +4,25 @@ Oxlint rules enforcing property-based test laws, test placement, and test naming
|
|
|
4
4
|
|
|
5
5
|
## Rules
|
|
6
6
|
|
|
7
|
-
| Rule
|
|
8
|
-
|
|
|
9
|
-
| `no-test-file-in-src`
|
|
10
|
-
| `src-property-test-cell`
|
|
11
|
-
| `test-file-outside-tests-dir`
|
|
12
|
-
| `test-suffix-outside-src`
|
|
13
|
-
| `tests-dir-helpers-in-fixtures`
|
|
14
|
-
| `no-io-module-in-source-test`
|
|
15
|
-
| `behaviour-test-requires-gherkin`
|
|
16
|
-
| `behaviour-exercises-use-case`
|
|
17
|
-
| `behaviour-one-feature-per-file`
|
|
18
|
-
| `
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
| `*.
|
|
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` or `.differential.test.ts` — the two 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
|
+
|
|
21
|
+
| suffix | layer | doubles | location |
|
|
22
|
+
| ----------------------------- | ------------ | ------------------------ | ------------------ |
|
|
23
|
+
| `*.workflow.property.test.ts` | Property | none — pure core | ONLY under `src/` |
|
|
24
|
+
| `*.integration.test.ts` | Behaviour | permitted, at ports only | NEVER under `src/` |
|
|
25
|
+
| `*.differential.test.ts` | Differential | permitted, at ports only | NEVER under `src/` |
|
|
24
26
|
|
|
25
27
|
## Enrollment
|
|
26
28
|
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ declare const _default: {
|
|
|
27
27
|
'tests-dir-helpers-in-fixtures': import("@oxlint/plugins").Rule;
|
|
28
28
|
'no-io-module-in-source-test': import("@oxlint/plugins").Rule;
|
|
29
29
|
'tests-import-public-api': import("@oxlint/plugins").Rule;
|
|
30
|
+
'differential-test-requires-harness': import("@oxlint/plugins").Rule;
|
|
30
31
|
};
|
|
31
32
|
configs: {
|
|
32
33
|
recommended: {
|
package/dist/index.mjs
CHANGED
|
@@ -12,6 +12,7 @@ const NESTED_TEST_DIR = "__tests__";
|
|
|
12
12
|
const TEST_TREE_DIRS = /* @__PURE__ */ new Set([...SANCTIONED_TEST_DIRS, NESTED_TEST_DIR]);
|
|
13
13
|
const PROPERTY_SUFFIX = ".property.test.ts";
|
|
14
14
|
const INTEGRATION_SUFFIX = ".integration.test.ts";
|
|
15
|
+
const DIFFERENTIAL_SUFFIX = ".differential.test.ts";
|
|
15
16
|
/**
|
|
16
17
|
* Forbidden outright. A schema's laws are generated, so a hand-written
|
|
17
18
|
* `*.schema.test.ts` only ever restates coverage that already exists.
|
|
@@ -26,6 +27,7 @@ const SCHEMA_SUFFIX = ".schema.test.ts";
|
|
|
26
27
|
*/
|
|
27
28
|
const WORKFLOW_TEST_BASENAME = /^[^.]+\.workflow\.property\.test\.ts$/;
|
|
28
29
|
const GHERKIN_PACKAGE = "@systemfsoftware/effect-gherkin-spec";
|
|
30
|
+
const DIFFERENTIAL_PACKAGE = "@systemfsoftware/differential-spec";
|
|
29
31
|
const FOREIGN_RUNNERS = /* @__PURE__ */ new Set(["vitest", "@effect/vitest"]);
|
|
30
32
|
const RUNNER_NAMES = /* @__PURE__ */ new Set([
|
|
31
33
|
"it",
|
|
@@ -40,7 +42,7 @@ const NO_SUBJECT_IMPORT_NAME = "a *.integration.test.ts that reaches no package
|
|
|
40
42
|
const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
|
|
41
43
|
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";
|
|
42
44
|
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.";
|
|
43
|
-
const meta$
|
|
45
|
+
const meta$23 = {
|
|
44
46
|
type: "problem",
|
|
45
47
|
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." },
|
|
46
48
|
schema: [],
|
|
@@ -153,7 +155,7 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
|
|
|
153
155
|
* path names nothing satisfies the rule the same way a real one does.
|
|
154
156
|
*/
|
|
155
157
|
const behaviourExercisesUseCase = defineRule({
|
|
156
|
-
meta: meta$
|
|
158
|
+
meta: meta$23,
|
|
157
159
|
create(context) {
|
|
158
160
|
let reached = false;
|
|
159
161
|
return {
|
|
@@ -203,7 +205,7 @@ const TOO_MANY_FEATURES_NAME = "a *.integration.test.ts accumulating multiple Fe
|
|
|
203
205
|
const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
|
|
204
206
|
const TOO_MANY_FEATURES_ACTUAL = "a behaviour file with two or more Feature(...) calls";
|
|
205
207
|
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.";
|
|
206
|
-
const meta$
|
|
208
|
+
const meta$22 = {
|
|
207
209
|
type: "problem",
|
|
208
210
|
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." },
|
|
209
211
|
schema: [],
|
|
@@ -248,7 +250,7 @@ const hasAnyFeatureCall = (program) => {
|
|
|
248
250
|
return false;
|
|
249
251
|
};
|
|
250
252
|
const behaviourOneFeaturePerFile = defineRule({
|
|
251
|
-
meta: meta$
|
|
253
|
+
meta: meta$22,
|
|
252
254
|
create(context) {
|
|
253
255
|
return { "Program:exit"(node) {
|
|
254
256
|
if (!isBehaviourTest$2(basenameOf(context.filename))) return;
|
|
@@ -289,7 +291,7 @@ const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
|
|
|
289
291
|
const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
|
|
290
292
|
const MISSING_MAKE_FEATURE_ACTUAL = "a behaviour file that never constructs a Gherkin feature";
|
|
291
293
|
const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
|
|
292
|
-
const meta$
|
|
294
|
+
const meta$21 = {
|
|
293
295
|
type: "problem",
|
|
294
296
|
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." },
|
|
295
297
|
schema: [],
|
|
@@ -317,7 +319,7 @@ const foreignRunnerNameOf = (specifier) => {
|
|
|
317
319
|
};
|
|
318
320
|
const isBehaviourTest$1 = (basename) => basename.endsWith(INTEGRATION_SUFFIX);
|
|
319
321
|
const behaviourTestRequiresGherkin = defineRule({
|
|
320
|
-
meta: meta$
|
|
322
|
+
meta: meta$21,
|
|
321
323
|
create(context) {
|
|
322
324
|
const basename = basenameOf(context.filename);
|
|
323
325
|
return { Program(node) {
|
|
@@ -380,7 +382,7 @@ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwEr
|
|
|
380
382
|
const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
|
|
381
383
|
const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
|
|
382
384
|
const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
|
|
383
|
-
const meta$
|
|
385
|
+
const meta$20 = {
|
|
384
386
|
type: "suggestion",
|
|
385
387
|
docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
|
|
386
388
|
schema: [],
|
|
@@ -460,7 +462,7 @@ const isTestFunctionCall = (node) => {
|
|
|
460
462
|
return false;
|
|
461
463
|
};
|
|
462
464
|
const dampTestNaming = defineRule({
|
|
463
|
-
meta: meta$
|
|
465
|
+
meta: meta$20,
|
|
464
466
|
create(context) {
|
|
465
467
|
return { CallExpression(node) {
|
|
466
468
|
if (!isTestFunctionCall(node)) return;
|
|
@@ -482,6 +484,100 @@ const dampTestNaming = defineRule({
|
|
|
482
484
|
}
|
|
483
485
|
});
|
|
484
486
|
//#endregion
|
|
487
|
+
//#region src/rules/differential-test-requires-harness.config.ts
|
|
488
|
+
const meta$19 = {
|
|
489
|
+
type: "problem",
|
|
490
|
+
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." },
|
|
491
|
+
schema: [],
|
|
492
|
+
messages: {
|
|
493
|
+
rawRunnerCall: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
|
|
494
|
+
runnerImport: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
|
|
495
|
+
missingHarnessImport: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
|
|
496
|
+
missingHarnessUsage: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}."
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/rules/differential-test-requires-harness.ts
|
|
501
|
+
const HARNESS_PRESCRIPTION = "import { Differential, Metamorphic } from @systemfsoftware/differential-spec and express the test as Differential.compare({ reference, candidate }).on(arb).assert(oracle) or Metamorphic.on(system).relation({ transformInput, assertOutput }).on(arb)";
|
|
502
|
+
const memberObjectName = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
|
|
503
|
+
const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
|
|
504
|
+
const recordHarnessBindings = (node, bindings) => {
|
|
505
|
+
if (node.source.value !== "@systemfsoftware/differential-spec") return;
|
|
506
|
+
for (const specifier of node.specifiers) bindings.add(specifier.local.name);
|
|
507
|
+
};
|
|
508
|
+
const isForeignRunnerImport = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS.has(node.source.value);
|
|
509
|
+
const differentialTestRequiresHarness = defineRule({
|
|
510
|
+
meta: meta$19,
|
|
511
|
+
create(context) {
|
|
512
|
+
if (!context.filename.endsWith(".differential.test.ts")) return {};
|
|
513
|
+
const harnessBindings = /* @__PURE__ */ new Set();
|
|
514
|
+
let violations = 0;
|
|
515
|
+
let hasHarnessInvocation = false;
|
|
516
|
+
return {
|
|
517
|
+
ImportDeclaration(node) {
|
|
518
|
+
if (isForeignRunnerImport(node)) {
|
|
519
|
+
context.report({
|
|
520
|
+
node,
|
|
521
|
+
messageId: "runnerImport",
|
|
522
|
+
data: {
|
|
523
|
+
name: `runner import from ${String(node.source.value)} in a differential test file`,
|
|
524
|
+
expected: HARNESS_PRESCRIPTION,
|
|
525
|
+
actual: "a direct vitest / @effect/vitest runner import bypasses the differential oracle",
|
|
526
|
+
fix: `delete the runner import; ${HARNESS_PRESCRIPTION}`
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
violations += 1;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
recordHarnessBindings(node, harnessBindings);
|
|
533
|
+
},
|
|
534
|
+
CallExpression(node) {
|
|
535
|
+
const name = calleeName(node.callee);
|
|
536
|
+
if (name !== void 0 && RUNNER_NAMES.has(name)) {
|
|
537
|
+
context.report({
|
|
538
|
+
node,
|
|
539
|
+
messageId: "rawRunnerCall",
|
|
540
|
+
data: {
|
|
541
|
+
name: `raw runner call (${name}) in a differential test file`,
|
|
542
|
+
expected: HARNESS_PRESCRIPTION,
|
|
543
|
+
actual: `${name}(...) bypasses the differential oracle`,
|
|
544
|
+
fix: `rewrite using ${HARNESS_PRESCRIPTION}`
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
violations += 1;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (name !== void 0 && harnessBindings.has(name)) hasHarnessInvocation = true;
|
|
551
|
+
},
|
|
552
|
+
"Program:exit"(node) {
|
|
553
|
+
if (harnessBindings.size === 0) {
|
|
554
|
+
context.report({
|
|
555
|
+
node,
|
|
556
|
+
messageId: "missingHarnessImport",
|
|
557
|
+
data: {
|
|
558
|
+
name: `differential test file without ${DIFFERENTIAL_PACKAGE} import`,
|
|
559
|
+
expected: HARNESS_PRESCRIPTION,
|
|
560
|
+
actual: "no differential harness import found",
|
|
561
|
+
fix: HARNESS_PRESCRIPTION
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!hasHarnessInvocation && violations === 0) context.report({
|
|
567
|
+
node,
|
|
568
|
+
messageId: "missingHarnessUsage",
|
|
569
|
+
data: {
|
|
570
|
+
name: `differential test file imports ${DIFFERENTIAL_PACKAGE} but never invokes it`,
|
|
571
|
+
expected: HARNESS_PRESCRIPTION,
|
|
572
|
+
actual: "the harness is imported but no Differential.compare or Metamorphic.on chain runs",
|
|
573
|
+
fix: HARNESS_PRESCRIPTION
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
//#endregion
|
|
485
581
|
//#region src/rules/in-source-test-prop-only.config.ts
|
|
486
582
|
const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vitest` block";
|
|
487
583
|
const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
|
|
@@ -2327,7 +2423,8 @@ const checkPropCall = (provenance, context, call) => {
|
|
|
2327
2423
|
});
|
|
2328
2424
|
return;
|
|
2329
2425
|
}
|
|
2330
|
-
if (
|
|
2426
|
+
if (shape.callsDomainFunction) return;
|
|
2427
|
+
context.report({
|
|
2331
2428
|
node: call,
|
|
2332
2429
|
messageId: "noDomainFunction",
|
|
2333
2430
|
data: {
|
|
@@ -2403,45 +2500,64 @@ const RAW_FC_METHODS = /* @__PURE__ */ new Set([
|
|
|
2403
2500
|
"property",
|
|
2404
2501
|
"asyncProperty"
|
|
2405
2502
|
]);
|
|
2406
|
-
const
|
|
2503
|
+
const PROPERTY_FILE_KIND = {
|
|
2504
|
+
suffix: PROPERTY_TEST_SUFFIX,
|
|
2505
|
+
plainExpected: "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests",
|
|
2506
|
+
plainFix: "move the scenario test to a plain *.test.ts file, or rewrite it as a property with arbitraries and a boolean-returning predicate",
|
|
2507
|
+
rawFcExpected: "it.prop(...) or it.effect.prop(...) from @effect/vitest",
|
|
2508
|
+
rawFcActualSuffix: "bypasses the vitest/Effect integration",
|
|
2509
|
+
rawFcFix: "rewrite as it.prop(name, [arbitraries], predicate) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
|
|
2510
|
+
};
|
|
2511
|
+
const DIFFERENTIAL_FILE_KIND = {
|
|
2512
|
+
suffix: DIFFERENTIAL_SUFFIX,
|
|
2513
|
+
plainExpected: "the differential harness — Differential.compare(...).on(arb).assert(oracle) or Metamorphic.on(system).relation(...).on(arb)",
|
|
2514
|
+
plainFix: "rewrite via @systemfsoftware/differential-spec; scenario tests do not belong in a differential file",
|
|
2515
|
+
rawFcExpected: "the harness owns fast-check — pass an fc.Arbitrary to the harness, never call fc.assert/fc.check directly",
|
|
2516
|
+
rawFcActualSuffix: "bypasses the differential harness oracle",
|
|
2517
|
+
rawFcFix: "build the arbitrary in tests/__fixtures__ and hand it to Differential.compare(...).on(arb) or Metamorphic.on(...).relation(...).on(arb)"
|
|
2518
|
+
};
|
|
2519
|
+
const reportPlain = (context, node, messageId, actual, kind) => {
|
|
2407
2520
|
context.report({
|
|
2408
2521
|
node,
|
|
2409
2522
|
messageId,
|
|
2410
2523
|
data: {
|
|
2411
|
-
name: `scenario test (${actual}) in a ${
|
|
2412
|
-
expected:
|
|
2524
|
+
name: `scenario test (${actual}) in a ${kind.suffix} file`,
|
|
2525
|
+
expected: kind.plainExpected,
|
|
2413
2526
|
actual: `${actual} runs a single example, not a property`,
|
|
2414
|
-
fix:
|
|
2527
|
+
fix: kind.plainFix
|
|
2528
|
+
}
|
|
2529
|
+
});
|
|
2530
|
+
};
|
|
2531
|
+
const reportRawFc = (context, node, method, kind) => {
|
|
2532
|
+
context.report({
|
|
2533
|
+
node,
|
|
2534
|
+
messageId: "rawFastCheck",
|
|
2535
|
+
data: {
|
|
2536
|
+
name: `raw fc.${method}(...) in a ${kind.suffix} file`,
|
|
2537
|
+
expected: kind.rawFcExpected,
|
|
2538
|
+
actual: `fc.${method}(...) ${kind.rawFcActualSuffix}`,
|
|
2539
|
+
fix: kind.rawFcFix
|
|
2415
2540
|
}
|
|
2416
2541
|
});
|
|
2417
2542
|
};
|
|
2418
|
-
const createPropertyFileVisitors = (context) => ({ CallExpression(node) {
|
|
2543
|
+
const createPropertyFileVisitors = (context, kind) => ({ CallExpression(node) {
|
|
2419
2544
|
const callee = node.callee;
|
|
2420
2545
|
if (callee.type === "Identifier") {
|
|
2421
|
-
if (callee.name === "it" || callee.name === "test") reportPlain(context, node, "plainIt", `${callee.name}(...)
|
|
2546
|
+
if (callee.name === "it" || callee.name === "test") reportPlain(context, node, "plainIt", `${callee.name}(...)`, kind);
|
|
2422
2547
|
return;
|
|
2423
2548
|
}
|
|
2424
2549
|
if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
|
|
2425
2550
|
const object = callee.object;
|
|
2426
2551
|
if (object.type === "Identifier" && object.name === "fc" && RAW_FC_METHODS.has(callee.property.name)) {
|
|
2427
|
-
context.
|
|
2428
|
-
node,
|
|
2429
|
-
messageId: "rawFastCheck",
|
|
2430
|
-
data: {
|
|
2431
|
-
name: `raw fc.${callee.property.name}(...) in a ${PROPERTY_TEST_SUFFIX} file`,
|
|
2432
|
-
expected: "it.prop(...) or it.effect.prop(...) from @effect/vitest",
|
|
2433
|
-
actual: `fc.${callee.property.name}(...) bypasses the vitest/Effect integration`,
|
|
2434
|
-
fix: "rewrite as it.prop(name, [arbitraries], predicate) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
|
|
2435
|
-
}
|
|
2436
|
-
});
|
|
2552
|
+
reportRawFc(context, node, callee.property.name, kind);
|
|
2437
2553
|
return;
|
|
2438
2554
|
}
|
|
2439
2555
|
if (object.type === "Identifier" && object.name === "it") {
|
|
2440
|
-
if (callee.property.name === "effect") reportPlain(context, node, "plainEffectIt", "it.effect(...)");
|
|
2441
|
-
else if (PROP_MODIFIERS.has(callee.property.name)) reportPlain(context, node, "plainIt", `it.${callee.property.name}(...)
|
|
2556
|
+
if (callee.property.name === "effect") reportPlain(context, node, "plainEffectIt", "it.effect(...)", kind);
|
|
2557
|
+
else if (PROP_MODIFIERS.has(callee.property.name)) reportPlain(context, node, "plainIt", `it.${callee.property.name}(...)`, kind);
|
|
2442
2558
|
return;
|
|
2443
2559
|
}
|
|
2444
|
-
if (PROP_MODIFIERS.has(callee.property.name) && object.type === "MemberExpression" && object.object.type === "Identifier" && object.object.name === "it" && object.property.type === "Identifier" && object.property.name === "effect") reportPlain(context, node, "plainEffectIt", `it.effect.${callee.property.name}(...)
|
|
2560
|
+
if (PROP_MODIFIERS.has(callee.property.name) && object.type === "MemberExpression" && object.object.type === "Identifier" && object.object.name === "it" && object.property.type === "Identifier" && object.property.name === "effect") reportPlain(context, node, "plainEffectIt", `it.effect.${callee.property.name}(...)`, kind);
|
|
2445
2561
|
} });
|
|
2446
2562
|
const FAST_CHECK_IMPORT_DATA = {
|
|
2447
2563
|
name: "FastCheck import in a scenario test file",
|
|
@@ -2478,11 +2594,12 @@ const createScenarioFileVisitors = (context) => ({
|
|
|
2478
2594
|
});
|
|
2479
2595
|
}
|
|
2480
2596
|
});
|
|
2597
|
+
const kindOf = (filename) => filename.endsWith(".differential.test.ts") ? DIFFERENTIAL_FILE_KIND : PROPERTY_FILE_KIND;
|
|
2481
2598
|
const propertyFilePurity = defineRule({
|
|
2482
2599
|
meta: meta$5,
|
|
2483
2600
|
create(context) {
|
|
2484
|
-
if (!
|
|
2485
|
-
if (context.filename.endsWith(".property.test.ts")) return createPropertyFileVisitors(context);
|
|
2601
|
+
if (!isTestFile(basenameOf(context.filename))) return {};
|
|
2602
|
+
if (context.filename.endsWith(".property.test.ts") || context.filename.endsWith(".differential.test.ts")) return createPropertyFileVisitors(context, kindOf(context.filename));
|
|
2486
2603
|
return createScenarioFileVisitors(context);
|
|
2487
2604
|
}
|
|
2488
2605
|
});
|
|
@@ -2583,26 +2700,25 @@ const testFileOutsideTestsDir = defineRule({
|
|
|
2583
2700
|
});
|
|
2584
2701
|
//#endregion
|
|
2585
2702
|
//#region src/rules/test-suffix-outside-src.config.ts
|
|
2586
|
-
const UNSANCTIONED_SUFFIX_EXPECTED = "exactly *.integration.test.ts outside src/";
|
|
2703
|
+
const UNSANCTIONED_SUFFIX_EXPECTED = "exactly *.integration.test.ts or *.differential.test.ts outside src/";
|
|
2587
2704
|
const UNSANCTIONED_SUFFIX_ACTUAL = "an unsanctioned test suffix outside src/";
|
|
2588
|
-
const UNSANCTIONED_SUFFIX_FIX = "name what this file exercises. Every scenario restates a literal from a pure cell (a lookup-table entry, a constant, a mapping) -> it is a change detector, not a test: delete it. It drives the package through its public surface -> rename it *.integration.test.ts, the one behaviour suffix, whether or not a layer doubles at a port. It is a property over a pure cell -> it does not belong outside src/: convert it to an in-source if (import.meta.vitest) block in the module it covers";
|
|
2705
|
+
const UNSANCTIONED_SUFFIX_FIX = "name what this file exercises. Every scenario restates a literal from a pure cell (a lookup-table entry, a constant, a mapping) -> it is a change detector, not a test: delete it. It drives the package through its public surface -> rename it *.integration.test.ts, the one behaviour suffix, whether or not a layer doubles at a port. It proves parity or a metamorphic relation between implementations -> rename it *.differential.test.ts and express it through @systemfsoftware/differential-spec. It is a property over a pure cell -> it does not belong outside src/: convert it to an in-source if (import.meta.vitest) block in the module it covers";
|
|
2589
2706
|
//#endregion
|
|
2590
2707
|
//#region src/rules/test-suffix-outside-src.ts
|
|
2591
2708
|
const testSuffixOutsideSrc = defineRule({
|
|
2592
2709
|
meta: {
|
|
2593
2710
|
type: "problem",
|
|
2594
|
-
docs: { description: "Outside src/, a test file must end .integration.test.ts
|
|
2711
|
+
docs: { description: "Outside src/, a test file must end .integration.test.ts or .differential.test.ts. Integration is the behaviour suffix; differential is the parity/metamorphic suffix driven by @systemfsoftware/differential-spec. Whether the layer doubles at a port is a judgement the suffix no longer encodes." },
|
|
2595
2712
|
schema: [],
|
|
2596
2713
|
messages: { unsanctionedSuffix: MESSAGE$7 }
|
|
2597
2714
|
},
|
|
2598
2715
|
create(context) {
|
|
2599
2716
|
const filename = context.filename;
|
|
2717
|
+
if (isUnderSrc(filename)) return {};
|
|
2600
2718
|
const basename = basenameOf(filename);
|
|
2601
|
-
|
|
2719
|
+
if (!isTestFile(basename)) return {};
|
|
2720
|
+
if (basename.endsWith(".integration.test.ts") || basename.endsWith(".differential.test.ts")) return {};
|
|
2602
2721
|
return { Program(node) {
|
|
2603
|
-
if (isUnderSrc(filename)) return;
|
|
2604
|
-
if (!isTestFile(basename)) return;
|
|
2605
|
-
if (suffixIsAllowed) return;
|
|
2606
2722
|
context.report({
|
|
2607
2723
|
node,
|
|
2608
2724
|
messageId: "unsanctionedSuffix",
|
|
@@ -2741,7 +2857,8 @@ const recommendedRules = {
|
|
|
2741
2857
|
[rule("behaviour-one-feature-per-file")]: "error",
|
|
2742
2858
|
[rule("tests-dir-helpers-in-fixtures")]: "error",
|
|
2743
2859
|
[rule("no-io-module-in-source-test")]: "error",
|
|
2744
|
-
[rule("tests-import-public-api")]: "error"
|
|
2860
|
+
[rule("tests-import-public-api")]: "error",
|
|
2861
|
+
[rule("differential-test-requires-harness")]: "error"
|
|
2745
2862
|
};
|
|
2746
2863
|
var src_default = {
|
|
2747
2864
|
meta: { name: PLUGIN_NAME },
|
|
@@ -2768,7 +2885,8 @@ var src_default = {
|
|
|
2768
2885
|
"no-pseudo-gherkin-unit-tests": noPseudoGherkinUnitTests,
|
|
2769
2886
|
"tests-dir-helpers-in-fixtures": testsDirHelpersInFixtures,
|
|
2770
2887
|
"no-io-module-in-source-test": noIoModuleInSourceTest,
|
|
2771
|
-
"tests-import-public-api": testsImportPublicApi
|
|
2888
|
+
"tests-import-public-api": testsImportPublicApi,
|
|
2889
|
+
"differential-test-requires-harness": differentialTestRequiresHarness
|
|
2772
2890
|
},
|
|
2773
2891
|
configs: { recommended: { rules: recommendedRules } }
|
|
2774
2892
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/oxlint-plugin-test-discipline",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.6.0",
|
|
5
5
|
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"tsdown": "^0.23.0",
|
|
60
60
|
"typescript": "^7",
|
|
61
61
|
"vitest": "^5",
|
|
62
|
-
"@systemfsoftware/
|
|
62
|
+
"@systemfsoftware/tsconfig": "^1.3.6",
|
|
63
63
|
"@systemfsoftware/tsdown-config": "^0.1.0",
|
|
64
64
|
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
65
|
-
"@systemfsoftware/
|
|
65
|
+
"@systemfsoftware/stryker-config": "^0.1.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"effect": "4.0.0-rc.116",
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Rule } from '@oxlint/plugins';
|
|
2
|
-
|
|
3
|
-
declare const _default: {
|
|
4
|
-
meta: {
|
|
5
|
-
name: string;
|
|
6
|
-
};
|
|
7
|
-
rules: {
|
|
8
|
-
'in-source-test-prop-only': Rule;
|
|
9
|
-
'in-source-test-targets-private': Rule;
|
|
10
|
-
'no-test-file-in-src': Rule;
|
|
11
|
-
'src-property-test-cell': Rule;
|
|
12
|
-
'test-file-outside-tests-dir': Rule;
|
|
13
|
-
'test-suffix-outside-src': Rule;
|
|
14
|
-
'behaviour-test-requires-gherkin': Rule;
|
|
15
|
-
'behaviour-exercises-use-case': Rule;
|
|
16
|
-
'behaviour-one-feature-per-file': Rule;
|
|
17
|
-
'no-pseudo-gherkin-unit-tests': Rule;
|
|
18
|
-
'tests-dir-helpers-in-fixtures': Rule;
|
|
19
|
-
'no-io-module-in-source-test': Rule;
|
|
20
|
-
'tests-import-public-api': Rule;
|
|
21
|
-
};
|
|
22
|
-
configs: {
|
|
23
|
-
recommended: {
|
|
24
|
-
rules: {
|
|
25
|
-
readonly [x: string]: "error";
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
export default _default;
|
|
31
|
-
|
|
32
|
-
export { }
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Rule } from '@oxlint/plugins';
|
|
2
|
-
|
|
3
|
-
declare const _default: {
|
|
4
|
-
meta: {
|
|
5
|
-
name: string;
|
|
6
|
-
};
|
|
7
|
-
rules: {
|
|
8
|
-
'in-source-test-prop-only': Rule;
|
|
9
|
-
'in-source-test-targets-private': Rule;
|
|
10
|
-
'no-test-file-in-src': Rule;
|
|
11
|
-
'src-property-test-cell': Rule;
|
|
12
|
-
'test-file-outside-tests-dir': Rule;
|
|
13
|
-
'test-suffix-outside-src': Rule;
|
|
14
|
-
'behaviour-test-requires-gherkin': Rule;
|
|
15
|
-
'behaviour-exercises-use-case': Rule;
|
|
16
|
-
'behaviour-one-feature-per-file': Rule;
|
|
17
|
-
'no-pseudo-gherkin-unit-tests': Rule;
|
|
18
|
-
'tests-dir-helpers-in-fixtures': Rule;
|
|
19
|
-
'no-io-module-in-source-test': Rule;
|
|
20
|
-
'tests-import-public-api': Rule;
|
|
21
|
-
};
|
|
22
|
-
configs: {
|
|
23
|
-
recommended: {
|
|
24
|
-
rules: {
|
|
25
|
-
readonly [x: string]: "error";
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
export default _default;
|
|
31
|
-
|
|
32
|
-
export { }
|