Linting là việc kiểm tra tự động mã nguồn để tìm lỗi lập trình và phong cách. Việc kiểm tra này được thực hiện bởi công cụ phân tích code tĩnh gọi là linter. Một code formatter, tuy nhiên, là công cụ liên quan đến định dạng mã nguồn để nó tuân thủ chặt chẽ một tập quy tắc được cấu hình trước. Linter thường báo cáo vi phạm, nhưng thường để lập trình viên tự sửa, trong khi code formatter xu hướng áp dụng quy tắc trực tiếp vào mã nguồn, từ đó tự động sửa lỗi định dạng.
Nhiệm vụ tạo phong cách code nhất quán trong dự án thường đòi hỏi giới thiệu công cụ linting và formatting riêng biệt, nhưng trong một số trường hợp, một công cụ đơn lẻ có thể giải quyết cả hai vấn đề. Ví dụ tốt cho trường hợp sau là RuboCop, công cụ chúng ta sẽ xem xét kỹ trong bài này. Bạn sẽ học cách cài đặt nó trong dự án Ruby và điều chỉnh các tùy chọn cấu hình sao cho đầu ra phù hợp với kỳ vọng. Ngoài việc tích hợp vào quy trình phát triển cục bộ, bạn cũng sẽ học cách làm cho nó trở thành một phần của quy trình continuous integration.
Cài đặt RuboCop
Cài đặt RuboCop rất đơn giản qua RubyGems:
$ gem install rubocop
Kiểm tra phiên bản đã cài:
$ rubocop --version
1.18.3
Nếu bạn thích dùng Bundler, đặt đoạn sau vào Gemfile và chạy bundle install. Phần require: false báo cho Bundler.require không require gem đó trong code vì nó chỉ dùng từ dòng lệnh.
gem 'rubocop', require: false
Kiểm tra phiên bản:
$ bundle exec rubocop --version
1.18.3
Chạy RuboCop
Bạn có thể chạy RuboCop với cài đặt mặc định trên dự án bằng cách gõ rubocop (hoặc bundle exec rubocop nếu cài bằng Bundler). Nếu không truyền đối số, nó sẽ kiểm tra tất cả file Ruby trong thư mục hiện tại và các thư mục con. Thay vào đó, bạn có thể truyền danh sách file và thư mục cần phân tích.
$ bundle exec rubocop
$ bundle exec rubocop src/lib
Mặc định, RuboCop thực thi nhiều hướng dẫn trong Ruby Style Guide do cộng đồng dẫn dắt. Sau khi chạy, bạn có thể nhận được một số lỗi (offenses). Mỗi vi phạm được báo cáo đều đi kèm mọi thông tin cần thiết để giải quyết, như mô tả vi phạm, file và số dòng xảy ra.

Ở cuối báo cáo, bạn thấy dòng mô tả số file đã kiểm tra, tổng số vi phạm, và bao nhiêu vi phạm có thể tự động sửa. Nếu thêm đối số -a hoặc --auto-correct, RuboCop sẽ cố gắng tự động sửa các vấn đề tìm thấy (những cái có tiền tố [Correctable]).
$ bundle exec rubocop -a

Chú ý mỗi vi phạm đã sửa giờ có tiền tố [Corrected]. Tóm tắt số vi phạm đã sửa cũng xuất hiện ở cuối báo cáo. Trong ví dụ trên, còn một vi phạm có thể sửa nhưng không được auto-fix, ngay cả sau cờ -a. Điều này là vì một số sửa tự động có thể thay đổi ngữ nghĩa code nhẹ, nên RuboCop coi nó không an toàn. Nếu muốn auto-correct những vi phạm này nữa, dùng cờ -A hoặc --auto-correct-all.
$ bundle exec rubocop -A

