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.
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
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,
|
|
};
|
|
},
|
|
});
|