chore(lint): vendor anti-slop oxlint plugin and add batched cleanup pipeline
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.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { defineRule } from "@oxlint/plugins";
|
||||
|
||||
import type { ESTree, SourceCode } from "@oxlint/plugins";
|
||||
|
||||
import { lexicalTypeParameterNames } from "../shared/lexical-type-parameters.ts";
|
||||
|
||||
type Parameter = ESTree.ParamPattern;
|
||||
type ParameterOwner =
|
||||
| ESTree.ArrowFunctionExpression
|
||||
| ESTree.Function
|
||||
| ESTree.TSCallSignatureDeclaration
|
||||
| ESTree.TSConstructSignatureDeclaration
|
||||
| ESTree.TSConstructorType
|
||||
| ESTree.TSFunctionType
|
||||
| ESTree.TSMethodSignature;
|
||||
|
||||
function parameterAnnotation(parameter: Parameter): ESTree.TSTypeAnnotation | null | undefined {
|
||||
if (parameter.type === "TSParameterProperty") {
|
||||
return parameterAnnotation(parameter.parameter);
|
||||
}
|
||||
if (parameter.type === "RestElement") {
|
||||
return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
|
||||
}
|
||||
if (parameter.type === "AssignmentPattern") {
|
||||
return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
|
||||
}
|
||||
return parameter.typeAnnotation;
|
||||
}
|
||||
|
||||
function parameterName(parameter: Parameter, sourceCode: SourceCode): string {
|
||||
return parameter.type === "Identifier"
|
||||
? parameter.name
|
||||
: sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
|
||||
}
|
||||
|
||||
/** Ban the broad object type on function inputs, including local aliases to object. */
|
||||
export const noObjectParametersRule = defineRule({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.",
|
||||
},
|
||||
messages: {
|
||||
objectParameter:
|
||||
"Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function.",
|
||||
},
|
||||
},
|
||||
createOnce(context) {
|
||||
const aliases = new Map<string, ESTree.TSType>();
|
||||
|
||||
const resolvesToObject = (
|
||||
type: ESTree.TSType,
|
||||
shadowedAliases: ReadonlySet<string>,
|
||||
visited = new Set<string>(),
|
||||
): boolean => {
|
||||
if (type.type === "TSObjectKeyword") return true;
|
||||
if (type.type === "TSParenthesizedType")
|
||||
return resolvesToObject(type.typeAnnotation, shadowedAliases, visited);
|
||||
if (type.type === "TSUnionType") {
|
||||
return type.types.some((member) =>
|
||||
resolvesToObject(member, shadowedAliases, visited),
|
||||
);
|
||||
}
|
||||
if (
|
||||
type.type !== "TSTypeReference" ||
|
||||
type.typeName.type !== "Identifier" ||
|
||||
(type.typeArguments !== null &&
|
||||
type.typeArguments !== undefined &&
|
||||
type.typeArguments.params.length > 0) ||
|
||||
visited.has(type.typeName.name) ||
|
||||
shadowedAliases.has(type.typeName.name)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const alias = aliases.get(type.typeName.name);
|
||||
if (alias === undefined) return false;
|
||||
const nextVisited = new Set(visited);
|
||||
nextVisited.add(type.typeName.name);
|
||||
return resolvesToObject(alias, shadowedAliases, nextVisited);
|
||||
};
|
||||
|
||||
const checkParameters = (node: ParameterOwner) => {
|
||||
const shadowedAliases = lexicalTypeParameterNames(
|
||||
node,
|
||||
context.sourceCode.visitorKeys,
|
||||
);
|
||||
for (const parameter of node.params) {
|
||||
const annotation = parameterAnnotation(parameter);
|
||||
if (annotation === null || annotation === undefined) continue;
|
||||
if (!resolvesToObject(annotation.typeAnnotation, shadowedAliases)) continue;
|
||||
context.report({
|
||||
node: annotation.typeAnnotation,
|
||||
messageId: "objectParameter",
|
||||
data: { parameter: parameterName(parameter, context.sourceCode) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
aliases.clear();
|
||||
for (const statement of node.body) {
|
||||
const declaration =
|
||||
statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
||||
if (
|
||||
declaration?.type === "TSTypeAliasDeclaration" &&
|
||||
(declaration.typeParameters === null || declaration.typeParameters === undefined)
|
||||
) {
|
||||
aliases.set(declaration.id.name, declaration.typeAnnotation);
|
||||
}
|
||||
}
|
||||
},
|
||||
ArrowFunctionExpression: checkParameters,
|
||||
FunctionDeclaration: checkParameters,
|
||||
FunctionExpression: checkParameters,
|
||||
TSCallSignatureDeclaration: checkParameters,
|
||||
TSConstructSignatureDeclaration: checkParameters,
|
||||
TSConstructorType: checkParameters,
|
||||
TSDeclareFunction: checkParameters,
|
||||
TSEmptyBodyFunctionExpression: checkParameters,
|
||||
TSFunctionType: checkParameters,
|
||||
TSMethodSignature: checkParameters,
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user