Một quy tắc tốt là chạy test suite sau khi dùng autocorrect để đảm bảo hành vi code không thay đổi bất ngờ.
Cấu hình RuboCop
RuboCop có thể cấu hình qua file .rubocop.yml đặt ở gốc dự án. Nếu muốn dùng cùng kiểm tra cho mọi dự án, bạn có thể đặt file config toàn cục trong thư mục home (~/.rubocop.yml) hoặc thư mục config XDG (~/.config/rubocop/config.yml). File config toàn cục sẽ được dùng nếu file config cục bộ không tìm thấy trong thư mục hiện tại hoặc các thư mục cha.
Cấu hình mặc định của RuboCop đặt trong thư mục home config (~/.config/rubocop/default.yml), và mọi file config khác đều kế thừa từ nó. Điều này có nghĩa khi thiết lập config dự án, bạn chỉ cần thay đổi những gì khác với mặc định. Có thể là bật/tắt một số kiểm tra hoặc thay đổi hành vi nếu chúng chấp nhận tham số.
RuboCop gọi mỗi kiểm tra riêng lẻ là cops, mỗi cái chịu trách nhiệm phát hiện một vi phạm cụ thể. Các cops có sẵn cũng được nhóm thành các department sau:
- Style cops chủ yếu dựa trên Ruby Style Guide, kiểm tra tính nhất quán của code.
- Layout cops bắt lỗi liên quan đến định dạng, như sử dụng khoảng trắng.
- Lint cops phát hiện lỗi tiềm ẩn trong code, tương tự
ruby -w, nhưng với nhiều kiểm tra thêm. - Metric cops xử lý vấn đề liên quan đến đo lường code như độ dài class và method.
- Naming cops quan tâm đến quy ước đặt tên.
- Security cops giúp bắt các vấn đề bảo mật tiềm ẩn.
- Bundler cops kiểm tra thói quen xấu trong file Bundler (như
Gemfile). - Gemspec cops kiểm tra thói quen xấu trong file
.gemspec.
Cũng có thể mở rộng RuboCop qua các linter và formatter bổ sung. Bạn có thể tự xây dựng extension hoặc tận dụng các extension hiện có nếu liên quan đến dự án. Ví dụ, extension Rails có sẵn để thực thi best practice và quy ước code Rails.

