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:
{
"name": "@app/oxc",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./oxlint": "./src/oxlint/index.ts"
},
"scripts": {
"typecheck": "tsgo --noEmit"
}
}Oxlint setup
You then wire all the rules inside a root .oxlintrc.json, referencing the app name in jsPlugins. Nothing fancy.
{
"$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, useMatchfromeffect - Add a
_prefix for private functions (easier to review code) - Blocked all
useEffectanduseState(I usexstatefor 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.mdthat no feature is completed if the linting is not clean π
I have the following:
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 π
