ESLint là một công cụ phân tích mã nguồn tĩnh (static analysis) dành cho ngôn ngữ JavaScript.
Nicholas C. Zakas viết công cụ này vào tháng 6 năm 2013 để giải quyết một chuyện rất cụ thể: chỉ ra các mẫu mã nguồn có vấn đề hoặc lệch quy chuẩn lập trình mà không phải chạy chương trình. JavaScript là ngôn ngữ động, kiểu dữ liệu lỏng lẻo (loosely-typed), lại không có bước biên dịch (compilation) bắt buộc nào chặn bạn lại trước khi mã đến tay người dùng. Sai sót vì thế hay chờ đến lúc ứng dụng đang chạy mới chịu lộ ra. ESLint phân tích cú pháp mã nguồn rồi đối chiếu với bộ quy tắc đã định nghĩa sẵn, nên lỗi hiện ra ngay trên bàn phím thay vì trên môi trường thật.
Triết lý cốt lõi của ESLint là không áp đặt: mọi quy tắc đều có thể cắm thêm (pluggable) hoặc thay bằng thứ khác, tùy nhu cầu của từng dự án.

ESLint là gì?
Công cụ này đọc mã nguồn của bạn như một cái máy soi: tìm các mẫu mà nó cho là có vấn đề, rồi báo lại. Không chạy chương trình, không cần một dòng test nào. Mục tiêu chỉ có hai: giữ mã nhất quán và chặn trước những lỗi logic còn nằm im. ESLint chạy trên nền Node.js nên cài qua trình quản lý gói npm là xong, và tốc độ thực thi đủ nhanh để bật thẳng trong editor.
Thứ làm ESLint khác các công cụ cùng thời là kiến trúc mở rộng. "Mọi thứ đều có thể cắm thêm" không phải khẩu hiệu suông: bạn thay được bộ quy tắc (rule), trình định dạng báo cáo (formatter), thậm chí cả bộ phân tích cú pháp (parser). Công cụ không ép bạn theo phong cách lập trình nào. Bạn tự định nghĩa, hoặc mượn một bộ quy chuẩn của cộng đồng về dùng, miễn hợp với văn hóa kỹ thuật của tổ chức mình. Tôi nghĩ chính chỗ mở đó, chứ không phải bộ quy tắc mặc định, mới là thứ giữ ESLint ở lại lâu đến vậy.
Linting khác gì với định dạng code và kiểm tra kiểu?
Ba công cụ này hay bị gộp làm một, nhất là khi cả ba cùng nhảy lên lúc bạn nhấn lưu. Nhưng chúng soi những thứ khác nhau.
Linter tìm các mẫu mã có khả năng gây lỗi logic hoặc đi ngược best practice. Trình định dạng mã
(formatter) chỉ quan tâm vẻ ngoài: khoảng trắng, thụt đầu dòng, dấu
ngoặc. Lấy quy tắc no-unsafe-finally làm ví dụ. Nó chặn các câu lệnh điều hướng luồng chương trình
nằm bên trong khối finally, một lỗi logic tinh vi có thể khiến chương trình cư xử khác hẳn ý bạn.
Trình định dạng chạy qua đúng đoạn đó và thấy mọi thứ ổn, vì xét về hình thức thì nó ổn thật.

Với trình kiểm tra kiểu (type checker) như TypeScript, quan hệ là bổ trợ chứ không chồng lấn. Type checker giữ cho dữ liệu nhất quán để tránh lỗi thời gian chạy (runtime). Linter bắt cả những đoạn hợp lệ hoàn toàn về mặt kiểu nhưng vẫn vi phạm tiêu chuẩn chất lượng hoặc bốc mùi sai logic. Chạy đủ cả ba thì mỗi lớp bịt một loại lỗ khác nhau. Bỏ bớt một lớp không làm dự án sập ngay, nó chỉ đẩy lỗi sang chỗ đắt tiền hơn để phát hiện.
Rule là đơn vị cơ bản của ESLint
Mỗi quy tắc (rule) trả lời đúng một câu hỏi: đoạn mã này có đáp ứng kỳ vọng cụ thể kia không? Có
quy tắc tự sửa được (fixable, ký hiệu 🔧), chạy một lệnh là mã tự đổi. Có quy tắc chỉ dám gợi ý
(suggestions, ký hiệu 💡) rồi để bạn quyết, vì đụng vào là đụng tới logic ứng dụng. Hai ví dụ gần
gũi nhất: semi kiểm soát dấu chấm phẩy, còn no-unused-vars réo tên những biến bạn khai báo rồi
bỏ đó.