Khi tạo file cấu hình lần đầu, bạn sẽ nhận nhiều cảnh báo về các cops mới được thêm nhưng chưa cấu hình. Điều này vì RuboCop thêm cops mới mỗi release, và chúng được đặt ở trạng thái pending đặc biệt cho đến khi được bật/tắt rõ ràng trong config người dùng. Bạn có thể bật/tắt từng cops trong thông báo hoặc dùng đoạn sau để bật tất cả cops mới (khuyến nghị). Sau đó, thông báo sẽ bị ẩn.
# .rubocop.yml
AllCops:
NewCops: enable
Nếu không muốn loay hoay với file cấu hình và nhiều tùy chọn của RuboCop, hãy xem dự án Standard. Nó chủ yếu là phiên bản RuboCop được cấu hình sẵn nhằm thực thi phong cách nhất quán trong dự án Ruby mà không cho phép tùy chỉnh quy tắc. Bài lightning talk nơi nó được công bố lần đầu đưa ra thêm chi tiết về nguồn gốc và động lực.
Bạn có thể cài bằng cách thêm dòng sau vào Gemfile và chạy bundle install.
# Gemfile
gem "standard", group: [:development, :test]
Sau đó, thực thi Standard từ dòng lệnh như sau:
$ bundle exec standardrb
Thêm RuboCop vào dự án đã tồn tại
Hầu hết Rubyists không có đặc quyền làm việc trên dự án greenfield. Nhiều thời gian phát triển dành cho codebase legacy có thể tạo ra lượng lớn vi phạm linting không thể xử lý ngay. May mắn thay, RuboCop có tính năng hữu ích tạo allowlist các vi phạm hiện có, có thể giải quyết dần theo thời gian. Lợi ích là cho phép giới thiệu linting vào dự án hiện có mà không bị bủa vây bởi núi lỗi linting khó quản lý trong khi vẫn gắn cờ các vi phạm mới từ nay về sau.
$ bundle exec rubocop
523 files inspected, 1018 offenses detected
Tạo file config allowlist bằng lệnh:
$ bundle exec rubocop --auto-gen-config
Added inheritance from `.rubocop_todo.yml` in `.rubocop.yml`.
Created .rubocop_todo.yml.
.rubocop_todo.yml trong thư mục hiện tại nơi tất cả vi phạm hiện tại bị bỏ qua. Cuối cùng, nó làm cho .rubocop.yml kế thừa từ .rubocop_todo.yml để chạy RuboCop lại trên codebase không còn sinh ra vi phạm nào.
$ bundle exec rubocop
523 files inspected, no offenses detected
Khi tạo file allowlist, RuboCop sẽ tắt hoàn toàn một cop nếu số vi phạm vượt quá ngưỡng nhất định (15 mặc định). Thông thường không phải điều bạn muốn vì nó ngăn code mới được kiểm tra bởi cop đó do số vi phạm hiện có. May mắn thay, có thể tăng ngưỡng để cops không bị tắt ngay cả khi vi phạm nhiều.
$ bundle exec rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 10000
Tùy chọn --auto-gen-only-exclude đảm bảo mỗi cop trong allowlist có block Exclude liệt kê tất cả file vi phạm, thay vì Max đặt số file loại trừ tối đa cho cop. Việc đặt --exclude-limit cũng thay đổi số file tối đa có thể thêm vào block Exclude cho mỗi cop. Chỉ định số tùy ý lớn hơn tổng file đang kiểm tra đảm bảo không cop nào bị tắt hoàn toàn, và code mới thêm vào file cũ hoặc mới sẽ được kiểm tra tương ứng.
Sửa các vi phạm hiện hữu
Sau khi tạo file .rubocop_todo.yml, quan trọng không quên các vi phạm hiện có, mà giải quyết chúng từng cái một. Bạn làm bằng cách xóa file khỏi block Exclude của một cop, sửa vi phạm được báo cáo, chạy test suite để tránh bug, và commit. Khi đã xóa hết file khỏi một cop, bạn có thể xóa cop khỏi file thủ công, hoặc tạo lại file allowlist lần nữa. Đừng quên dùng tùy chọn --auto-correct khi có thể để quy trình nhanh hơn.
Áp dụng Style Guide
RuboCop rất linh hoạt, làm cho nó phù hợp mọi loại dự án. Tuy nhiên, có thể mất nhiều thời gian cấu hình quy tắc theo yêu cầu, đặc biệt nếu bạn không đồng ý với nhiều quy tắc mặc định. Trong trường hợp đó, áp dụng style guide hiện có có thể có lợi. Một số công ty đã phát hành Ruby style guide công khai, như Shopify và Airbnb. Việc dùng style guide ưa thích trong RuboCop đạt được bằng cách thêm gem liên quan vào Gemfile:
# Gemfile
gem "rubocop-shopify", require: false
Sau đó, require nó trong config dự án:
# .rubocop.yml
inherit_gem:
rubocop-shopify: rubocop.yml
Chặn lỗi Linting
Mặc dù RuboCop là công cụ tuyệt vời, nó đôi khi sinh false positive hoặc đề xuất sửa code theo cách gây hại cho ý định của lập trình viên. Khi tình huống đó nảy sinh, bạn có thể bỏ qua vi phạm bằng comment trong mã nguồn. Bạn có thể nhắc các cops hoặc department cụ thể để tắt, như dưới đây:
# rubocop:disable Layout/LineLength, Style
[..]
# rubocop:enable Layout/LineLength, Style
Hoặc bạn có thể tắt tất cả cops cho một đoạn code một lần:
# rubocop:disable all
[..]
# rubocop:enable all
Nếu dùng comment cuối dòng, các cops chỉ định sẽ bị tắt chỉ trên dòng đó.
for x in (0..10) # rubocop:disable Style/For
Tích hợp Editor
Việc xem cảnh báo và lỗi do RuboCop sinh ra ngay khi gõ code trong editor thay vì phải chạy kiểm tra qua dòng lệnh mỗi lần rất tiện lợi. May mắn thay, tích hợp RuboCop có sẵn trong hầu hết editor và IDE phổ biến, chủ yếu qua plugin thứ ba. Trong Visual Studio Code, bạn chỉ cần cài extension Ruby này và đặt sau vào file settings.json của user:
{
"ruby.lint": {
"rubocop": true
}
}
Nếu dùng Vim hoặc Neovim, bạn có thể hiển thị chẩn đoán RuboCop qua coc.nvim. Cần cài Solargraph language server (gem install solargraph), tiếp theo cài extension coc-solargraph (:CocInstall coc-solargraph). Sau đó, cấu hình file coc-settings.json như sau:
{
"coc.preferences.formatOnSaveFiletypes": ["ruby"],
"solargraph.autoformat": true,
"solargraph.diagnostics": true,
"solargraph.formatting": true
}

Thiết lập Pre-commit Hook
Cách tuyệt vời để đảm bảo mọi code Ruby trong dự án được lint và format đúng trước khi commit vào source control là thiết lập Git pre-commit hook chạy RuboCop trên mỗi file staged. Bài này chỉ cách làm với Overcommit, công cụ quản lý và cấu hình Git pre-commit hook, nhưng bạn cũng có thể tích hợp RuboCop với công cụ khác nếu đã có workflow pre-commit.
Trước hết, cài Overcommit qua RubyGems và cài vào dự án:
$ gem install overcommit
$ overcommit --install # tại gốc dự án
Lệnh thứ hai sẽ tạo file setting riêng repo (.overcommit.yml) trong thư mục hiện tại và backup các hook hiện có. File này kế thừa config mặc định, nên bạn chỉ cần chỉ định config liên quan đến mặc định. Ví dụ, bạn có thể bật RuboCop pre-commit hook qua đoạn sau:
# .overcommit.yml
PreCommit:
RuboCop:
enabled: true
on_warn: fail
problem_on_unmodified_line: ignore
command: ['bundle', 'exec', 'rubocop']
problem_on_unmodified_line: ignore bỏ qua warning/lỗi trên dòng không được staged. Bạn có thể duyệt tất cả tùy chọn hook và phạm vi giá trị chấp nhận được trên trang GitHub dự án. Có thể cần chạy overcommit --sign sau khi thay đổi file config để thay đổi có hiệu lực.

