assets/anti-slop/effect/index.ts
import { eslintCompatPlugin } from "@oxlint/plugins";
import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts";
/** Opt-in Oxlint rules for Effect service and Layer architecture. */
const antiSlopEffectPlugin = eslintCompatPlugin({
meta: { name: "anti-slop-effect" },
rules: {
"no-service-constructor-imports": noServiceConstructorImportsRule,
},
});
export default antiSlopEffectPlugin;
assets/anti-slop/effect/rules/no-service-constructor-imports.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
const SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
function isProjectLocalImport(source: string): boolean {
return source.startsWith("./") || source.startsWith("../");
}
function getImportedName(specifier: ESTree.ImportSpecifier): string {
if (specifier.imported.type === "Identifier") return specifier.imported.name;
return specifier.imported.value;
}
/** Keep dependency-bearing Effect service constructors local to their owning capability modules. */
export const noServiceConstructorImportsRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow project-local make<CapabilityName> imports outside test and spec files.",
},
messages: {
serviceConstructorImport:
'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.',
},
},
create(context) {
const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/"));
return {
ImportDeclaration(node) {
if (isTestFile || !isProjectLocalImport(node.source.value)) return;
for (const specifier of node.specifiers) {
if (specifier.type !== "ImportSpecifier") continue;
const importedName = getImportedName(specifier);
if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)) continue;
context.report({
node: specifier,
messageId: "serviceConstructorImport",
data: { name: importedName },
});
}
},
};
},
});
assets/anti-slop/index.ts
import { eslintCompatPlugin } from "@oxlint/plugins";
import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts";
import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts";
import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts";
import { noModuleMockingRule } from "./rules/no-module-mocking.ts";
import { noObjectParametersRule } from "./rules/no-object-parameters.ts";
import { noReflectApplyRule } from "./rules/no-reflect-apply.ts";
import { noReflectGetRule } from "./rules/no-reflect-get.ts";
import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts";
import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts";
import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts";
import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts";
import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts";
import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts";
import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts";
import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
const antiSlopPlugin = eslintCompatPlugin({
meta: { name: "anti-slop" },
rules: {
"no-chained-type-assertions": noChainedTypeAssertionsRule,
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
"no-known-value-widening": noKnownValueWideningRule,
"no-module-mocking": noModuleMockingRule,
"no-object-parameters": noObjectParametersRule,
"no-reflect-apply": noReflectApplyRule,
"no-reflect-get": noReflectGetRule,
"no-runtime-typeof": noRuntimeTypeofRule,
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
"no-unknown-parameters": noUnknownParametersRule,
"no-unknown-returns": noUnknownReturnsRule,
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
"no-widen-then-assert": noWidenThenAssertRule,
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
},
});
export default antiSlopPlugin;
assets/anti-slop/rules/no-chained-type-assertions.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression {
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
}
function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression {
let current = expression;
while (current.type === "ParenthesizedExpression") {
current = current.expression;
}
return current;
}
function isConstAssertion(node: TypeAssertionExpression): boolean {
const { typeAnnotation } = node;
return (
typeAnnotation.type === "TSTypeReference" &&
typeAnnotation.typeName.type === "Identifier" &&
typeAnnotation.typeName.name === "const"
);
}
function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean {
let current: ESTree.Expression = node;
let parent = node.parent;
while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
current = parent;
parent = parent.parent;
}
return !isTypeAssertionExpression(parent) || parent.expression !== current;
}
function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean {
let assertionCount = 0;
let hasNonConstAssertion = false;
let current: ESTree.Expression = node;
while (isTypeAssertionExpression(current)) {
assertionCount += 1;
hasNonConstAssertion ||= !isConstAssertion(current);
current = unwrapParenthesizedExpression(current.expression);
}
return assertionCount > 1 && hasNonConstAssertion;
}
/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
export const noChainedTypeAssertionsRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
},
messages: {
chained:
"This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
},
},
createOnce(context) {
const checkTypeAssertion = (node: TypeAssertionExpression) => {
if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
context.report({ node, messageId: "chained" });
};
return {
TSAsExpression: checkTypeAssertion,
TSTypeAssertion: checkTypeAssertion,
};
},
});
assets/anti-slop/rules/no-conditional-empty-object-spread.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
function unwrapParentheses(node: ESTree.Expression): ESTree.Expression {
let current = node;
while (current.type === "ParenthesizedExpression") {
current = current.expression;
}
return current;
}
function isEmptyObjectExpression(node: ESTree.Expression): boolean {
return node.type === "ObjectExpression" && node.properties.length === 0;
}
function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean {
const conditional = unwrapParentheses(node);
return (
conditional.type === "ConditionalExpression" &&
(isEmptyObjectExpression(conditional.consequent) ||
isEmptyObjectExpression(conditional.alternate))
);
}
/** Ban conditional empty-object spreads without changing their omission semantics. */
export const noConditionalEmptyObjectSpreadRule = defineRule({
meta: {
type: "suggestion",
docs: {
description:
"Disallow object spreads that conditionally spread an empty object to omit fields.",
},
messages: {
avoid:
"This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
},
},
createOnce(context) {
return {
SpreadElement(node) {
if (node.parent.type !== "ObjectExpression") return;
if (isConditionalEmptyObjectSpread(node.argument)) {
context.report({ node, messageId: "avoid" });
}
},
};
},
});
assets/anti-slop/rules/no-known-value-widening.ts
import { defineRule } from "@oxlint/plugins";
import {
classifyUnsafeDictionaryValue,
classifyWideningTarget,
createTypeEnvironment,
isKnownEvidenceExpression,
type TypeEnvironment,
type WideningTarget,
} from "../shared/dictionary-types.ts";
import {
containsUnknownType,
functionParameterBindingName,
functionParameterTypeAnnotation,
} from "../shared/function-parameters.ts";
import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
type FunctionExpression = ESTree.ArrowFunctionExpression | ESTree.Function;
function unwrapExpression(expression: ESTree.Expression): ESTree.Expression {
let current = expression;
while (
current.type === "ParenthesizedExpression" ||
current.type === "TSAsExpression" ||
current.type === "TSSatisfiesExpression" ||
current.type === "TSTypeAssertion" ||
current.type === "TSNonNullExpression"
) {
current = current.expression;
}
return current;
}
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 variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
if (variable.defs.length !== 1) return null;
const [definition] = variable.defs;
return definition?.type === "Variable" && definition.node.type === "VariableDeclarator"
? definition.node
: null;
}
function isStableConstVariable(variable: Variable, declarator: ESTree.VariableDeclarator): boolean {
return (
declarator.parent.type === "VariableDeclaration" &&
declarator.parent.kind === "const" &&
variable.references.every((reference) => reference.init || !reference.isWrite())
);
}
function hasKnownEvidence(
sourceCode: SourceCode,
expression: ESTree.Expression,
visitedVariables = new Set<Variable>(),
): boolean {
if (isKnownEvidenceExpression(expression)) return true;
const unwrapped = unwrapExpression(expression);
if (unwrapped.type !== "Identifier") return false;
const variable = resolveVariable(sourceCode, unwrapped);
if (variable === null || visitedVariables.has(variable)) return false;
const declarator = variableDeclarator(variable);
if (
declarator === null ||
declarator.init === null ||
!isStableConstVariable(variable, declarator)
) {
return false;
}
visitedVariables.add(variable);
return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
}
function isFunctionExpression(node: ESTree.Node): node is FunctionExpression {
return (
node.type === "ArrowFunctionExpression" ||
node.type === "FunctionDeclaration" ||
node.type === "FunctionExpression" ||
node.type === "TSDeclareFunction" ||
node.type === "TSEmptyBodyFunctionExpression"
);
}
function localFunctionForCall(
sourceCode: SourceCode,
callee: ESTree.Expression,
): FunctionExpression | null {
const unwrapped = unwrapExpression(callee);
if (isFunctionExpression(unwrapped)) return unwrapped;
if (unwrapped.type !== "Identifier") return null;
const variable = resolveVariable(sourceCode, unwrapped);
if (variable === null || variable.defs.length !== 1) return null;
const [definition] = variable.defs;
if (definition === undefined) return null;
if (definition.type === "FunctionName" && isFunctionExpression(definition.node)) {
return definition.node;
}
if (definition.type !== "Variable" || definition.node.type !== "VariableDeclarator") {
return null;
}
const initializer = definition.node.init;
if (initializer === null) return null;
const unwrappedInitializer = unwrapExpression(initializer);
return isFunctionExpression(unwrappedInitializer) ? unwrappedInitializer : null;
}
function variableTypeAnnotation(
sourceCode: SourceCode,
variable: Variable,
): ESTree.TSTypeAnnotation | null {
if (variable.defs.length !== 1) return null;
const [definition] = variable.defs;
if (definition === undefined) return null;
if (
definition.type === "Variable" &&
definition.node.type === "VariableDeclarator" &&
definition.node.id.type === "Identifier"
) {
return definition.node.id.typeAnnotation ?? null;
}
if (definition.type !== "Parameter" || !isFunctionExpression(definition.node)) {
return null;
}
const parameter = definition.node.params.find(
(candidate) =>
functionParameterBindingName(candidate, sourceCode) === variable.name,
);
return parameter === undefined ? null : (functionParameterTypeAnnotation(parameter) ?? null);
}
function hasInformativeType(
type: ESTree.TSType,
environment: TypeEnvironment,
): boolean {
return classifyUnsafeDictionaryValue(type, environment) === null;
}
function hasKnownCallArgumentEvidence(
sourceCode: SourceCode,
expression: ESTree.Expression,
environment: TypeEnvironment,
visitedVariables = new Set<Variable>(),
): boolean {
if (expression.type === "ParenthesizedExpression" || expression.type === "TSNonNullExpression") {
return hasKnownCallArgumentEvidence(
sourceCode,
expression.expression,
environment,
visitedVariables,
);
}
if (expression.type === "TSAsExpression" || expression.type === "TSTypeAssertion") {
return hasInformativeType(expression.typeAnnotation, environment);
}
if (expression.type === "TSSatisfiesExpression") {
return hasKnownCallArgumentEvidence(
sourceCode,
expression.expression,
environment,
visitedVariables,
);
}
if (expression.type === "CallExpression") {
const owner = localFunctionForCall(sourceCode, expression.callee);
const returnType = owner?.returnType?.typeAnnotation;
return returnType !== undefined && hasInformativeType(returnType, environment);
}
if (expression.type !== "Identifier") return isKnownEvidenceExpression(expression);
const variable = resolveVariable(sourceCode, expression);
if (variable === null || visitedVariables.has(variable)) return false;
const annotation = variableTypeAnnotation(sourceCode, variable);
if (annotation !== null) {
return hasInformativeType(annotation.typeAnnotation, environment);
}
const declarator = variableDeclarator(variable);
if (
declarator === null ||
declarator.init === null ||
!isStableConstVariable(variable, declarator)
) {
return false;
}
visitedVariables.add(variable);
return hasKnownCallArgumentEvidence(
sourceCode,
declarator.init,
environment,
visitedVariables,
);
}
function typePredicateSubjectIndex(
sourceCode: SourceCode,
owner: FunctionExpression,
): number | null {
const predicate = owner.returnType?.typeAnnotation;
if (predicate?.type !== "TSTypePredicate" || predicate.parameterName.type !== "Identifier") {
return null;
}
const predicateParameterName = predicate.parameterName.name;
const index = owner.params.findIndex(
(parameter) =>
functionParameterBindingName(parameter, sourceCode) === predicateParameterName,
);
return index === -1 ? null : index;
}
function annotationTarget(
annotation: ESTree.TSTypeAnnotation | null | undefined,
environment: TypeEnvironment,
): WideningTarget | null {
return annotation === null || annotation === undefined
? null
: classifyWideningTarget(annotation.typeAnnotation, environment);
}
function enclosingFunction(node: ESTree.Node): FunctionExpression | null {
let current: ESTree.Node | null = node.parent;
while (current !== null && current.type !== "Program") {
if (
current.type === "ArrowFunctionExpression" ||
current.type === "FunctionDeclaration" ||
current.type === "FunctionExpression"
) {
return current;
}
current = current.parent;
}
return null;
}
function sourceKeyName(sourceCode: SourceCode, key: ESTree.PropertyKey): string {
if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
if (key.type === "Literal") return String(key.value);
return sourceCode.getText(key);
}
function functionName(sourceCode: SourceCode, owner: FunctionExpression | null): string {
if (owner === null) return "anonymous function";
if (owner.id !== null) return owner.id.name;
const parent = owner.parent;
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier")
return parent.id.name;
if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
return "anonymous function";
}
function isEmptyObjectExpression(expression: ESTree.Expression): boolean {
const unwrapped = unwrapExpression(expression);
return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
}
function isDictionaryAccumulatorTarget(destination: WideningTarget): boolean {
return destination.kind === "open dictionary" || destination.kind === "generic container";
}
function hasParentAssertion(node: ESTree.Node): boolean {
return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
}
/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
export const noKnownValueWideningRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
},
messages: {
widening:
"The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract.",
},
},
createOnce(context) {
let environment: TypeEnvironment | null = null;
const reportFlow = (
expression: ESTree.Expression,
destination: WideningTarget | null,
subject: string,
) => {
if (destination === null) return;
if (
isDictionaryAccumulatorTarget(destination) &&
isEmptyObjectExpression(expression)
) {
return;
}
if (!hasKnownEvidence(context.sourceCode, expression)) return;
context.report({
node: expression,
messageId: "widening",
data: { subject, target: destination.kind },
});
};
const targetFromAnnotation = (annotation: ESTree.TSTypeAnnotation | null | undefined) =>
environment === null ? null : annotationTarget(annotation, environment);
return {
Program(node) {
environment = createTypeEnvironment(
node,
context.sourceCode.visitorKeys,
);
},
VariableDeclarator(node) {
if (node.init === null || node.id.type !== "Identifier") return;
reportFlow(
node.init,
targetFromAnnotation(node.id.typeAnnotation),
`binding \`${node.id.name}\``,
);
},
PropertyDefinition(node) {
if (node.value === null) return;
reportFlow(
node.value,
targetFromAnnotation(node.typeAnnotation),
`property \`${sourceKeyName(context.sourceCode, node.key)}\``,
);
},
AccessorProperty(node) {
if (node.value === null) return;
reportFlow(
node.value,
targetFromAnnotation(node.typeAnnotation),
`property \`${sourceKeyName(context.sourceCode, node.key)}\``,
);
},
AssignmentExpression(node) {
if (node.operator !== "=" || node.left.type !== "Identifier") return;
const variable = resolveVariable(context.sourceCode, node.left);
if (variable === null) return;
const declarator = variableDeclarator(variable);
if (declarator === null || declarator.id.type !== "Identifier") return;
reportFlow(
node.right,
targetFromAnnotation(declarator.id.typeAnnotation),
`binding \`${declarator.id.name}\``,
);
},
CallExpression(node) {
if (environment === null) return;
const owner = localFunctionForCall(context.sourceCode, node.callee);
if (owner === null) return;
const parameterIndex = typePredicateSubjectIndex(context.sourceCode, owner);
if (parameterIndex === null) return;
const parameter = owner.params[parameterIndex];
const argument = node.arguments[parameterIndex];
if (parameter === undefined || argument === undefined || argument.type === "SpreadElement") {
return;
}
const parameterAnnotation = functionParameterTypeAnnotation(parameter);
if (
parameterAnnotation === null ||
parameterAnnotation === undefined ||
!containsUnknownType(parameterAnnotation.typeAnnotation)
) {
return;
}
if (
!hasKnownCallArgumentEvidence(
context.sourceCode,
argument,
environment,
)
) {
return;
}
context.report({
node: argument,
messageId: "widening",
data: {
subject: `argument for parameter \`${functionParameterBindingName(parameter, context.sourceCode)}\` of \`${functionName(context.sourceCode, owner)}\``,
target: "unknown",
},
});
},
ReturnStatement(node) {
if (node.argument === null) return;
const owner = enclosingFunction(node);
reportFlow(
node.argument,
targetFromAnnotation(owner?.returnType),
`return value of \`${functionName(context.sourceCode, owner)}\``,
);
},
ArrowFunctionExpression(node) {
if (node.body.type === "BlockStatement") return;
reportFlow(
node.body,
targetFromAnnotation(node.returnType),
`return value of \`${functionName(context.sourceCode, node)}\``,
);
},
TSAsExpression(node) {
if (environment === null || hasParentAssertion(node)) return;
reportFlow(
node.expression,
classifyWideningTarget(node.typeAnnotation, environment),
"assertion",
);
},
TSTypeAssertion(node) {
if (environment === null || hasParentAssertion(node)) return;
reportFlow(
node.expression,
classifyWideningTarget(node.typeAnnotation, environment),
"assertion",
);
},
};
},
});
assets/anti-slop/rules/no-module-mocking.ts
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" });
}
},
};
},
});
assets/anti-slop/rules/no-object-parameters.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
import {
functionParameterBindingName,
functionParameterTypeAnnotation,
} from "../shared/function-parameters.ts";
import {
createTypeAliasEnvironment,
resolvedTypeMatches,
type TypeAliasEnvironment,
} from "../shared/type-alias-resolution.ts";
type ParameterOwner =
| ESTree.ArrowFunctionExpression
| ESTree.Function
| ESTree.TSCallSignatureDeclaration
| ESTree.TSConstructSignatureDeclaration
| ESTree.TSConstructorType
| ESTree.TSFunctionType
| ESTree.TSMethodSignature;
/** 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) {
let environment: TypeAliasEnvironment | null = null;
const resolvesToObject = (type: ESTree.TSType): boolean =>
environment !== null &&
resolvedTypeMatches(type, environment, (resolved, matches) => {
if (resolved.type === "TSObjectKeyword") return true;
if (resolved.type === "TSParenthesizedType") {
return matches(resolved.typeAnnotation);
}
return (
resolved.type === "TSUnionType" && resolved.types.some(matches)
);
});
const checkParameters = (node: ParameterOwner) => {
for (const parameter of node.params) {
const annotation = functionParameterTypeAnnotation(parameter);
if (annotation === null || annotation === undefined) continue;
if (!resolvesToObject(annotation.typeAnnotation)) continue;
context.report({
node: annotation.typeAnnotation,
messageId: "objectParameter",
data: { parameter: functionParameterBindingName(parameter, context.sourceCode) },
});
}
};
return {
Program(node) {
environment = createTypeAliasEnvironment(
node,
context.sourceCode.visitorKeys,
);
},
ArrowFunctionExpression: checkParameters,
FunctionDeclaration: checkParameters,
FunctionExpression: checkParameters,
TSCallSignatureDeclaration: checkParameters,
TSConstructSignatureDeclaration: checkParameters,
TSConstructorType: checkParameters,
TSDeclareFunction: checkParameters,
TSEmptyBodyFunctionExpression: checkParameters,
TSFunctionType: checkParameters,
TSMethodSignature: checkParameters,
};
},
});
assets/anti-slop/rules/no-reflect-apply.ts
import { defineRule } from "@oxlint/plugins";
import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts";
/** Ban Reflect.apply, which bypasses ordinary typed function calls. */
export const noReflectApplyRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface.",
},
messages: {
reflectApply:
"Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface.",
},
},
createOnce(context) {
return {
CallExpression(node) {
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "apply")) {
context.report({ node, messageId: "reflectApply" });
}
},
};
},
});
assets/anti-slop/rules/no-reflect-get.ts
import { defineRule } from "@oxlint/plugins";
import { isGlobalReflectMethodCall } from "../shared/reflect-method.ts";
/** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
export const noReflectGetRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow Reflect.get; use typed property access or parse dynamic input into a domain type.",
},
messages: {
reflectGet:
"Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it.",
},
},
createOnce(context) {
return {
CallExpression(node) {
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
if (isGlobalReflectMethodCall(context.sourceCode, node.callee, "get")) {
context.report({ node, messageId: "reflectGet" });
}
},
};
},
});
assets/anti-slop/rules/no-runtime-typeof.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
type RuntimeFunction = ESTree.ArrowFunctionExpression | ESTree.Function;
function isRuntimeFunction(node: ESTree.Node): node is RuntimeFunction {
return (
node.type === "ArrowFunctionExpression" ||
node.type === "FunctionDeclaration" ||
node.type === "FunctionExpression"
);
}
function isInsideTypeGuard(node: ESTree.Node): boolean {
let current: ESTree.Node | null = node.parent;
while (current !== null && current.type !== "Program") {
if (isRuntimeFunction(current)) {
return current.returnType?.typeAnnotation.type === "TSTypePredicate";
}
current = current.parent;
}
return false;
}
/** Return whether typeof safely probes for the existence of a possibly absent binding. */
function isExistenceProbe(node: ESTree.UnaryExpression): boolean {
const parent = node.parent;
if (parent.type !== "BinaryExpression") return false;
if (!["===", "!==", "==", "!="].includes(parent.operator)) return false;
const other = parent.left === node ? parent.right : parent.left;
return other.type === "Literal" && other.value === "undefined";
}
/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
export const noRuntimeTypeofRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
},
messages: {
runtimeTypeof:
"A `typeof` check narrows a representation without establishing its contract. Parse input at its I/O boundary, then branch on the domain value.",
},
schema: [
{
type: "object",
properties: {
allowInTypeGuards: { type: "boolean" },
},
additionalProperties: false,
},
],
defaultOptions: [{ allowInTypeGuards: false }],
},
createOnce(context) {
return {
UnaryExpression(node) {
const option = context.options?.[0];
const allowInTypeGuards =
typeof option === "object" &&
option !== null &&
!Array.isArray(option) &&
option.allowInTypeGuards === true;
if (
node.operator === "typeof" &&
!isExistenceProbe(node) &&
(!allowInTypeGuards || !isInsideTypeGuard(node))
) {
context.report({ node, messageId: "runtimeTypeof" });
}
},
};
},
});
assets/anti-slop/rules/no-shape-in-symbol-names.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
const FORBIDDEN_SYMBOL_NAME = "shape";
function containsForbiddenSymbolName(name: string): boolean {
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
}
/** Return whether an identifier names a statically accessed member owned by another value. */
function isBorrowedMemberName(node: ESTree.Node): boolean {
const parent = node.parent;
if (parent === null || parent.type !== "MemberExpression") return false;
return parent.property === node && parent.computed === false;
}
/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
export const noForbiddenTermInSymbolNamesRule = defineRule({
meta: {
type: "problem",
docs: {
description:
'Disallow the case-insensitive substring "shape" in JavaScript, TypeScript, private, and JSX symbol names.',
},
messages: {
forbiddenSymbolName:
'Rename symbol "{{name}}" for its domain role; "shape" describes structure rather than ownership.',
},
},
createOnce(context) {
const reportForbiddenSymbolName = (node: ESTree.Node & { name: string }) => {
if (!containsForbiddenSymbolName(node.name) || isBorrowedMemberName(node)) return;
context.report({
node,
messageId: "forbiddenSymbolName",
data: { name: node.name },
});
};
return {
Identifier: reportForbiddenSymbolName,
PrivateIdentifier: reportForbiddenSymbolName,
JSXIdentifier: reportForbiddenSymbolName,
};
},
});
assets/anti-slop/rules/no-unknown-parameters.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
import {
containsUnknownType,
functionParameterBindingName,
functionParameterTypeAnnotation,
} from "../shared/function-parameters.ts";
type ParameterOwner =
| ESTree.ArrowFunctionExpression
| ESTree.Function
| ESTree.TSCallSignatureDeclaration
| ESTree.TSConstructSignatureDeclaration
| ESTree.TSConstructorType
| ESTree.TSFunctionType
| ESTree.TSMethodSignature;
function isTypePredicateSubject(owner: ParameterOwner, parameterName: string): boolean {
const predicate = owner.returnType?.typeAnnotation;
return (
predicate?.type === "TSTypePredicate" &&
predicate.parameterName.type === "Identifier" &&
predicate.parameterName.name === parameterName
);
}
/** Disallow unknown inputs except explicitly named error-cause enrichment. */
export const noUnknownParametersRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow explicitly unknown function parameters except `cause` and type-predicate subjects; decode unknown input at its I/O boundary instead.",
},
messages: {
unknownParameter:
"Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function.",
},
},
createOnce(context) {
const checkParameters = (node: ParameterOwner) => {
for (const parameter of node.params) {
const annotation = functionParameterTypeAnnotation(parameter);
if (annotation === null || annotation === undefined) continue;
if (!containsUnknownType(annotation.typeAnnotation)) continue;
const name = functionParameterBindingName(parameter, context.sourceCode);
if (name === "cause" || isTypePredicateSubject(node, name)) continue;
context.report({
node: annotation.typeAnnotation,
messageId: "unknownParameter",
data: { parameter: name },
});
}
};
return {
ArrowFunctionExpression: checkParameters,
FunctionDeclaration: checkParameters,
FunctionExpression: checkParameters,
TSCallSignatureDeclaration: checkParameters,
TSConstructSignatureDeclaration: checkParameters,
TSConstructorType: checkParameters,
TSDeclareFunction: checkParameters,
TSEmptyBodyFunctionExpression: checkParameters,
TSFunctionType: checkParameters,
TSMethodSignature: checkParameters,
};
},
});
assets/anti-slop/rules/no-unknown-returns.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
import {
createTypeAliasEnvironment,
resolvedTypeMatches,
type TypeAliasEnvironment,
} from "../shared/type-alias-resolution.ts";
type FunctionWithReturnType =
| ESTree.ArrowFunctionExpression
| ESTree.Function
| ESTree.TSCallSignatureDeclaration
| ESTree.TSConstructSignatureDeclaration
| ESTree.TSConstructorType
| ESTree.TSFunctionType
| ESTree.TSMethodSignature;
/** Ban function contracts that return unknown instead of a parsed domain type. */
export const noUnknownReturnsRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow functions whose explicit return contract is unknown or Promise<unknown>.",
},
messages: {
unknownReturn:
"This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.",
},
},
createOnce(context) {
let environment: TypeAliasEnvironment | null = null;
const resolvesToUnknown = (type: ESTree.TSType): boolean =>
environment !== null &&
resolvedTypeMatches(type, environment, (resolved, matches) => {
if (resolved.type === "TSUnknownKeyword") return true;
if (resolved.type === "TSParenthesizedType") {
return matches(resolved.typeAnnotation);
}
if (resolved.type === "TSUnionType") return resolved.types.some(matches);
if (
resolved.type !== "TSTypeReference" ||
resolved.typeName.type !== "Identifier" ||
(resolved.typeName.name !== "Promise" &&
resolved.typeName.name !== "PromiseLike")
) {
return false;
}
const value = resolved.typeArguments?.params[0];
return value !== undefined && matches(value);
});
const checkReturnType = (node: FunctionWithReturnType) => {
const annotation = node.returnType;
if (annotation === null || annotation === undefined) return;
if (!resolvesToUnknown(annotation.typeAnnotation)) return;
context.report({ node: annotation.typeAnnotation, messageId: "unknownReturn" });
};
return {
Program(node) {
environment = createTypeAliasEnvironment(
node,
context.sourceCode.visitorKeys,
);
},
ArrowFunctionExpression: checkReturnType,
FunctionDeclaration: checkReturnType,
FunctionExpression: checkReturnType,
TSCallSignatureDeclaration: checkReturnType,
TSConstructSignatureDeclaration: checkReturnType,
TSConstructorType: checkReturnType,
TSDeclareFunction: checkReturnType,
TSEmptyBodyFunctionExpression: checkReturnType,
TSFunctionType: checkReturnType,
TSMethodSignature: checkReturnType,
};
},
});
assets/anti-slop/rules/no-unknown-type-aliases.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree } from "@oxlint/plugins";
import {
createTypeAliasEnvironment,
resolvedTypeMatches,
type TypeAliasEnvironment,
} from "../shared/type-alias-resolution.ts";
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
export const noUnknownTypeAliasesRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
},
messages: {
unknownAlias:
"Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type.",
},
},
createOnce(context) {
let environment: TypeAliasEnvironment | null = null;
const resolvesToUnknown = (type: ESTree.TSType): boolean =>
environment !== null &&
resolvedTypeMatches(type, environment, (resolved, matches) => {
if (resolved.type === "TSUnknownKeyword") return true;
if (resolved.type === "TSParenthesizedType") {
return matches(resolved.typeAnnotation);
}
return resolved.type === "TSUnionType" && resolved.types.some(matches);
});
return {
Program(node) {
environment = createTypeAliasEnvironment(
node,
context.sourceCode.visitorKeys,
);
},
TSTypeAliasDeclaration(node) {
if (!resolvesToUnknown(node.typeAnnotation)) return;
context.report({
node: node.id,
messageId: "unknownAlias",
data: { alias: node.id.name },
});
},
};
},
});
assets/anti-slop/rules/no-unsafe-dictionary-type.ts
import { defineRule } from "@oxlint/plugins";
import {
classifyUnsafeDictionary,
classifyUnsafeDictionaryValue,
createTypeEnvironment,
type TypeEnvironment,
} from "../shared/dictionary-types.ts";
import { visibleTypeAlias } from "../shared/type-alias-resolution.ts";
import type { ESTree } from "@oxlint/plugins";
const typeNodeKinds: ReadonlySet<string> = new Set([
"JSDocNonNullableType",
"JSDocNullableType",
"JSDocUnknownType",
"TSAnyKeyword",
"TSArrayType",
"TSBigIntKeyword",
"TSBooleanKeyword",
"TSConditionalType",
"TSConstructorType",
"TSFunctionType",
"TSImportType",
"TSIndexedAccessType",
"TSInferType",
"TSIntersectionType",
"TSIntrinsicKeyword",
"TSLiteralType",
"TSMappedType",
"TSNamedTupleMember",
"TSNeverKeyword",
"TSNullKeyword",
"TSNumberKeyword",
"TSObjectKeyword",
"TSParenthesizedType",
"TSStringKeyword",
"TSSymbolKeyword",
"TSTemplateLiteralType",
"TSThisType",
"TSTupleType",
"TSTypeLiteral",
"TSTypeOperator",
"TSTypePredicate",
"TSTypeQuery",
"TSTypeReference",
"TSUndefinedKeyword",
"TSUnionType",
"TSUnknownKeyword",
"TSVoidKeyword",
]);
function isTypeNode(node: ESTree.Node): node is ESTree.TSType {
return typeNodeKinds.has(node.type);
}
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
return type.typeName.type === "Identifier" ? type.typeName.name : null;
}
function isInsideTypeAliasDeclaration(node: ESTree.Node): boolean {
let current: ESTree.Node | null = node.parent;
while (current !== null && current.type !== "Program") {
if (current.type === "TSTypeAliasDeclaration") return true;
current = current.parent;
}
return false;
}
function isPlainAliasConsumerUse(node: ESTree.TSType, environment: TypeEnvironment): boolean {
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
const name = typeReferenceName(node);
return (
name !== null &&
visibleTypeAlias(name, node, environment.typeAliases) !== null &&
!isInsideTypeAliasDeclaration(node)
);
}
function isInsideTypeParameterConstraint(node: ESTree.TSType): boolean {
let child: ESTree.Node = node;
let parent: ESTree.Node | null = child.parent;
while (parent !== null && parent.type !== "Program") {
if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
child = parent;
parent = child.parent;
}
return false;
}
function shouldReportType(node: ESTree.TSType, environment: TypeEnvironment): boolean {
if (isInsideTypeParameterConstraint(node)) return false;
if (isPlainAliasConsumerUse(node, environment)) return false;
if (classifyUnsafeDictionary(node, environment) === null) return false;
let current: ESTree.Node | null = node.parent;
while (current !== null && current.type !== "Program") {
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null)
return false;
current = current.parent;
}
return true;
}
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
export const noUnsafeDictionaryTypeRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.",
},
messages: {
unsafeDictionary:
"This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion.",
},
},
createOnce(context) {
let environment: TypeEnvironment | null = null;
const report = (node: ESTree.Node, value: string) => {
context.report({ node, messageId: "unsafeDictionary", data: { value } });
};
const reportIfUnsafe = (node: ESTree.TSType) => {
if (environment === null || !shouldReportType(node, environment)) return;
const unsafe = classifyUnsafeDictionary(node, environment);
if (unsafe === null) return;
report(node, unsafe.unsafeValue);
};
return {
Program(node) {
environment = createTypeEnvironment(
node,
context.sourceCode.visitorKeys,
);
},
TSTypeReference: reportIfUnsafe,
TSTypeLiteral: reportIfUnsafe,
TSMappedType: reportIfUnsafe,
TSIndexSignature(node) {
if (
environment === null ||
node.typeAnnotation === null ||
node.parent.type === "TSTypeLiteral"
)
return;
const unsafe = classifyUnsafeDictionaryValue(
node.typeAnnotation.typeAnnotation,
environment,
);
if (unsafe !== null) report(node, unsafe.unsafeValue);
},
};
},
});
assets/anti-slop/rules/no-widen-then-assert.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree, Variable } from "@oxlint/plugins";
type BroadTypeKind = "top" | "object" | "record";
type KnownValueEvidence = {
readonly type: ESTree.TSType | null;
};
const functionBoundaryTypes = new Set([
"ArrowFunctionExpression",
"FunctionDeclaration",
"FunctionExpression",
"TSDeclareFunction",
"TSEmptyBodyFunctionExpression",
]);
function unwrapExpressionParentheses(expression: ESTree.Expression): ESTree.Expression {
let current = expression;
while (current.type === "ParenthesizedExpression") current = current.expression;
return current;
}
function unwrapTypeParentheses(type: ESTree.TSType): ESTree.TSType {
let current = type;
while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
return current;
}
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
return type.typeName.type === "Identifier" ? type.typeName.name : null;
}
function isUnknownOrAnyType(type: ESTree.TSType): boolean {
const unwrapped = unwrapTypeParentheses(type);
return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
}
function isBroadRecordKeyType(type: ESTree.TSType): boolean {
const unwrapped = unwrapTypeParentheses(type);
if (
unwrapped.type === "TSStringKeyword" ||
unwrapped.type === "TSNumberKeyword" ||
unwrapped.type === "TSSymbolKeyword"
) {
return true;
}
if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
}
function isBroadRecordType(type: ESTree.TSType): boolean {
const unwrapped = unwrapTypeParentheses(type);
if (unwrapped.type === "TSTypeReference") {
if (typeReferenceName(unwrapped) === "Readonly") {
const [inner] = unwrapped.typeArguments?.params ?? [];
return inner !== undefined && isBroadRecordType(inner);
}
if (typeReferenceName(unwrapped) !== "Record") return false;
const parameters = unwrapped.typeArguments?.params ?? [];
return (
parameters.length === 2 &&
parameters[0] !== undefined &&
parameters[1] !== undefined &&
isBroadRecordKeyType(parameters[0]) &&
isUnknownOrAnyType(parameters[1])
);
}
if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
const [member] = unwrapped.members;
const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
return (
member?.type === "TSIndexSignature" &&
member.parameters.length === 1 &&
parameter !== undefined &&
isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) &&
isUnknownOrAnyType(member.typeAnnotation.typeAnnotation)
);
}
function broadTypeKind(type: ESTree.TSType): BroadTypeKind | null {
const unwrapped = unwrapTypeParentheses(type);
if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
if (unwrapped.type === "TSObjectKeyword") return "object";
return isBroadRecordType(unwrapped) ? "record" : null;
}
function assertedExpression(
node: ESTree.TSAsExpression | ESTree.TSTypeAssertion,
): ESTree.Expression {
return unwrapExpressionParentheses(node.expression);
}
function assertionFromExpression(
expression: ESTree.Expression,
): ESTree.TSAsExpression | ESTree.TSTypeAssertion | null {
const unwrapped = unwrapExpressionParentheses(expression);
return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion"
? unwrapped
: null;
}
function normalizedTypeText(sourceText: string, type: ESTree.TSType): string {
return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, "");
}
function typesHaveSameSyntax(
sourceText: string,
left: ESTree.TSType | null,
right: ESTree.TSType,
): boolean {
return (
left !== null &&
normalizedTypeText(sourceText, unwrapTypeParentheses(left)) ===
normalizedTypeText(sourceText, unwrapTypeParentheses(right))
);
}
function isDefinitelyObjectType(type: ESTree.TSType): boolean {
const unwrapped = unwrapTypeParentheses(type);
switch (unwrapped.type) {
case "TSArrayType":
case "TSConstructorType":
case "TSFunctionType":
case "TSMappedType":
case "TSObjectKeyword":
case "TSTupleType":
return true;
case "TSTypeLiteral":
return unwrapped.members.length > 0;
case "TSIntersectionType":
return unwrapped.types.every(isDefinitelyObjectType);
case "TSTypeOperator":
return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
default:
return false;
}
}
function isDefinitelyNarrowerRecordType(type: ESTree.TSType): boolean {
const unwrapped = unwrapTypeParentheses(type);
if (unwrapped.type === "TSTypeLiteral") {
return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
}
if (unwrapped.type !== "TSTypeReference") return false;
if (typeReferenceName(unwrapped) === "Readonly") {
const [inner] = unwrapped.typeArguments?.params ?? [];
return inner !== undefined && isDefinitelyNarrowerRecordType(inner);
}
if (typeReferenceName(unwrapped) !== "Record") return false;
const parameters = unwrapped.typeArguments?.params ?? [];
return (
parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1])
);
}
function functionBoundary(node: ESTree.Node): ESTree.Node | null {
let current = node.parent;
while (current !== null && current.type !== "Program") {
if (functionBoundaryTypes.has(current.type)) return current;
current = current.parent;
}
return null;
}
function resolvedVariableForIdentifier(
scopes: readonly {
readonly references: readonly {
readonly identifier: ESTree.Node;
readonly resolved: Variable | null;
}[];
}[],
identifier: ESTree.IdentifierReference,
): Variable | null {
for (const scope of scopes) {
const reference = scope.references.find(
(candidate) =>
candidate.identifier.start === identifier.start &&
candidate.identifier.end === identifier.end,
);
if (reference !== undefined) return reference.resolved;
}
return null;
}
function variableDeclarator(variable: Variable): ESTree.VariableDeclarator | null {
for (const definition of variable.defs) {
if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") {
return definition.node;
}
}
return null;
}
function knownValueEvidence(
expression: ESTree.Expression,
scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
boundary: ESTree.Node | null,
visitedVariables: ReadonlySet<Variable>,
): KnownValueEvidence | null {
const unwrapped = unwrapExpressionParentheses(expression);
if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
return { type: unwrapped.typeAnnotation };
}
if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") {
return { type: null };
}
if (
unwrapped.type === "ArrayExpression" ||
unwrapped.type === "ArrowFunctionExpression" ||
unwrapped.type === "ClassExpression" ||
unwrapped.type === "FunctionExpression" ||
unwrapped.type === "NewExpression" ||
unwrapped.type === "ObjectExpression"
) {
return { type: null };
}
if (unwrapped.type !== "Identifier") return null;
const variable = resolvedVariableForIdentifier(scopes, unwrapped);
if (variable === null || visitedVariables.has(variable)) return null;
const annotatedIdentifier = variable.identifiers.find(
(identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined,
);
const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
if (annotation !== undefined && annotatedIdentifier !== undefined) {
if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) {
return null;
}
return { type: annotation };
}
const declarator = variableDeclarator(variable);
if (
declarator === null ||
declarator.parent.type !== "VariableDeclaration" ||
declarator.parent.kind !== "const" ||
declarator.init === null ||
variable.references.some((reference) => reference.isWrite() && !reference.init) ||
functionBoundary(declarator) !== boundary
) {
return null;
}
return knownValueEvidence(
declarator.init,
scopes,
boundary,
new Set([...visitedVariables, variable]),
);
}
function widenedBinding(
variable: Variable,
scopes: Parameters<typeof resolvedVariableForIdentifier>[0],
): {
readonly broadKind: BroadTypeKind;
readonly evidence: KnownValueEvidence;
readonly declaredAt: number;
readonly boundary: ESTree.Node | null;
} | null {
const declarator = variableDeclarator(variable);
if (
declarator === null ||
declarator.parent.type !== "VariableDeclaration" ||
declarator.parent.kind !== "const" ||
declarator.id.type !== "Identifier" ||
declarator.init === null ||
variable.references.some((reference) => reference.isWrite() && !reference.init)
) {
return null;
}
const boundary = functionBoundary(declarator);
const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
const initializerAssertion = assertionFromExpression(declarator.init);
const initializerBroadKind =
initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType);
const broadKind = declaredBroadKind ?? initializerBroadKind;
if (broadKind === null) return null;
const originalExpression =
initializerAssertion !== null && initializerBroadKind !== null
? assertedExpression(initializerAssertion)
: declarator.init;
const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable]));
return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary };
}
function assertionIsNarrower(
sourceText: string,
broadKind: BroadTypeKind,
evidence: KnownValueEvidence,
assertedType: ESTree.TSType,
): boolean {
if (broadTypeKind(assertedType) !== null) return false;
if (broadKind === "top") return true;
if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) return true;
if (broadKind === "object") return isDefinitelyObjectType(assertedType);
return isDefinitelyNarrowerRecordType(assertedType);
}
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
export const noWidenThenAssertRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.",
},
messages: {
widenThenAssert:
'Binding "{{name}}" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once.',
},
},
createOnce(context) {
let scopes: Parameters<typeof resolvedVariableForIdentifier>[0] = [];
const checkAssertion = (node: ESTree.TSAsExpression | ESTree.TSTypeAssertion) => {
const expression = assertedExpression(node);
if (expression.type !== "Identifier") return;
const variable = resolvedVariableForIdentifier(scopes, expression);
if (variable === null) return;
const widened = widenedBinding(variable, scopes);
if (
widened === null ||
node.start <= widened.declaredAt ||
functionBoundary(node) !== widened.boundary ||
!assertionIsNarrower(
context.sourceCode.text,
widened.broadKind,
widened.evidence,
node.typeAnnotation,
)
) {
return;
}
context.report({
node,
messageId: "widenThenAssert",
data: { name: expression.name },
});
};
return {
Program() {
scopes = context.sourceCode.scopeManager.scopes;
},
TSAsExpression: checkAssertion,
TSTypeAssertion: checkAssertion,
};
},
});
assets/anti-slop/rules/require-safety-comment-for-type-assertion.ts
import { defineRule } from "@oxlint/plugins";
import type { ESTree, SourceCode } from "@oxlint/plugins";
type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
const DEFAULT_SAFETY_MARKERS = ["SAFETY"] as const;
const commentOwnerKinds = new Set([
"ExpressionStatement",
"PropertyDefinition",
"ReturnStatement",
"ThrowStatement",
"VariableDeclaration",
]);
function isConstAssertion(node: TypeAssertion): boolean {
return (
node.typeAnnotation.type === "TSTypeReference" &&
node.typeAnnotation.typeName.type === "Identifier" &&
node.typeAnnotation.typeName.name === "const"
);
}
function configuredSafetyMarkers(option: unknown): readonly string[] {
if (typeof option !== "object" || option === null || !("markers" in option)) {
return DEFAULT_SAFETY_MARKERS;
}
const configured = option.markers;
if (!Array.isArray(configured)) return DEFAULT_SAFETY_MARKERS;
const markers = configured.flatMap((marker) =>
typeof marker === "string" && marker.trim().length > 0 ? [marker.trim()] : [],
);
return markers.length > 0 ? markers : DEFAULT_SAFETY_MARKERS;
}
function markerPattern(markers: readonly string[]): RegExp {
const alternation = markers
.map((marker) => marker.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`))
.join("|");
return new RegExp(
String.raw`(?:^|[^\p{L}\p{N}_])(?:${alternation})\s*:\s*\S`,
"u",
);
}
function hasSafetyJustificationBefore(
sourceCode: SourceCode,
owner: ESTree.Node,
assertion: TypeAssertion,
pattern: RegExp,
): boolean {
return sourceCode
.getCommentsBefore(owner)
.some(
(comment) => comment.end <= assertion.start && pattern.test(comment.value),
);
}
function hasSafetyComment(
sourceCode: SourceCode,
node: TypeAssertion,
pattern: RegExp,
): boolean {
let current: ESTree.Node = node;
while (true) {
if (hasSafetyJustificationBefore(sourceCode, current, node, pattern)) return true;
if (commentOwnerKinds.has(current.type)) {
const exportDeclaration = current.parent;
return (
exportDeclaration.type === "ExportNamedDeclaration" &&
exportDeclaration.declaration === current &&
hasSafetyJustificationBefore(sourceCode, exportDeclaration, node, pattern)
);
}
if (current.parent.type === "Program") return false;
current = current.parent;
}
}
/** Require every non-const type assertion to state the invariant TypeScript cannot express. */
export const requireSafetyCommentForTypeAssertionRule = defineRule({
meta: {
type: "problem",
docs: {
description:
"Require a nearby SAFETY comment for every TypeScript type assertion except const assertions.",
},
messages: {
missingSafetyComment:
"This type assertion has no `{{marker}}:` justification. State the checked invariant immediately before the assertion or its containing statement.",
},
schema: [
{
type: "object",
properties: {
markers: {
type: "array",
items: { type: "string", minLength: 1 },
minItems: 1,
uniqueItems: true,
},
},
additionalProperties: false,
},
],
defaultOptions: [{ markers: ["SAFETY"] }],
},
createOnce(context) {
const patterns = new Map<string, RegExp>();
const checkAssertion = (node: TypeAssertion) => {
if (isConstAssertion(node)) return;
const markers = configuredSafetyMarkers(context.options?.[0]);
const patternKey = markers.join("\u0000");
const pattern = patterns.get(patternKey) ?? markerPattern(markers);
patterns.set(patternKey, pattern);
if (hasSafetyComment(context.sourceCode, node, pattern)) return;
context.report({
node,
messageId: "missingSafetyComment",
data: { marker: markers[0] ?? DEFAULT_SAFETY_MARKERS[0] },
});
};
return {
TSAsExpression: checkAssertion,
TSTypeAssertion: checkAssertion,
};
},
});
assets/anti-slop/shared/dictionary-types.ts
import type { ESTree } from "@oxlint/plugins";
import {
createTypeAliasEnvironment,
hasVisibleTypeBinding,
visibleTypeAlias,
type TypeAliasEnvironment as LexicalTypeAliasEnvironment,
} from "./type-alias-resolution.ts";
const BUILT_INS = new Set([
"Record",
"Readonly",
"Partial",
"Required",
"Pick",
"Omit",
"PropertyKey",
"NonNullable",
]);
const TRANSPARENT_WRAPPERS = new Set(["Readonly", "Partial", "Required", "NonNullable"]);
type TypeAliasEnvironment = ReadonlyMap<string, ESTree.TSType>;
type ResolvedType = {
readonly type: ESTree.TSType;
readonly substitutions: TypeAliasEnvironment;
};
export type UnsafeDictionary = {
readonly kind: "unsafe-dictionary";
readonly unsafeValue: "any" | "empty-object" | "object" | "union" | "unknown";
};
export type WideningTargetKind =
| "anonymous object"
| "generic container"
| "object"
| "open dictionary"
| "unknown";
export type WideningTarget = {
readonly kind: WideningTargetKind;
};
export type TypeEnvironment = {
readonly interfaces: ReadonlyMap<string, readonly ESTree.TSInterfaceDeclaration[]>;
readonly typeAliases: LexicalTypeAliasEnvironment;
};
function declaredStatement(statement: ESTree.Statement): ESTree.Node | null {
return statement.type === "ExportNamedDeclaration" ||
statement.type === "ExportDefaultDeclaration"
? (statement.declaration ?? null)
: statement;
}
export function createTypeEnvironment(
program: ESTree.Program,
visitorKeys: Readonly<Record<string, readonly string[]>>,
): TypeEnvironment {
const interfaces = new Map<string, ESTree.TSInterfaceDeclaration[]>();
for (const statement of program.body) {
const declaration = declaredStatement(statement);
if (declaration?.type !== "TSInterfaceDeclaration") continue;
const declarations = interfaces.get(declaration.id.name) ?? [];
declarations.push(declaration);
interfaces.set(declaration.id.name, declarations);
}
return {
interfaces,
typeAliases: createTypeAliasEnvironment(program, visitorKeys),
};
}
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
return type.typeName.type === "Identifier" ? type.typeName.name : null;
}
function isBuiltIn(
name: string,
use: ESTree.Node,
environment: TypeEnvironment,
): boolean {
return (
BUILT_INS.has(name) &&
!hasVisibleTypeBinding(name, use, environment.typeAliases)
);
}
function isUnappliedReferenceTo(type: ESTree.TSType, name: string): boolean {
const unwrapped = unwrapTransparentType(type);
return (
unwrapped.type === "TSTypeReference" &&
typeReferenceName(unwrapped) === name &&
(unwrapped.typeArguments === null ||
unwrapped.typeArguments === undefined ||
unwrapped.typeArguments.params.length === 0)
);
}
function unwrapTransparentType(type: ESTree.TSType): ESTree.TSType {
let current = type;
while (
current.type === "TSParenthesizedType" ||
(current.type === "TSTypeOperator" && current.operator === "readonly")
) {
current = current.typeAnnotation;
}
return current;
}
function isNeverType(type: ESTree.TSType): boolean {
return unwrapTransparentType(type).type === "TSNeverKeyword";
}
function isEffectivelyEmptyMember(member: ESTree.TSSignature): boolean {
return (
member.type === "TSPropertySignature" &&
member.optional === true &&
member.typeAnnotation !== null &&
member.typeAnnotation !== undefined &&
isNeverType(member.typeAnnotation.typeAnnotation)
);
}
function isEffectivelyEmptyTypeLiteral(type: ESTree.TSTypeLiteral): boolean {
return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
}
function isEffectivelyEmptyInterface(
declarations: readonly ESTree.TSInterfaceDeclaration[],
): boolean {
if (declarations.length !== 1) return false;
const [type] = declarations;
return (
type !== undefined &&
type.extends.length === 0 &&
(type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember))
);
}
function resolvedSubstitutionArgument(
type: ESTree.TSType,
base: TypeAliasEnvironment,
resolving: ReadonlySet<string> = new Set(),
): ESTree.TSType {
const unwrapped = unwrapTransparentType(type);
if (unwrapped.type !== "TSTypeReference") return type;
const name = typeReferenceName(unwrapped);
if (name === null || resolving.has(name)) return type;
const substitution = base.get(name);
if (substitution === undefined) return type;
const nextResolving = new Set(resolving);
nextResolving.add(name);
return resolvedSubstitutionArgument(substitution, base, nextResolving);
}
function aliasSubstitution(
alias: ESTree.TSTypeAliasDeclaration,
type: ESTree.TSTypeReference,
base: TypeAliasEnvironment,
): TypeAliasEnvironment | null {
const parameters = alias.typeParameters?.params ?? [];
const arguments_ = type.typeArguments?.params ?? [];
const next = new Map(base);
for (const [index, parameter] of parameters.entries()) {
const argument = arguments_[index] ?? parameter.default;
if (argument === null || argument === undefined) return null;
next.set(parameter.name.name, resolvedSubstitutionArgument(argument, next));
}
return next;
}
function unsafeDirectValue(
type: ESTree.TSType,
environment: TypeEnvironment,
substitutions: TypeAliasEnvironment,
resolvingAliases: ReadonlySet<string>,
): UnsafeDictionary["unsafeValue"] | null {
const unwrapped = unwrapTransparentType(type);
if (unwrapped.type === "TSUnknownKeyword") return "unknown";
if (unwrapped.type === "TSAnyKeyword") return "any";
if (unwrapped.type === "TSObjectKeyword") return "object";
if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped))
return "empty-object";
if (unwrapped.type === "TSUnionType") {
return unwrapped.types.some(
(member) => unsafeDirectValue(member, environment, substitutions, resolvingAliases) !== null,
)
? "union"
: null;
}
if (unwrapped.type === "TSIntersectionType") {
const unsafeMembers = unwrapped.types.map((member) =>
unsafeDirectValue(member, environment, substitutions, resolvingAliases),
);
if (unsafeMembers.includes("any")) return "any";
return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null)
? unsafeMembers[0]
: null;
}
if (unwrapped.type !== "TSTypeReference") return null;
const name = typeReferenceName(unwrapped);
if (name === null) return null;
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
const wrapped = unwrapped.typeArguments?.params[0];
return wrapped === undefined
? null
: unsafeDirectValue(wrapped, environment, substitutions, resolvingAliases);
}
const substitution = substitutions.get(name);
if (substitution !== undefined) {
return isUnappliedReferenceTo(substitution, name)
? null
: unsafeDirectValue(substitution, environment, substitutions, resolvingAliases);
}
const interfaceDeclarations = environment.interfaces.get(name);
if (interfaceDeclarations !== undefined) {
return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
}
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
if (alias === null || resolvingAliases.has(name)) return null;
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
if (nextSubstitutions === null) return null;
const nextResolving = new Set(resolvingAliases);
nextResolving.add(name);
return unsafeDirectValue(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
}
function dictionaryValueTypes(
type: ESTree.TSType,
environment: TypeEnvironment,
substitutions: TypeAliasEnvironment,
resolvingAliases: ReadonlySet<string>,
): readonly ResolvedType[] {
const unwrapped = unwrapTransparentType(type);
if (unwrapped.type === "TSTypeLiteral") {
return unwrapped.members.flatMap((member): readonly ResolvedType[] =>
member.type === "TSIndexSignature" && member.typeAnnotation !== null
? [{ type: member.typeAnnotation.typeAnnotation, substitutions }]
: [],
);
}
if (unwrapped.type === "TSMappedType") {
return unwrapped.typeAnnotation === null
? []
: [{ type: unwrapped.typeAnnotation, substitutions }];
}
if (unwrapped.type !== "TSTypeReference") return [];
const name = typeReferenceName(unwrapped);
if (name === null) return [];
const substitution = substitutions.get(name);
if (substitution !== undefined) {
return isUnappliedReferenceTo(substitution, name)
? []
: dictionaryValueTypes(substitution, environment, substitutions, resolvingAliases);
}
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
const wrapped = unwrapped.typeArguments?.params[0];
return wrapped === undefined
? []
: dictionaryValueTypes(wrapped, environment, substitutions, resolvingAliases);
}
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
const value = unwrapped.typeArguments?.params[1] ?? null;
return value === null ? [] : [{ type: value, substitutions }];
}
if (
(name === "Pick" || name === "Omit") &&
isBuiltIn(name, unwrapped, environment)
) {
const source = unwrapped.typeArguments?.params[0];
return source === undefined
? []
: dictionaryValueTypes(source, environment, substitutions, resolvingAliases);
}
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
if (alias === null || resolvingAliases.has(name)) return [];
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
if (nextSubstitutions === null) return [];
const nextResolving = new Set(resolvingAliases);
nextResolving.add(name);
return dictionaryValueTypes(alias.typeAnnotation, environment, nextSubstitutions, nextResolving);
}
export function classifyUnsafeDictionaryValue(
valueType: ESTree.TSType,
environment: TypeEnvironment,
): UnsafeDictionary | null {
const unsafeValue = unsafeDirectValue(valueType, environment, new Map(), new Set());
return unsafeValue === null ? null : { kind: "unsafe-dictionary", unsafeValue };
}
export function classifyUnsafeDictionary(
type: ESTree.TSType,
environment: TypeEnvironment,
): UnsafeDictionary | null {
for (const valueType of dictionaryValueTypes(type, environment, new Map(), new Set())) {
const unsafeValue = unsafeDirectValue(
valueType.type,
environment,
valueType.substitutions,
new Set(),
);
if (unsafeValue !== null) return { kind: "unsafe-dictionary", unsafeValue };
}
return null;
}
export function classifyWideningTarget(
type: ESTree.TSType,
environment: TypeEnvironment,
): WideningTarget | null {
const unwrapped = unwrapTransparentType(type);
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
if (unwrapped.type === "TSTypeLiteral") {
return unwrapped.members.some((member) => member.type === "TSIndexSignature")
? { kind: "open dictionary" }
: unwrapped.members.length > 0
? { kind: "anonymous object" }
: null;
}
if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
if (unwrapped.type !== "TSTypeReference") return null;
const name = typeReferenceName(unwrapped);
if (name === null) return null;
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
const wrapped = unwrapped.typeArguments?.params[0];
return wrapped === undefined ? null : classifyWideningTarget(wrapped, environment);
}
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
return hasBroadRecordKey(unwrapped, environment, new Map())
? { kind: "open dictionary" }
: null;
}
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
if (alias === null) return null;
if ((alias.typeParameters?.params.length ?? 0) > 0) {
const substitutions = aliasSubstitution(alias, unwrapped, new Map());
const resolved =
substitutions === null
? null
: classifyAliasBroadTarget(
alias.typeAnnotation,
environment,
substitutions,
new Set([name]),
);
return resolved?.kind === "open dictionary" ? { kind: "generic container" } : null;
}
const substitutions = aliasSubstitution(alias, unwrapped, new Map());
if (substitutions === null) return null;
const resolved = classifyAliasBroadTarget(
alias.typeAnnotation,
environment,
substitutions,
new Set([name]),
);
return resolved;
}
function hasBroadRecordKey(
type: ESTree.TSTypeReference,
environment: TypeEnvironment,
substitutions: TypeAliasEnvironment,
): boolean {
const key = type.typeArguments?.params[0];
return key === undefined || isBroadMappedKey(key, environment, substitutions);
}
function isBroadMappedKey(
type: ESTree.TSType,
environment: TypeEnvironment,
substitutions: TypeAliasEnvironment,
visitedAliases: ReadonlySet<string> = new Set(),
): boolean {
const unwrapped = unwrapTransparentType(type);
if (
unwrapped.type === "TSStringKeyword" ||
unwrapped.type === "TSNumberKeyword" ||
unwrapped.type === "TSSymbolKeyword"
) {
return true;
}
if (unwrapped.type === "TSUnionType") {
return unwrapped.types.some((member) =>
isBroadMappedKey(member, environment, substitutions, visitedAliases),
);
}
if (unwrapped.type !== "TSTypeReference") return false;
const name = typeReferenceName(unwrapped);
if (name === null) return false;
const substitution = substitutions.get(name);
if (substitution !== undefined && !isUnappliedReferenceTo(substitution, name)) {
return isBroadMappedKey(substitution, environment, substitutions, visitedAliases);
}
if (name === "PropertyKey" && isBuiltIn(name, unwrapped, environment)) return true;
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
if (
alias === null ||
(alias.typeParameters?.params.length ?? 0) > 0 ||
visitedAliases.has(name)
) {
return false;
}
const nextVisited = new Set(visitedAliases);
nextVisited.add(name);
return isBroadMappedKey(alias.typeAnnotation, environment, substitutions, nextVisited);
}
function classifyAliasBroadTarget(
type: ESTree.TSType,
environment: TypeEnvironment,
substitutions: TypeAliasEnvironment,
resolvingAliases: ReadonlySet<string>,
): WideningTarget | null {
const unwrapped = unwrapTransparentType(type);
if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
if (unwrapped.type === "TSTypeLiteral") {
return unwrapped.members.some((member) => member.type === "TSIndexSignature")
? { kind: "open dictionary" }
: null;
}
if (unwrapped.type === "TSMappedType") {
return isBroadMappedKey(unwrapped.constraint, environment, substitutions)
? { kind: "open dictionary" }
: null;
}
if (unwrapped.type !== "TSTypeReference") return null;
const name = typeReferenceName(unwrapped);
if (name === null) return null;
const substitution = substitutions.get(name);
if (substitution !== undefined) {
return isUnappliedReferenceTo(substitution, name)
? null
: classifyAliasBroadTarget(
substitution,
environment,
substitutions,
resolvingAliases,
);
}
if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, unwrapped, environment)) {
const wrapped = unwrapped.typeArguments?.params[0];
return wrapped === undefined
? null
: classifyAliasBroadTarget(wrapped, environment, substitutions, resolvingAliases);
}
if (name === "Record" && isBuiltIn(name, unwrapped, environment)) {
return hasBroadRecordKey(unwrapped, environment, substitutions)
? { kind: "open dictionary" }
: null;
}
const alias = visibleTypeAlias(name, unwrapped, environment.typeAliases);
if (alias === null || resolvingAliases.has(name)) return null;
const nextSubstitutions = aliasSubstitution(alias, unwrapped, substitutions);
if (nextSubstitutions === null) return null;
const nextResolving = new Set(resolvingAliases);
nextResolving.add(name);
return classifyAliasBroadTarget(
alias.typeAnnotation,
environment,
nextSubstitutions,
nextResolving,
);
}
export function isPopulatedObjectExpression(expression: ESTree.Expression): boolean {
let current = expression;
while (
current.type === "ParenthesizedExpression" ||
current.type === "TSAsExpression" ||
current.type === "TSTypeAssertion" ||
current.type === "TSNonNullExpression"
) {
current = current.expression;
}
return current.type === "ObjectExpression" && current.properties.length > 0;
}
export function isKnownEvidenceExpression(expression: ESTree.Expression): boolean {
let current = expression;
while (
current.type === "ParenthesizedExpression" ||
current.type === "TSAsExpression" ||
current.type === "TSTypeAssertion" ||
current.type === "TSNonNullExpression" ||
current.type === "TSSatisfiesExpression"
) {
current = current.expression;
}
if (current.type === "ObjectExpression") return true;
return (
current.type === "ArrayExpression" ||
current.type === "ArrowFunctionExpression" ||
current.type === "ClassExpression" ||
current.type === "FunctionExpression" ||
current.type === "NewExpression" ||
current.type === "Literal" ||
current.type === "TemplateLiteral" ||
current.type === "UnaryExpression"
);
}
assets/anti-slop/shared/function-parameters.ts
import type { ESTree, SourceCode } from "@oxlint/plugins";
export type FunctionParameter = ESTree.ParamPattern;
/** Return whether a type is or contains TypeScript's absorbing unknown top type. */
export function containsUnknownType(type: ESTree.TSType): boolean {
if (type.type === "TSUnknownKeyword") return true;
if (type.type === "TSParenthesizedType") return containsUnknownType(type.typeAnnotation);
return type.type === "TSUnionType" && type.types.some(containsUnknownType);
}
/** Return the TypeScript annotation attached to a function parameter or its wrapped binding. */
export function functionParameterTypeAnnotation(
parameter: FunctionParameter,
): ESTree.TSTypeAnnotation | null | undefined {
if (parameter.type === "TSParameterProperty") {
return functionParameterTypeAnnotation(parameter.parameter);
}
if (parameter.type === "RestElement") {
return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.argument);
}
if (parameter.type === "AssignmentPattern") {
return parameter.typeAnnotation ?? functionParameterTypeAnnotation(parameter.left);
}
return parameter.typeAnnotation;
}
/** Return only a function parameter's local binding, excluding its annotation and default value. */
export function functionParameterBindingName(
parameter: FunctionParameter,
sourceCode: SourceCode,
): string {
if (parameter.type === "TSParameterProperty") {
return functionParameterBindingName(parameter.parameter, sourceCode);
}
if (parameter.type === "AssignmentPattern") {
return functionParameterBindingName(parameter.left, sourceCode);
}
if (parameter.type === "RestElement") {
return functionParameterBindingName(parameter.argument, sourceCode);
}
if (parameter.type === "Identifier") return parameter.name;
const sourceText = sourceCode.getText(parameter);
const annotationStart = parameter.typeAnnotation?.start;
return annotationStart === undefined
? sourceText
: sourceText.slice(0, annotationStart - parameter.start).trimEnd();
}
assets/anti-slop/shared/lexical-type-parameters.ts
import type { ESTree } from "@oxlint/plugins";
type VisitorKeys = Readonly<Record<string, readonly string[]>>;
function isNode(value: unknown): value is ESTree.Node {
return (
typeof value === "object" &&
value !== null &&
"type" in value &&
typeof value.type === "string"
);
}
function collectInferTypeParameterNames(
node: ESTree.Node,
visitorKeys: VisitorKeys,
names: Set<string>,
): void {
if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
const record = node as unknown as Readonly<Record<string, unknown>>;
for (const key of visitorKeys[node.type] ?? []) {
const value = record[key];
if (isNode(value)) {
collectInferTypeParameterNames(value, visitorKeys, names);
continue;
}
if (!Array.isArray(value)) continue;
for (const child of value) {
if (isNode(child)) collectInferTypeParameterNames(child, visitorKeys, names);
}
}
}
/** Collect type binders that are in scope at a node and can shadow module aliases. */
export function lexicalTypeParameterNames(
node: ESTree.Node,
visitorKeys: VisitorKeys,
): ReadonlySet<string> {
const names = new Set<string>();
let descendant: ESTree.Node = node;
let current: ESTree.Node | null = node;
while (current !== null && current.type !== "Program") {
if ("typeParameters" in current) {
for (const parameter of current.typeParameters?.params ?? []) {
names.add(parameter.name.name);
}
}
if (
current.type === "TSMappedType" &&
(descendant === current.nameType || descendant === current.typeAnnotation)
) {
names.add(current.key.name);
}
if (current.type === "TSConditionalType" && descendant === current.trueType) {
collectInferTypeParameterNames(current.extendsType, visitorKeys, names);
}
descendant = current;
current = current.parent;
}
return names;
}
assets/anti-slop/shared/reflect-method.ts
import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
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 isGlobalReflect(sourceCode: SourceCode, expression: ESTree.Expression): boolean {
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
if (sourceCode.isGlobalReference(expression)) return true;
const variable = resolveVariable(sourceCode, expression);
return variable === null || variable.defs.length === 0;
}
/** Reports whether a call target names one method on the global Reflect object. */
export function isGlobalReflectMethodCall(
sourceCode: SourceCode,
callee: ESTree.Expression,
methodName: string,
): boolean {
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
if (!isGlobalReflect(sourceCode, callee.object)) return false;
const property = callee.property;
return callee.computed
? property.type === "Literal" && property.value === methodName
: property.type === "Identifier" && property.name === methodName;
}
assets/anti-slop/shared/type-alias-resolution.ts
import type { ESTree } from "@oxlint/plugins";
import { lexicalTypeParameterNames } from "./lexical-type-parameters.ts";
type VisitorKeys = Readonly<Record<string, readonly string[]>>;
type TypeScope = ESTree.Node;
type TypeBinding = {
readonly alias: ESTree.TSTypeAliasDeclaration | null;
readonly name: string;
readonly scope: TypeScope;
};
type Substitution = {
readonly substitutions: Substitutions;
readonly type: ESTree.TSType;
};
type Substitutions = ReadonlyMap<string, Substitution>;
export type TypeAliasEnvironment = {
readonly aliases: readonly ESTree.TSTypeAliasDeclaration[];
readonly bindingsByName: ReadonlyMap<string, readonly TypeBinding[]>;
readonly visitorKeys: VisitorKeys;
};
export type ResolvedTypeMatcher = (
type: ESTree.TSType,
matches: (child: ESTree.TSType) => boolean,
) => boolean;
const environmentsByProgram = new WeakMap<ESTree.Program, TypeAliasEnvironment>();
function isNode(value: unknown): value is ESTree.Node {
return (
typeof value === "object" &&
value !== null &&
"type" in value &&
typeof value.type === "string"
);
}
function enclosingTypeScope(node: ESTree.Node): TypeScope {
let current: ESTree.Node | null = node.parent;
while (current !== null) {
if (
current.type === "Program" ||
current.type === "BlockStatement" ||
current.type === "TSModuleBlock" ||
current.type === "StaticBlock" ||
current.type === "SwitchStatement"
) {
return current;
}
current = current.parent;
}
return node;
}
function declaredTypeBinding(node: ESTree.Node): {
readonly alias: ESTree.TSTypeAliasDeclaration | null;
readonly name: string;
} | null {
if (node.type === "TSTypeAliasDeclaration") {
return { alias: node, name: node.id.name };
}
if (
node.type === "TSInterfaceDeclaration" ||
node.type === "TSEnumDeclaration" ||
node.type === "ClassDeclaration" ||
node.type === "ClassExpression"
) {
return node.id === null ? null : { alias: null, name: node.id.name };
}
if (
node.type === "ImportSpecifier" ||
node.type === "ImportDefaultSpecifier" ||
node.type === "ImportNamespaceSpecifier"
) {
return { alias: null, name: node.local.name };
}
return null;
}
function collectTypeBindings(
node: ESTree.Node,
visitorKeys: VisitorKeys,
bindingsByName: Map<string, TypeBinding[]>,
aliases: ESTree.TSTypeAliasDeclaration[],
): void {
const declared = declaredTypeBinding(node);
if (declared !== null) {
const bindings = bindingsByName.get(declared.name) ?? [];
bindings.push({ ...declared, scope: enclosingTypeScope(node) });
bindingsByName.set(declared.name, bindings);
if (declared.alias !== null) aliases.push(declared.alias);
}
// SAFETY: Oxlint's visitor keys identify only ESTree child-node properties.
const fields = node as unknown as Readonly<Record<string, unknown>>;
for (const key of visitorKeys[node.type] ?? []) {
const value = fields[key];
if (isNode(value)) {
collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
continue;
}
if (!Array.isArray(value)) continue;
for (const child of value) {
if (isNode(child)) {
collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
}
}
}
}
/** Collect every lexical type alias and competing type binding in a program. */
export function createTypeAliasEnvironment(
program: ESTree.Program,
visitorKeys: VisitorKeys,
): TypeAliasEnvironment {
const cached = environmentsByProgram.get(program);
if (cached !== undefined) return cached;
const bindingsByName = new Map<string, TypeBinding[]>();
const aliases: ESTree.TSTypeAliasDeclaration[] = [];
collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
const environment = { aliases, bindingsByName, visitorKeys };
environmentsByProgram.set(program, environment);
return environment;
}
function ancestorDistance(ancestor: ESTree.Node, node: ESTree.Node): number | null {
let current: ESTree.Node | null = node;
let distance = 0;
while (current !== null) {
if (current === ancestor) return distance;
current = current.parent;
distance += 1;
}
return null;
}
function nearestTypeBindings(
name: string,
use: ESTree.Node,
environment: TypeAliasEnvironment,
): readonly TypeBinding[] {
const candidates = environment.bindingsByName.get(name) ?? [];
let nearestDistance = Number.POSITIVE_INFINITY;
let nearest: TypeBinding[] = [];
for (const candidate of candidates) {
const distance = ancestorDistance(candidate.scope, use);
if (distance === null || distance > nearestDistance) continue;
if (distance === nearestDistance) {
nearest.push(candidate);
continue;
}
nearestDistance = distance;
nearest = [candidate];
}
return nearest;
}
/** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
export function visibleTypeAlias(
name: string,
use: ESTree.Node,
environment: TypeAliasEnvironment,
): ESTree.TSTypeAliasDeclaration | null {
if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name)) return null;
const bindings = nearestTypeBindings(name, use, environment);
return bindings.length === 1 ? (bindings[0]?.alias ?? null) : null;
}
/** Return whether a local declaration shadows a built-in type at this use. */
export function hasVisibleTypeBinding(
name: string,
use: ESTree.Node,
environment: TypeAliasEnvironment,
): boolean {
return (
lexicalTypeParameterNames(use, environment.visitorKeys).has(name) ||
nearestTypeBindings(name, use, environment).length > 0
);
}
function typeReferenceName(type: ESTree.TSTypeReference): string | null {
return type.typeName.type === "Identifier" ? type.typeName.name : null;
}
function aliasSubstitutions(
alias: ESTree.TSTypeAliasDeclaration,
reference: ESTree.TSTypeReference,
base: Substitutions,
): Substitutions | null {
const parameters = alias.typeParameters?.params ?? [];
const arguments_ = reference.typeArguments?.params ?? [];
const next = new Map(base);
for (const [index, parameter] of parameters.entries()) {
const explicitArgument = arguments_[index];
const argument = explicitArgument ?? parameter.default;
if (argument === null || argument === undefined) return null;
const argumentSubstitutions = explicitArgument === undefined ? next : base;
next.set(parameter.name.name, {
type: argument,
substitutions: new Map(argumentSubstitutions),
});
}
return next;
}
/** Match a type after resolving visible aliases and substituting their type parameters. */
export function resolvedTypeMatches(
type: ESTree.TSType,
environment: TypeAliasEnvironment,
matcher: ResolvedTypeMatcher,
): boolean {
const evaluate = (
current: ESTree.TSType,
substitutions: Substitutions,
resolvingAliases: ReadonlySet<ESTree.TSTypeAliasDeclaration>,
): boolean => {
if (current.type === "TSTypeReference") {
const name = typeReferenceName(current);
if (name !== null) {
const substitution = substitutions.get(name);
if (substitution !== undefined && !current.typeArguments?.params.length) {
return evaluate(
substitution.type,
substitution.substitutions,
resolvingAliases,
);
}
const alias = visibleTypeAlias(name, current, environment);
if (alias !== null && !resolvingAliases.has(alias)) {
const nextSubstitutions = aliasSubstitutions(alias, current, substitutions);
if (nextSubstitutions !== null) {
const nextResolving = new Set(resolvingAliases);
nextResolving.add(alias);
return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving);
}
}
}
}
return matcher(current, (child) =>
evaluate(child, substitutions, resolvingAliases),
);
};
return evaluate(type, new Map(), new Set());
}
scripts/install.mjs
#!/usr/bin/env node
import { cpSync, existsSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const skillRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const source = resolve(skillRoot, "assets/anti-slop");
const arguments_ = process.argv.slice(2);
const targetArgument = arguments_.find((argument) => !argument.startsWith("--"));
const target = resolve(process.cwd(), targetArgument ?? "tools/oxlint/anti-slop");
const force = arguments_.includes("--force");
if (existsSync(target) && !force) {
console.error(`Refusing to overwrite ${target}. Re-run with --force only after reviewing the existing files.`);
process.exit(1);
}
mkdirSync(dirname(target), { recursive: true });
cpSync(source, target, { recursive: true, force });
console.log(`Copied the anti-slop plugin to ${target}`);
console.log(`Configure Oxlint with: ${target}/index.ts`);
SKILL.md
---
name: install-anti-slop
description: Install and configure the generic and optional Effect anti-slop Oxlint plugins in a local TypeScript or JavaScript repository. Use whenever a user asks to add anti-slop lint rules, copy the anti-slop plugin, configure opinionated Oxlint rules, or migrate an existing local anti-slop setup.
---
# Install anti-slop
Install the bundled Oxlint plugin into the current repository and integrate it with the repository's existing lint setup. Preserve unrelated work and adapt to the project's package manager and configuration style.
## Procedure
1. Inspect the repository before changing it:
- Read its agent instructions.
- Check `git status` and preserve unrelated changes.
- Identify the package manager from `packageManager` and lockfiles.
- Find Oxlint configuration (`oxlint.config.*`, `.oxlintrc*`, or a Vite+ config).
- Check whether anti-slop files or rules already exist. Do not overwrite them without reviewing the diff.
2. Copy the bundled plugin from this skill. Run from the target repository:
```bash
node <skill-directory>/scripts/install.mjs
```
This creates `tools/oxlint/anti-slop/`. Pass another relative destination as the first argument when the repository has an established tooling layout. The script refuses to replace an existing destination; only use `--force` after backing up and reviewing existing files.
3. Install current compatible dependencies rather than trusting versions remembered by the agent:
- If the repository already depends on `oxlint`, read its installed version from the package manager or lockfile and install `@oxlint/plugins` at exactly that version. Pin it exactly rather than by range so future upgrades move both packages together.
- Only when the repository has no `oxlint` dependency, query `npm view oxlint version` and `npm view @oxlint/plugins version`, then install the same current version of both packages.
- `oxlint` is a development dependency. The copied source imports `@oxlint/plugins`, so install it as a development dependency for a local-only plugin.
- Do not replace the package manager or rewrite unrelated dependency ranges.
4. Register the generic plugin, configure ignores, and enable all generic rules. For `oxlint.config.ts` or `.oxlintrc.json`, merge these fields with the existing configuration:
```ts
ignorePatterns: [
".agent/**",
".agents/**",
".claude/**",
".codex/**",
".continue/**",
".cursor/**",
".gemini/**",
".opencode/**",
".pi/**",
".roo/**",
".windsurf/**",
"tools/oxlint/anti-slop/**",
],
jsPlugins: [
{ name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" },
],
```
Keep every existing ignore. Adjust the final pattern when the plugin was copied elsewhere. Inspect the repository for other project-local agent tooling directories and add them rather than linting installed skills, hooks, or generated agent configuration as application source. Do not broadly ignore all dot-directories, because some repositories keep owned source or checks in them.
For Vite+, add these fields to `lint.ignorePatterns` and `lint.jsPlugins`. Also merge the same patterns into `fmt.ignorePatterns` so `vp check` does not reformat installed agent assets or the vendored plugin. Merge existing entries instead of replacing them.
Enable these rules at `"error"`:
```json
{
"anti-slop/no-chained-type-assertions": "error",
"anti-slop/no-conditional-empty-object-spread": "error",
"anti-slop/no-known-value-widening": "error",
"anti-slop/no-module-mocking": "error",
"anti-slop/no-object-parameters": "error",
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "error",
"anti-slop/no-runtime-typeof": "error",
"anti-slop/no-shape-in-symbol-names": "error",
"anti-slop/no-unknown-parameters": "error",
"anti-slop/no-unknown-returns": "error",
"anti-slop/no-unknown-type-aliases": "error",
"anti-slop/no-unsafe-dictionary-type": "error",
"anti-slop/no-widen-then-assert": "error",
"anti-slop/require-safety-comment-for-type-assertion": "error"
}
```
If the repository declares `effect` in a package manifest, or the user explicitly requests Effect rules, also register the opt-in Effect plugin:
```ts
jsPlugins: [
{
name: "anti-slop-effect",
specifier: "./tools/oxlint/anti-slop/effect/index.ts",
},
],
rules: {
"anti-slop-effect/no-service-constructor-imports": "error",
},
```
Merge these entries with the generic plugin configuration rather than replacing it. Do not enable the Effect plugin merely because Effect appears transitively in a lockfile; require a direct package-manifest dependency or an explicit user request. The rule covers relative project imports. Report package-alias imports as a current limitation rather than pretending they are enforced.
5. Run the repository's lint command and typecheck. For Vite+, run the repository's full `vp check` command after adding both lint and format ignores. If findings appear in owned project source, report them and fix them only when the user asked for migration/cleanup. Do not suppress rules, weaken rule severity, add unsafe casts, or mechanically launder types to make lint pass.
6. Review the final diff and clearly report:
- copied path,
- dependency versions installed,
- configuration changed,
- checks run and any remaining findings.
## Migration guidance
When replacing an older local copy, compare its rules and diagnostics before overwriting. Keep project-specific rules in their own plugin. The default anti-slop plugin is intentionally generic; framework-specific policy belongs in an explicit opt-in group such as `anti-slop-effect`. Prefer inference, `as const`, `satisfies`, named owner contracts, and boundary parsing when resolving findings.