Trong cấu hình, mỗi quy tắc nhận một trong ba mức độ nghiêm trọng (severity levels):
off(0): Tắt hẳn quy tắc.warn(1): Bật ở dạng cảnh báo. Bạn nhìn thấy nó, nhưng mã thoát của chương trình không đổi.error(2): Bật ở dạng lỗi. Chương trình dừng lại với mã thoát là 1, và đây chính là mức làm đỏ một bước kiểm tra tự động.
eslint.config.js và hệ thống cấu hình phẳng
Kể từ phiên bản v10, eslint.config.js (hệ thống cấu hình phẳng, flat config) là định dạng cấu hình
duy nhất còn được hỗ trợ. Thay đổi lặng lẽ hơn nằm ở thuật toán tìm tệp cấu hình: ESLint bắt đầu tìm
từ chính thư mục của tệp đang được kiểm tra (linted file), thay vì bám vào thư mục làm việc hiện tại
(CWD) như trước. Nghe khô khan, nhưng với monorepo thì đây là thứ đáng giá nhất trong cả bản phát
hành. Nhiều tệp cấu hình cùng tồn tại và cùng có hiệu lực trong một lần chạy, mỗi dự án con giữ tiêu
chuẩn riêng, không ai phải nhồi hết ngoại lệ vào một tệp khổng lồ nữa. Hệ thống cũ (eslintrc) bị
loại bỏ hoàn toàn, đổi lấy sự bớt mơ hồ và hiệu năng xử lý tốt hơn ở các dự án quy mô lớn.

Plugin, parser và cấu hình chia sẻ
Tiện ích mở rộng (plugin) thực chất chỉ là các module npm gói sẵn một tập quy tắc và cấu hình bổ
sung, nhờ đó ESLint hiểu được các thư viện như React hay
Angular. Muốn đọc được mã, ESLint cần một bộ phân tích cú pháp (parser)
biến văn bản thô thành cây cú pháp trừu tượng (abstract syntax tree, AST). Mặc định là Espree. Gặp
cú pháp không tiêu chuẩn thì bạn thay parser khác vào. Còn cấu hình chia sẻ (shareable configs), kiểu
eslint-config-airbnb-base, cho bạn mượn nguyên một bộ quy chuẩn phổ biến thay vì ngồi bật tắt từng
quy tắc. Được cái nhanh. Mất cái là bạn thừa hưởng luôn cả những lựa chọn mình không đồng tình.

ESLint làm việc với TypeScript như thế nào?
Cầu nối là @typescript-eslint/parser, một bộ phân tích cú pháp tùy chỉnh giúp ESLint đọc và hiểu
các cấu trúc đặc thù của TypeScript. Từ đó linter và trình biên dịch TypeScript chia nhau việc: một
bên lo an toàn kiểu, một bên lo chất lượng và logic. Có một chi tiết nhỏ cho thấy sự phối hợp này
được chăm tới đâu. Trong các phiên bản gần đây, quy tắc max-params bổ sung tùy chọn countThis để
bỏ qua tham số this khi đếm số lượng tham số của hàm TypeScript. Ở TypeScript, this đứng chỗ đó
thường chỉ để định nghĩa kiểu dữ liệu cho ngữ cảnh của hàm chứ không phải một tham số thực tế lúc
thực thi, nên đếm nó vào là báo nhầm.
Tự viết rule: đi trên cây AST
Viết một quy tắc riêng không đáng sợ như cái tên của nó. Bạn khởi tạo dự án, khai báo phần siêu dữ
liệu (metadata) mô tả loại quy tắc, tài liệu hướng dẫn và khả năng sửa lỗi. Phần thật sự quan trọng
là hàm create(context): ở đó bạn định nghĩa các phương thức duyệt (visitor methods) để bắt các nút
(node) trên cây AST. Chọn đúng loại node, chẳng hạn VariableDeclarator, là bạn đã đứng ngay chỗ
cần đứng trong cấu trúc logic của mã nguồn.

