Membangun Custom ESLint Rules: Menegakkan Standar Kode dan DX yang Lebih Baik
1. Pendahuluan
Sebagai developer, kita semua tahu pentingnya kode yang bersih, konsisten, dan bebas bug. Di sinilah ESLint menjadi salah satu senjata andalan kita. ESLint membantu kita menegakkan standar gaya kode, menemukan potensi masalah, dan memastikan semua orang di tim menulis kode dengan cara yang sama. Tapi bagaimana jika aturan bawaan ESLint atau plugin populer lainnya tidak cukup untuk kebutuhan unik proyek atau tim Anda? 🤔
Seringkali, setiap tim atau proyek memiliki “aturan main” sendiri yang melampaui sekadar spasi atau titik koma. Mungkin ada pola arsitektur spesifik yang harus diikuti, praktik keamanan tertentu yang wajib diterapkan, atau bahkan konvensi penamaan komponen yang sangat detail. Inilah saatnya kita perlu melangkah lebih jauh dan membangun custom ESLint rules sendiri.
Dengan custom rules, Anda bisa:
- ✅ Otomatis menegakkan standar kode yang sangat spesifik untuk proyek Anda.
- 🎯 Mencegah bug umum yang sering terjadi di tim Anda.
- 💡 Meningkatkan pengalaman developer (DX) dengan memberikan feedback instan di editor.
- 📌 Mengotomatiskan sebagian proses code review.
Artikel ini akan memandu Anda untuk memahami anatomi sebuah custom rule ESLint dan cara membangunnya dari nol. Siap untuk menjadi “penjaga gerbang” kualitas kode Anda sendiri? Mari kita mulai!
2. Mengapa Custom Rules? Lebih dari Sekadar Gaya Kode
ESLint sangat hebat dalam hal gaya kode. Ia bisa memastikan Anda menggunakan single quotes atau double quotes, ada tidaknya titik koma, atau bahkan panjang baris kode. Namun, kebutuhan tim seringkali lebih kompleks.
Pertimbangkan skenario berikut:
- Enforcement Arsitektur: Anda mungkin punya konvensi bahwa semua utility functions harus diletakkan di folder
src/utilsdan di-import dengan alias@utils. Custom rule bisa melarang import langsung dari path relatif ke file utilitas di luar folder tersebut. - Praktik Keamanan: Tim Anda memutuskan bahwa tidak boleh ada penggunaan langsung
localStorageuntuk menyimpan token autentikasi. Sebagai gantinya, harus menggunakan wrapper khusus yang sudah terenkripsi. Custom rule bisa mendeteksi dan melaporkan setiap panggilanlocalStorage.setItem()ataulocalStorage.getItem(). - Konvensi Khusus Framework/Library: Untuk proyek React, Anda mungkin ingin memastikan setiap komponen yang menerima prop
childrenjuga memiliki propdata-testiduntuk mempermudah end-to-end testing. - Penggunaan Fungsi Terlarang: Anda mungkin ingin melarang penggunaan fungsi bawaan JavaScript tertentu seperti
eval()atau bahkanDate.now()jika Anda punya utility waktu global yang harus selalu digunakan untuk konsistensi.
Contoh terakhir ini, melarang Date.now(), adalah kandidat sempurna untuk custom rule pertama kita. Ini adalah masalah umum di mana developer sering lupa menggunakan utility waktu yang sudah disediakan tim, yang bisa menyebabkan inkonsistensi dalam timestamp atau masalah timezone.
3. Anatomi Sebuah Custom Rule ESLint
Sebelum kita menulis kode, mari kita pahami bagaimana sebuah custom rule ESLint bekerja. Setiap rule pada dasarnya adalah modul JavaScript yang mengekspor sebuah objek dengan dua properti utama: meta dan create.
// rules/my-custom-rule.js
module.exports = {
meta: {
// Informasi metadata tentang rule
},
create(context) {
// Fungsi ini akan mengembalikan objek "visitor"
// yang akan memeriksa Abstract Syntax Tree (AST) kode Anda
return {
// Metode visitor (misalnya, CallExpression, Identifier, JSXAttribute)
// yang akan dipanggil saat ESLint menemukan node AST yang sesuai
};
},
};
Mari kita bedah properti-properti ini:
📌 meta Object
Objek meta berisi metadata tentang rule Anda. Ini penting untuk dokumentasi, konfigurasi, dan bagaimana ESLint memperlakukan rule tersebut.
type: Menentukan jenis rule. Bisa'problem'(potensi bug),'suggestion'(perbaikan yang disarankan), atau'layout'(masalah gaya kode).docs: Objek yang berisi informasi dokumentasi.description: Deskripsi singkat tentang apa yang diperiksa rule ini.category: Kategori rule (misalnya, ‘Best Practices’, ‘Security’, ‘Stylistic Issues’).recommended: Boolean yang menunjukkan apakah rule ini harus diaktifkan secara default di konfigurasieslint:recommended.url: URL ke dokumentasi lengkap rule Anda.
fixable: String ('code'atau'whitespace') jika rule Anda bisa otomatis memperbaiki masalah. Jika tidak, hilangkan properti ini.schema: Mendefinisikan opsi konfigurasi yang dapat diterima oleh rule Anda. Ini memungkinkan developer untuk menyesuaikan perilaku rule.
🎯 create(context) Function
Ini adalah inti dari rule Anda. Fungsi create menerima objek context sebagai argumen dan harus mengembalikan objek “visitor”. Objek visitor ini memiliki metode yang sesuai dengan jenis node di Abstract Syntax Tree (AST) kode Anda.
Apa itu AST? 💡
Bayangkan kode Anda sebagai sebuah kalimat. AST adalah representasi terstruktur dari kalimat tersebut, seperti pohon silsilah. Setiap kata, operator, ekspresi, atau pernyataan adalah “node” di pohon itu. ESLint menggunakan AST untuk memahami struktur kode Anda dan mencari pola tertentu. Misalnya, Date.now() akan dipecah menjadi node Identifier (Date), MemberExpression (Date.now), dan CallExpression (pemanggilan now()).
Ketika ESLint memproses file kode, ia akan “mengunjungi” setiap node di AST dan memanggil metode yang sesuai di objek visitor Anda.
-
contextObject: Objek ini menyediakan informasi dan helper yang berguna untuk rule Anda:context.report(options): Fungsi paling penting untuk melaporkan pelanggaran.optionsbisa berisinode(node AST yang melanggar),message(pesan kesalahan), danfix(fungsi untuk perbaikan otomatis).context.getSourceCode(): Mengembalikan objekSourceCodeyang memungkinkan Anda mengakses teks kode asli, komentar, dan token.context.getFilename(): Mengembalikan nama file yang sedang di-lint.context.options: Array opsi yang dikonfigurasi untuk rule ini di.eslintrc.js.
-
Metode Visitor: Ini adalah kunci untuk berinteraksi dengan AST. Anda bisa mendefinisikan metode untuk berbagai jenis node AST. Contoh:
Identifier(node): Dipanggil untuk setiap pengenal (variabel, nama fungsi).CallExpression(node): Dipanggil untuk setiap pemanggilan fungsi.VariableDeclarator(node): Dipanggil untuk setiap deklarasi variabel.JSXAttribute(node): Dipanggil untuk setiap atribut JSX (misalnya, di React).
4. Membangun Rule Pertama Anda: no-direct-date-now
Mari kita terapkan konsep ini untuk membuat rule yang melarang penggunaan Date.now() secara langsung.
Tujuan Rule:
Mendeteksi dan melaporkan setiap kali Date.now() dipanggil dalam kode, dan menyarankan penggunaan fungsi utility getCurrentTimestamp() sebagai gantinya.
Langkah 1: Buat Struktur Proyek
Buat folder untuk plugin ESLint Anda. Misalnya: eslint-plugin-my-team.
eslint-plugin-my-team/
├── index.js
└── lib/
└── rules/
└── no-direct-date-now.js
└── tests/
└── lib/
└── rules/
└── no-direct-date-now.js
Langkah 2: Tulis Rule (lib/rules/no-direct-date-now.js)
// lib/rules/no-direct-date-now.js
/**
* @fileoverview Disallow direct calls to Date.now()
* @author Your Name
*/
"use strict";
module.exports = {
meta: {
type: 'problem', // Ini adalah potensi bug atau anti-pattern
docs: {
description: 'Disallow direct calls to Date.now(). Use `getCurrentTimestamp()` utility instead.',
category: 'Best Practices', // Masuk kategori praktik terbaik
recommended: false, // Tidak direkomendasikan secara default
url: 'https://your-team.com/docs/eslint/no-direct-date-now', // Ganti dengan URL dokumentasi tim Anda
},
schema: [], // Rule ini tidak menerima opsi konfigurasi apa pun
},
create(context) {
return {
// Kita tertarik pada setiap CallExpression (pemanggilan fungsi)
CallExpression(node) {
// Periksa apakah ini adalah pemanggilan metode
if (node.callee.type === 'MemberExpression') {
const object = node.callee.object; // Bagian "Date"
const property = node.callee.property; // Bagian "now"
// Periksa apakah objek adalah Identifier dengan nama 'Date'
// dan properti adalah Identifier dengan nama 'now'
if (
object.type === 'Identifier' && object.name === 'Date' &&
property.type === 'Identifier' && property.name === 'now'
) {
// Jika cocok, laporkan pelanggaran
context.report({
node, // Node AST yang melanggar
message: 'Avoid direct calls to Date.now(). Use `getCurrentTimestamp()` utility instead.',
});
}
}
},
};
},
};
Penjelasan Kode:
- Kita mendefinisikan objek
metadengantype: 'problem'karena ini adalah anti-pattern yang bisa menyebabkan masalah. - Di fungsi
create, kita mengembalikan objek visitor. Kita hanya perlu mendengarkan eventCallExpression(node)karenaDate.now()adalah pemanggilan fungsi. - Di dalam
CallExpression, kita memeriksa struktur AST dariDate.now().node.callee.type === 'MemberExpression'berarti kita memanggil sebuah metode dari sebuah objek (misalnyaobj.method()).node.callee.objectakan merujuk ke bagianDate.node.callee.propertyakan merujuk ke bagiannow.
- Jika semua kondisi cocok, kita memanggil
context.report()dengan node yang melanggar dan pesan kesalahan yang jelas.
Langkah 3: Uji Manual Rule Anda
Untuk menguji rule ini, Anda bisa membuat file .eslintrc.js sementara di root proyek eslint-plugin-my-team dan sebuah file test.js.
test.js:
// test.js
const timestamp = Date.now(); // Ini harusnya kena lint
console.log(timestamp);
const safeTimestamp = getCurrentTimestamp(); // Ini harusnya aman
console.log(safeTimestamp);
const myDate = new Date(); // Ini juga aman
console.log(myDate.getFullYear());
.eslintrc.js:
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
plugins: [
// Arahkan ESLint untuk memuat plugin dari folder saat ini
// Nama plugin akan menjadi 'my-team'
'./',
],
rules: {
// Aktifkan rule custom Anda
'my-team/no-direct-date-now': 'error',
// Asumsikan getCurrentTimestamp() adalah global atau diimport
'no-undef': ['error', { 'typeof': true }],
},
};
Sekarang, jalankan npx eslint test.js dari root eslint-plugin-my-team. Anda seharusnya melihat kesalahan:
test.js
1:17 error Avoid direct calls to Date.now(). Use `getCurrentTimestamp()` utility instead. my-team/no-direct-date-now
❌ 1 problem (1 error, 0 warnings)
5. Menguji Custom Rule Anda dengan Unit Test
Pengujian manual itu bagus untuk verifikasi awal, tetapi untuk memastikan rule Anda robust dan tidak rusak di masa depan, unit testing adalah kuncinya. ESLint menyediakan RuleTester yang sangat berguna untuk ini.
Langkah 1: Buat File Test (lib/tests/lib/rules/no-direct-date-now.js)
// lib/tests/lib/rules/no-direct-date-now.js
/**
* @fileoverview Tests for no-direct-date-now rule.
* @author Your Name
*/
"use strict";
const { RuleTester } = require('eslint');
const rule = require('../../../rules/no-direct-date-now'); // Import rule yang akan diuji
// Buat instance RuleTester
const ruleTester = new RuleTester({
// Konfigurasi parser untuk mendukung fitur JS modern (misalnya ES6)
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
});
// Jalankan tes
ruleTester.run('no-direct-date-now', rule, {
// Array kode yang VALID (tidak boleh ada pelanggaran)
valid: [
{ code: 'const timestamp = getCurrentTimestamp();' },
{ code: 'const date = new Date();' },
{ code: 'console.log("Hello, world!");' },
{ code: 'const now = () => { return Date.now(); }; // OK jika di dalam fungsi, mungkin ada pengecualian' },
],
// Array kode yang INVALID (harus ada pelanggaran)
invalid: [
{
code: 'const timestamp = Date.now();',
errors: [{ messageId: 'avoidDirectCall' }], // messageId akan kita definisikan nanti di rule
},
{
code: 'function logTime() { console.log(Date.now()); }',
errors: [{ messageId: 'avoidDirectCall' }],
},
{
code: 'if (Date.now() > someTime) { /* ... */ }',
errors: [{ messageId: 'avoidDirectCall' }],
},
],
});
⚠️ Penting: Untuk test yang lebih modular dan mudah di-maintain, Anda bisa menggunakan messageId di objek meta rule Anda.
Langkah 2: Perbarui Rule dengan messageId
// lib/rules/no-direct-date-now.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow direct calls to Date.now(). Use `getCurrentTimestamp()` utility instead.',
category: 'Best Practices',
recommended: false,
url: 'https://your-team.com/docs/eslint/no-direct-date-now',
},
schema: [],
messages: { // Tambahkan properti messages
avoidDirectCall: 'Avoid direct calls to Date.now(). Use `getCurrentTimestamp()` utility instead.',
},
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'MemberExpression') {
const object = node.callee.object;
const property = node.callee.property;
if (
object.type === 'Identifier' && object.name === 'Date' &&
property.type === 'Identifier' && property.name === 'now'
) {
context.report({
node,
messageId: 'avoidDirectCall', // Gunakan messageId di sini
});
}
}
},
};
},
};
Sekarang, Anda bisa menjalankan tes dengan runner seperti mocha atau jest (atau cukup dengan Node.js jika Anda hanya memiliki satu file test).
Misalnya, dengan Mocha:
- Install
mocha:npm install mocha --save-dev - Tambahkan script di
package.json:"test": "mocha lib/tests" - Jalankan
npm test.
Ini akan memastikan rule Anda bekerja seperti yang diharapkan untuk berbagai skenario.
6. Mengintegrasikan dan Mendistribusikan Rule Anda (Plugins)
Setelah rule Anda selesai dan teruji, langkah selanjutnya adalah mengintegrasikannya ke dalam proyek Anda dan mungkin mendistribusikannya sebagai plugin ESLint.
Struktur Plugin
File index.js di root folder plugin Anda adalah pintu masuk utama. Ini akan mengekspor objek yang mendefinisikan rules, configs, processors, dll.
// index.js
module.exports = {
rules: {
'no-direct-date-now': require('./lib/rules/no-direct-date-now'),
// Tambahkan rule lain di sini jika ada
},
configs: {
// Anda bisa mendefinisikan set konfigurasi default
// Misalnya, 'recommended' yang mengaktifkan beberapa rule Anda
recommended: {
plugins: ['my-team'],
rules: {
'my-team/no-direct-date-now': 'error',
},
},
},
};
Menggunakan Plugin di Proyek Anda
Untuk menggunakan plugin ini di proyek lain:
-
Instal Plugin: Jika Anda ingin mendistribusikannya secara publik, publikasikan ke npm. Jika hanya untuk internal, Anda bisa menggunakan
npm linkatau menginstal dari path lokal/repositori Git.npm install eslint-plugin-my-team --save-dev -
Konfigurasi
.eslintrc.js:// .eslintrc.js di proyek yang menggunakan plugin module.exports = { // ... konfigurasi lainnya ... plugins: [ 'my-team', // Nama plugin Anda ], extends: [ // ... extend konfigurasi lain (misalnya 'airbnb', 'plugin:react/recommended') 'plugin:my-team/recommended', // Menggunakan konfigurasi recommended dari plugin Anda