Ein eigenes ESLint-Plugin entwickeln
Wie du eigene ESLint-Regeln für die Konventionen deines Teams baust: AST-Selektoren, Regeltests, Auto-Fixer und ein gemeinsames Plugin.

Die eingebauten Regeln von ESLint und die gängigen Plugins decken die üblichen Code-Qualitätsprobleme ab: ungenutzte Variablen, fehlendes Error-Handling, konsistenter Stil. Aber jedes Team hat Konventionen, die kein öffentliches Plugin durchsetzt. „Immer unseren eigenen Logger statt console.log verwenden.“ „Das Analytics-SDK niemals außerhalb von Event-Handlern aufrufen.“ „API-Route-Handler müssen den Request-Body mit unserem Schema-Validator validieren.“
Eigene ESLint-Regeln verwandeln diese teamspezifischen Konventionen in automatisierte Prüfungen. Statt Verstöße erst im Code Review zu finden, erkennt sie der Editor bereits beim Tippen. Diese Anleitung baut ein eigenes Plugin von Grund auf — inklusive Regelerstellung, AST-Navigation, Auto-Fixing und Testing.
Den AST verstehen
ESLint-Regeln arbeiten auf Abstract Syntax Trees. Wenn ESLint deinen Code parst, baut es einen Baum, in dem jeder Knoten ein syntaktisches Element darstellt: Funktionsdeklarationen, Variablenzuweisungen, Methodenaufrufe, Bedingungen.
// This code:
console.log("hello");
// Produces this AST (simplified):
const ast = {
type: "Program",
body: [
{
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "MemberExpression",
object: { type: "Identifier", name: "console" },
property: { type: "Identifier", name: "log" },
},
arguments: [
{ type: "Literal", value: "hello" },
],
},
},
],
};
// ESLint rules are visitors that react to specific node types.
// When ESLint encounters a CallExpression node, it calls your
// rule's CallExpression handler with that node.Das Plugin aufsetzen
# Project structure
mkdir eslint-plugin-ourteam && cd eslint-plugin-ourteam
npm init -y
npm install -D eslint @types/eslint typescript vitest{
"name": "eslint-plugin-ourteam",
"version": "1.0.0",
"main": "dist/index.js",
"files": ["dist"],
"scripts": {
"build": "tsc",
"test": "vitest"
}
}// src/index.ts — plugin entry point
import { noConsoleLog } from "./rules/no-console-log";
import { requireBodyValidation } from "./rules/require-body-validation";
import { useCustomLogger } from "./rules/use-custom-logger";
const plugin = {
rules: {
"no-console-log": noConsoleLog,
"require-body-validation": requireBodyValidation,
"use-custom-logger": useCustomLogger,
},
};
export default plugin;Deine erste Regel schreiben
Regel: „In Produktionscode unseren eigenen Logger statt console.log verwenden.“
// src/rules/use-custom-logger.ts
import { Rule } from "eslint";
export const useCustomLogger: Rule.RuleModule = {
meta: {
type: "suggestion",
docs: {
description: "Enforce using @ourteam/logger instead of console methods",
},
fixable: "code",
messages: {
useLogger:
"Use logger.{{method}}() from @ourteam/logger instead of console.{{method}}()",
},
schema: [],
},
create(context) {
// Map console methods to logger methods
const methodMap: Record<string, string> = {
log: "info",
info: "info",
warn: "warn",
error: "error",
debug: "debug",
};
return {
// This visitor fires for every MemberExpression node
MemberExpression(node) {
if (
node.object.type === "Identifier" &&
node.object.name === "console" &&
node.property.type === "Identifier" &&
node.property.name in methodMap
) {
const consoleMethod = node.property.name;
const loggerMethod = methodMap[consoleMethod];
context.report({
node,
messageId: "useLogger",
data: { method: consoleMethod },
fix(fixer) {
// Replace "console.log" with "logger.info"
return fixer.replaceText(node, `logger.${loggerMethod}`);
},
});
}
},
};
},
};Eine komplexere Regel: Body-Validierung erzwingen
Regel: „Jeder Express-Route-Handler mit einer POST/PUT/PATCH-Methode muss validateBody() aufrufen, bevor auf req.body zugegriffen wird.“
// src/rules/require-body-validation.ts
import { Rule } from "eslint";
import { Node } from "estree";
export const requireBodyValidation: Rule.RuleModule = {
meta: {
type: "problem",
docs: {
description: "Require validateBody() before accessing req.body in route handlers",
},
messages: {
missingValidation:
"req.body accessed without calling validateBody() first. " +
"Add validateBody(schema) before using request body data.",
},
schema: [],
},
create(context) {
return {
// Match: app.post("/path", handler) or router.put("/path", handler)
'CallExpression[callee.property.name=/^(post|put|patch)$/]'(
node: Rule.Node
) {
const callExpr = node as unknown as {
arguments: Node[];
};
// Find the handler function (last argument)
const handler = callExpr.arguments.at(-1);
if (!handler) return;
if (
handler.type !== "ArrowFunctionExpression" &&
handler.type !== "FunctionExpression"
) {
return;
}
const body =
handler.body.type === "BlockStatement"
? handler.body.body
: [];
let hasValidation = false;
let reqBodyAccess: Rule.Node | null = null;
for (const stmt of body) {
// Check if validateBody is called
const source = context.getSourceCode().getText(stmt as Rule.Node);
if (source.includes("validateBody")) {
hasValidation = true;
}
// Check if req.body is accessed
if (!reqBodyAccess && source.includes("req.body")) {
reqBodyAccess = stmt as Rule.Node;
}
}
if (reqBodyAccess && !hasValidation) {
context.report({
node: reqBodyAccess,
messageId: "missingValidation",
});
}
},
};
},
};Regeln testen
ESLint stellt einen RuleTester bereit, der das Testen von Regeln unkompliziert macht. Jeder Testfall liefert Code und die erwarteten Fehler (oder deren Fehlen).
// src/rules/__tests__/use-custom-logger.test.ts
import { RuleTester } from "eslint";
import { useCustomLogger } from "../use-custom-logger";
import { describe, it } from "vitest";
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2020, sourceType: "module" },
});
describe("use-custom-logger", () => {
it("should enforce using custom logger", () => {
ruleTester.run("use-custom-logger", useCustomLogger, {
valid: [
// These should NOT trigger the rule
'logger.info("message")',
'logger.error("failed", error)',
'logger.warn("deprecated")',
'someObject.log("this is fine")', // Not console
],
invalid: [
{
code: 'console.log("hello")',
errors: [{ messageId: "useLogger" }],
output: 'logger.info("hello")', // Verify auto-fix output
},
{
code: 'console.error("failed", err)',
errors: [{ messageId: "useLogger" }],
output: 'logger.error("failed", err)',
},
{
code: 'console.warn("deprecated")',
errors: [{ messageId: "useLogger" }],
output: 'logger.warn("deprecated")',
},
],
});
});
});// ❌ Testing only the happy path
const weakTests = {
valid: ['logger.info("ok")'],
invalid: [
{ code: 'console.log("bad")', errors: [{ messageId: "useLogger" }] },
],
};
// ✅ Testing edge cases thoroughly
const thoroughTests = {
valid: [
'logger.info("message")', // Correct usage
'someObject.log("not console")', // Different object
'console.table(data)', // Method not in our map
'const console = {}; console.log()', // Shadowed console
],
invalid: [
// All console methods that should trigger
{ code: 'console.log("x")', errors: 1, output: 'logger.info("x")' },
{ code: 'console.info("x")', errors: 1, output: 'logger.info("x")' },
{ code: 'console.warn("x")', errors: 1, output: 'logger.warn("x")' },
{ code: 'console.error("x")', errors: 1, output: 'logger.error("x")' },
{ code: 'console.debug("x")', errors: 1, output: 'logger.debug("x")' },
// Multiple violations in one file
{
code: 'console.log("a"); console.error("b")',
errors: 2,
},
],
};Das Plugin veröffentlichen und verwenden
# Build the plugin
npm run build
# For internal teams: publish to your registry
npm publish --registry https://npm.internal.company.com
# Or install directly from git
npm install -D git+https://github.com/ourteam/eslint-plugin-ourteam.git// eslint.config.mjs — using the plugin in flat config
import ourteamPlugin from "eslint-plugin-ourteam";
export default [
{
plugins: {
ourteam: ourteamPlugin,
},
rules: {
"ourteam/use-custom-logger": "error",
"ourteam/require-body-validation": "error",
"ourteam/no-console-log": "warn",
},
},
{
// Disable logger rule in test files
files: ["**/*.test.ts", "**/*.spec.ts"],
rules: {
"ourteam/use-custom-logger": "off",
},
},
];Die wichtigsten Erkenntnisse
- Eigene ESLint-Regeln automatisieren das Feedback aus Code Reviews — wenn du immer wieder denselben Review-Kommentar hinterlässt, schreib eine Regel; der Editor erkennt es, bevor der PR geöffnet wird
- Regeln sind AST-Visitor — jede Regel registriert Handler für Knotentypen (CallExpression, MemberExpression); ESLint ruft deinen Handler auf, wenn es diesen Knotentyp findet
- Nutze AST Explorer, um die Knotenstruktur zu verstehen — füge dein Ziel-Code-Muster in astexplorer.net ein, um die genauen Knotentypen und Eigenschaften zu sehen, auf die deine Regel matchen soll
- Auto-Fixer erhöhen die Akzeptanz — eine Regel mit
--fix-Unterstützung wird genutzt; eine Regel, die nur warnt, wird ignoriert; implementiere Fixer für deterministische Transformationen - Teste Randfälle, nicht nur den Happy Path — verdeckte (shadowed) Variablen, unterschiedliche Objektformen und Methodenketten, die ähnlich aussehen, aber nicht console sind, brauchen alle Testabdeckung