Đôi khi, nếu muốn commit file không pass tất cả kiểm tra (như work-in-progress), bạn có thể bỏ qua từng kiểm tra caso-by-case:
$ SKIP=RuboCop git commit -m "WIP: Unfinished work"
Thêm RuboCop vào CI Workflow
Chạy kiểm tra RuboCop trên mỗi pull request là cách khác ngăn code định dạng kém bị merge vào dự án. Mặc dù bạn có thể thiết lập với bất kỳ công cụ CI nào, bài này chỉ thảo luận cách chạy RuboCop qua GitHub Actions.
Bước đầu là tạo thư mục .github/workflows ở gốc dự án và file rubocop.yml trong thư mục mới. Mở file trong editor và cập nhật như sau:
# .github/workflows/rubocop.yml
name: Lint code with RuboCop
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop
File workflow trên mô tả một job sẽ chạy khi code được push lên GitHub hoặc khi pull request được tạo nhánh bất kỳ. Một job là chuỗi các step chạy tuần tự. Job này chạy một lần trên Ubuntu, MacOS, Windows mới nhất do GitHub Actions cung cấp (định nghĩa bởi runs-on và strategy.matrix). Step đầu checkout code, step tiếp theo thiết lập Ruby toolchain và dependencies, step cuối chạy RuboCop.
Khi xong chỉnh sửa file, lưu, commit, push lên GitHub. Sau đó, bạn sẽ có hiển thị inline các vấn đề báo cáo trên các check-in và pull request tiếp theo.

Auto-formatters thay thế
Mặc dù RuboCop cung cấp khả năng auto-format toàn diện, cũng quan trọng nhận biết các công cụ thay thế phòng khi nó không đáp ứng nhu cầu.
Prettier
Prettier bắt đầu như code formatter có quan điểm cho JavaScript, nhưng giờ hỗ trợ nhiều ngôn ngữ khác, bao gồm Ruby. Cài plugin Ruby của nó rất đơn giản: thêm gem prettier vào Gemfile và chạy bundle.
# Gemfile
gem 'prettier'
Lúc này, bạn có thể format code Ruby với Prettier qua lệnh:
$ bundle exec rbprettier --write '**/*.rb'
Một số quy tắc của Prettier xung đột với RuboCop, nên cần tắt các kiểm tra định dạng của RuboCop để không can thiệp vào Prettier. May mắn thay, dễ dàng tắt các kiểm tra RuboCop xung đột hoặc không cần thiết với Prettier. Chỉ cần kế thừa config RuboCop của Prettier ở đầu file .rubocop.yml của dự án:
# .rubocop.yml
inherit_gem:
prettier: rubocop.yml
Khi chạy RuboCop (với bundle exec rubocop) từ bây giờ, nó sẽ không báo vi phạm liên quan đến layout, nhường đường cho Prettier sửa theo quy tắc của nó. Bạn cũng có thể cấu hình output của Prettier qua file config, có thể chia sẻ giữa code JavaScript và Ruby trong cùng dự án.
RubyFmt
RubyFmt là code formatter hoàn toàn mới viết bằng Rust và đang phát triển tích cực. Giống Prettier, nó dự định là formatter chứ không phải công cụ phân tích code. Nó chưa thấy bản release ổn định, nên bạn nên đợi trước khi adopt, nhưng chắc chắn là cái cần theo dõi.
Kết luận
Linting và auto-formatting code mang lại nhiều lợi ích cho codebase, đặc biệt trong bối cảnh team phát triển. Ngay cả khi bạn không thích bị bảo cách format code, cần nhớ linting không chỉ dành cho bạn. Nó cũng dành cho những người bạn cộng tác để mọi người tuân thủ cùng quy ước, từ đó loại bỏ nhược điểm đối phó với nhiều phong cách code trong cùng dự án.
Cũng quan trọng không coi output của linter như kinh thánh, hãy cấu hình nó sao cho mang lại lợi ích nhất mà không làm xao nhãng mục tiêu chính. Với cài đặt mở rộng của RuboCop, điều này không nên thành vấn đề. Tuy nhiên, nếu thấy cấu hình RuboCop tốn quá nhiều thời gian, bạn có thể dùng style guide định sẵn như đã thảo luận, hoặc adopt Standard cho lựa chọn no-config mà mọi người chỉ dùng thay vì lo lắng về chi tiết nhỏ.
Cảm ơn đã đọc, và happy coding!