Vendor the anti-slop Oxlint plugin at tools/oxlint/anti-slop and register it in oxlint.config.ts, with Oxlint's own rule categories disabled so ESLint stays the general-purpose linter. Add scripts/anti-slop.mjs (bun run deslop) mirroring the React Doctor batch interface: next-batch, check-batch, active, release, top, file. Batch handoff directories now double as file claims shared across clones via ~/.openchamber/maintenance-claims, so concurrent maintenance batches from either pipeline never select the same file. Harden both scheduled maintenance flows: stop on a dirty worktree, stop on NO BATCH AVAILABLE, validate per package instead of workspace-wide, and pin react-doctor to 0.9.12. The anti-slop task command documents concrete good and bad fixes and forbids laundering types to satisfy a rule.
92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
import { defineRule } from "@oxlint/plugins";
|
|
|
|
import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
|
|
|
|
const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
|
|
|
|
function resolveVariable(
|
|
sourceCode: SourceCode,
|
|
identifier: ESTree.IdentifierReference,
|
|
): Variable | null {
|
|
let scope: Scope | null = sourceCode.getScope(identifier);
|
|
while (scope !== null) {
|
|
const variable = scope.set.get(identifier.name);
|
|
if (variable !== undefined) return variable;
|
|
scope = scope.upper;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function importedName(node: ESTree.Node): string | null {
|
|
if (node.type !== "ImportSpecifier") return null;
|
|
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
}
|
|
|
|
function isTestFrameworkObject(
|
|
sourceCode: SourceCode,
|
|
expression: ESTree.Expression,
|
|
): expression is ESTree.IdentifierReference {
|
|
if (expression.type !== "Identifier") return false;
|
|
if (
|
|
(expression.name === "vi" || expression.name === "jest") &&
|
|
sourceCode.isGlobalReference(expression)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const variable = resolveVariable(sourceCode, expression);
|
|
if (variable === null || variable.defs.length === 0) {
|
|
return expression.name === "vi" || expression.name === "jest";
|
|
}
|
|
return variable.defs.some((definition) => {
|
|
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") {
|
|
return false;
|
|
}
|
|
const source = definition.parent.source.value;
|
|
const name = importedName(definition.node);
|
|
return (source === "vitest" && name === "vi") || (source === "@jest/globals" && name === "jest");
|
|
});
|
|
}
|
|
|
|
function moduleMockCall(sourceCode: SourceCode, callee: ESTree.Expression): boolean {
|
|
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
|
|
if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
|
|
const property = callee.property;
|
|
const method = callee.computed
|
|
? property.type === "Literal" &&
|
|
(property.value === "doMock" ||
|
|
property.value === "mock" ||
|
|
property.value === "unstable_mockModule")
|
|
? property.value
|
|
: null
|
|
: property.type === "Identifier"
|
|
? property.name
|
|
: null;
|
|
return method !== null && moduleMockMethods.has(method);
|
|
}
|
|
|
|
/** Ban test framework module mocking in favor of real dependency seams. */
|
|
export const noModuleMockingRule = defineRule({
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description:
|
|
"Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces.",
|
|
},
|
|
messages: {
|
|
moduleMock:
|
|
"Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.",
|
|
},
|
|
},
|
|
createOnce(context) {
|
|
return {
|
|
CallExpression(node) {
|
|
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
|
|
if (moduleMockCall(context.sourceCode, node.callee)) {
|
|
context.report({ node, messageId: "moduleMock" });
|
|
}
|
|
},
|
|
};
|
|
},
|
|
});
|