@systemfsoftware/oxlint-plugin-test-discipline 4.1.0 → 6.1.3
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 +2 -52
- package/README.md +84 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +616 -435
- package/dist/no-inline-suppression.d.ts +1 -0
- package/dist/no-inline-suppression.mjs +144 -0
- package/dist/oxlint-plugin-test-discipline-untrimmed.d.ts +1 -0
- package/dist/oxlint-plugin-test-discipline.d.ts +1 -0
- package/package.json +20 -24
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$29 = {
|
|
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: [],
|
|
@@ -23,7 +23,7 @@ const memberPropertyName$1 = (callee) => {
|
|
|
23
23
|
if (callee.property.type !== "Identifier") return null;
|
|
24
24
|
return callee.property.name;
|
|
25
25
|
};
|
|
26
|
-
const calleeName$
|
|
26
|
+
const calleeName$3 = (callee) => {
|
|
27
27
|
if (callee.type === "Identifier") return callee.name;
|
|
28
28
|
if (callee.type === "MemberExpression") return memberPropertyName$1(callee);
|
|
29
29
|
return null;
|
|
@@ -39,7 +39,7 @@ const argName = (arg) => {
|
|
|
39
39
|
return inlineName(arg);
|
|
40
40
|
};
|
|
41
41
|
const inspect = (node) => {
|
|
42
|
-
const callee = calleeName$
|
|
42
|
+
const callee = calleeName$3(node.callee);
|
|
43
43
|
if (callee === null || !isEmitCallee$1(callee)) return null;
|
|
44
44
|
const name = argName(node.arguments[0]);
|
|
45
45
|
if (name === null) return null;
|
|
@@ -49,7 +49,7 @@ const inspect = (node) => {
|
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
51
|
const banRawSpanNameEmit = defineRule({
|
|
52
|
-
meta: meta$
|
|
52
|
+
meta: meta$29,
|
|
53
53
|
create(context) {
|
|
54
54
|
return { CallExpression(node) {
|
|
55
55
|
const emitCall = inspect(node);
|
|
@@ -74,6 +74,11 @@ const TEST_BASENAME = /\.(?:test|spec)\.[cm]?tsx?$/;
|
|
|
74
74
|
const TYPE_TEST_BASENAME = /\.tst\.[cm]?tsx?$/;
|
|
75
75
|
const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
|
|
76
76
|
/**
|
|
77
|
+
* The basename of a test the `effect-e2e` runner collects. Mirrors the runner's
|
|
78
|
+
* default pattern `tests/**\/*.e2e.ts`, which is `.ts` only.
|
|
79
|
+
*/
|
|
80
|
+
const E2E_TEST_BASENAME = /\.e2e\.ts$/;
|
|
81
|
+
/**
|
|
77
82
|
* The only test location sanctioned under `src/`. A workflow property test
|
|
78
83
|
* earns colocation with the workflow it covers, but not adjacency: it lives in
|
|
79
84
|
* a `__tests__` directory beside that workflow, never as a sibling file.
|
|
@@ -81,8 +86,6 @@ const SANCTIONED_TEST_DIRS = /* @__PURE__ */ new Set(["tests"]);
|
|
|
81
86
|
const NESTED_TEST_DIR = "__tests__";
|
|
82
87
|
const TEST_TREE_DIRS = /* @__PURE__ */ new Set([...SANCTIONED_TEST_DIRS, NESTED_TEST_DIR]);
|
|
83
88
|
const PROPERTY_SUFFIX = ".property.test.ts";
|
|
84
|
-
const INTEGRATION_SUFFIX = ".integration.test.ts";
|
|
85
|
-
const DIFFERENTIAL_SUFFIX = ".differential.test.ts";
|
|
86
89
|
/**
|
|
87
90
|
* Forbidden outright. A schema's laws are generated, so a hand-written
|
|
88
91
|
* `*.schema.test.ts` only ever restates coverage that already exists.
|
|
@@ -110,30 +113,31 @@ const FOREIGN_RUNNERS = {
|
|
|
110
113
|
...FOREIGN_VITEST_SOURCES,
|
|
111
114
|
[LAWFUL_RUNNER]: true
|
|
112
115
|
};
|
|
113
|
-
/**
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
/** fast-check calls that run a property themselves instead of handing an arbitrary to a harness. */
|
|
117
|
+
const RAW_FC_METHODS = {
|
|
118
|
+
assert: true,
|
|
119
|
+
check: true,
|
|
120
|
+
property: true,
|
|
121
|
+
asyncProperty: true
|
|
118
122
|
};
|
|
119
123
|
const RUNNER_NAMES = /* @__PURE__ */ new Set([
|
|
120
124
|
"it",
|
|
121
125
|
"test",
|
|
122
126
|
"describe"
|
|
123
127
|
]);
|
|
124
|
-
const MESSAGE$
|
|
128
|
+
const MESSAGE$6 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
125
129
|
const ABSENCE_MESSAGE = "{{name}} is untested. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
126
130
|
//#endregion
|
|
127
131
|
//#region src/rules/behaviour-exercises-use-case.config.ts
|
|
128
|
-
const NO_SUBJECT_IMPORT_NAME = "a
|
|
132
|
+
const NO_SUBJECT_IMPORT_NAME = "a behaviour test that reaches no package code";
|
|
129
133
|
const NO_SUBJECT_IMPORT_EXPECTED = "an import of the package code under test";
|
|
130
|
-
const NO_SUBJECT_IMPORT_ACTUAL = "a
|
|
134
|
+
const NO_SUBJECT_IMPORT_ACTUAL = "a test importing @systemfsoftware/effect-gherkin-spec whose every runtime import is vitest, @effect/vitest, @systemfsoftware/vitest, the gherkin spec package, effect, a Node builtin, or the file itself";
|
|
131
135
|
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.";
|
|
132
|
-
const meta$
|
|
136
|
+
const meta$28 = {
|
|
133
137
|
type: "problem",
|
|
134
|
-
docs: { description: "A
|
|
138
|
+
docs: { description: "A behaviour test - any test file that imports @systemfsoftware/effect-gherkin-spec, whatever its name - 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." },
|
|
135
139
|
schema: [],
|
|
136
|
-
messages: { noSubjectImport: MESSAGE$
|
|
140
|
+
messages: { noSubjectImport: MESSAGE$6 }
|
|
137
141
|
};
|
|
138
142
|
//#endregion
|
|
139
143
|
//#region src/rules/path.ts
|
|
@@ -143,7 +147,6 @@ const basenameOf = (filename) => Array$1.lastNonEmpty(segmentsOf(filename));
|
|
|
143
147
|
/** Directory segments only — the basename never counts as a directory. */
|
|
144
148
|
const directoriesOf = (filename) => Array$1.initNonEmpty(segmentsOf(filename));
|
|
145
149
|
const isUnderSrc = (filename) => directoriesOf(filename).includes("src");
|
|
146
|
-
const isRawVitestPackage = (filename) => directoriesOf(filename).some((segment) => RAW_VITEST_PACKAGES[segment] === true);
|
|
147
150
|
const isInSanctionedTestDir = (filename) => directoriesOf(filename).some((segment) => SANCTIONED_TEST_DIRS.has(segment));
|
|
148
151
|
const isInTestsImportScope = (filename) => {
|
|
149
152
|
const segments = segmentsOf(filename);
|
|
@@ -153,10 +156,146 @@ const isInTestsImportScope = (filename) => {
|
|
|
153
156
|
};
|
|
154
157
|
const isInConfiguredTestDir = (filename, dirs) => directoriesOf(filename).some((segment) => dirs.includes(segment));
|
|
155
158
|
const isTestFile = (basename) => TEST_BASENAME.test(basename);
|
|
156
|
-
|
|
159
|
+
/**
|
|
160
|
+
* A file the `effect-e2e` runner collects: an `.e2e.ts` basename under a `tests`
|
|
161
|
+
* directory with no `__fixtures__` directory directly beneath a `tests`
|
|
162
|
+
* directory, which is the runner's `tests/__fixtures__/**` exclusion.
|
|
163
|
+
*/
|
|
164
|
+
const isE2eTestFile = (filename) => {
|
|
165
|
+
const directories = directoriesOf(filename);
|
|
166
|
+
return E2E_TEST_BASENAME.test(basenameOf(filename)) && directories.some((segment) => SANCTIONED_TEST_DIRS.has(segment)) && !directories.some((segment, index) => segment === "__fixtures__" && SANCTIONED_TEST_DIRS.has(directories[index - 1] ?? ""));
|
|
167
|
+
};
|
|
157
168
|
const CELL_SUFFIX = /\.([^.]+)\.[cm]?tsx?$/;
|
|
158
169
|
const cellOf = (basename) => CELL_SUFFIX.exec(basename)?.[1];
|
|
159
170
|
//#endregion
|
|
171
|
+
//#region src/rules/prop-call.ts
|
|
172
|
+
const PROP_MODIFIERS$1 = /* @__PURE__ */ new Set([
|
|
173
|
+
"only",
|
|
174
|
+
"skip",
|
|
175
|
+
"todo"
|
|
176
|
+
]);
|
|
177
|
+
const isPropCallee$1 = (callee) => {
|
|
178
|
+
if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
|
|
179
|
+
if (callee.property.name === "prop") {
|
|
180
|
+
const object = callee.object;
|
|
181
|
+
if (object.type === "Identifier") return object.name === "it";
|
|
182
|
+
return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
|
|
183
|
+
}
|
|
184
|
+
return PROP_MODIFIERS$1.has(callee.property.name) && isPropCallee$1(callee.object);
|
|
185
|
+
};
|
|
186
|
+
const getPredicate = (node) => Array$1.findLast(node.arguments, (arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/rules/lane.ts
|
|
189
|
+
const HARNESS_LANES = [
|
|
190
|
+
[GHERKIN_PACKAGE, "behaviour"],
|
|
191
|
+
[CONFORMANCE_PACKAGE, "conformance"],
|
|
192
|
+
[DIFFERENTIAL_PACKAGE, "differential"],
|
|
193
|
+
[TRACE_SPEC_PACKAGE, "trace"]
|
|
194
|
+
];
|
|
195
|
+
const isPackageOrSubpath = (source, pkg) => source === pkg || source.startsWith(`${pkg}/`);
|
|
196
|
+
/** The lane a harness import selects: the package itself or any subpath of it. */
|
|
197
|
+
const harnessLaneOf = (source) => HARNESS_LANES.find(([pkg]) => isPackageOrSubpath(source, pkg))?.[1];
|
|
198
|
+
const isTypeOnly = (node) => node.importKind === "type" || node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
|
|
199
|
+
/**
|
|
200
|
+
* Whether an import declaration registers end-to-end tests: it takes the
|
|
201
|
+
* runner entry's `test` (an alias counts) or its namespace as a value. The
|
|
202
|
+
* rest of the entry (`expect`, `describe`, hooks, the composition API, types)
|
|
203
|
+
* registers no test, so importing it selects no lane.
|
|
204
|
+
*/
|
|
205
|
+
const importsE2eTest = (node) => node.source.value === "@systemfsoftware/effect-e2e/runner" && node.specifiers.some((specifier) => specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && specifier.imported.name === "test");
|
|
206
|
+
/**
|
|
207
|
+
* Where a value import brings FastCheck in: the whole declaration for the
|
|
208
|
+
* `fast-check` module (or a subpath), else each `FastCheck` named value import.
|
|
209
|
+
*/
|
|
210
|
+
const fastCheckImportSites = (node) => isTypeOnly(node) ? [] : isPackageOrSubpath(node.source.value, "fast-check") ? [node] : node.specifiers.filter((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && specifier.imported.name === "FastCheck");
|
|
211
|
+
const isNode$5 = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
212
|
+
const childrenOf = (node, keys) => (keys[node.type] ?? []).flatMap((key) => {
|
|
213
|
+
const value = Reflect.get(node, key);
|
|
214
|
+
if (Array.isArray(value)) return value.filter(isNode$5);
|
|
215
|
+
return isNode$5(value) ? [value] : [];
|
|
216
|
+
});
|
|
217
|
+
const callsProperty = (program, keys) => {
|
|
218
|
+
const pending = [program];
|
|
219
|
+
for (let node = pending.pop(); node !== void 0; node = pending.pop()) {
|
|
220
|
+
if (node.type === "CallExpression" && isPropCallee$1(node.callee)) return true;
|
|
221
|
+
pending.push(...childrenOf(node, keys));
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
};
|
|
225
|
+
const derive = (program, keys) => {
|
|
226
|
+
const lanes = /* @__PURE__ */ new Set();
|
|
227
|
+
for (const statement of program.body) {
|
|
228
|
+
if (statement.type !== "ImportDeclaration" || isTypeOnly(statement)) continue;
|
|
229
|
+
const lane = harnessLaneOf(statement.source.value);
|
|
230
|
+
if (lane !== void 0) lanes.add(lane);
|
|
231
|
+
if (importsE2eTest(statement)) lanes.add("e2e");
|
|
232
|
+
if (fastCheckImportSites(statement).length > 0) lanes.add("property");
|
|
233
|
+
}
|
|
234
|
+
if (!lanes.has("property") && callsProperty(program, keys)) lanes.add("property");
|
|
235
|
+
return lanes;
|
|
236
|
+
};
|
|
237
|
+
const NO_LANES = /* @__PURE__ */ new Set();
|
|
238
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
239
|
+
/**
|
|
240
|
+
* The lanes the linted test file is in, derived once per program and shared by
|
|
241
|
+
* every rule. A file is in a lane when Vitest runs it as a test (the `.test.ts` /
|
|
242
|
+
* `.spec.ts` ending) or the `effect-e2e` runner collects it (`isE2eTestFile`);
|
|
243
|
+
* a fixture or helper a test imports is not a test.
|
|
244
|
+
* Call it from a visitor: the AST is complete once traversal starts.
|
|
245
|
+
*/
|
|
246
|
+
const lanesOf = (context) => {
|
|
247
|
+
if (!isTestFile(basenameOf(context.filename)) && !isE2eTestFile(context.filename)) return NO_LANES;
|
|
248
|
+
const program = context.sourceCode.ast;
|
|
249
|
+
const cached = cache.get(program);
|
|
250
|
+
if (cached !== void 0) return cached;
|
|
251
|
+
const lanes = derive(program, context.sourceCode.visitorKeys);
|
|
252
|
+
cache.set(program, lanes);
|
|
253
|
+
return lanes;
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* The property lane holds a file only when no differential harness does: the
|
|
257
|
+
* differential requirements already include the property ones, and a
|
|
258
|
+
* differential file imports FastCheck for its arbitraries.
|
|
259
|
+
*/
|
|
260
|
+
const isPropertyFile = (lanes) => lanes.has("property") && !lanes.has("differential");
|
|
261
|
+
/**
|
|
262
|
+
* A property test beside Gherkin, conformance or trace scenarios: the property
|
|
263
|
+
* belongs in its own workflow property file, never mixed into a harness test.
|
|
264
|
+
*/
|
|
265
|
+
const mixesPropertyIntoAHarness = (lanes) => isPropertyFile(lanes) && (lanes.has("behaviour") || lanes.has("conformance") || lanes.has("trace"));
|
|
266
|
+
/**
|
|
267
|
+
* What a package is, declared once in its own lint config's `settings`:
|
|
268
|
+
* `vitest-runner` for the packages that are the test framework itself, `e2e`
|
|
269
|
+
* for the packages that hold end-to-end suites. Every rule reads the same
|
|
270
|
+
* declaration, so two rules can never disagree about it, and no rule takes a
|
|
271
|
+
* role option (oxlint refuses one as a configuration error). The rule cannot
|
|
272
|
+
* see how a package is wired, so it takes the declaration at its word; a
|
|
273
|
+
* package that declares a role without being what it names gets that role's
|
|
274
|
+
* treatment all the same, and the declaration is reviewed where it is made, in
|
|
275
|
+
* the config diff.
|
|
276
|
+
*/
|
|
277
|
+
const SETTINGS_KEY = "@systemfsoftware/oxlint-plugin-test-discipline";
|
|
278
|
+
const Role = Schema.Literals(["vitest-runner", "e2e"]);
|
|
279
|
+
const PluginSettings = Schema.Struct({ [SETTINGS_KEY]: Schema.optional(Schema.Struct({ role: Schema.optional(Role) })) });
|
|
280
|
+
const declaresRole = (context, role) => Option.exists(Schema.decodeUnknownOption(PluginSettings)(context.settings), (settings) => settings[SETTINGS_KEY]?.role === role);
|
|
281
|
+
/**
|
|
282
|
+
* Whether the linted file's package declares itself the Vitest runner. Settings
|
|
283
|
+
* that do not decode declare no role, so every raw Vitest import is then refused.
|
|
284
|
+
*/
|
|
285
|
+
const isRunnerPackage = (context) => declaresRole(context, "vitest-runner");
|
|
286
|
+
/**
|
|
287
|
+
* Whether the linted file's package declares itself an end-to-end package.
|
|
288
|
+
* Settings that do not decode declare no role.
|
|
289
|
+
*/
|
|
290
|
+
const isE2ePackage = (context) => declaresRole(context, "e2e");
|
|
291
|
+
const importsARunner = (program) => program.body.some((statement) => statement.type === "ImportDeclaration" && !isTypeOnly(statement) && Object.keys(FOREIGN_RUNNERS).some((runner) => isPackageOrSubpath(statement.source.value, runner)));
|
|
292
|
+
/**
|
|
293
|
+
* The runner lane: a test in a package that declares the `vitest-runner` role
|
|
294
|
+
* and imports a Vitest runner module (`vitest`, `@effect/vitest`, the fork, or a
|
|
295
|
+
* subpath of one). Outside such a package the same imports select no lane.
|
|
296
|
+
*/
|
|
297
|
+
const isInRunnerLane = (context) => isTestFile(basenameOf(context.filename)) && isRunnerPackage(context) && importsARunner(context.sourceCode.ast);
|
|
298
|
+
//#endregion
|
|
160
299
|
//#region src/rules/behaviour-exercises-use-case.ts
|
|
161
300
|
const EFFECT_PACKAGE = "effect";
|
|
162
301
|
const FOUNDATION_PACKAGES = /* @__PURE__ */ new Set([
|
|
@@ -184,7 +323,7 @@ const isFoundationImport = (source, filename) => {
|
|
|
184
323
|
return FOUNDATION_PACKAGES.has(source) || source.startsWith(`${EFFECT_PACKAGE}/`) || source.startsWith("node:");
|
|
185
324
|
};
|
|
186
325
|
/** The stem of a path with its final extension stripped, for identity comparisons. */
|
|
187
|
-
const stemOf = (file) => file.replace(/\.[
|
|
326
|
+
const stemOf = (file) => file.replace(/\.[^./]+$/, "");
|
|
188
327
|
/**
|
|
189
328
|
* A relative module specifier resolved against the linted file's directory,
|
|
190
329
|
* with `..` segments collapsed. Existence is never checked: one file cannot
|
|
@@ -244,7 +383,7 @@ const DIST_SEGMENT = /(?:^|\/)dist\//;
|
|
|
244
383
|
* rule the same way a real one does.
|
|
245
384
|
*/
|
|
246
385
|
const behaviourExercisesUseCase = defineRule({
|
|
247
|
-
meta: meta$
|
|
386
|
+
meta: meta$28,
|
|
248
387
|
create(context) {
|
|
249
388
|
let reached = false;
|
|
250
389
|
return {
|
|
@@ -259,7 +398,7 @@ const behaviourExercisesUseCase = defineRule({
|
|
|
259
398
|
if (typeof node.value === "string" && DIST_SEGMENT.test(node.value)) reached = true;
|
|
260
399
|
},
|
|
261
400
|
"Program:exit"(node) {
|
|
262
|
-
if (!
|
|
401
|
+
if (!lanesOf(context).has("behaviour")) return;
|
|
263
402
|
if (reached) return;
|
|
264
403
|
for (const statement of node.body) {
|
|
265
404
|
if (statement.type !== "ImportDeclaration") continue;
|
|
@@ -286,21 +425,21 @@ const behaviourExercisesUseCase = defineRule({
|
|
|
286
425
|
});
|
|
287
426
|
//#endregion
|
|
288
427
|
//#region src/rules/behaviour-one-feature-per-file.config.ts
|
|
289
|
-
const TOO_FEW_FEATURES_NAME = "a
|
|
428
|
+
const TOO_FEW_FEATURES_NAME = "a behaviour test that constructs no Feature";
|
|
290
429
|
const TOO_FEW_FEATURES_EXPECTED = "exactly one Feature(...) — the capability the file proves";
|
|
291
|
-
const TOO_FEW_FEATURES_ACTUAL = "a
|
|
430
|
+
const TOO_FEW_FEATURES_ACTUAL = "a test importing @systemfsoftware/effect-gherkin-spec with zero Feature(...) calls";
|
|
292
431
|
const TOO_FEW_FEATURES_FIX = "one file, one capability. A Feature is what the file is for; if there is none, the file is not a behaviour test — delete it or add the missing Feature call.";
|
|
293
|
-
const TOO_MANY_FEATURES_NAME = "a
|
|
432
|
+
const TOO_MANY_FEATURES_NAME = "a behaviour test accumulating multiple Feature(...) calls";
|
|
294
433
|
const TOO_MANY_FEATURES_EXPECTED = "exactly one Feature(...) — every additional one signals a junk drawer";
|
|
295
|
-
const TOO_MANY_FEATURES_ACTUAL = "a
|
|
434
|
+
const TOO_MANY_FEATURES_ACTUAL = "a test importing @systemfsoftware/effect-gherkin-spec with two or more Feature(...) calls";
|
|
296
435
|
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.";
|
|
297
|
-
const meta$
|
|
436
|
+
const meta$27 = {
|
|
298
437
|
type: "problem",
|
|
299
|
-
docs: { description: "A
|
|
438
|
+
docs: { description: "A behaviour test - any test file that imports @systemfsoftware/effect-gherkin-spec, whatever its name - 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." },
|
|
300
439
|
schema: [],
|
|
301
440
|
messages: {
|
|
302
|
-
tooFewFeatures: MESSAGE$
|
|
303
|
-
tooManyFeatures: MESSAGE$
|
|
441
|
+
tooFewFeatures: MESSAGE$6,
|
|
442
|
+
tooManyFeatures: MESSAGE$6
|
|
304
443
|
}
|
|
305
444
|
};
|
|
306
445
|
//#endregion
|
|
@@ -338,10 +477,10 @@ const hasAnyFeatureCall = (program) => {
|
|
|
338
477
|
return false;
|
|
339
478
|
};
|
|
340
479
|
const behaviourOneFeaturePerFile = defineRule({
|
|
341
|
-
meta: meta$
|
|
480
|
+
meta: meta$27,
|
|
342
481
|
create(context) {
|
|
343
482
|
return { "Program:exit"(node) {
|
|
344
|
-
if (!
|
|
483
|
+
if (!lanesOf(context).has("behaviour")) return;
|
|
345
484
|
const excess = findSecondFeatureCall(node);
|
|
346
485
|
if (excess !== null) {
|
|
347
486
|
const total = countFeatureCalls(node);
|
|
@@ -372,71 +511,26 @@ const behaviourOneFeaturePerFile = defineRule({
|
|
|
372
511
|
});
|
|
373
512
|
//#endregion
|
|
374
513
|
//#region src/rules/behaviour-test-requires-gherkin.config.ts
|
|
375
|
-
const
|
|
376
|
-
const FOREIGN_RUNNER_ACTUAL = "a test runner imported directly from vitest, @effect/vitest or @systemfsoftware/vitest in a behaviour file";
|
|
377
|
-
const FOREIGN_RUNNER_FIX = "import { it } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it })";
|
|
378
|
-
const MISSING_MAKE_FEATURE_NAME = "a *.integration.test.ts without makeFeature";
|
|
514
|
+
const MISSING_MAKE_FEATURE_NAME = "a behaviour test without makeFeature";
|
|
379
515
|
const MISSING_MAKE_FEATURE_EXPECTED = "makeFeature imported from @systemfsoftware/effect-gherkin-spec";
|
|
380
|
-
const MISSING_MAKE_FEATURE_ACTUAL = "a
|
|
516
|
+
const MISSING_MAKE_FEATURE_ACTUAL = "a test that imports @systemfsoftware/effect-gherkin-spec but never constructs a Gherkin feature";
|
|
381
517
|
const MISSING_MAKE_FEATURE_FIX = "import { makeFeature } from @systemfsoftware/effect-gherkin-spec and declare `const Feature = makeFeature({ it, layer })`";
|
|
382
|
-
const meta$
|
|
518
|
+
const meta$26 = {
|
|
383
519
|
type: "problem",
|
|
384
|
-
docs: { description: "A
|
|
520
|
+
docs: { description: "A test that imports @systemfsoftware/effect-gherkin-spec (the behaviour lane) drives its suite through makeFeature from that package." },
|
|
385
521
|
schema: [],
|
|
386
|
-
messages: {
|
|
387
|
-
foreignRunner: MESSAGE$8,
|
|
388
|
-
missingMakeFeature: MESSAGE$8
|
|
389
|
-
}
|
|
522
|
+
messages: { missingMakeFeature: MESSAGE$6 }
|
|
390
523
|
};
|
|
391
524
|
//#endregion
|
|
392
525
|
//#region src/rules/behaviour-test-requires-gherkin.ts
|
|
393
|
-
const
|
|
394
|
-
const isMakeFeatureSpecifier = (specifier) => {
|
|
395
|
-
const imported = specifier.imported;
|
|
396
|
-
return imported.type === "Identifier" && imported.name === "makeFeature";
|
|
397
|
-
};
|
|
398
|
-
/**
|
|
399
|
-
* `null` for any specifier that cannot name a runner. The decode is unreachable
|
|
400
|
-
* for a string-literal import name because the narrowing above rejects it first;
|
|
401
|
-
* it exists so removing that narrowing fails loudly instead of silently.
|
|
402
|
-
*/
|
|
403
|
-
const foreignRunnerNameOf = (specifier) => {
|
|
404
|
-
if (specifier.imported.type !== "Identifier") return null;
|
|
405
|
-
const { name } = Schema.decodeSync(ImportedIdentifier)(specifier.imported);
|
|
406
|
-
return RUNNER_NAMES.has(name) ? name : null;
|
|
407
|
-
};
|
|
526
|
+
const importsMakeFeature = (statement) => harnessLaneOf(statement.source.value) === "behaviour" && statement.specifiers.some((specifier) => specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "makeFeature");
|
|
408
527
|
const behaviourTestRequiresGherkin = defineRule({
|
|
409
|
-
meta: meta$
|
|
528
|
+
meta: meta$26,
|
|
410
529
|
create(context) {
|
|
411
|
-
const basename = basenameOf(context.filename);
|
|
412
530
|
return { Program(node) {
|
|
413
|
-
if (!
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (statement.type !== "ImportDeclaration") continue;
|
|
417
|
-
const sourceValue = statement.source.value;
|
|
418
|
-
if (sourceValue === "@systemfsoftware/effect-gherkin-spec") for (const specifier of statement.specifiers) {
|
|
419
|
-
if (specifier.type !== "ImportSpecifier") continue;
|
|
420
|
-
if (isMakeFeatureSpecifier(specifier)) hasMakeFeature = true;
|
|
421
|
-
}
|
|
422
|
-
if (FOREIGN_RUNNERS[sourceValue] !== true) continue;
|
|
423
|
-
for (const specifier of statement.specifiers) {
|
|
424
|
-
if (specifier.type !== "ImportSpecifier") continue;
|
|
425
|
-
const runnerName = foreignRunnerNameOf(specifier);
|
|
426
|
-
if (runnerName === null) continue;
|
|
427
|
-
context.report({
|
|
428
|
-
node: specifier,
|
|
429
|
-
messageId: "foreignRunner",
|
|
430
|
-
data: {
|
|
431
|
-
name: runnerName,
|
|
432
|
-
expected: FOREIGN_RUNNER_EXPECTED,
|
|
433
|
-
actual: FOREIGN_RUNNER_ACTUAL,
|
|
434
|
-
fix: FOREIGN_RUNNER_FIX
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (!hasMakeFeature) context.report({
|
|
531
|
+
if (!lanesOf(context).has("behaviour")) return;
|
|
532
|
+
if (node.body.some((statement) => statement.type === "ImportDeclaration" && importsMakeFeature(statement))) return;
|
|
533
|
+
context.report({
|
|
440
534
|
node,
|
|
441
535
|
messageId: "missingMakeFeature",
|
|
442
536
|
data: {
|
|
@@ -455,33 +549,25 @@ const HARNESS_MEMBERS = {
|
|
|
455
549
|
released: true
|
|
456
550
|
};
|
|
457
551
|
const LEGACY_CHECK_MEMBER = "check";
|
|
458
|
-
const HARNESS_PRESCRIPTION$
|
|
459
|
-
const meta$
|
|
552
|
+
const HARNESS_PRESCRIPTION$1 = "import { Conformance } from @systemfsoftware/conformance-spec and express the test as Conformance.linearizable(implementation, { commands, model, run, fibers, operations }) in the scenario body";
|
|
553
|
+
const meta$25 = {
|
|
460
554
|
type: "problem",
|
|
461
|
-
docs: { description: "Conformance tests must use the @systemfsoftware/conformance-spec Conformance barrel: Conformance.linearizable, Conformance.sequential, or Conformance.released.
|
|
555
|
+
docs: { description: "Conformance tests must use the @systemfsoftware/conformance-spec Conformance barrel: Conformance.linearizable, Conformance.sequential, or Conformance.released. A test that imports @systemfsoftware/conformance-spec is in the conformance lane. Runner imports and calls in this lane are vitest-from-systemfsoftware-vitest findings. The retired Linearizable.check / SequentialModel.check / Released.check shapes are rejected, and importing Conformance without invoking a check is equally non-compliant." },
|
|
462
556
|
schema: [],
|
|
463
557
|
messages: {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
missingHarnessImport: MESSAGE$8,
|
|
467
|
-
legacyHarnessCall: MESSAGE$8,
|
|
468
|
-
missingHarnessUsage: MESSAGE$8
|
|
558
|
+
legacyHarnessCall: MESSAGE$6,
|
|
559
|
+
missingHarnessUsage: MESSAGE$6
|
|
469
560
|
}
|
|
470
561
|
};
|
|
471
562
|
//#endregion
|
|
472
563
|
//#region src/rules/conformance-test-requires-harness.ts
|
|
473
|
-
const memberObjectName$
|
|
564
|
+
const memberObjectName$2 = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
|
|
474
565
|
const memberPropertyName = (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" ? callee.property.name : void 0;
|
|
475
566
|
const memberName = (callee) => callee.type === "Identifier" ? callee.name : memberPropertyName(callee);
|
|
476
|
-
const calleeName$
|
|
477
|
-
const isRawRunnerCall = (callee) => {
|
|
478
|
-
const name = calleeName$1(callee);
|
|
479
|
-
return name !== void 0 && RUNNER_NAMES.has(name);
|
|
480
|
-
};
|
|
481
|
-
const isForeignRunnerImport$1 = (node) => typeof node.source.value === "string" && FOREIGN_RUNNERS[node.source.value] === true;
|
|
567
|
+
const calleeName$2 = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName$2(callee);
|
|
482
568
|
const namesConformanceExport = (specifier) => specifier.imported.type === "Identifier" && specifier.imported.name === "Conformance";
|
|
483
569
|
const recordBindings = (node, packageBindings, conformanceBindings) => {
|
|
484
|
-
if (node.source.value !== "
|
|
570
|
+
if (harnessLaneOf(node.source.value) !== "conformance") return;
|
|
485
571
|
for (const specifier of node.specifiers) {
|
|
486
572
|
packageBindings.add(specifier.local.name);
|
|
487
573
|
if (specifier.type === "ImportSpecifier" && namesConformanceExport(specifier)) conformanceBindings.add(specifier.local.name);
|
|
@@ -489,65 +575,40 @@ const recordBindings = (node, packageBindings, conformanceBindings) => {
|
|
|
489
575
|
};
|
|
490
576
|
const isHarnessInvocation = (callee, conformanceBindings) => {
|
|
491
577
|
const member = memberName(callee);
|
|
492
|
-
const root = calleeName$
|
|
578
|
+
const root = calleeName$2(callee);
|
|
493
579
|
return member !== void 0 && HARNESS_MEMBERS[member] === true && root !== void 0 && conformanceBindings.has(root);
|
|
494
580
|
};
|
|
495
581
|
const isLegacyCheck = (callee, packageBindings) => {
|
|
496
|
-
const root = calleeName$
|
|
582
|
+
const root = calleeName$2(callee);
|
|
497
583
|
return memberName(callee) === "check" && root !== void 0 && packageBindings.has(root);
|
|
498
584
|
};
|
|
499
|
-
const legacyCallText = (callee) => callee.type === "Identifier" ? callee.name : `${calleeName$
|
|
585
|
+
const legacyCallText = (callee) => callee.type === "Identifier" ? callee.name : `${calleeName$2(callee)}.${LEGACY_CHECK_MEMBER}`;
|
|
500
586
|
const legacyCheckError = (callee) => ({
|
|
501
587
|
messageId: "legacyHarnessCall",
|
|
502
588
|
data: {
|
|
503
589
|
name: `a .${LEGACY_CHECK_MEMBER}(...) call in a conformance test file`,
|
|
504
|
-
expected: HARNESS_PRESCRIPTION$
|
|
590
|
+
expected: HARNESS_PRESCRIPTION$1,
|
|
505
591
|
actual: `${legacyCallText(callee)} is not a check the barrel exposes`,
|
|
506
|
-
fix: HARNESS_PRESCRIPTION$
|
|
592
|
+
fix: HARNESS_PRESCRIPTION$1
|
|
507
593
|
}
|
|
508
594
|
});
|
|
509
595
|
const conformanceTestRequiresHarness = defineRule({
|
|
510
|
-
meta: meta$
|
|
596
|
+
meta: meta$25,
|
|
511
597
|
create(context) {
|
|
512
|
-
if (!context.filename.endsWith(".conformance.test.ts")) return {};
|
|
513
598
|
const packageBindings = /* @__PURE__ */ new Set();
|
|
514
599
|
const conformanceBindings = /* @__PURE__ */ new Set();
|
|
600
|
+
let conformance = false;
|
|
515
601
|
let violations = 0;
|
|
516
602
|
let hasHarnessInvocation = false;
|
|
517
603
|
return {
|
|
604
|
+
Program() {
|
|
605
|
+
conformance = lanesOf(context).has("conformance");
|
|
606
|
+
},
|
|
518
607
|
ImportDeclaration(node) {
|
|
519
|
-
if (
|
|
520
|
-
context.report({
|
|
521
|
-
node,
|
|
522
|
-
messageId: "runnerImport",
|
|
523
|
-
data: {
|
|
524
|
-
name: `runner import from ${String(node.source.value)} in a conformance test file`,
|
|
525
|
-
expected: HARNESS_PRESCRIPTION$2,
|
|
526
|
-
actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the conformance check",
|
|
527
|
-
fix: `delete the runner import; ${HARNESS_PRESCRIPTION$2}`
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
violations += 1;
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
recordBindings(node, packageBindings, conformanceBindings);
|
|
608
|
+
if (conformance) recordBindings(node, packageBindings, conformanceBindings);
|
|
534
609
|
},
|
|
535
610
|
CallExpression(node) {
|
|
536
|
-
if (
|
|
537
|
-
const name = calleeName$1(node.callee) ?? "";
|
|
538
|
-
context.report({
|
|
539
|
-
node,
|
|
540
|
-
messageId: "rawRunnerCall",
|
|
541
|
-
data: {
|
|
542
|
-
name: `raw runner call (${name}) in a conformance test file`,
|
|
543
|
-
expected: HARNESS_PRESCRIPTION$2,
|
|
544
|
-
actual: `${name}(...) bypasses the conformance check`,
|
|
545
|
-
fix: `rewrite using ${HARNESS_PRESCRIPTION$2}`
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
violations += 1;
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
611
|
+
if (!conformance) return;
|
|
551
612
|
if (isHarnessInvocation(node.callee, conformanceBindings)) {
|
|
552
613
|
hasHarnessInvocation = true;
|
|
553
614
|
return;
|
|
@@ -561,27 +622,14 @@ const conformanceTestRequiresHarness = defineRule({
|
|
|
561
622
|
}
|
|
562
623
|
},
|
|
563
624
|
"Program:exit"(node) {
|
|
564
|
-
if (
|
|
565
|
-
context.report({
|
|
566
|
-
node,
|
|
567
|
-
messageId: "missingHarnessImport",
|
|
568
|
-
data: {
|
|
569
|
-
name: `conformance test file without the ${CONFORMANCE_PACKAGE} import`,
|
|
570
|
-
expected: HARNESS_PRESCRIPTION$2,
|
|
571
|
-
actual: "no conformance check import found",
|
|
572
|
-
fix: HARNESS_PRESCRIPTION$2
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
if (!hasHarnessInvocation && violations === 0) context.report({
|
|
625
|
+
if (conformance && !hasHarnessInvocation && violations === 0) context.report({
|
|
578
626
|
node,
|
|
579
627
|
messageId: "missingHarnessUsage",
|
|
580
628
|
data: {
|
|
581
629
|
name: `conformance test file imports ${CONFORMANCE_PACKAGE} but never invokes it`,
|
|
582
|
-
expected: HARNESS_PRESCRIPTION$
|
|
630
|
+
expected: HARNESS_PRESCRIPTION$1,
|
|
583
631
|
actual: "no Conformance.linearizable, Conformance.sequential, or Conformance.released call runs",
|
|
584
|
-
fix: HARNESS_PRESCRIPTION$
|
|
632
|
+
fix: HARNESS_PRESCRIPTION$1
|
|
585
633
|
}
|
|
586
634
|
});
|
|
587
635
|
}
|
|
@@ -608,7 +656,7 @@ const INVALID_BEHAVIOR_CASE_FIX = "Convert behavior to PascalCase (e.g., throwEr
|
|
|
608
656
|
const INVALID_CONDITION_CASE_FIX = "Convert condition to PascalCase (e.g., passwordInvalid → PasswordInvalid)";
|
|
609
657
|
const EMPTY_BEHAVIOR_ACTUAL = "Empty string between Should_ and _When_";
|
|
610
658
|
const EMPTY_CONDITION_ACTUAL = "Empty string after _When_";
|
|
611
|
-
const meta$
|
|
659
|
+
const meta$24 = {
|
|
612
660
|
type: "suggestion",
|
|
613
661
|
docs: { description: "Enforce DAMP (Descriptive and Meaningful Phrases) test naming format: Should_[ExpectedBehavior]_When_[Condition]" },
|
|
614
662
|
schema: [],
|
|
@@ -688,7 +736,7 @@ const isTestFunctionCall = (node) => {
|
|
|
688
736
|
return false;
|
|
689
737
|
};
|
|
690
738
|
const dampTestNaming = defineRule({
|
|
691
|
-
meta: meta$
|
|
739
|
+
meta: meta$24,
|
|
692
740
|
create(context) {
|
|
693
741
|
if (TYPE_TEST_BASENAME.test(basenameOf(context.filename))) return {};
|
|
694
742
|
return { CallExpression(node) {
|
|
@@ -712,92 +760,70 @@ const dampTestNaming = defineRule({
|
|
|
712
760
|
});
|
|
713
761
|
//#endregion
|
|
714
762
|
//#region src/rules/differential-test-requires-harness.config.ts
|
|
715
|
-
const
|
|
763
|
+
const HARNESS_PRESCRIPTION = "import { Differential, Metamorphic } from @systemfsoftware/differential-spec and express the test as Differential.compare({ name, reference, candidate }).on(arb).assert(oracle) or Metamorphic.on({ name, system }).relation({ transformInput, assertOutput }).on(arb)";
|
|
764
|
+
const RAW_FAST_CHECK_EXPECTED$1 = "the harness owns fast-check — pass an fc.Arbitrary to the harness, never call fc.assert/fc.check directly";
|
|
765
|
+
const RAW_FAST_CHECK_ACTUAL$1 = "bypasses the differential harness oracle";
|
|
766
|
+
const RAW_FAST_CHECK_FIX$1 = "build the arbitrary in tests/__fixtures__ and hand it to Differential.compare(...).on(arb) or Metamorphic.on(...).relation(...).on(arb)";
|
|
767
|
+
const meta$23 = {
|
|
716
768
|
type: "problem",
|
|
717
|
-
docs: { description: "
|
|
769
|
+
docs: { description: "A test that imports @systemfsoftware/differential-spec (the differential lane) runs through that harness: raw fc.assert/fc.check/fc.property/fc.asyncProperty calls are forbidden in it, and importing the harness without invoking it is non-compliant. Runner imports and calls in this lane are vitest-from-systemfsoftware-vitest findings." },
|
|
718
770
|
schema: [],
|
|
719
771
|
messages: {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
missingHarnessImport: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.",
|
|
723
|
-
missingHarnessUsage: "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}."
|
|
772
|
+
rawFastCheck: MESSAGE$6,
|
|
773
|
+
missingHarnessUsage: MESSAGE$6
|
|
724
774
|
}
|
|
725
775
|
};
|
|
726
776
|
//#endregion
|
|
727
777
|
//#region src/rules/differential-test-requires-harness.ts
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
|
|
778
|
+
const memberObjectName$1 = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
|
|
779
|
+
const calleeName$1 = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName$1(callee);
|
|
731
780
|
const recordHarnessBindings = (node, bindings) => {
|
|
732
|
-
if (node.source.value !== "
|
|
781
|
+
if (harnessLaneOf(node.source.value) !== "differential") return;
|
|
733
782
|
for (const specifier of node.specifiers) bindings.add(specifier.local.name);
|
|
734
783
|
};
|
|
735
|
-
|
|
784
|
+
/** The fast-check runner a raw `fc.assert(...)` style call names, if any. */
|
|
785
|
+
const rawFastCheckMethod = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "fc" && callee.property.type === "Identifier" && RAW_FC_METHODS[callee.property.name] === true ? callee.property.name : void 0;
|
|
736
786
|
const differentialTestRequiresHarness = defineRule({
|
|
737
|
-
meta: meta$
|
|
787
|
+
meta: meta$23,
|
|
738
788
|
create(context) {
|
|
739
|
-
if (!context.filename.endsWith(".differential.test.ts")) return {};
|
|
740
789
|
const harnessBindings = /* @__PURE__ */ new Set();
|
|
741
|
-
let
|
|
790
|
+
let differential = false;
|
|
742
791
|
let hasHarnessInvocation = false;
|
|
743
792
|
return {
|
|
793
|
+
Program() {
|
|
794
|
+
differential = lanesOf(context).has("differential");
|
|
795
|
+
},
|
|
744
796
|
ImportDeclaration(node) {
|
|
745
|
-
if (
|
|
746
|
-
context.report({
|
|
747
|
-
node,
|
|
748
|
-
messageId: "runnerImport",
|
|
749
|
-
data: {
|
|
750
|
-
name: `runner import from ${String(node.source.value)} in a differential test file`,
|
|
751
|
-
expected: HARNESS_PRESCRIPTION$1,
|
|
752
|
-
actual: "a direct vitest / @effect/vitest / @systemfsoftware/vitest runner import bypasses the differential oracle",
|
|
753
|
-
fix: `delete the runner import; ${HARNESS_PRESCRIPTION$1}`
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
violations += 1;
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
recordHarnessBindings(node, harnessBindings);
|
|
797
|
+
if (differential) recordHarnessBindings(node, harnessBindings);
|
|
760
798
|
},
|
|
761
799
|
CallExpression(node) {
|
|
762
|
-
|
|
763
|
-
|
|
800
|
+
if (!differential) return;
|
|
801
|
+
const method = rawFastCheckMethod(node.callee);
|
|
802
|
+
if (method !== void 0) {
|
|
764
803
|
context.report({
|
|
765
804
|
node,
|
|
766
|
-
messageId: "
|
|
805
|
+
messageId: "rawFastCheck",
|
|
767
806
|
data: {
|
|
768
|
-
name: `raw
|
|
769
|
-
expected:
|
|
770
|
-
actual:
|
|
771
|
-
fix:
|
|
807
|
+
name: `raw fc.${method}(...) in a differential test file`,
|
|
808
|
+
expected: RAW_FAST_CHECK_EXPECTED$1,
|
|
809
|
+
actual: `fc.${method}(...) ${RAW_FAST_CHECK_ACTUAL$1}`,
|
|
810
|
+
fix: RAW_FAST_CHECK_FIX$1
|
|
772
811
|
}
|
|
773
812
|
});
|
|
774
|
-
violations += 1;
|
|
775
813
|
return;
|
|
776
814
|
}
|
|
815
|
+
const name = calleeName$1(node.callee);
|
|
777
816
|
if (name !== void 0 && harnessBindings.has(name)) hasHarnessInvocation = true;
|
|
778
817
|
},
|
|
779
818
|
"Program:exit"(node) {
|
|
780
|
-
if (
|
|
781
|
-
context.report({
|
|
782
|
-
node,
|
|
783
|
-
messageId: "missingHarnessImport",
|
|
784
|
-
data: {
|
|
785
|
-
name: `differential test file without ${DIFFERENTIAL_PACKAGE} import`,
|
|
786
|
-
expected: HARNESS_PRESCRIPTION$1,
|
|
787
|
-
actual: "no differential harness import found",
|
|
788
|
-
fix: HARNESS_PRESCRIPTION$1
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if (!hasHarnessInvocation && violations === 0) context.report({
|
|
819
|
+
if (differential && !hasHarnessInvocation) context.report({
|
|
794
820
|
node,
|
|
795
821
|
messageId: "missingHarnessUsage",
|
|
796
822
|
data: {
|
|
797
823
|
name: `differential test file imports ${DIFFERENTIAL_PACKAGE} but never invokes it`,
|
|
798
|
-
expected: HARNESS_PRESCRIPTION
|
|
824
|
+
expected: HARNESS_PRESCRIPTION,
|
|
799
825
|
actual: "the harness is imported but no Differential.compare or Metamorphic.on chain runs",
|
|
800
|
-
fix: HARNESS_PRESCRIPTION
|
|
826
|
+
fix: HARNESS_PRESCRIPTION
|
|
801
827
|
}
|
|
802
828
|
});
|
|
803
829
|
}
|
|
@@ -805,16 +831,64 @@ const differentialTestRequiresHarness = defineRule({
|
|
|
805
831
|
}
|
|
806
832
|
});
|
|
807
833
|
//#endregion
|
|
834
|
+
//#region src/rules/e2e-tier-placement.config.ts
|
|
835
|
+
const OUTSIDE_PACKAGE_EXPECTED = "end-to-end tests only in a package that declares the e2e role in its lint settings";
|
|
836
|
+
const OUTSIDE_PACKAGE_ACTUAL = "a test registered with the effect-e2e runner in a package that declares no e2e role";
|
|
837
|
+
const OUTSIDE_PACKAGE_FIX = "move the suite into an end-to-end package; if this package is one, declare the e2e role under the plugin key in its lint config settings";
|
|
838
|
+
const INTEGRATION_IN_PACKAGE_EXPECTED = "only end-to-end tests in a package that declares the e2e role";
|
|
839
|
+
const INTEGRATION_IN_PACKAGE_ACTUAL = "an integration test (Gherkin harness) in an end-to-end package";
|
|
840
|
+
const INTEGRATION_IN_PACKAGE_FIX = "move the integration test into the package it covers, or write it with the runner test so it is an end-to-end test";
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/rules/e2e-tier-placement.ts
|
|
843
|
+
const e2eTierPlacement = defineRule({
|
|
844
|
+
meta: {
|
|
845
|
+
type: "problem",
|
|
846
|
+
docs: { description: "Reports an end-to-end test (a test file that value-imports `test`, or the namespace, from `@systemfsoftware/effect-e2e/runner`, whatever its name or location) in a package whose lint settings do not declare the `e2e` role; in a package that declares it, reports an integration test (a file importing the Gherkin harness and not in the e2e lane). The role is declared once in the package's own lint config settings under the plugin's key, and no rule takes a role option." },
|
|
847
|
+
schema: [],
|
|
848
|
+
messages: {
|
|
849
|
+
e2eOutsideE2ePackage: MESSAGE$6,
|
|
850
|
+
integrationInE2ePackage: MESSAGE$6
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
create(context) {
|
|
854
|
+
return { Program(node) {
|
|
855
|
+
const lanes = lanesOf(context);
|
|
856
|
+
const name = basenameOf(context.filename);
|
|
857
|
+
const e2ePackage = isE2ePackage(context);
|
|
858
|
+
if (lanes.has("e2e") && !e2ePackage) context.report({
|
|
859
|
+
node,
|
|
860
|
+
messageId: "e2eOutsideE2ePackage",
|
|
861
|
+
data: {
|
|
862
|
+
name,
|
|
863
|
+
expected: OUTSIDE_PACKAGE_EXPECTED,
|
|
864
|
+
actual: OUTSIDE_PACKAGE_ACTUAL,
|
|
865
|
+
fix: OUTSIDE_PACKAGE_FIX
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
if (lanes.has("behaviour") && !lanes.has("e2e") && e2ePackage) context.report({
|
|
869
|
+
node,
|
|
870
|
+
messageId: "integrationInE2ePackage",
|
|
871
|
+
data: {
|
|
872
|
+
name,
|
|
873
|
+
expected: INTEGRATION_IN_PACKAGE_EXPECTED,
|
|
874
|
+
actual: INTEGRATION_IN_PACKAGE_ACTUAL,
|
|
875
|
+
fix: INTEGRATION_IN_PACKAGE_FIX
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
} };
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
//#endregion
|
|
808
882
|
//#region src/rules/in-source-test-prop-only.config.ts
|
|
809
883
|
const NON_PROP_CALL_NAME = "a non-property test call inside an `import.meta.vitest` block";
|
|
810
884
|
const NON_PROP_CALL_EXPECTED = "only `it.prop` or `it.effect.prop` member-chain calls (standard modifiers included) with boolean predicates";
|
|
811
885
|
const NON_PROP_CALL_ACTUAL = "a bare or member-chain test call other than `it.prop`/`it.effect.prop`";
|
|
812
|
-
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
|
|
886
|
+
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 a Gherkin behaviour test under tests/, or rewrite a real invariant as `it.prop` over a schema-derived arbitrary";
|
|
813
887
|
const meta$21 = {
|
|
814
888
|
type: "problem",
|
|
815
889
|
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." },
|
|
816
890
|
schema: [],
|
|
817
|
-
messages: { nonPropCall: MESSAGE$
|
|
891
|
+
messages: { nonPropCall: MESSAGE$6 }
|
|
818
892
|
};
|
|
819
893
|
//#endregion
|
|
820
894
|
//#region src/rules/vitest-guard.ts
|
|
@@ -841,19 +915,19 @@ const isInsideConsequent = (node, consequent) => {
|
|
|
841
915
|
};
|
|
842
916
|
//#endregion
|
|
843
917
|
//#region src/rules/in-source-test-prop-only.ts
|
|
844
|
-
const PROP_MODIFIERS
|
|
918
|
+
const PROP_MODIFIERS = /* @__PURE__ */ new Set([
|
|
845
919
|
"only",
|
|
846
920
|
"skip",
|
|
847
921
|
"todo"
|
|
848
922
|
]);
|
|
849
|
-
const isPropCallee
|
|
923
|
+
const isPropCallee = (callee) => {
|
|
850
924
|
if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
|
|
851
925
|
if (callee.property.name === "prop") {
|
|
852
926
|
const object = callee.object;
|
|
853
927
|
if (object.type === "Identifier") return object.name === "it";
|
|
854
928
|
return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
|
|
855
929
|
}
|
|
856
|
-
return PROP_MODIFIERS
|
|
930
|
+
return PROP_MODIFIERS.has(callee.property.name) && isPropCallee(callee.object);
|
|
857
931
|
};
|
|
858
932
|
const BANNED_TEST_ROOTS = /* @__PURE__ */ new Set([
|
|
859
933
|
"it",
|
|
@@ -888,7 +962,7 @@ const inSourceTestPropOnly = defineRule({
|
|
|
888
962
|
},
|
|
889
963
|
CallExpression(node) {
|
|
890
964
|
if (guards.length === 0) return;
|
|
891
|
-
if (isPropCallee
|
|
965
|
+
if (isPropCallee(node.callee)) return;
|
|
892
966
|
const root = rootNameOf(node.callee);
|
|
893
967
|
if (root === void 0 || !BANNED_TEST_ROOTS.has(root)) return;
|
|
894
968
|
if (guards.find((g) => isInsideConsequent(node, g.consequent)) === void 0) return;
|
|
@@ -918,14 +992,14 @@ const NOT_MODULE_LEVEL_FIX = "move the block to module level — a nested block
|
|
|
918
992
|
const NO_PRIVATE_TARGET_NAME = "an `import.meta.vitest` block touching no module-level binding";
|
|
919
993
|
const NO_PRIVATE_TARGET_EXPECTED = "an in-source test exercising this module's own code — a private helper, or an exported operation named in the `it.prop` `subject` slot";
|
|
920
994
|
const NO_PRIVATE_TARGET_ACTUAL = "an in-source block referencing only exported-call or imported names";
|
|
921
|
-
const NO_PRIVATE_TARGET_FIX = "touch this module. A private helper is referenced directly; an exported operation is named in the `it.prop` `subject` slot beside its declaration — that is the one home for its laws and the file whose mutants they run. What the block cannot hold is a behaviour binding no module-level name: test the public surface from tests/ as
|
|
995
|
+
const NO_PRIVATE_TARGET_FIX = "touch this module. A private helper is referenced directly; an exported operation is named in the `it.prop` `subject` slot beside its declaration — that is the one home for its laws and the file whose mutants they run. What the block cannot hold is a behaviour binding no module-level name: test the public surface from tests/ as a Gherkin behaviour test instead";
|
|
922
996
|
const meta$20 = {
|
|
923
997
|
type: "problem",
|
|
924
|
-
docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise this module's own code — a private helper, or an exported operation named in the `it.prop` `subject` slot; a public-surface test belongs in tests/ as
|
|
998
|
+
docs: { description: "In-source `if (import.meta.vitest)` blocks under src/ must be at module level and exercise this module's own code — a private helper, or an exported operation named in the `it.prop` `subject` slot; a public-surface test belongs in tests/ as a Gherkin behaviour test." },
|
|
925
999
|
schema: [],
|
|
926
1000
|
messages: {
|
|
927
|
-
notModuleLevel: MESSAGE$
|
|
928
|
-
noPrivateTarget: MESSAGE$
|
|
1001
|
+
notModuleLevel: MESSAGE$6,
|
|
1002
|
+
noPrivateTarget: MESSAGE$6
|
|
929
1003
|
}
|
|
930
1004
|
};
|
|
931
1005
|
//#endregion
|
|
@@ -1041,8 +1115,8 @@ const meta$19 = {
|
|
|
1041
1115
|
docs: { description: "A *.model.ts fixture is the pure model a conformance check stands the implementation against, so it may not import the package whose tests it serves. The package is derived from the path: the directory the tests tree lives in. An import, re-export, dynamic import, or require of that package name — or of any subpath of it — is the implementation reaching its own oracle. A relative specifier is judged the same way lexically: resolved against the fixture directory, it must stay inside the tests tree, so ../../src/Lock.js is the implementation again while ./Locks.js is a sibling fixture." },
|
|
1042
1116
|
schema: [],
|
|
1043
1117
|
messages: {
|
|
1044
|
-
subjectImport: MESSAGE$
|
|
1045
|
-
relativeEscape: MESSAGE$
|
|
1118
|
+
subjectImport: MESSAGE$6,
|
|
1119
|
+
relativeEscape: MESSAGE$6
|
|
1046
1120
|
}
|
|
1047
1121
|
};
|
|
1048
1122
|
//#endregion
|
|
@@ -1146,42 +1220,25 @@ const modelFixtureImportsSubject = defineRule({
|
|
|
1146
1220
|
});
|
|
1147
1221
|
//#endregion
|
|
1148
1222
|
//#region src/rules/no-assert-in-property.config.ts
|
|
1149
|
-
const MESSAGE$
|
|
1223
|
+
const MESSAGE$5 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1150
1224
|
const meta$18 = {
|
|
1151
1225
|
type: "problem",
|
|
1152
1226
|
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." },
|
|
1153
1227
|
schema: [],
|
|
1154
1228
|
messages: {
|
|
1155
|
-
expectCall: MESSAGE$
|
|
1156
|
-
assertCall: MESSAGE$
|
|
1157
|
-
rawFcRun: MESSAGE$
|
|
1229
|
+
expectCall: MESSAGE$5,
|
|
1230
|
+
assertCall: MESSAGE$5,
|
|
1231
|
+
rawFcRun: MESSAGE$5
|
|
1158
1232
|
}
|
|
1159
1233
|
};
|
|
1160
1234
|
//#endregion
|
|
1161
|
-
//#region src/rules/prop-call.ts
|
|
1162
|
-
const PROP_MODIFIERS = /* @__PURE__ */ new Set([
|
|
1163
|
-
"only",
|
|
1164
|
-
"skip",
|
|
1165
|
-
"todo"
|
|
1166
|
-
]);
|
|
1167
|
-
const isPropCallee = (callee) => {
|
|
1168
|
-
if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return false;
|
|
1169
|
-
if (callee.property.name === "prop") {
|
|
1170
|
-
const object = callee.object;
|
|
1171
|
-
if (object.type === "Identifier") return object.name === "it";
|
|
1172
|
-
return object.type === "MemberExpression" && object.property.type === "Identifier" && object.property.name === "effect" && object.object.type === "Identifier" && object.object.name === "it";
|
|
1173
|
-
}
|
|
1174
|
-
return PROP_MODIFIERS.has(callee.property.name) && isPropCallee(callee.object);
|
|
1175
|
-
};
|
|
1176
|
-
const getPredicate = (node) => Array$1.findLast(node.arguments, (arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
|
|
1177
|
-
//#endregion
|
|
1178
1235
|
//#region src/rules/no-assert-in-property.ts
|
|
1179
1236
|
const ASSERT_IDENTIFIER = /^assert/;
|
|
1180
1237
|
const FC_RUN_METHODS = /* @__PURE__ */ new Set(["assert", "check"]);
|
|
1181
1238
|
const isInsidePropPredicate = (node) => {
|
|
1182
1239
|
const parent = node.parent;
|
|
1183
1240
|
if (parent === null) return false;
|
|
1184
|
-
if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && Option.fromNullishOr(parent.parent).pipe(Option.exists((grandparent) => grandparent.type === "CallExpression" && isPropCallee(grandparent.callee)))) return true;
|
|
1241
|
+
if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && Option.fromNullishOr(parent.parent).pipe(Option.exists((grandparent) => grandparent.type === "CallExpression" && isPropCallee$1(grandparent.callee)))) return true;
|
|
1185
1242
|
return isInsidePropPredicate(parent);
|
|
1186
1243
|
};
|
|
1187
1244
|
const noAssertInProperty = defineRule({
|
|
@@ -1370,7 +1427,7 @@ const IO_SPECIFIERS = {
|
|
|
1370
1427
|
"node:https": true,
|
|
1371
1428
|
"node:dns": true,
|
|
1372
1429
|
"effect/FileSystem": true,
|
|
1373
|
-
"effect/
|
|
1430
|
+
"effect/process/ChildProcessSpawner": true,
|
|
1374
1431
|
"@effect/platform/FileSystem": true,
|
|
1375
1432
|
"@effect/platform/CommandExecutor": true,
|
|
1376
1433
|
"@effect/platform-node": true,
|
|
@@ -1444,7 +1501,7 @@ const noIoModuleInSourceTest = defineRule({
|
|
|
1444
1501
|
});
|
|
1445
1502
|
//#endregion
|
|
1446
1503
|
//#region src/rules/no-nested-quantification.config.ts
|
|
1447
|
-
const MESSAGE$
|
|
1504
|
+
const MESSAGE$4 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1448
1505
|
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([]))) });
|
|
1449
1506
|
const ITERATOR_METHODS = /* @__PURE__ */ new Set([
|
|
1450
1507
|
"every",
|
|
@@ -1471,7 +1528,7 @@ const meta$15 = {
|
|
|
1471
1528
|
type: "problem",
|
|
1472
1529
|
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." },
|
|
1473
1530
|
schema: [Schema.toJsonSchemaDocument(Options$2).schema],
|
|
1474
|
-
messages: { nestedQuantification: MESSAGE$
|
|
1531
|
+
messages: { nestedQuantification: MESSAGE$4 }
|
|
1475
1532
|
};
|
|
1476
1533
|
//#endregion
|
|
1477
1534
|
//#region src/rules/no-nested-quantification.ts
|
|
@@ -1631,7 +1688,7 @@ const noNestedQuantification = defineRule({
|
|
|
1631
1688
|
const basename = context.filename.slice(context.filename.lastIndexOf("/") + 1);
|
|
1632
1689
|
return { CallExpression(node) {
|
|
1633
1690
|
if (exempt.has(basename)) return;
|
|
1634
|
-
if (!isPropCallee(node.callee)) return;
|
|
1691
|
+
if (!isPropCallee$1(node.callee)) return;
|
|
1635
1692
|
Option.match(getPredicate(node), {
|
|
1636
1693
|
onNone: () => {},
|
|
1637
1694
|
onSome: (predicate) => check(context, node, predicate)
|
|
@@ -1641,15 +1698,15 @@ const noNestedQuantification = defineRule({
|
|
|
1641
1698
|
});
|
|
1642
1699
|
//#endregion
|
|
1643
1700
|
//#region src/rules/no-pseudo-gherkin-unit-tests.config.ts
|
|
1644
|
-
const NO_LAYER_IN_FEATURE_NAME = "a
|
|
1701
|
+
const NO_LAYER_IN_FEATURE_NAME = "a behaviour test feature with no environment double";
|
|
1645
1702
|
const NO_LAYER_IN_FEATURE_EXPECTED = "a Feature builder chained with .withLayer(layer) or .withScenarioLayer(layer)";
|
|
1646
1703
|
const NO_LAYER_IN_FEATURE_ACTUAL = "a Feature(...) call without .withLayer or .withScenarioLayer";
|
|
1647
|
-
const NO_LAYER_IN_FEATURE_FIX = "
|
|
1704
|
+
const NO_LAYER_IN_FEATURE_FIX = "a behaviour 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)).";
|
|
1648
1705
|
const meta$14 = {
|
|
1649
1706
|
type: "problem",
|
|
1650
|
-
docs: { description: "
|
|
1707
|
+
docs: { description: "In a behaviour test - any test file that imports @systemfsoftware/effect-gherkin-spec, whatever its name - every Feature builder must configure at least one environment Layer (.withLayer or .withScenarioLayer) to enforce real integration boundaries under WGI-CLS1." },
|
|
1651
1708
|
schema: [],
|
|
1652
|
-
messages: { noLayerInFeature: MESSAGE$
|
|
1709
|
+
messages: { noLayerInFeature: MESSAGE$6 }
|
|
1653
1710
|
};
|
|
1654
1711
|
//#endregion
|
|
1655
1712
|
//#region src/rules/no-pseudo-gherkin-unit-tests.ts
|
|
@@ -1681,38 +1738,44 @@ const findRootFeatureCall = (callNode) => {
|
|
|
1681
1738
|
const noPseudoGherkinUnitTests = defineRule({
|
|
1682
1739
|
meta: meta$14,
|
|
1683
1740
|
create(context) {
|
|
1684
|
-
|
|
1685
|
-
return {
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1741
|
+
let behaviour = false;
|
|
1742
|
+
return {
|
|
1743
|
+
Program() {
|
|
1744
|
+
behaviour = lanesOf(context).has("behaviour");
|
|
1745
|
+
},
|
|
1746
|
+
CallExpression(node) {
|
|
1747
|
+
if (!behaviour) return;
|
|
1748
|
+
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
|
|
1749
|
+
if (node.callee.property.name === "body") {
|
|
1750
|
+
const rootFeature = findRootFeatureCall(node);
|
|
1751
|
+
if (rootFeature !== null && !hasLayerInChain(node)) context.report({
|
|
1752
|
+
node: rootFeature,
|
|
1753
|
+
messageId: "noLayerInFeature",
|
|
1754
|
+
data: {
|
|
1755
|
+
name: NO_LAYER_IN_FEATURE_NAME,
|
|
1756
|
+
expected: NO_LAYER_IN_FEATURE_EXPECTED,
|
|
1757
|
+
actual: NO_LAYER_IN_FEATURE_ACTUAL,
|
|
1758
|
+
fix: NO_LAYER_IN_FEATURE_FIX
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1699
1762
|
}
|
|
1700
1763
|
}
|
|
1701
|
-
}
|
|
1764
|
+
};
|
|
1702
1765
|
}
|
|
1703
1766
|
});
|
|
1704
1767
|
//#endregion
|
|
1705
1768
|
//#region src/rules/no-silent-return.config.ts
|
|
1706
|
-
const MESSAGE$
|
|
1769
|
+
const MESSAGE$3 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
1707
1770
|
const meta$13 = {
|
|
1708
1771
|
type: "problem",
|
|
1709
1772
|
docs: { description: "Property predicates (it.prop / it.effect.prop from @systemfsoftware/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." },
|
|
1710
1773
|
schema: [],
|
|
1711
1774
|
messages: {
|
|
1712
|
-
bareReturn: MESSAGE$
|
|
1713
|
-
nonBooleanReturn: MESSAGE$
|
|
1714
|
-
missingReturn: MESSAGE$
|
|
1715
|
-
nonBooleanBody: MESSAGE$
|
|
1775
|
+
bareReturn: MESSAGE$3,
|
|
1776
|
+
nonBooleanReturn: MESSAGE$3,
|
|
1777
|
+
missingReturn: MESSAGE$3,
|
|
1778
|
+
nonBooleanBody: MESSAGE$3
|
|
1716
1779
|
}
|
|
1717
1780
|
};
|
|
1718
1781
|
//#endregion
|
|
@@ -1855,7 +1918,7 @@ const noSilentReturn = defineRule({
|
|
|
1855
1918
|
meta: meta$13,
|
|
1856
1919
|
create(context) {
|
|
1857
1920
|
return { CallExpression(node) {
|
|
1858
|
-
if (!isPropCallee(node.callee)) return;
|
|
1921
|
+
if (!isPropCallee$1(node.callee)) return;
|
|
1859
1922
|
Option.match(getPredicate(node), {
|
|
1860
1923
|
onNone: () => {},
|
|
1861
1924
|
onSome: (predicate) => checkFn(context, predicate)
|
|
@@ -1871,6 +1934,12 @@ const testFileInSrcDetail = (dir) => ({
|
|
|
1871
1934
|
actual: `a test file under src/ that is neither the schema-laws entry point nor a <stem>.workflow.property.test.ts inside a ${dir} directory`,
|
|
1872
1935
|
fix: `pick the arm matching what this file exercises. A workflow law -> rename to <stem>.workflow.property.test.ts inside src/<path>/${dir}/. A kernel/policy/schema property or characterization suite -> convert it to an in-source \`if (import.meta.vitest)\` block in the module it covers. The package public surface -> move outside src/ to tests/<name>.integration.test.ts. No arm matches -> delete this file`
|
|
1873
1936
|
});
|
|
1937
|
+
/** A file with the sanctioned name and place whose content selects no property lane. */
|
|
1938
|
+
const notAPropertyDetail = (dir) => ({
|
|
1939
|
+
expected: testFileInSrcDetail(dir).expected,
|
|
1940
|
+
actual: `a <stem>.workflow.property.test.ts inside a ${dir} directory whose content is not a property test (no FastCheck import and no it.prop / it.effect.prop call, or a differential harness import)`,
|
|
1941
|
+
fix: `state each workflow law as it.prop(...) / it.effect.prop(...) over FastCheck arbitraries. A scenario or differential suite -> move it outside src/ to tests/, named for the lane its imports select`
|
|
1942
|
+
});
|
|
1874
1943
|
const propertyTestLocationDetail = (dir) => ({
|
|
1875
1944
|
expected: `src/**/${dir}/<stem>.workflow.property.test.ts — a property test one directory down from the workflow it covers, never beside it`,
|
|
1876
1945
|
actual: `a property test under src/ that is not a single-segment <stem>.workflow.property.test.ts inside a ${dir} directory`,
|
|
@@ -1883,12 +1952,12 @@ const SCHEMA_TEST_DETAIL = {
|
|
|
1883
1952
|
};
|
|
1884
1953
|
const meta$12 = {
|
|
1885
1954
|
type: "problem",
|
|
1886
|
-
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
|
|
1955
|
+
docs: { description: "Under src/, the only sanctioned test file is a single-segment <stem>.workflow.property.test.ts inside a sanctioned test directory whose content is a property test (it imports FastCheck or calls it.prop / it.effect.prop), 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 a Gherkin behaviour test. The sanctioned directory list is the sanctionedDirs option, defaulting to the one directory this repo runs." },
|
|
1887
1956
|
schema: [Schema.toJsonSchemaDocument(Options$1).schema],
|
|
1888
1957
|
messages: {
|
|
1889
|
-
testFileInSrc: MESSAGE$
|
|
1890
|
-
schemaTestInSrc: MESSAGE$
|
|
1891
|
-
propertyTestOutsideTestsDir: MESSAGE$
|
|
1958
|
+
testFileInSrc: MESSAGE$6,
|
|
1959
|
+
schemaTestInSrc: MESSAGE$6,
|
|
1960
|
+
propertyTestOutsideTestsDir: MESSAGE$6
|
|
1892
1961
|
}
|
|
1893
1962
|
};
|
|
1894
1963
|
//#endregion
|
|
@@ -1905,9 +1974,10 @@ const noTestFileInSrc = defineRule({
|
|
|
1905
1974
|
const isPropertyTest = basename.endsWith(PROPERTY_SUFFIX);
|
|
1906
1975
|
const isSchemaTest = basename.endsWith(SCHEMA_SUFFIX);
|
|
1907
1976
|
const colocated = isInConfiguredTestDir(context.filename, sanctionedDirs);
|
|
1908
|
-
|
|
1909
|
-
const [messageId, detail] = violationOf(basename, isPropertyTest, sanctionedDirs[0]);
|
|
1977
|
+
const sanctionedName = !isSchemaTest && WORKFLOW_TEST_BASENAME.test(basename) && colocated;
|
|
1978
|
+
const [messageId, detail] = sanctionedName ? ["testFileInSrc", notAPropertyDetail(sanctionedDirs[0])] : violationOf(basename, isPropertyTest, sanctionedDirs[0]);
|
|
1910
1979
|
return { Program(node) {
|
|
1980
|
+
if (sanctionedName && isPropertyFile(lanesOf(context))) return;
|
|
1911
1981
|
context.report({
|
|
1912
1982
|
node,
|
|
1913
1983
|
messageId,
|
|
@@ -2085,7 +2155,7 @@ const pbtNaming = defineRule({
|
|
|
2085
2155
|
});
|
|
2086
2156
|
//#endregion
|
|
2087
2157
|
//#region src/rules/prop-arbitrary-schema-origin.config.ts
|
|
2088
|
-
const MESSAGE$
|
|
2158
|
+
const MESSAGE$2 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2089
2159
|
const VIOLATION_NAME$2 = "a hand-built fast-check arbitrary with no schema underneath";
|
|
2090
2160
|
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";
|
|
2091
2161
|
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";
|
|
@@ -2106,8 +2176,8 @@ const meta$10 = {
|
|
|
2106
2176
|
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." },
|
|
2107
2177
|
schema: [],
|
|
2108
2178
|
messages: {
|
|
2109
|
-
handBuiltArbitrary: MESSAGE$
|
|
2110
|
-
stockDerivedArbitrary: MESSAGE$
|
|
2179
|
+
handBuiltArbitrary: MESSAGE$2,
|
|
2180
|
+
stockDerivedArbitrary: MESSAGE$2
|
|
2111
2181
|
}
|
|
2112
2182
|
};
|
|
2113
2183
|
//#endregion
|
|
@@ -2188,7 +2258,7 @@ const dynamicEdgeOf = (declarator, init, name) => {
|
|
|
2188
2258
|
};
|
|
2189
2259
|
const vocabularyOf = (edge) => {
|
|
2190
2260
|
if (SCHEMA_SOURCE_PATTERN.test(edge.source)) return "schema";
|
|
2191
|
-
if (edge.source
|
|
2261
|
+
if (edge.source === "effect/Arbitrary") return "schema";
|
|
2192
2262
|
if (edge.source === "effect" && edge.imported !== null) {
|
|
2193
2263
|
if (SCHEMA_NAMESPACE_NAMES[edge.imported] === true) return "schema";
|
|
2194
2264
|
if (FASTCHECK_NAMESPACE_NAMES[edge.imported] === true) return "handBuilt";
|
|
@@ -2220,7 +2290,7 @@ var Provenance = class {
|
|
|
2220
2290
|
if (resolveLocal$1(name, node, this.getScope).kind !== "import") return false;
|
|
2221
2291
|
const edge = this.imports.get(name);
|
|
2222
2292
|
if (edge === void 0) return false;
|
|
2223
|
-
if (edge.source
|
|
2293
|
+
if (edge.source === "effect/Arbitrary") return true;
|
|
2224
2294
|
return edge.source === "effect" && edge.imported === "Arbitrary";
|
|
2225
2295
|
}
|
|
2226
2296
|
classifyCall(name, node) {
|
|
@@ -2417,7 +2487,7 @@ const propArbitrarySchemaOrigin = defineRule({
|
|
|
2417
2487
|
}
|
|
2418
2488
|
if (!isNode$2(value)) return;
|
|
2419
2489
|
if (value.type === "IfStatement" && mentionsImportMetaVitest$2(value.test)) return;
|
|
2420
|
-
if (value.type === "CallExpression" && isPropCallee(value.callee)) out.push(value);
|
|
2490
|
+
if (value.type === "CallExpression" && isPropCallee$1(value.callee)) out.push(value);
|
|
2421
2491
|
for (const [key, child] of Object.entries(value)) {
|
|
2422
2492
|
if (key === "parent") continue;
|
|
2423
2493
|
collect(child, out);
|
|
@@ -2466,7 +2536,7 @@ const propArbitrarySchemaOrigin = defineRule({
|
|
|
2466
2536
|
});
|
|
2467
2537
|
//#endregion
|
|
2468
2538
|
//#region src/rules/prop-fixture-schema-origin.config.ts
|
|
2469
|
-
const MESSAGE$
|
|
2539
|
+
const MESSAGE$1 = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2470
2540
|
const VIOLATION_NAME$1 = "a hand-rolled recursive schema fixture inside a test block";
|
|
2471
2541
|
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";
|
|
2472
2542
|
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";
|
|
@@ -2475,7 +2545,7 @@ const meta$9 = {
|
|
|
2475
2545
|
type: "problem",
|
|
2476
2546
|
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." },
|
|
2477
2547
|
schema: [],
|
|
2478
|
-
messages: { handRolledRecursiveFixture: MESSAGE$
|
|
2548
|
+
messages: { handRolledRecursiveFixture: MESSAGE$1 }
|
|
2479
2549
|
};
|
|
2480
2550
|
//#endregion
|
|
2481
2551
|
//#region src/rules/prop-fixture-schema-origin.ts
|
|
@@ -2677,7 +2747,7 @@ const propFixtureSchemaOrigin = defineRule({
|
|
|
2677
2747
|
});
|
|
2678
2748
|
//#endregion
|
|
2679
2749
|
//#region src/rules/prop-generated-law-duplicate.config.ts
|
|
2680
|
-
const MESSAGE
|
|
2750
|
+
const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
2681
2751
|
const COMPILER_NAME = "a runtime probe of a compile-time guarantee";
|
|
2682
2752
|
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";
|
|
2683
2753
|
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";
|
|
@@ -2691,8 +2761,8 @@ const meta$8 = {
|
|
|
2691
2761
|
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." },
|
|
2692
2762
|
schema: [],
|
|
2693
2763
|
messages: {
|
|
2694
|
-
compilerDuplicate: MESSAGE
|
|
2695
|
-
noDomainFunction: MESSAGE
|
|
2764
|
+
compilerDuplicate: MESSAGE,
|
|
2765
|
+
noDomainFunction: MESSAGE
|
|
2696
2766
|
}
|
|
2697
2767
|
};
|
|
2698
2768
|
//#endregion
|
|
@@ -2851,7 +2921,7 @@ const propGeneratedLawDuplicate = defineRule({
|
|
|
2851
2921
|
return;
|
|
2852
2922
|
}
|
|
2853
2923
|
if (!isNode(value)) return;
|
|
2854
|
-
if (value.type === "CallExpression" && isPropCallee(value.callee)) out.push(value);
|
|
2924
|
+
if (value.type === "CallExpression" && isPropCallee$1(value.callee)) out.push(value);
|
|
2855
2925
|
for (const [key, child] of Object.entries(value)) {
|
|
2856
2926
|
if (key === "parent") continue;
|
|
2857
2927
|
collect(child, out);
|
|
@@ -2884,129 +2954,110 @@ const propGeneratedLawDuplicate = defineRule({
|
|
|
2884
2954
|
});
|
|
2885
2955
|
//#endregion
|
|
2886
2956
|
//#region src/rules/property-file-purity.config.ts
|
|
2887
|
-
const
|
|
2888
|
-
const
|
|
2957
|
+
const PLAIN_EXPECTED = "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests";
|
|
2958
|
+
const PLAIN_FIX = "move the scenario test to a test file that imports no FastCheck and calls no it.prop, or rewrite it as a property with arbitraries and a boolean-returning predicate";
|
|
2959
|
+
const RAW_FAST_CHECK_EXPECTED = "it.prop(...) or it.effect.prop(...) from @systemfsoftware/vitest";
|
|
2960
|
+
const RAW_FAST_CHECK_ACTUAL = "bypasses the vitest/Effect integration";
|
|
2961
|
+
const RAW_FAST_CHECK_FIX = "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)";
|
|
2962
|
+
const PROPERTY_HOME = "a property test in src/<dir>/__tests__/<stem>.workflow.property.test.ts, apart from any harness test";
|
|
2963
|
+
const MIXED_FIX = "move the property beside the workflow it covers in its own <stem>.workflow.property.test.ts; this file keeps the harness scenarios only";
|
|
2964
|
+
const MIXED_FAST_CHECK_IMPORT_DATA = {
|
|
2965
|
+
name: "a FastCheck import in a Gherkin, conformance or trace test",
|
|
2966
|
+
expected: PROPERTY_HOME,
|
|
2967
|
+
actual: "a test that imports a Gherkin, conformance or trace harness also imports FastCheck",
|
|
2968
|
+
fix: MIXED_FIX
|
|
2969
|
+
};
|
|
2970
|
+
const MIXED_PROP_CALL_DATA = {
|
|
2971
|
+
name: "a property test in a Gherkin, conformance or trace test",
|
|
2972
|
+
expected: PROPERTY_HOME,
|
|
2973
|
+
actual: "a test that imports a Gherkin, conformance or trace harness also calls it.prop / it.effect.prop",
|
|
2974
|
+
fix: MIXED_FIX
|
|
2975
|
+
};
|
|
2889
2976
|
const meta$7 = {
|
|
2890
2977
|
type: "problem",
|
|
2891
|
-
docs: { description: "
|
|
2978
|
+
docs: { description: "A test that imports FastCheck or calls it.prop / it.effect.prop is a property test file, and it holds only property tests: no plain it()/test()/it.effect(), no raw fc.assert/fc.check/fc.property/fc.asyncProperty. A test that also imports the Gherkin, conformance or trace harness is reported for each FastCheck import and each it.prop / it.effect.prop call: property tests never mix with harness scenarios. A test that imports @systemfsoftware/differential-spec is held to the differential harness rule instead." },
|
|
2892
2979
|
schema: [],
|
|
2893
2980
|
messages: {
|
|
2894
|
-
plainIt: MESSAGE$
|
|
2895
|
-
plainEffectIt: MESSAGE$
|
|
2896
|
-
rawFastCheck: MESSAGE$
|
|
2897
|
-
fastCheckImport: MESSAGE$
|
|
2898
|
-
propCall: MESSAGE$
|
|
2981
|
+
plainIt: MESSAGE$6,
|
|
2982
|
+
plainEffectIt: MESSAGE$6,
|
|
2983
|
+
rawFastCheck: MESSAGE$6,
|
|
2984
|
+
fastCheckImport: MESSAGE$6,
|
|
2985
|
+
propCall: MESSAGE$6
|
|
2899
2986
|
}
|
|
2900
2987
|
};
|
|
2901
2988
|
//#endregion
|
|
2902
2989
|
//#region src/rules/property-file-purity.ts
|
|
2903
|
-
const
|
|
2904
|
-
"assert",
|
|
2905
|
-
"check",
|
|
2906
|
-
"property",
|
|
2907
|
-
"asyncProperty"
|
|
2908
|
-
]);
|
|
2909
|
-
const PROPERTY_FILE_KIND = {
|
|
2910
|
-
suffix: PROPERTY_TEST_SUFFIX,
|
|
2911
|
-
plainExpected: "it.prop(...) or it.effect.prop(...) — property files never mix with scenario tests",
|
|
2912
|
-
plainFix: "move the scenario test to a plain *.test.ts file, or rewrite it as a property with arbitraries and a boolean-returning predicate",
|
|
2913
|
-
rawFcExpected: "it.prop(...) or it.effect.prop(...) from @systemfsoftware/vitest",
|
|
2914
|
-
rawFcActualSuffix: "bypasses the vitest/Effect integration",
|
|
2915
|
-
rawFcFix: "rewrite as it.prop(name, { of, subject, runs }, holds) returning a boolean; fc.* stays for building arbitraries (fc.pre, fc.stringMatching, ...)"
|
|
2916
|
-
};
|
|
2917
|
-
const DIFFERENTIAL_FILE_KIND = {
|
|
2918
|
-
suffix: DIFFERENTIAL_SUFFIX,
|
|
2919
|
-
plainExpected: "the differential harness — Differential.compare(...).on(arb).assert(oracle) or Metamorphic.on(system).relation(...).on(arb)",
|
|
2920
|
-
plainFix: "rewrite via @systemfsoftware/differential-spec; scenario tests do not belong in a differential file",
|
|
2921
|
-
rawFcExpected: "the harness owns fast-check — pass an fc.Arbitrary to the harness, never call fc.assert/fc.check directly",
|
|
2922
|
-
rawFcActualSuffix: "bypasses the differential harness oracle",
|
|
2923
|
-
rawFcFix: "build the arbitrary in tests/__fixtures__ and hand it to Differential.compare(...).on(arb) or Metamorphic.on(...).relation(...).on(arb)"
|
|
2924
|
-
};
|
|
2925
|
-
const reportPlain = (context, node, messageId, actual, kind) => {
|
|
2990
|
+
const reportPlain = (context, node, messageId, actual) => {
|
|
2926
2991
|
context.report({
|
|
2927
2992
|
node,
|
|
2928
2993
|
messageId,
|
|
2929
2994
|
data: {
|
|
2930
|
-
name: `scenario test (${actual}) in a
|
|
2931
|
-
expected:
|
|
2995
|
+
name: `scenario test (${actual}) in a property test file`,
|
|
2996
|
+
expected: PLAIN_EXPECTED,
|
|
2932
2997
|
actual: `${actual} runs a single example, not a property`,
|
|
2933
|
-
fix:
|
|
2998
|
+
fix: PLAIN_FIX
|
|
2934
2999
|
}
|
|
2935
3000
|
});
|
|
2936
3001
|
};
|
|
2937
|
-
const reportRawFc = (context, node, method
|
|
3002
|
+
const reportRawFc = (context, node, method) => {
|
|
2938
3003
|
context.report({
|
|
2939
3004
|
node,
|
|
2940
3005
|
messageId: "rawFastCheck",
|
|
2941
3006
|
data: {
|
|
2942
|
-
name: `raw fc.${method}(...) in a
|
|
2943
|
-
expected:
|
|
2944
|
-
actual: `fc.${method}(...) ${
|
|
2945
|
-
fix:
|
|
3007
|
+
name: `raw fc.${method}(...) in a property test file`,
|
|
3008
|
+
expected: RAW_FAST_CHECK_EXPECTED,
|
|
3009
|
+
actual: `fc.${method}(...) ${RAW_FAST_CHECK_ACTUAL}`,
|
|
3010
|
+
fix: RAW_FAST_CHECK_FIX
|
|
2946
3011
|
}
|
|
2947
3012
|
});
|
|
2948
3013
|
};
|
|
2949
|
-
const
|
|
3014
|
+
const checkCall = (context, node) => {
|
|
2950
3015
|
const callee = node.callee;
|
|
2951
3016
|
if (callee.type === "Identifier") {
|
|
2952
|
-
if (callee.name === "it" || callee.name === "test") reportPlain(context, node, "plainIt", `${callee.name}(...)
|
|
3017
|
+
if (callee.name === "it" || callee.name === "test") reportPlain(context, node, "plainIt", `${callee.name}(...)`);
|
|
2953
3018
|
return;
|
|
2954
3019
|
}
|
|
2955
3020
|
if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
|
|
2956
3021
|
const object = callee.object;
|
|
2957
|
-
if (object.type === "Identifier" && object.name === "fc" && RAW_FC_METHODS
|
|
2958
|
-
reportRawFc(context, node, callee.property.name
|
|
3022
|
+
if (object.type === "Identifier" && object.name === "fc" && RAW_FC_METHODS[callee.property.name] === true) {
|
|
3023
|
+
reportRawFc(context, node, callee.property.name);
|
|
2959
3024
|
return;
|
|
2960
3025
|
}
|
|
2961
3026
|
if (object.type === "Identifier" && object.name === "it") {
|
|
2962
|
-
if (callee.property.name === "effect") reportPlain(context, node, "plainEffectIt", "it.effect(...)"
|
|
2963
|
-
else if (PROP_MODIFIERS.has(callee.property.name)) reportPlain(context, node, "plainIt", `it.${callee.property.name}(...)
|
|
3027
|
+
if (callee.property.name === "effect") reportPlain(context, node, "plainEffectIt", "it.effect(...)");
|
|
3028
|
+
else if (PROP_MODIFIERS$1.has(callee.property.name)) reportPlain(context, node, "plainIt", `it.${callee.property.name}(...)`);
|
|
2964
3029
|
return;
|
|
2965
3030
|
}
|
|
2966
|
-
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}(...)
|
|
2967
|
-
} });
|
|
2968
|
-
const FAST_CHECK_IMPORT_DATA = {
|
|
2969
|
-
name: "FastCheck import in a scenario test file",
|
|
2970
|
-
expected: `property tests (and every FastCheck usage) live in ${PROPERTY_TEST_SUFFIX} files`,
|
|
2971
|
-
actual: `FastCheck imported by a file that is not ${PROPERTY_TEST_SUFFIX}`,
|
|
2972
|
-
fix: "move the property test to a *.property.test.ts file; this file keeps plain it() scenario tests only"
|
|
2973
|
-
};
|
|
2974
|
-
const reportFastCheckImport = (context, node) => {
|
|
2975
|
-
context.report({
|
|
2976
|
-
node,
|
|
2977
|
-
messageId: "fastCheckImport",
|
|
2978
|
-
data: FAST_CHECK_IMPORT_DATA
|
|
2979
|
-
});
|
|
3031
|
+
if (PROP_MODIFIERS$1.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}(...)`);
|
|
2980
3032
|
};
|
|
2981
|
-
const createScenarioFileVisitors = (context) => ({
|
|
2982
|
-
ImportDeclaration(node) {
|
|
2983
|
-
if (node.source.value === "fast-check" || node.source.value.startsWith("fast-check/")) {
|
|
2984
|
-
reportFastCheckImport(context, node);
|
|
2985
|
-
return;
|
|
2986
|
-
}
|
|
2987
|
-
for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "FastCheck") reportFastCheckImport(context, specifier);
|
|
2988
|
-
},
|
|
2989
|
-
CallExpression(node) {
|
|
2990
|
-
if (!isPropCallee(node.callee)) return;
|
|
2991
|
-
context.report({
|
|
2992
|
-
node,
|
|
2993
|
-
messageId: "propCall",
|
|
2994
|
-
data: {
|
|
2995
|
-
name: "property test in a non-property test file",
|
|
2996
|
-
expected: `it.prop / it.effect.prop calls live in ${PROPERTY_TEST_SUFFIX} files`,
|
|
2997
|
-
actual: "a property test mixed into a test file that is not a property file",
|
|
2998
|
-
fix: "move this test to a *.property.test.ts file — property and non-property tests never mix"
|
|
2999
|
-
}
|
|
3000
|
-
});
|
|
3001
|
-
}
|
|
3002
|
-
});
|
|
3003
|
-
const kindOf = (filename) => filename.endsWith(".differential.test.ts") ? DIFFERENTIAL_FILE_KIND : PROPERTY_FILE_KIND;
|
|
3004
3033
|
const propertyFilePurity = defineRule({
|
|
3005
3034
|
meta: meta$7,
|
|
3006
3035
|
create(context) {
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
return
|
|
3036
|
+
let property = false;
|
|
3037
|
+
let mixed = false;
|
|
3038
|
+
return {
|
|
3039
|
+
Program() {
|
|
3040
|
+
const lanes = lanesOf(context);
|
|
3041
|
+
mixed = mixesPropertyIntoAHarness(lanes);
|
|
3042
|
+
property = !mixed && isPropertyFile(lanes);
|
|
3043
|
+
},
|
|
3044
|
+
ImportDeclaration(node) {
|
|
3045
|
+
if (!mixed) return;
|
|
3046
|
+
for (const site of fastCheckImportSites(node)) context.report({
|
|
3047
|
+
node: site,
|
|
3048
|
+
messageId: "fastCheckImport",
|
|
3049
|
+
data: MIXED_FAST_CHECK_IMPORT_DATA
|
|
3050
|
+
});
|
|
3051
|
+
},
|
|
3052
|
+
CallExpression(node) {
|
|
3053
|
+
if (mixed && isPropCallee$1(node.callee)) context.report({
|
|
3054
|
+
node,
|
|
3055
|
+
messageId: "propCall",
|
|
3056
|
+
data: MIXED_PROP_CALL_DATA
|
|
3057
|
+
});
|
|
3058
|
+
else if (property) checkCall(context, node);
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3010
3061
|
}
|
|
3011
3062
|
});
|
|
3012
3063
|
//#endregion
|
|
@@ -3023,7 +3074,7 @@ const meta$6 = {
|
|
|
3023
3074
|
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." },
|
|
3024
3075
|
schema: [Schema.toJsonSchemaDocument(Options).schema],
|
|
3025
3076
|
messages: {
|
|
3026
|
-
unsanctionedCell: MESSAGE$
|
|
3077
|
+
unsanctionedCell: MESSAGE$6,
|
|
3027
3078
|
missingCellTest: ABSENCE_MESSAGE
|
|
3028
3079
|
}
|
|
3029
3080
|
};
|
|
@@ -3084,7 +3135,7 @@ const testFileOutsideTestsDir = defineRule({
|
|
|
3084
3135
|
type: "problem",
|
|
3085
3136
|
docs: { description: "Test files outside src/ must live under a tests/ directory; free-standing test files have no sanctioned home." },
|
|
3086
3137
|
schema: [],
|
|
3087
|
-
messages: { strayTestFile: MESSAGE$
|
|
3138
|
+
messages: { strayTestFile: MESSAGE$6 }
|
|
3088
3139
|
},
|
|
3089
3140
|
create(context) {
|
|
3090
3141
|
if (isUnderSrc(context.filename)) return {};
|
|
@@ -3106,34 +3157,112 @@ const testFileOutsideTestsDir = defineRule({
|
|
|
3106
3157
|
});
|
|
3107
3158
|
//#endregion
|
|
3108
3159
|
//#region src/rules/test-suffix-outside-src.config.ts
|
|
3109
|
-
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3160
|
+
/** The lane word each lane is named by: the dotted segment just before `.test.ts`. */
|
|
3161
|
+
const LANE_WORDS = {
|
|
3162
|
+
behaviour: "integration",
|
|
3163
|
+
conformance: "conformance",
|
|
3164
|
+
differential: "differential",
|
|
3165
|
+
trace: "trace",
|
|
3166
|
+
runner: "runner"
|
|
3167
|
+
};
|
|
3168
|
+
/**
|
|
3169
|
+
* The lanes that take precedence over behaviour when a file is in several: its
|
|
3170
|
+
* name is a harness's. None outranks another, so a file importing several of
|
|
3171
|
+
* these harnesses may take the word of any one of them.
|
|
3172
|
+
*/
|
|
3173
|
+
const SPECIFIC_LANES = [
|
|
3174
|
+
"conformance",
|
|
3175
|
+
"differential",
|
|
3176
|
+
"trace"
|
|
3177
|
+
];
|
|
3178
|
+
const suffixOf = (lane) => `.${LANE_WORDS[lane]}.test.ts`;
|
|
3179
|
+
/** The name the effect-e2e runner collects: a bare `.e2e.ts` suffix, not a `.<word>.test.ts` lane word. */
|
|
3180
|
+
const E2E_SUFFIX = ".e2e.ts";
|
|
3181
|
+
const LANE_MISMATCH_ACTUAL = "a lane word that names no lane its imports select";
|
|
3182
|
+
const LANE_MISMATCH_FIX = "rename the file for a lane it imports: conformance-spec -> *.conformance.test.ts; differential-spec -> *.differential.test.ts; trace-spec -> *.trace.test.ts (a file importing several of these may take any of their words); effect-gherkin-spec alone -> *.integration.test.ts; a runner module alone, in a vitest-runner package -> *.runner.test.ts; a test registered with the effect-e2e runner -> <subject>.e2e.ts";
|
|
3183
|
+
const NO_LANE_EXPECTED = "an import of @systemfsoftware/effect-gherkin-spec, conformance-spec, differential-spec or trace-spec, or of the effect-e2e runner's test";
|
|
3184
|
+
const NO_LANE_ACTUAL = "a test outside src/ whose imports select no lane";
|
|
3185
|
+
const NO_LANE_FIX = "public surface or real medium -> drive it through makeFeature from @systemfsoftware/effect-gherkin-spec; parity -> differential-spec; span graph -> trace-spec; concurrent or stateful behaviour -> conformance-spec; runner suite -> register tests with test from @systemfsoftware/effect-e2e/runner in a <subject>.e2e.ts file; restated literal -> delete";
|
|
3186
|
+
const PROPERTY_OUTSIDE_SRC_EXPECTED = "a property test in src/<dir>/__tests__/<stem>.workflow.property.test.ts beside the workflow it covers";
|
|
3187
|
+
const PROPERTY_OUTSIDE_SRC_ACTUAL = "a test outside src/ whose only lane is the property lane";
|
|
3188
|
+
const PROPERTY_OUTSIDE_SRC_FIX = "move the property beside the workflow it covers, or drive the behaviour through a harness and name the file for that lane";
|
|
3189
|
+
const meta$4 = {
|
|
3190
|
+
type: "problem",
|
|
3191
|
+
docs: { description: "Outside src/, a test file is named for the lanes its imports select: the dotted segment before .test.ts is integration (the Gherkin harness), conformance, differential or trace, and when a file imports a harness besides the Gherkin one it takes that harness's word; a file importing several of the conformance, differential and trace harnesses may take any of their words. In a package whose lint settings declare the vitest-runner role, a test importing a Vitest runner module and no harness is in the runner lane and is named *.runner.test.ts. A file importing the effect-e2e runner's test or its namespace is in the e2e lane and is named <subject>.e2e.ts (the runner's collection, under tests/ outside tests/__fixtures__/); that name outranks every other lane word, and *.e2e.test.ts is not that name. A test whose imports select no lane, or only the property lane, is reported. The name triggers no requirement; the imports do." },
|
|
3192
|
+
schema: [],
|
|
3193
|
+
messages: {
|
|
3194
|
+
laneMismatch: MESSAGE$6,
|
|
3195
|
+
noLane: MESSAGE$6,
|
|
3196
|
+
propertyOutsideSrc: MESSAGE$6
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3112
3199
|
//#endregion
|
|
3113
3200
|
//#region src/rules/test-suffix-outside-src.ts
|
|
3201
|
+
/** The lane word: the dotted segment immediately before `.test.ts`; earlier segments name the subject. */
|
|
3202
|
+
const LANE_WORD = /\.([^.]+)\.test\.ts$/;
|
|
3203
|
+
/**
|
|
3204
|
+
* The lanes the file may be named for: every conformance, differential or trace
|
|
3205
|
+
* lane it selects (any one of their words will do), else behaviour, else the
|
|
3206
|
+
* runner lane a `vitest-runner` package grants.
|
|
3207
|
+
*/
|
|
3208
|
+
const nameableLanes = (lanes, runner) => {
|
|
3209
|
+
const specific = SPECIFIC_LANES.filter((lane) => lanes.has(lane));
|
|
3210
|
+
if (specific.length > 0) return specific;
|
|
3211
|
+
if (lanes.has("behaviour")) return ["behaviour"];
|
|
3212
|
+
return runner ? ["runner"] : [];
|
|
3213
|
+
};
|
|
3114
3214
|
const testSuffixOutsideSrc = defineRule({
|
|
3115
|
-
meta:
|
|
3116
|
-
type: "problem",
|
|
3117
|
-
docs: { description: "Outside src/, a test file ends .integration.test.ts, .differential.test.ts, .trace.test.ts, or .conformance.test.ts. A runner-driving package (Vitest itself, the fork, effect-spec-runtime) keeps plain *.test.ts runners and nested-run probes, so the lane taxonomy does not apply there." },
|
|
3118
|
-
schema: [],
|
|
3119
|
-
messages: { unsanctionedSuffix: MESSAGE$8 }
|
|
3120
|
-
},
|
|
3215
|
+
meta: meta$4,
|
|
3121
3216
|
create(context) {
|
|
3122
3217
|
const filename = context.filename;
|
|
3123
|
-
if (isUnderSrc(filename)) return {};
|
|
3124
|
-
if (isRawVitestPackage(filename)) return {};
|
|
3125
3218
|
const basename = basenameOf(filename);
|
|
3126
|
-
if (!isTestFile(basename)) return {};
|
|
3127
|
-
if (isBehaviourBasename(basename) || basename.endsWith(".differential.test.ts") || basename.endsWith(".trace.test.ts") || basename.endsWith(".conformance.test.ts")) return {};
|
|
3219
|
+
if (isUnderSrc(filename) || !(isTestFile(basename) || isE2eTestFile(filename))) return {};
|
|
3128
3220
|
return { Program(node) {
|
|
3221
|
+
const lanes = lanesOf(context);
|
|
3222
|
+
if (lanes.has("e2e")) {
|
|
3223
|
+
if (E2E_TEST_BASENAME.test(basename)) return;
|
|
3224
|
+
context.report({
|
|
3225
|
+
node,
|
|
3226
|
+
messageId: "laneMismatch",
|
|
3227
|
+
data: {
|
|
3228
|
+
name: basename,
|
|
3229
|
+
expected: E2E_SUFFIX,
|
|
3230
|
+
actual: LANE_MISMATCH_ACTUAL,
|
|
3231
|
+
fix: LANE_MISMATCH_FIX
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
const nameable = nameableLanes(lanes, isInRunnerLane(context));
|
|
3237
|
+
if (nameable.length === 0) {
|
|
3238
|
+
const property = lanes.has("property");
|
|
3239
|
+
context.report({
|
|
3240
|
+
node,
|
|
3241
|
+
messageId: property ? "propertyOutsideSrc" : "noLane",
|
|
3242
|
+
data: property ? {
|
|
3243
|
+
name: basename,
|
|
3244
|
+
expected: PROPERTY_OUTSIDE_SRC_EXPECTED,
|
|
3245
|
+
actual: PROPERTY_OUTSIDE_SRC_ACTUAL,
|
|
3246
|
+
fix: PROPERTY_OUTSIDE_SRC_FIX
|
|
3247
|
+
} : {
|
|
3248
|
+
name: basename,
|
|
3249
|
+
expected: NO_LANE_EXPECTED,
|
|
3250
|
+
actual: NO_LANE_ACTUAL,
|
|
3251
|
+
fix: NO_LANE_FIX
|
|
3252
|
+
}
|
|
3253
|
+
});
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
const word = LANE_WORD.exec(basename)?.[1];
|
|
3257
|
+
if (nameable.some((lane) => LANE_WORDS[lane] === word)) return;
|
|
3129
3258
|
context.report({
|
|
3130
3259
|
node,
|
|
3131
|
-
messageId: "
|
|
3260
|
+
messageId: "laneMismatch",
|
|
3132
3261
|
data: {
|
|
3133
3262
|
name: basename,
|
|
3134
|
-
expected:
|
|
3135
|
-
actual:
|
|
3136
|
-
fix:
|
|
3263
|
+
expected: nameable.map(suffixOf).join(" or "),
|
|
3264
|
+
actual: LANE_MISMATCH_ACTUAL,
|
|
3265
|
+
fix: LANE_MISMATCH_FIX
|
|
3137
3266
|
}
|
|
3138
3267
|
});
|
|
3139
3268
|
} };
|
|
@@ -3149,15 +3278,15 @@ const HELPER_FIX = "move it under tests/__fixtures__/ — *.schema.ts if it decl
|
|
|
3149
3278
|
const testsDirHelpersInFixtures = defineRule({
|
|
3150
3279
|
meta: {
|
|
3151
3280
|
type: "problem",
|
|
3152
|
-
docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__
|
|
3281
|
+
docs: { description: "Under tests/, the only non-test modules are helpers and fixtures, and they live inside tests/__fixtures__/; the .e2e.ts files the effect-e2e runner collects are tests, not helpers." },
|
|
3153
3282
|
schema: [],
|
|
3154
|
-
messages: { helperOutsideFixtures: MESSAGE$
|
|
3283
|
+
messages: { helperOutsideFixtures: MESSAGE$6 }
|
|
3155
3284
|
},
|
|
3156
3285
|
create(context) {
|
|
3157
3286
|
const filename = context.filename;
|
|
3158
3287
|
if (isUnderSrc(filename)) return {};
|
|
3159
3288
|
const basename = basenameOf(filename);
|
|
3160
|
-
if (isTestFile(basename)) return {};
|
|
3289
|
+
if (isTestFile(basename) || isE2eTestFile(filename)) return {};
|
|
3161
3290
|
const dirs = directoriesOf(filename);
|
|
3162
3291
|
if (!dirs.includes("tests")) return {};
|
|
3163
3292
|
if (dirs.includes("__fixtures__")) return {};
|
|
@@ -3184,7 +3313,7 @@ const meta$2 = {
|
|
|
3184
3313
|
type: "problem",
|
|
3185
3314
|
docs: { description: "Forbid package-level tests from relative-importing src or climbing into an internal folder" },
|
|
3186
3315
|
schema: [],
|
|
3187
|
-
messages: { sourceReachIn: MESSAGE$
|
|
3316
|
+
messages: { sourceReachIn: MESSAGE$6 }
|
|
3188
3317
|
};
|
|
3189
3318
|
//#endregion
|
|
3190
3319
|
//#region src/rules/tests-import-public-api.ts
|
|
@@ -3240,8 +3369,6 @@ const testsImportPublicApi = defineRule({
|
|
|
3240
3369
|
});
|
|
3241
3370
|
//#endregion
|
|
3242
3371
|
//#region src/rules/trace-test-requires-taxonomy.config.ts
|
|
3243
|
-
const HARNESS_PRESCRIPTION = "import { Suite, Case, Rel, Graph, Observe, Stimulus } from @systemfsoftware/trace-spec and terminate every case on a relation hold — .holds(Rel.all(Rel.exists(PlaceOrder), Rel.child(PlaceOrder, PaymentCapture)))";
|
|
3244
|
-
const MISSING_HARNESS_ACTUAL = "no trace-spec import found in a file named as a trace spec";
|
|
3245
3372
|
const HTTP_MEMBERS = {
|
|
3246
3373
|
body: true,
|
|
3247
3374
|
status: true,
|
|
@@ -3262,12 +3389,11 @@ const RAW_EMIT_ACTUAL = "a raw span emit call inside a trace spec";
|
|
|
3262
3389
|
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";
|
|
3263
3390
|
const meta$1 = {
|
|
3264
3391
|
type: "problem",
|
|
3265
|
-
docs: { description: "A
|
|
3392
|
+
docs: { description: "A test that imports @systemfsoftware/trace-spec (the trace lane) terminates its 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." },
|
|
3266
3393
|
schema: [],
|
|
3267
3394
|
messages: {
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
rawEmitCall: MESSAGE$8
|
|
3395
|
+
httpTermination: MESSAGE$6,
|
|
3396
|
+
rawEmitCall: MESSAGE$6
|
|
3271
3397
|
}
|
|
3272
3398
|
};
|
|
3273
3399
|
//#endregion
|
|
@@ -3304,7 +3430,6 @@ const rawEmitName = (node) => {
|
|
|
3304
3430
|
if (name === null || !isEmitCallee(name)) return null;
|
|
3305
3431
|
return name;
|
|
3306
3432
|
};
|
|
3307
|
-
const hasHarnessBinding = (node) => node.source.value === "@systemfsoftware/trace-spec" && node.specifiers.length > 0;
|
|
3308
3433
|
const literalValueOf = (node) => {
|
|
3309
3434
|
if (node === void 0 || node.type !== "Literal") return null;
|
|
3310
3435
|
return node.value;
|
|
@@ -3436,58 +3561,68 @@ const reportHttpTermination = (context, node) => {
|
|
|
3436
3561
|
const traceTestRequiresTaxonomy = defineRule({
|
|
3437
3562
|
meta: meta$1,
|
|
3438
3563
|
create(context) {
|
|
3439
|
-
|
|
3440
|
-
let hasHarnessImport = false;
|
|
3564
|
+
let trace = false;
|
|
3441
3565
|
return {
|
|
3442
|
-
|
|
3443
|
-
|
|
3566
|
+
Program() {
|
|
3567
|
+
trace = lanesOf(context).has("trace");
|
|
3444
3568
|
},
|
|
3445
3569
|
CallExpression(node) {
|
|
3570
|
+
if (!trace) return;
|
|
3446
3571
|
reportRawEmit(context, node);
|
|
3447
3572
|
reportHttpTermination(context, node);
|
|
3448
|
-
},
|
|
3449
|
-
"Program:exit"(node) {
|
|
3450
|
-
if (hasHarnessImport) return;
|
|
3451
|
-
context.report({
|
|
3452
|
-
node,
|
|
3453
|
-
messageId: "missingHarnessImport",
|
|
3454
|
-
data: {
|
|
3455
|
-
name: `a *.trace.test.ts without ${TRACE_SPEC_PACKAGE}`,
|
|
3456
|
-
expected: HARNESS_PRESCRIPTION,
|
|
3457
|
-
actual: MISSING_HARNESS_ACTUAL,
|
|
3458
|
-
fix: HARNESS_PRESCRIPTION
|
|
3459
|
-
}
|
|
3460
|
-
});
|
|
3461
3573
|
}
|
|
3462
3574
|
};
|
|
3463
3575
|
}
|
|
3464
3576
|
});
|
|
3465
3577
|
//#endregion
|
|
3466
3578
|
//#region src/rules/vitest-from-systemfsoftware-vitest.config.ts
|
|
3467
|
-
const MESSAGE = "{{name}} is forbidden. Expected: {{expected}}. Actual: {{actual}}. Fix: {{fix}}.";
|
|
3468
3579
|
const VIOLATION_NAME = "a value import from a foreign vitest runner";
|
|
3469
3580
|
const VIOLATION_EXPECTED = "every test value imported from @systemfsoftware/vitest, which re-exports all of Vitest";
|
|
3470
3581
|
const VIOLATION_ACTUAL = "a named, namespace, default, side-effect or dynamic value import from vitest or @effect/vitest";
|
|
3471
3582
|
const VIOLATION_FIX = "import it from @systemfsoftware/vitest instead of vitest or @effect/vitest";
|
|
3583
|
+
const RUNNER_LANES = [
|
|
3584
|
+
"conformance",
|
|
3585
|
+
"differential",
|
|
3586
|
+
"behaviour"
|
|
3587
|
+
];
|
|
3588
|
+
/** The lanes in which a plain runner call bypasses the harness. */
|
|
3589
|
+
const CALL_LANES = {
|
|
3590
|
+
conformance: true,
|
|
3591
|
+
differential: true,
|
|
3592
|
+
behaviour: false
|
|
3593
|
+
};
|
|
3594
|
+
const PRESCRIPTIONS = {
|
|
3595
|
+
conformance: HARNESS_PRESCRIPTION$1,
|
|
3596
|
+
differential: HARNESS_PRESCRIPTION,
|
|
3597
|
+
behaviour: "import { it } from @systemfsoftware/effect-gherkin-spec and build the suite with makeFeature({ it, layer })"
|
|
3598
|
+
};
|
|
3472
3599
|
const meta = {
|
|
3473
3600
|
type: "problem",
|
|
3474
|
-
docs: { description: "Every value a test uses from Vitest is imported from @systemfsoftware/vitest, which re-exports all of Vitest. Any value import from vitest or the upstream @effect/vitest — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal." },
|
|
3601
|
+
docs: { description: "Every value a test uses from Vitest is imported from @systemfsoftware/vitest, which re-exports all of Vitest. Any value import from vitest or the upstream @effect/vitest — named, namespace, default, side-effect or dynamic — is refused; a type-only import stays legal. A package whose config declares { role: \"vitest-runner\" } is the test framework itself, so its raw Vitest imports are its own lane and are not refused; the rule takes the declaration at its word, and a package that declares the role without being the runner is treated the same. In a test that imports the Gherkin, differential or conformance harness, that harness owns the runner: it, test and describe imported from any runner package are refused, and in the differential and conformance lanes so is a plain runner call." },
|
|
3475
3602
|
schema: [],
|
|
3476
|
-
messages: {
|
|
3603
|
+
messages: {
|
|
3604
|
+
vitestImport: MESSAGE$6,
|
|
3605
|
+
runnerImport: MESSAGE$6,
|
|
3606
|
+
rawRunnerCall: MESSAGE$6
|
|
3607
|
+
}
|
|
3477
3608
|
};
|
|
3478
3609
|
//#endregion
|
|
3479
3610
|
//#region src/rules/vitest-from-systemfsoftware-vitest.ts
|
|
3480
|
-
const isTypeOnly = (node) => {
|
|
3481
|
-
if (node.importKind === "type") return true;
|
|
3482
|
-
if (node.specifiers.length === 0) return false;
|
|
3483
|
-
return node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
|
|
3484
|
-
};
|
|
3485
3611
|
const isForeignVitestLiteral = (node) => node.type === "Literal" && typeof node.value === "string" && FOREIGN_VITEST_SOURCES[node.value] === true;
|
|
3612
|
+
/** The runner a value import specifier names; a string-literal import name never names one. */
|
|
3613
|
+
const runnerNameOf = (specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && specifier.imported.type === "Identifier" && RUNNER_NAMES.has(specifier.imported.name) ? specifier.imported.name : void 0;
|
|
3614
|
+
/** A value the declaration binds besides a runner: what still makes it a foreign vitest import. */
|
|
3615
|
+
const bindsANonRunnerValue = (node) => node.specifiers.length === 0 || node.specifiers.some((specifier) => runnerNameOf(specifier) === void 0 && !(specifier.type === "ImportSpecifier" && specifier.importKind === "type"));
|
|
3616
|
+
const memberObjectName = (callee) => callee.type === "MemberExpression" && callee.object.type === "Identifier" ? callee.object.name : void 0;
|
|
3617
|
+
const calleeName = (callee) => callee.type === "Identifier" ? callee.name : memberObjectName(callee);
|
|
3486
3618
|
const vitestFromSystemfsoftwareVitest = defineRule({
|
|
3487
3619
|
meta,
|
|
3488
3620
|
create(context) {
|
|
3489
|
-
|
|
3621
|
+
const runnerPackage = isRunnerPackage(context);
|
|
3622
|
+
const importedRunners = /* @__PURE__ */ new Set();
|
|
3623
|
+
let lane;
|
|
3490
3624
|
const report = (node) => {
|
|
3625
|
+
if (runnerPackage) return;
|
|
3491
3626
|
context.report({
|
|
3492
3627
|
node,
|
|
3493
3628
|
messageId: "vitestImport",
|
|
@@ -3499,12 +3634,56 @@ const vitestFromSystemfsoftwareVitest = defineRule({
|
|
|
3499
3634
|
}
|
|
3500
3635
|
});
|
|
3501
3636
|
};
|
|
3637
|
+
/**
|
|
3638
|
+
* In a harness lane the harness owns the runner: one finding per runner the
|
|
3639
|
+
* import names, and the lane arm is the only reporter of that runner.
|
|
3640
|
+
*/
|
|
3641
|
+
const reportRunnerImports = (node, harness) => {
|
|
3642
|
+
for (const specifier of node.specifiers) {
|
|
3643
|
+
const runner = runnerNameOf(specifier);
|
|
3644
|
+
if (runner === void 0) continue;
|
|
3645
|
+
importedRunners.add(specifier.local.name);
|
|
3646
|
+
context.report({
|
|
3647
|
+
node: specifier,
|
|
3648
|
+
messageId: "runnerImport",
|
|
3649
|
+
data: {
|
|
3650
|
+
name: `${runner} imported from ${node.source.value} in a ${harness} test`,
|
|
3651
|
+
expected: PRESCRIPTIONS[harness],
|
|
3652
|
+
actual: `a runner imported from a runner package bypasses the ${harness} harness`,
|
|
3653
|
+
fix: `delete the runner import; ${PRESCRIPTIONS[harness]}`
|
|
3654
|
+
}
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
};
|
|
3502
3658
|
return {
|
|
3659
|
+
Program() {
|
|
3660
|
+
const lanes = lanesOf(context);
|
|
3661
|
+
lane = RUNNER_LANES.find((harness) => lanes.has(harness));
|
|
3662
|
+
},
|
|
3503
3663
|
ImportDeclaration(node) {
|
|
3504
|
-
if (FOREIGN_VITEST_SOURCES[node.source.value] !== true) return;
|
|
3505
3664
|
if (isTypeOnly(node)) return;
|
|
3665
|
+
const source = node.source.value;
|
|
3666
|
+
const harness = FOREIGN_RUNNERS[source] === true ? lane : void 0;
|
|
3667
|
+
if (harness !== void 0) reportRunnerImports(node, harness);
|
|
3668
|
+
if (FOREIGN_VITEST_SOURCES[source] !== true) return;
|
|
3669
|
+
if (harness !== void 0 && !bindsANonRunnerValue(node)) return;
|
|
3506
3670
|
report(node);
|
|
3507
3671
|
},
|
|
3672
|
+
CallExpression(node) {
|
|
3673
|
+
if (lane === void 0 || !CALL_LANES[lane]) return;
|
|
3674
|
+
const name = calleeName(node.callee);
|
|
3675
|
+
if (name === void 0 || !RUNNER_NAMES.has(name) || importedRunners.has(name)) return;
|
|
3676
|
+
context.report({
|
|
3677
|
+
node,
|
|
3678
|
+
messageId: "rawRunnerCall",
|
|
3679
|
+
data: {
|
|
3680
|
+
name: `raw runner call (${name}) in a ${lane} test`,
|
|
3681
|
+
expected: PRESCRIPTIONS[lane],
|
|
3682
|
+
actual: `${name}(...) bypasses the ${lane} harness`,
|
|
3683
|
+
fix: `rewrite using ${PRESCRIPTIONS[lane]}`
|
|
3684
|
+
}
|
|
3685
|
+
});
|
|
3686
|
+
},
|
|
3508
3687
|
ImportExpression(node) {
|
|
3509
3688
|
if (!isForeignVitestLiteral(node.source)) return;
|
|
3510
3689
|
report(node);
|
|
@@ -3543,6 +3722,7 @@ const recommendedRules = {
|
|
|
3543
3722
|
[rule("src-property-test-cell")]: "error",
|
|
3544
3723
|
[rule("test-file-outside-tests-dir")]: "error",
|
|
3545
3724
|
[rule("test-suffix-outside-src")]: "error",
|
|
3725
|
+
[rule("e2e-tier-placement")]: "error",
|
|
3546
3726
|
[rule("behaviour-test-requires-gherkin")]: "error",
|
|
3547
3727
|
[rule("behaviour-exercises-use-case")]: "error",
|
|
3548
3728
|
[rule("behaviour-one-feature-per-file")]: "error",
|
|
@@ -3576,6 +3756,7 @@ var src_default = {
|
|
|
3576
3756
|
"src-property-test-cell": srcPropertyTestCell,
|
|
3577
3757
|
"test-file-outside-tests-dir": testFileOutsideTestsDir,
|
|
3578
3758
|
"test-suffix-outside-src": testSuffixOutsideSrc,
|
|
3759
|
+
"e2e-tier-placement": e2eTierPlacement,
|
|
3579
3760
|
"behaviour-test-requires-gherkin": behaviourTestRequiresGherkin,
|
|
3580
3761
|
"behaviour-exercises-use-case": behaviourExercisesUseCase,
|
|
3581
3762
|
"behaviour-one-feature-per-file": behaviourOneFeaturePerFile,
|