Thấy mã không đạt yêu cầu, bạn gọi context.report() để thông báo lỗi. Muốn tử tế hơn thì đính kèm
một hàm fix(fixer), ví dụ dùng fixer.replaceText() để thay đoạn mã vi phạm bằng đoạn đúng. Phần
kiểm thử cũng có sẵn: lớp RuleTester chạy được kịch bản cho cả trường hợp mã đúng (valid) lẫn mã
sai (invalid), không phải lôi thêm thư viện bên thứ ba nào vào.
Chạy ESLint trong editor và CI
Chỗ ESLint có ích nhất là ngay trong trình soạn thảo. Cài extension, gạch đỏ hiện lên lúc bạn còn
đang gõ dở, sửa luôn, không ai kịp nhìn thấy. Ngoài editor còn có giao diện dòng lệnh (CLI), gọi
bằng npx eslint yourfile.js. Và cuối cùng là quy trình tích hợp liên tục
(CI). Đây mới là chốt chặn thật, vì nó không phụ thuộc vào chuyện ai đó có
nhớ bật extension hay không: mã lỗi bị giữ lại trước khi kịp hợp nhất vào kho lưu trữ chính của dự
án.

ESLint v10 đã thay đổi những gì?
Phiên bản v10 chốt sổ hệ thống cấu hình cũ, không chừa đường lùi. Các tệp .eslintrc.* và
.eslintignore không còn được hỗ trợ, biến môi trường ESLINT_USE_FLAT_CONFIG cũng chính thức mất
hiệu lực. Loạt tham số dòng lệnh phục vụ hệ thống cũ bị xóa sạch: --no-eslintrc, --env,
--resolve-plugins-relative-to, --rulesdir và --ignore-path. Cả các chú thích môi trường dạng
/* eslint-env */ nằm trong mã nguồn giờ cũng bị báo lỗi thay vì được xử lý như trước. Dự án nào
còn dính eslintrc mà muốn lên v10 thì chỉ có một đường: viết lại cấu hình.

Về hạ tầng kỹ thuật, v10 đòi Node.js phiên bản ^20.19.0, ^22.13.0 hoặc >=24. Đổi lại cũng có
thứ đáng lấy. ESLint theo dõi được tham chiếu JSX (JSX references), nhờ đó phân tích phạm vi (scope
analysis) chính xác hơn và bạn bớt phải nhìn những cảnh báo sai về biến đã định nghĩa mà không được
sử dụng. Hàm loadESLint() từ nay luôn trả về class ESLint.
Ai đang viết bộ quy tắc thì phải đọc kỹ hơn một chút. Nhiều phương thức cũ trong đối tượng context
đã bị loại bỏ, thay bằng thuộc tính trực tiếp: context.getSourceCode() giờ là context.sourceCode,
còn context.parserPath thì không còn được hỗ trợ. Đáng chú ý nhất là phạm vi của nút Program trên
cây AST, nay bao trọn văn bản nguồn, tính cả khoảng trắng và chú thích ở đầu lẫn cuối tệp. Quy tắc
nào từng ngầm dựa vào phạm vi cũ thì nên kiểm tra lại trước khi nâng cấp.
Tài liệu tham khảo
- About - ESLint - Pluggable JavaScript Linter
- Core Concepts - ESLint - Pluggable JavaScript Linter
- Getting Started with ESLint - ESLint - Pluggable JavaScript Linter
- Rules Reference - ESLint - Pluggable JavaScript Linter
- ESLint v10.0.0 released - ESLint - Pluggable JavaScript Linter
- Custom Rule Tutorial - ESLint - Pluggable JavaScript Linter
- typescript-eslint
- Static analysis | web.dev