Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .agents/skills/install-anti-slop/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
name: install-anti-slop
description: Install and configure the anti-slop Oxlint plugin 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:
- Query `npm view oxlint version` and `npm view @oxlint/plugins version`.
- Install the same current version of both packages with the repository's package manager.
- `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 plugin and enable all rules. For `oxlint.config.ts` or `.oxlintrc.json`, add:

```ts
jsPlugins: [
{ name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" },
],
```

For Vite+, add that same entry to `lint.jsPlugins`. Merge it with 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-object-parameters": "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-type-aliases": "error",
"anti-slop/no-unsafe-dictionary-type": "error",
"anti-slop/no-widen-then-assert": "error"
}
```

5. Run the repository's lint command and typecheck. If findings appear, 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; anti-slop is intentionally generic. Prefer inference, `as const`, `satisfies`, named owner contracts, and boundary parsing when resolving findings.
31 changes: 31 additions & 0 deletions .agents/skills/install-anti-slop/assets/anti-slop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { definePlugin } 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 { noObjectParametersRule } from "./rules/no-object-parameters.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 { 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";

/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
const antiSlopPlugin = definePlugin({
meta: { name: "anti-slop" },
rules: {
"no-chained-type-assertions": noChainedTypeAssertionsRule,
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
"no-known-value-widening": noKnownValueWideningRule,
"no-object-parameters": noObjectParametersRule,
"no-runtime-typeof": noRuntimeTypeofRule,
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
"no-unknown-parameters": noUnknownParametersRule,
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
"no-widen-then-assert": noWidenThenAssertRule,
},
});

export default antiSlopPlugin;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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:
"Chained type assertions discard existing type evidence and fabricate the target type without parsing. Preserve the value's original precise type, or parse genuinely unknown input at its boundary before using it.",
},
},
create(context) {
const checkTypeAssertion = (node: TypeAssertionExpression) => {
if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
context.report({ node, messageId: "chained" });
};

return {
TSAsExpression: checkTypeAssertion,
TSTypeAssertion: checkTypeAssertion,
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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:
"Do not use conditional empty-object spreads. Prefer a direct property or build the object in separate statements.",
},
},
create(context) {
return {
SpreadElement(node) {
if (node.parent.type !== "ObjectExpression") return;

if (isConditionalEmptyObjectSpread(node.argument)) {
context.report({ node, messageId: "avoid" });
}
},
};
},
});
Loading