Bỏ qua điều hướng

ESLint là gì? Công cụ phân tích mã nguồn tĩnh cho JavaScript

ESLint đọc mã nguồn JavaScript mà không cần chạy chương trình, bắt lỗi logic từ sớm và giữ cả nhóm đi theo cùng một bộ quy tắc chung.

Tuan Tran Van
10 phút đọc
Mục lục (10 phần)
  1. ESLint là gì?
  2. Linting khác gì với định dạng code và kiểm tra kiểu?
  3. Rule là đơn vị cơ bản của ESLint
  4. eslint.config.js và hệ thống cấu hình phẳng
  5. Plugin, parser và cấu hình chia sẻ
  6. ESLint làm việc với TypeScript như thế nào?
  7. Tự viết rule: đi trên cây AST
  8. Chạy ESLint trong editor và CI
  9. ESLint v10 đã thay đổi những gì?
  10. Tài liệu tham khảo

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 đọc mã nguồn JavaScript và chỉ ra các mẫu có vấn đề mà không cần chạy chương trình

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.

Ba lớp bảo vệ mã nguồn: trình định dạng lo hình thức, trình kiểm tra kiểu lo dữ liệu, còn linter bắt lỗi logic và vi phạm quy chuẩn

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ỏ đó.

Cấu trúc một rule của ESLint và ba mức độ nghiêm trọng off, warn, error

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.

ESLint v10 tìm tệp cấu hình từ thư mục của chính tệp đang kiểm tra thay vì thư mục làm việc hiện tại, cho phép nhiều cấu hình cùng hoạt động trong monorepo

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.

Ba lớp mở rộng của ESLint: plugin bổ sung quy tắc, parser chuyển mã thành cây AST, cấu hình chia sẻ áp dụng nhanh bộ quy chuẩn có sẵn

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.

Quy trình viết rule tùy chỉnh: mã nguồn được phân tích thành cây AST, visitor duyệt qua từng node, context.report báo lỗi và fix tự động sửa

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 chạy ở ba nơi: báo lỗi ngay trong editor, chạy qua dòng lệnh, và chặn mã lỗi tại bước CI trước khi hợp nhất

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.*.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--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.

ESLint v10 loại bỏ eslintrc, các tham số dòng lệnh cũ và chú thích eslint-env, đồng thời nâng yêu cầu phiên bản Node.js

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

Chia sẻ bài viết

X / TwitterFacebookLinkedIn