β€’

tech

Custom linting rules to guide AI code

AI codes fast. Too fast for you to review that much slop code and bad patterns. But there is a solution: prevent bad patterns altogether with custom linting rules. Here is how I do this.


Sandro Maglione

Sandro Maglione

Software

Your next unlock in better AI coding: custom linting rules πŸ’‘

I noticed one to many times me fixing the same issues or wrong API usage.

Custom lints solve this problem completely, what you get is clean and complete.

Here is how I do it, and some of the rules I added πŸ‘‡


Step 1: Monorepo (please)

Let me repeat it again (it's never enough):

AI coding works better full stack, all in one monorepo, with full context end to end πŸ—οΈ

Read some of the previous newsletters to get some more details.

A monorepo also unlock project-specific custom linting rules πŸ”“

I am using oxlint, with the (recently introduced) JS Plugins Alpha.

I have a packages/oxc package in my monorepos where all the linting rules are implemented and provided.

Nothing fancy really, a single exported index.ts, with all the rules defined one each file:

package.json
{
  "name": "@app/oxc",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "exports": {
    "./oxlint": "./src/oxlint/index.ts"
  },
  "scripts": {
    "typecheck": "tsgo --noEmit"
  }
}
Example of custom linting rules I implemented in one of my projects.
Example of custom linting rules I implemented in one of my projects.

Oxlint setup

You then wire all the rules inside a root .oxlintrc.json, referencing the app name in jsPlugins. Nothing fancy.

.oxlintrc.json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "jsPlugins": ["@app/oxc/oxlint"],
  "rules": {
    "app/no-banned-type-assertions": "error",
    "app/no-comments": "error",
    "app/no-multiple-function-params": "error",
    "app/no-react-state-hooks": "error",
    "app/no-switch": "error",
    "app/no-sync-schema-apis": "error",
    "app/no-type-assertion": "warn",
    "app/private-function-prefix": "error",
  }
}

This is mostly it πŸ’πŸΌβ€β™‚οΈ

What about the actual rules implementation, you ask? It does not really much matter, leave it to the AI πŸ€–

The API allows to inspect code and report issues. But you can just leave it to the AI, no need to know the details of it.

In fact, probably what matters most is the actual description of the issue, so an AI agent hitting it knows how to fix it πŸ™Œ

Here is an example (to disable as any, as unknown, as never):

const bannedTypes = new Set([
  "TSAnyKeyword",
  "TSNeverKeyword",
  "TSUnknownKeyword",
]);

const rule = {
  meta: {
    type: "problem",
    docs: {
      description: "Disallow as any, as never, and as unknown assertions.",
    },
  },
  create(context: {
    report: (opts: { node: unknown; message: string }) => void;
  }) {
    return {
      TSAsExpression(node: { typeAnnotation: { type: string } }) {
        if (bannedTypes.has(node.typeAnnotation.type)) {
          context.report({
            node,
            message:
              "Do not assert to any, never, or unknown. Fix the type or use generics.",
          });
        }
      },
      TSTypeAssertion(node: { typeAnnotation: { type: string } }) {
        if (bannedTypes.has(node.typeAnnotation.type)) {
          context.report({
            node,
            message:
              "Do not assert to any, never, or unknown. Fix the type or use generics.",
          });
        }
      },
    };
  },
};

export default rule;

My custom rules

This is my genius workflow:

  • Let AI implement
  • Notice repeated bad patterns (read the AI code!)
  • Ask another AI to define a linting rule to block those

I was able to reduce the instructions inside AGENTS.md, and convert them to linting rules πŸ’‘

Here is what I have:

  • Banned type cast with as (outside of some narrow contexts)
  • Functions with more than one parameter must accept a single object parameter
  • Stop using switch, use Match from effect
  • Add a _ prefix for private functions (easier to review code)
  • Blocked all useEffect and useState (I use xstate for everything)

For your reference, here is the rule to move away from useEffect/useState and into xstate:

const bannedHooks = new Set(["useEffect", "useState"]);

const rule = {
  meta: {
    type: "problem",
    docs: {
      description:
        "Disallow React state hooks. Use xstate actors for state and side effects instead.",
    },
  },
  create(context: {
    report: (opts: { node: unknown; message: string }) => void;
  }) {
    return {
      CallExpression(node: {
        callee: {
          type: string;
          name?: string;
          property?: { type: string; name?: string };
        };
      }) {
        const callee = node.callee;
        const hookName =
          callee.type === "Identifier"
            ? callee.name
            : callee.type === "MemberExpression" &&
                callee.property?.type === "Identifier"
              ? callee.property.name
              : undefined;

        if (hookName !== undefined && bannedHooks.has(hookName)) {
          context.report({
            node: callee,
            message: `${hookName} is banned. Use xstate actors instead.`,
          });
        }
      },
    };
  },
};

export default rule;

All of this reduces your time to review AI code. Most bad patterns are reported by linting, and fixed by the AI before they get to you 🀝

Of course, make sure to specify inside AGENTS.md that no feature is completed if the linting is not clean πŸ‘€

I have the following:

AGENTS.md
Run `pnpm run verify` to verify each change,
which runs formatting, checks for type errors and linting errors and warnings.

No feature is complete if `pnpm run verify` does not pass without any errors.

TypeScript 7.0 (tsgo) is coming (I am already using it). My workflow with AI is also getting better and faster.

Yours should as well, take note πŸ“

See you next πŸ‘‹

Start here.

Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights

Not convinced? Well, let me tell you more about it