This article is based on a talk I gave at CI/CD Reintroduction: Generative AI Starts with an Environment That Does Not Run Wild [Actions/CircleCI].
Why custom ESLint?
1. At scale, "review alone" breaks down
In solo development, I naturally know the whole project—not just language and framework, but project anti-patterns. Mechanical checks mostly cover "things I forgot to consider," so the volume is not huge. Crisis management at the Prettier (auto-formatter) level is enough.
With more people, "code I do not know" grows and side effects of changes become unclear. New hires calmly do anti-patterns I said not to. Keeping product quality makes reviewers the bottleneck. "Growth expectation < rising review load" happens often.
When generative AI joins the team, it gets worse. PRs and changed lines increase; similar implementations multiply. It has improved lately, but I still see lots of copy-paste-with-tiny-tweaks code. With so many changed lines, review shrinks to unit tests only. It runs, but accident-prone code accumulates. Unsustainable.
2. If accidents repeat in the same places, build guardrails
Accidents mostly happen in the same places. Swallowed exceptions make incident response hell; type assertions and any weaken future changes; performance landmines (N+1, heavy work on the UI thread). How many times have I pushed with imports in the wrong place? "Do not write template-driven; call IonModal dynamically from ModalController"—if I say it that often, automatic detection is faster.
Existing ESLint is guardrails for the "public road." If the same accidents repeat, build custom guardrails. The person who knows the project's accident sites best is me—the one who knows the project.
- Did you call the Signal itself in the template instead of its value?
- Files that touch the DB belong in
**.query.service.ts, not**.service.ts—I decided that, right? - Soft private is out—hard private only. How many times do I have to say it?
Some may say "just document it." Documentation nobody reads is worse than custom ESLint as documentation. (Reference: a tweet that the person who knows the project's accident sites is yourself)
3. If I build the same guardrails anyway, cheaper and faster wins
Prompts for generative AI are like traffic rule textbooks. If everyone followed the textbook, police would be unnecessary—but AI finds unexpected shortcuts. Prompts cannot be guardrails.
Another approach: generative AI reviewing generative AI—GitHub Copilot reviewing pull requests, for example. But that is paid-only, and is it not slow? If I make it review at ESLint granularity, there are too many items; I want it on substantive work.
ESLint is cheap and fast (debateable), and with fixers it auto-corrects. For migration and automation alone, Codemod is handy too—but for guardrail duty, failing CI matters.
Custom ESLint does not fully "automate review." By automating chores, it lets me and AI reviewers focus on what matters.
Spot when stock ESLint is not enough
1. Project-dependent rules
First, patterns where "both are fine architecturally" but the project must choose. Template-driven is more web-standard; programmatic is more extensible later. The project has to decide.
Template-driven
<ion-button id="open-modal" expand="block">Open</ion-button>
<ion-modal trigger="open-modal" (willDismiss)="onWillDismiss($event)">
<ng-template>...</ng-template>
</ion-modal>
Programmatic
export class ExampleComponent {
private modalCtrl = inject(ModalController)
async openModal() {
const modal = await this.modalCtrl.create({
component: ModalExampleComponent,
});
modal.present();
}
}
2. Rules that do not exist yet
Sometimes the IDE accepts it but it becomes $any, and I want types to enforce it. An Ionic-specific case with no existing ESLint rule:
<!-- ❌ Before -->
<ion-item button="true" disabled="false"></ion-item>
<ion-toggle checked="true"></ion-toggle>
<!-- ✅ After -->
<ion-item [button]="true" [disabled]="false"></ion-item>
<ion-toggle [checked]="true"></ion-toggle>
<!-- ❌ Before -->
<ion-progress-bar value="50" buffer="75"></ion-progress-bar>
<ion-range min="0" max="100"></ion-range>
<!-- ✅ After -->
<ion-progress-bar [value]="50" [buffer]="75"></ion-progress-bar>
<ion-range [min]="0" [max]="100"></ion-range>
3. Rules I am tired of pointing out manually
Like TypeScript types, lifecycle method typing works with or without it—it disappears at compile time. But I want it to prevent accidents:
// ❌ Before
@Component({
selector: 'app-confirm',
templateUrl: './confirm.page.html',
styleUrls: ['./confirm.page.scss'],
})
export class SigninPage {
ionViewWillEnter() {}
}
// ✅ After
@Component({
selector: 'app-confirm',
templateUrl: './confirm.page.html',
styleUrls: ['./confirm.page.scss'],
})
export class SigninPage implements ionViewWillEnter {
ionViewWillEnter() {}
}
Build custom ESLint
1. Scaffolding
Starting from npm init is too much work. I want TypeScript and some automation.
I forked typescript-template-eslint-plugin—use "Use this template." That is eslint-plugin-rules. Besides ESLint rule tests, templates for new rules with ts-node save a lot of effort.
2. Choose a parser
A parser builds an abstract syntax tree from source. You can regex without a parser—it is painful, but that was my experience as a veteran.
Default is espree. To parse Angular templates I need @angular-eslint/template-parser.
3. Run npm run add-rule to generate base files
With the template, npm run add-rule generates base files.
4. Let generative AI do the work
Deciding a rule, writing a failing test, and implementing to pass—it is not a joke, generative AI is extremely good at this. Write the base test, have AI implement to pass, then add exception patterns one after another.
For Angular templates, simple HTML like <div></div> through the parser becomes a structure like this:
[
Element {
name: 'div',
attributes: [],
inputs: [],
outputs: [],
directives: [],
children: [],
references: [],
isSelfClosing: false,
// ... omitted for brevity
type: 'Element',
parent: { ... }
}
]
Understanding this structure tells me which nodes to inspect. Tell generative AI "with this structure, error when condition X" and I can delegate rule implementation.
Summary
Deciding a rule, writing a failing test, and implementing to pass is what generative AI excels at. Write the base test, implement to pass, then add exception patterns. Introduce in the project and keep adding exceptions—that is how I build custom ESLint.
I treat ESLint itself as disposable. Do not build custom ESLint to build ESLint—focus hard on machinery that only automates project rules.
See you next time.