Linting & Static Analysis
Automated tools that catch style violations and common bugs before review.
Overview
Linting is static analysis that identifies problematic patterns in code without running it. Linters enforce code style, catch common bugs (unused variables, missing await, undefined references), and flag potential security issues. ESLint for JavaScript/TypeScript and RuboCop for Ruby are the ecosystem standards. Linting complements formatting: Prettier owns layout; ESLint owns correctness and patterns.
Origin
lint was a Unix static analysis tool for C code written by Stephen Johnson at Bell Labs in 1978. JSLint (Douglas Crockford, 2002) brought the concept to JavaScript. JSHint (Anton Kovalyov, 2011) and ESLint (Nicholas Zakas, 2013) replaced it with configurable rule sets. ESLint's pluggable architecture (plugins for React, TypeScript, Tailwind) made it the universal JavaScript linter.
Examples
ESLint flat config for a TypeScript + React project
// eslint.config.ts (ESLint 9 flat config format)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import hooksPlugin from 'eslint-plugin-react-hooks';
import importPlugin from 'eslint-plugin-import';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
plugins: {
react: reactPlugin,
'react-hooks': hooksPlugin,
import: importPlugin,
},
languageOptions: {
parserOptions: { project: true, tsconfigRootDir: import.meta.dirname },
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'import/no-cycle': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
}
);no-floating-promises catches unawaited async calls that silently drop errors. import/no-cycle detects circular dependencies that cause mysterious undefined references. ESLint 9 flat config replaces .eslintrc and merges config objects sequentially.
RuboCop custom cops and enforcement in CI
# .rubocop.yml: extending with extra cops for Rails and performance
# require:
# - rubocop-rails
# - rubocop-performance
# - rubocop-rspec
#
# Rails/UniqueValidationWithoutIndex:
# Enabled: true
#
# Performance/ArraySemiInfiniteRangeSlice:
# Enabled: true
#
# RSpec/ExampleLength:
# Max: 20
#
# RSpec/MultipleExpectations:
# Max: 3
# CI enforcement (.github/workflows/lint.yml):
# - name: Run RuboCop
# run: |
# bundle exec rubocop # --parallel # --format github # Annotates PR with inline comments
# --format progress
# Custom cop example (lib/rubocop/cop/custom/no_raw_sql.rb):
class RuboCop::Cop::Custom::NoRawSql < RuboCop::Cop::Base
MSG = 'Avoid raw SQL strings. Use Arel or ActiveRecord query methods.'
def_node_matcher :raw_sql_call?, <<~PATTERN
(send _ {:find_by_sql :execute :select_all} (str _))
PATTERN
def on_send(node)
add_offense(node) if raw_sql_call?(node)
end
endrubocop-rails adds Rails-specific cops like UniqueValidationWithoutIndex (warns when validates :email, uniqueness: true has no database index) and DynamicFindBy. --format github produces GitHub Actions annotations that appear as inline PR comments.
Use Cases
- 01Pre-commit hooks that lint only staged files via lint-staged, giving sub-second feedback to developers before they can commit broken code
- 02CI pipeline lint checks that run in parallel with tests and fail the build on error-level violations
- 03Security-focused lint rules (eslint-plugin-security, brakeman for Ruby) that catch patterns like eval(), innerHTML assignment, and SQL interpolation
- 04Codebase-wide rule adoption: ESLint --fix or rubocop -a applies safe auto-corrections across thousands of files in a single command
When Not to Use
- //Do not enable every available rule; a warning-heavy linter output trains engineers to ignore it. Enable rules that have clear value in your codebase
- //Do not run the full linter on every file change in watch mode; it is slow. Use lint-staged for pre-commit and the full run in CI
- //Do not configure the same rule in both Prettier and ESLint; use eslint-config-prettier to disable ESLint formatting rules that conflict
Technical Notes
- ESLint's TypeScript parser (@typescript-eslint/parser) builds a TypeScript AST; type-aware rules (parserOptions: { project: true }) have access to the full type information but are significantly slower than syntax-only rules
- RuboCop uses Rubocop AST (abstract syntax tree) to match code patterns; custom cops use node pattern DSL (def_node_matcher) to match AST nodes without writing manual tree traversal
- Brakeman (Ruby security scanner) performs static analysis specifically for Rails CVEs: SQL injection via string interpolation in where(), XSS via html_safe and raw(), mass assignment via permit(*)
- Oxlint (Rust-based JavaScript linter, 2023) is 50-100x faster than ESLint but implements only a subset of rules; it can run as a first-pass fast linter before a slower ESLint scan