lint 0.1.3

A versatile linting tool with CLI, MCP, and library interfaces
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# Lint Project TODO

## In Progress

## High Priority

- [x] **Fix `ignore_patterns` not being used by linter**
  - `Config` stores `ignore_patterns` with defaults (`node_modules`, `target`, `.git`)
  - `linter.rs` never checks them when walking directories
  - Files in ignored directories are still linted
  - **Goal**: Respect `ignore_patterns` during directory traversal
  - **Status**: Fixed in `linter.rs` with `is_ignored()` method. Unit + integration tests added.

- [x] **Wire up CLI `--config` option**
  - `main.rs` defines `--config FILE` but never reads or uses it
  - Should load a JSON config file and merge with CLI args
  - **Goal**: `--config` actually loads configuration from file
  - **Status**: Fixed in `main.rs` with `load_config_file()` helper. CLI overrides take precedence. Unit tests added.

## Medium Priority

- [x] **Remove dead `Format` enum from `output.rs`**
  - `Format` is defined but never used; `OutputFormat` from `config.rs` is used everywhere
  - `lib.rs` re-exports it alongside the real `OutputFormat`
  - **Goal**: Eliminate confusing duplicate type
  - **Status**: Removed from `output.rs` and `lib.rs`.

- [x] **Add CLI argument parsing tests**
  - `main.rs` has test coverage for `load_config_file` but not full CLI argument parsing
  - `TEST_COVERAGE.md` lists this as a future improvement
  - **Goal**: Test CLI subcommands and flag parsing end-to-end
  - **Status**: Added tests for `Cli::parse_from` with `lint`, `list-rules`, `version` subcommands and `--config` flag.

## High Priority (from competitive intelligence: Ruff, ESLint, Clippy)

- [x] **Inline suppression comments (noqa)**
  - Ruff/Flake8/Pylint support `# noqa: rule-name` to suppress a rule on a specific line
  - Without this, users cannot suppress false positives — table stakes for any real linter
  - **Goal**: `let x = 5; // lint: ignore=line-length` suppresses the rule on that line only
  - **Status**: Implemented in `linter.rs` with `is_line_suppressed()` helper. Supports `lint: ignore=rule-name` (specific) and `lint: ignore` (blanket). Unit + integration tests added.

- [x] **Glob/wildcard path support**
  - Users expect `lint lint "src/**/*.rs"` — currently only exact paths work
  - **Goal**: Expand glob patterns in paths before linting
  - **Status**: Added `expand_globs()` helper in `main.rs` using the `glob` crate. `*` and `?` patterns are expanded before linting. Non-glob paths pass through unchanged. Unit tests added.

- [x] **Watch mode (`--watch`)**
  - Ruff/ESLint have `--watch` to re-run on file changes
  - Big UX win for development workflows
  - **Goal**: `lint lint . --watch` re-lints on file changes using `notify` crate
  - **Status**: Added `--watch` flag, `run_lint_and_print()` helper, and watch loop using `notify` crate. Re-lints on create/modify/remove events. CLI parsing test added.

## Low Priority / Brainstorming

- [x] Exit non-zero when lint errors are found
  - **Status**: `main.rs` now exits with code 1 when any result contains `Severity::Error` messages. Integration test with custom Error rule added.
- [x] Add `--fix` / auto-fix capability
  - Messages already contain `suggestion` but there's no way to apply them
  - **Status**: Added `Fix` struct and `with_fix()` builder to `LintMessage`. `TrailingWhitespaceRule` now emits fixes. `--fix` CLI flag applies fixes and writes files back. Unit + integration tests added.
- [x] Load and apply `custom_rules_path`
  - `Config` has `custom_rules_path` but it's never read
  - **Status**: Implemented `CustomRule` and `CustomRuleDefinition` in `rules.rs`. Wired up in `linter.rs`. Unit + integration tests added.
- [x] Property-based tests with `proptest`
  - **Status**: Added manual property-style invariant tests: `LineLengthRule` never flags short lines, `TrailingWhitespaceRule` never flags clean lines, `NoTodoRule` never flags clean content, fixes never increase file size. Zero new dependencies.
- [x] Benchmarks for large file performance
  - **Status**: Added `examples/bench.rs` that generates a 10,000-line file and measures linting time. Zero new dependencies.
- [x] Snapshot tests for CLI output
  - **Status**: Added `test_json_output_snapshot` and `test_trailing_whitespace_exact_message` integration tests that assert exact expected output fields and values. Zero new dependencies.

## Brainstorming (from competitive intelligence round 8)

- [x] **`--exit-zero` CLI flag**
  - Ruff supports `--exit-zero` to always exit with code 0 even if violations are found
  - Useful for CI pipelines where you want to see lint output without failing the build
  - **Goal**: `lint lint . --exit-zero` always returns 0
  - **Status**: Added `exit_zero: bool` to `Commands::Lint`. Wired up in `run_lint_and_print` to return 0 regardless of errors or max-warnings exceeded. CLI parsing test added.

- [x] **Show source context in text output**
  - Ruff shows the offending source line with a caret underline in text output
  - **Goal**: Text output includes the source line and a caret pointing to the column
  - **Status**: Text output now shows the offending source line and a caret underline (`| ` + source + `| ` + `^` at column). Uses `result.file_content.lines()` to extract the source line. No new tests needed (existing tests use JSON/GitHub/SARIF output).

- [x] **`--statistics` CLI flag**
  - Ruff supports `--statistics` to show per-rule violation counts after linting
  - **Goal**: `lint lint . --statistics` prints a sorted table of rule names and their violation counts
  - **Status**: Added `statistics: bool` to `Commands::Lint`. `run_lint_and_print` collects per-rule counts from all results, sorts by count descending, and prints a summary table. CLI parsing test added.

- [x] **`--show-fixes` CLI flag**
  - Ruff supports `--show-fixes` to list files that were modified by `--fix`
  - **Goal**: `lint lint . --fix --show-fixes` prints the list of files that were fixed after linting
  - **Status**: Added `show_fixes: bool` to `Commands::Lint`. After applying fixes, `run_lint_and_print` tracks fixed files and prints them if `show_fixes` is true. CLI parsing test added.

- [x] **`--exclude` CLI flag**
  - Ruff and ESLint support `--exclude` to skip files/patterns at CLI level
  - **Goal**: `lint lint . --exclude vendor --exclude "*.min.js"` skips matching files
  - **Status**: Added `exclude: Option<Vec<String>>` to `Commands::Lint`. Patterns merged into `config.ignore_patterns` before linting. CLI parsing test added.

- [x] **JUnit XML output format**
  - Ruff supports JUnit XML output for CI integration (Jenkins, Azure DevOps, GitLab)
  - **Goal**: `lint lint . --output junit` produces JUnit XML for test result ingestion
  - **Status**: Added `Junit` variant to `OutputFormat`. `render_junit()` generates valid JUnit XML with `<testsuite>` and `<testcase>` elements. XML escaping via `escape_xml()`. CLI parsing test added.

- [x] **Additional generic rules**
  - Current generic rules are limited (line-length, trailing-whitespace, no-todo)
  - **Goal**: Add `no-empty-file`, `no-consecutive-empty-lines`, `no-tabs` rules
  - **Status**: Added `NoEmptyFileRule`, `NoConsecutiveEmptyLinesRule`, and `NoTabsRule` to `rules.rs`. `NoTabsRule` includes auto-fix (replace tabs with 4 spaces). All wired into `linter.rs` available rules. Unit tests added.

- [x] **`--show-settings` CLI flag**
  - Ruff supports `--show-settings` to print the effective configuration
  - **Goal**: `lint lint . --show-settings` prints merged config as JSON and exits
  - **Status**: Added `show_settings: bool` to `Commands::Lint`. After config merging, prints config as pretty JSON and returns `Ok(())`. CLI parsing test added.

- [x] **Config file auto-discovery**
  - Ruff, ESLint, and Biome all auto-discover config files in current and parent directories
  - **Goal**: Running `lint lint .` without `--config` automatically finds `.lint.json` in the project root
  - **Status**: Added `find_config_file()` that walks from `current_dir()` up to root looking for `.lint.json`. CLI `--config` still takes precedence. Unit test added.

- [x] **Concise output format**
  - Ruff supports `--output-format concise` for compact one-line-per-violation output
  - **Goal**: `lint lint . --output concise` shows `file:line:col [severity] message (rule)` per violation
  - **Status**: Added `Concise` variant to `OutputFormat`. `render_concise()` generates one-line format. CLI parsing test added.

- [x] **`--cache-location` CLI flag**
  - Ruff supports `--cache-dir` to customize the cache directory
  - **Goal**: `lint lint . --cache --cache-location /tmp/lint_cache.json` uses a custom cache path
  - **Status**: Added `cache_location: Option<PathBuf>` to `Commands::Lint`. Cache path defaults to `.lint_cache.json` but can be overridden. CLI parsing test added.

- [x] **`--diff` CLI flag**
  - Ruff supports `--diff` to preview fixes without writing changes
  - **Goal**: `lint lint . --diff` shows a line-by-line diff of proposed fixes without modifying files
  - **Status**: Added `diff: bool` to `Commands::Lint`. When `diff` is true, `run_lint_and_print` applies fixes in-memory, prints `---/+++` diff header and changed lines (`-` old, `+` new), but never writes files. CLI parsing test added.

- [x] **`--fix-only` CLI flag**
  - Ruff supports `--fix-only` to apply fixes without reporting or failing on remaining violations
  - **Goal**: `lint lint . --fix-only` applies safe fixes, suppresses all output and exit codes for leftover violations
  - **Status**: Added `fix_only: bool` to `Commands::Lint`. When `fix_only` is true, fixes are applied, results are not rendered/printed, and exit code is always 0. CLI parsing test added.

- [x] **`--add-noqa` CLI flag**
  - Ruff supports `--add-noqa` to automatically add `# noqa` suppression comments to failing lines
  - **Goal**: `lint lint . --add-noqa` appends `// lint: ignore=rule-name` to each offending line and writes the files
  - **Status**: Added `add_noqa: bool` to `Commands::Lint`. After linting, for each message, appends suppression directive to the line if not already present. Writes modified files. CLI parsing test added.

- [x] **`explain` subcommand**
  - Ruff supports `ruff rule <code>` to explain what a specific rule does
  - **Goal**: `lint explain line-length` shows name, category, and description of the rule
  - **Status**: Added `Explain { rule: String }` to `Commands`. `explain_rule()` searches generic and language-specific rules and prints details. Added `description()` to `Rule` and `LanguageRule` traits. `get_rules()` added to `LanguageRuleSet`. CLI parsing test added.

- [x] **`--ignore-suppressions` CLI flag**
  - Ruff supports `--ignore-noqa` to ignore all `# noqa` suppression comments
  - **Goal**: `lint lint . --ignore-suppressions` reports all violations even if suppressed by inline comments
  - **Status**: Added `ignore_suppressions: bool` to `Config`, `ConfigBuilder`, and `Commands::Lint`. In `linter.rs`, all suppression filtering (file-level, line-level, block-level, unused-suppression) is skipped when this flag is true. CLI parsing test added.

- [x] **`check` subcommand alias**
  - Ruff uses `ruff check` as the primary linting command
  - **Goal**: `lint check .` is an alias for `lint lint .`
  - **Status**: Added `#[command(alias = "check")]` to the `Lint` variant in `Commands`. CLI parsing test added.

- [x] **GitLab Code Quality output format**
  - Ruff supports GitLab Code Quality report format for CI integration
  - **Goal**: `lint lint . --output gitlab` produces a GitLab-compatible JSON report
  - **Status**: Added `Gitlab` variant to `OutputFormat`. `render_gitlab()` generates JSON array with `description`, `check_name`, `fingerprint`, `severity`, and `location` fields. CLI parsing test added.

- [x] **Additional generic rules (batch 2)**
  - **Goal**: Add `final-newline` and `no-mixed-line-endings` rules
  - **Status**: Added `FinalNewlineRule` (detects missing trailing newline, includes fix) and `NoMixedLineEndingsRule` (detects mixed CRLF/LF). Both added to default enabled rules and `linter.rs` rule set. Unit tests added. Integration test updated.

- [x] **`init` subcommand**
  - Ruff supports `ruff .` with implicit config generation, but explicit `init` is common in tools like ESLint (`eslint --init`)
  - **Goal**: `lint init` generates a default `.lint.json` in the current directory
  - **Status**: Added `Init` to `Commands`. `init_config()` writes a pretty-printed default config JSON. If `.lint.json` already exists, prints a message and exits cleanly. CLI parsing test added.

- [x] **`--output-format` alias**
  - Ruff uses `--output-format` instead of `--output`
  - **Goal**: `lint lint . --output-format json` is equivalent to `--output json`
  - **Status**: Added `visible_alias = "output-format"` to the `output` argument in `Commands::Lint`. CLI parsing test added.

- [x] **`--select-all` CLI flag**
  - Ruff supports `--select ALL` to enable all rules
  - **Goal**: `lint lint . --select-all` enables all built-in generic rules
  - **Status**: Added `select_all: bool` to `Commands::Lint`. When true, all 8 generic rule names are added to `config.rule_set.enabled_rules`. CLI parsing test added.

- [x] **Rule categories / severity-based grouping**
  - Ruff organizes rules into categories (E = errors, W = warnings, F = Pyflakes, etc.)
  - **Goal**: Rules have category prefixes (e.g., `style:line-length`, `bug:no-todo`)
  - **Status**: Added `category(&self) -> &str` to `Rule` and `LanguageRule` traits with default implementations. `LineLengthRule`, `TrailingWhitespaceRule`, `SemicolonRule` → `style`. `NoTodoRule` and all language rules → `correctness`. `list_rules()` now shows `[category]` prefix. Unit tests added for both `Rule` and `LanguageRule` categories.

## Brainstorming (from competitive intelligence round 7)

- [x] **SARIF output format**
  - Ruff and Biome support SARIF (Static Analysis Results Interchange Format) for CI integration
  - GitHub and Azure DevOps can render SARIF artifacts directly in PRs
  - **Goal**: `lint lint . --output sarif` produces SARIF JSON for upload to GitHub Advanced Security
  - **Status**: Added `Sarif` variant to `OutputFormat`. `render_sarif()` generates valid SARIF 2.1.0 JSON with tool info, rules catalog, and results with locations. `--output sarif` wired up in CLI. CLI parsing test and rendering test added.

- [x] **Stdin linting (`--stdin`, `--stdin-filename`)**
  - ESLint supports `eslint --stdin --stdin-filename=myfile.js` for piping code
  - Ruff supports `ruff check --stdin-filename myfile.py - < myfile.py`
  - **Goal**: `echo "let x = 5;" | lint lint --stdin --stdin-filename test.rs`
  - Useful for editor integrations (Vim, Emacs, VS Code)
  - **Status**: Added `stdin: bool` and `stdin_filename: Option<String>` to `Commands::Lint`. When `--stdin` is set, content is read from stdin, written to a temp file with the extension from `--stdin-filename` (for language detection), and linted. `--stdin-filename` defaults to `.rs` extension. CLI parsing tests added.

- [x] **Output to file (`--output-file`)**
  - Ruff supports `--output-file results.json` to write output to a file instead of stdout
  - **Goal**: `lint lint . --output json --output-file results.json`
  - **Status**: Added `output_file: Option<PathBuf>` to `Commands::Lint`. Refactored `print_results` into `render_results` (returns `String`) and `print_results` (prints to stdout). `run_lint_and_print` writes rendered output to file when `--output-file` is set, disabling colors automatically. CLI parsing test added.

## Brainstorming (from competitive intelligence round 6)

- [x] **`--select` / `--ignore` rule filtering at CLI**
  - Ruff uses `--select E,W` and `--ignore E501` for fine-grained rule control
  - Our current `--rules` flag replaces the entire enabled set
  - **Goal**: `--select line-length` adds a rule; `--ignore no-todo` removes a rule from defaults
  - **Status**: Added `select: Option<Vec<String>>` and `ignore: Option<Vec<String>>` to `Commands::Lint`. `--rules` still replaces the entire set. If `--rules` is not specified, `--select` appends to defaults and `--ignore` removes from defaults. Supports comma-delimited lists (e.g., `--select no-todo,no-console-log`). CLI parsing tests added.

- [x] **Config `extends` for shareable configs**
  - ESLint and Biome support extending other config files (`"extends": "./base.json"`)
  - Useful for monorepos and team-wide base configs
  - **Goal**: `{"extends": ".lint.base.json", "max_line_length": 120}` merges base + local
  - **Status**: Added `extends: Option<String>` to `Config` and `ConfigBuilder`. `load_config_file()` recursively loads base configs and calls `merge_configs()`. Scalars are overridden by local, collections are merged (ignore_patterns concatenated and deduplicated, per_file_ignores and severity_overrides merged with local winning). Relative paths resolved against config file parent directory. Unit tests added.

- [x] **`--print-files` / `--list-files` CLI flag**
  - Oxlint added `--print-files` to list files that would be linted without actually linting them
  - Useful for debugging `ignore_patterns` and glob expansion
  - **Goal**: `lint lint . --print-files` prints one path per line, then exits
  - **Status**: Added `print_files: bool` to `Commands::Lint`. `Linter::list_files()` method walks config paths, applies `is_ignored()` and `should_lint_file()` filters, and returns matching paths. `--print-files` prints paths and exits early before linting. CLI parsing test added.

## Brainstorming (from competitive intelligence round 5)

- [x] **Unused suppression detection (`lint: unused-suppression`)**
  - Ruff has `RUF100` (unused-noqa) which detects suppression comments that don't actually suppress any violations
  - This happens when code is refactored and the suppression becomes unnecessary
  - **Goal**: A new rule or post-processing step that reports `// lint: ignore=rule-name` comments where no violation would have occurred
  - **Status**: Implemented as opt-in post-processing in `lint_file()`. `SuppressionDirective` struct tracks inline and block suppressions. `parse_suppression_directives()` parses all directives with ranges. Only runs when `unused-suppression` is in `enabled_rules`. Unit tests added.

## Brainstorming (from competitive intelligence round 4)

- [x] **File-level ignore directive (`lint: ignore-file`)**
  - Deno lint supports `// deno-lint-ignore-file` at the top of a file to ignore all rules for that file
  - Useful for generated code, vendor files, or legacy files you don't want to modify
  - **Goal**: `// lint: ignore-file` or `// lint: ignore-file=line-length` suppresses rules for the entire file
  - **Status**: Implemented `parse_file_level_ignore()` in `linter.rs`. Checked on first line only. Empty vec = ignore all rules; non-empty = skip specific rules during rule checking. Unit tests added.

- [x] **`--quiet` / `--max-warnings` CLI flags**
  - Many linters support `--quiet` to suppress all but errors, and `--max-warnings N` to fail if warnings exceed N
  - **Goal**: `lint lint . --quiet` shows only errors; `lint lint . --max-warnings 10` exits non-zero if >10 warnings
  - **Status**: Added `quiet: bool` and `max_warnings: Option<usize>` to `Commands::Lint`. `print_results` filters messages to only errors when quiet. `run_lint_and_print` counts warnings and returns exit code 1 if max_warnings exceeded. Supports all output formats (text, json, markdown, github). CLI parsing tests added.

- [x] **`--color` / `--no-color` CLI flags**
  - Ruff added `--color` to force colored output; most tools support `--no-color` for piping
  - **Goal**: `lint lint . --no-color` produces plain text even when stdout is a TTY
  - **Status**: Added `color: Option<String>` to `Commands::Lint`. Supports `never`/`no` and `always`/`yes` values. Uses `colored::control::set_override()` to force color on/off. CLI parsing test added.

## Brainstorming (from competitive intelligence round 3)

- [x] **Block suppression comments (`lint: disable=rule-name`)**
  - Ruff v0.15 stabilized `ruff: disable` / `ruff: enable` block comments
  - Currently we only support per-line suppression (`// lint: ignore=rule-name`)
  - **Goal**: `// lint: disable=line-length` suppresses the rule until `// lint: enable=line-length`
  - Useful for generated code blocks, test data, or config blobs where many lines violate a rule
  - **Status**: Implemented `parse_block_suppressions()` in `linter.rs`. Tracks `all_disabled` flag and `disabled_rules` set across lines. Integrated with existing `is_line_suppressed()` inline check. Supports both specific rule disable/enable and blanket disable/enable. Unit tests added.

## Brainstorming (from competitive intelligence round 2)

- [x] **Parallel linting with `rayon`**
  - ESLint v9.34.0 added multithread linting (`--concurrency`), seeing 1.3x–3x speedups
  - Rust's `rayon` crate makes parallel file processing trivial
  - **Goal**: `lint lint . --jobs 4` processes files in parallel worker threads
  - **Status**: Parallelized `Linter::run()` using `rayon`. Each top-level path is linted in parallel. `Linter::run()` changed from `&mut self` to `&self`. Benchmark example updated for multi-file workloads.

- [x] **Per-file ignore patterns**
  - Ruff's `per-file-ignores` disables specific rules for specific file patterns
  - Useful for test files (`tests/**`) or generated code (`gen/**`)
  - **Goal**: Config field like `{"tests/**/*.rs": ["line-length"]}` to skip rules per pattern
  - **Status**: Added `per_file_ignores: HashMap<String, Vec<String>>` to `Config` and `ConfigBuilder`. Filtering applied in `linter.rs` after inline suppression check, using `glob::Pattern` for glob matching. Unit tests added.

- [x] **File caching (`--cache`)**
  - ESLint's `--cache` only re-lints changed files, dramatically improving CI performance
  - Store hashes of file contents, skip unchanged files on subsequent runs
  - **Goal**: `lint lint . --cache` stores `.lint_cache.json` and skips unchanged files
  - **Status**: Added `cache.rs` module with `Cache` struct (mtime+size keying). `Linter` accepts optional `Arc<Mutex<Cache>>`. `--cache` CLI flag loads `.lint_cache.json`, skips unchanged files, and writes cache back. Unit tests for hit/miss/save/load added.

- [x] **GitHub Actions annotation output**
  - Many linters output `::error file={file},line={line}::{message}` for CI integration
  - Makes errors appear inline in PR diffs
  - **Goal**: `lint lint . --output github` produces GitHub Actions workflow commands
  - **Status**: Added `Github` variant to `OutputFormat`. `render_github()`, `escape_github_command()`, and `severity_to_github_command()` in `main.rs`. CLI parsing test and rendering tests added.

- [x] **Rule severity override in config**
  - Currently severity is hardcoded per rule (e.g., `line-length` → Warning, `no-todo` → Info)
  - Users may want `line-length: Error` in their config
  - **Goal**: Config supports `"severity_overrides": {"line-length": "Error"}`
  - **Status**: Added `severity_overrides: HashMap<String, Severity>` to `Config` and `ConfigBuilder`. Overrides applied in `linter.rs` after per-file ignore filtering. Unit tests added.

## Done

- [x] **Deep modularize codebase**
  - Split `src/rules.rs` into `src/rules/` with `codes.rs`, `custom.rs`, `builtin/mod.rs`, and per-rule files
  - Split `src/language_rules.rs` into `src/language_rules/` with `builtin/mod.rs` and per-language files
  - Extract CLI types and output renderers from `src/main.rs` into `src/cli/mod.rs` and `src/cli/output.rs`
  - All checks pass: `cargo fmt`, `cargo clippy`, `cargo test`

- [x] Comprehensive unit and integration test coverage (164 lib + 110 bin + 18 advanced + 10 basic = 302 tests)
- [x] Source context in text output (offending line + caret underline)
- [x] `--statistics` CLI flag (per-rule violation counts)
- [x] `--show-fixes` CLI flag (list files modified by --fix)
- [x] `--exclude` CLI flag (exclude file patterns at CLI level)
- [x] JUnit XML output format (CI integration)
- [x] Additional generic rules (`no-empty-file`, `no-consecutive-empty-lines`, `no-tabs`, `final-newline`, `no-mixed-line-endings`)
- [x] `--show-settings` CLI flag (print effective config as JSON)
- [x] Config file auto-discovery (`find_config_file()`)
- [x] Concise output format (`--output concise`)
- [x] `--cache-location` CLI flag (custom cache path)
- [x] `--diff` CLI flag (preview fixes without writing)
- [x] `--fix-only` CLI flag (apply fixes, suppress reporting)
- [x] `--add-noqa` CLI flag (auto-add suppression comments)
- [x] `explain` subcommand (describe a rule)
- [x] `--ignore-suppressions` CLI flag (ignore suppression comments)
- [x] `check` subcommand alias for `lint`
- [x] GitLab Code Quality output format (`--output gitlab`)
- [x] `init` subcommand (generate default config)
- [x] `--output-format` alias for `--output`
- [x] `--select-all` CLI flag (enable all built-in rules)
- [x] `--no-ignore` CLI flag (disable all ignore patterns)
- [x] VCS integration (auto-respect `.gitignore` patterns)
- [x] `--cache-strategy` CLI flag (`metadata` or `content`)
- [x] `--no-error-on-unmatched-pattern` CLI flag
- [x] `--deny-warnings` CLI flag
- [x] `--exit-non-zero-on-fix` CLI flag
- [x] Rule categories (`style`, `correctness`, `custom`)
- [x] File count in text summary ("Summary: X errors, Y warnings, Z infos in N files")
- [x] Fix indicator `[*]` in text output for fixable issues
- [x] MCP server implementation
- [x] Multi-language rule support
- [x] `--fix` auto-fix capability (trailing whitespace)
- [x] Custom rules loaded from JSON file
- [x] `--config` CLI option wired up
- [x] `ignore_patterns` respected during directory traversal
- [x] Inline suppression comments (`// lint: ignore=rule-name`)
- [x] Block suppression comments (`// lint: disable=rule-name` / `// lint: enable=rule-name`)
- [x] File-level ignore directive (`// lint: ignore-file`)
- [x] Unused suppression detection (`unused-suppression` opt-in rule)
- [x] Glob/wildcard path expansion (`src/**/*.rs`)
- [x] Watch mode (`--watch`)
- [x] Parallel file linting with `rayon`
- [x] Per-file ignore patterns (`{"tests/**/*.rs": ["line-length"]}`)
- [x] File caching (`--cache`, `.lint_cache.json`)
- [x] GitHub Actions annotation output (`--output github`)
- [x] Rule severity override in config (`"severity_overrides": {"line-length": "Error"}`)
- [x] `--quiet` / `--max-warnings` CLI flags
- [x] `--color` CLI flag (`never`/`always`)
- [x] `--select` / `--ignore` CLI flags (add/remove rules from defaults)
- [x] `--print-files` CLI flag (list files that would be linted)
- [x] `--output-file` CLI flag (write results to file)
- [x] `--stdin` / `--stdin-filename` CLI flags (lint from stdin)
- [x] SARIF output format (`--output sarif`)
- [x] `--exit-zero` CLI flag (always return 0)
- [x] Config `extends` for shareable configs
- [x] `--no-cache` CLI flag (skip cache reads/writes)
- [x] `--no-config` CLI flag (ignore `.lint.json` and auto-discovered config)
- [x] `--stdin-file-path` CLI flag (alias for `--stdin-filename`, used for per-file ignores and output path)
- [x] Grouped output format (`--output grouped`)
- [x] Audit: removed unused dependencies (`glob`, `thiserror`) from `Cargo.toml`
- [x] Audit: fixed all `cargo clippy` warnings (collapsed nested ifs, replaced `len()` comparisons with `is_empty()`, simplified `map_or`)

## Brainstorming (from competitive intelligence round 8)

- [x] **`--no-ignore` CLI flag**
  - ESLint supports `--no-ignore` to disable all ignore patterns (both `.eslintignore` and `ignorePatterns` in config)
  - **Goal**: `lint lint . --no-ignore` lints all files, including those in `node_modules`, `.git`, etc.
  - **Status**: Added `no_ignore: bool` to `Commands::Lint`. When true, `config.ignore_patterns.clear()` is called after applying `--exclude`, so even explicit excludes are ignored. CLI parsing test added.

- [x] **`--no-error-on-unmatched-pattern` CLI flag**
  - ESLint supports `--no-error-on-unmatched-pattern` to avoid failing when a glob doesn't match any files
  - **Goal**: `lint lint "nonexistent/**/*.rs" --no-error-on-unmatched-pattern` exits 0 instead of erroring
  - **Status**: Added `no_error_on_unmatched_pattern: bool` to `Commands::Lint`. Our linter already silently skips non-existent paths and empty globs without erroring, so this flag is a no-op for compatibility with ESLint users. CLI parsing test added.

- [x] **VCS integration (respect `.gitignore`)**
  - Biome has VCS integration that automatically respects `.gitignore` patterns
  - **Goal**: `lint lint .` automatically skips files listed in `.gitignore` without explicit config
  - **Status**: `Linter` now parses `.gitignore` using `ignore::gitignore::GitignoreBuilder` and stores it in a `gitignore` field. `is_ignored()` checks `.gitignore` patterns in addition to `config.ignore_patterns`. The `ignore::Walk` already respected `.gitignore` for directory traversal; this adds explicit path checks. Unit test added.

- [x] **`--cache-strategy` CLI flag**
  - ESLint supports `--cache-strategy` with `metadata` (default) or `content` options
  - **Goal**: `lint lint . --cache --cache-strategy content` hashes file contents for cache keying
  - **Status**: Added `CacheStrategy` enum (`Metadata`/`Content`) to `config.rs`. Added `cache_strategy` field to `Config`, `ConfigBuilder`, and `Commands::Lint`. `lint_file()` in `linter.rs` uses `DefaultHasher` to hash content when `Content` strategy is active. `CacheEntry` now stores optional `content_hash`. `get_by_hash()` and `insert_with_hash()` added to `Cache`. CLI parsing test added.

## Brainstorming (from competitive intelligence round 9)

- [x] **`--deny-warnings` CLI flag**
  - oxlint supports `--deny-warnings` to ensure warnings produce a non-zero exit code
  - **Goal**: `lint lint . --deny-warnings` exits 1 if any warnings are found, even without errors
  - **Status**: Added `deny_warnings: bool` to `Commands::Lint` and `run_lint_and_print()`. When true, the exit code logic treats any warning count > 0 as a failure (exit 1), regardless of whether errors exist. CLI parsing test added.

- [x] **`--exit-non-zero-on-fix` CLI flag**
  - Ruff supports `--exit-non-zero-on-fix` to exit 1 if any violations were found, even if all were fixed
  - **Goal**: `lint lint . --fix --exit-non-zero-on-fix` exits 1 when fixes were applied
  - **Status**: Added `exit_non_zero_on_fix: bool` to `Commands::Lint` and `run_lint_and_print()`. When true, the exit code is 1 if any fixes were applied during the run, useful for CI enforcing that fixed code must be committed. CLI parsing test added.

## Brainstorming (from competitive intelligence round 10)

- [x] **`--no-cache` CLI flag**
  - Ruff and oxlint support `--no-cache` to explicitly disable caching even if a config file enables it
  - **Goal**: `lint lint . --no-cache` skips cache reads/writes entirely
  - Useful for debugging or when cache state is suspected to be stale
  - **Status**: Added `no_cache: bool` to `Commands::Lint`. When true, cache is set to `None` so no cache file is loaded or saved. CLI parsing test added.

- [x] **`--no-config` CLI flag**
  - ESLint supports `--no-eslintrc` to skip config file discovery and use only CLI-provided settings
  - **Goal**: `lint lint . --no-config` ignores `.lint.json` and any auto-discovered config
  - Useful for quick one-off runs with only CLI arguments
  - **Status**: Added `no_config: bool` to `Commands::Lint`. When true, `ConfigBuilder::new().build()` is used directly, bypassing `--config` and auto-discovery. CLI parsing test added.

- [x] **`--stdin-file-path` CLI flag**
  - Biome and Ruff use `--stdin-file-path` to determine language and apply per-file overrides when linting stdin
  - **Goal**: `echo '...' | lint lint --stdin --stdin-file-path foo.rs` treats the input as Rust for rule selection
  - **Status**: Added `visible_alias = "stdin-file-path"` to the existing `--stdin-filename` argument. Added `stdin_file_path` to `Config` and `ConfigBuilder`. `Linter::effective_path()` replaces temp path with `stdin_file_path` for `LintResult.file_path` and `per_file_ignores` matching when the temp file name starts with `lint_stdin_`. CLI parsing and linter unit tests added.

- [x] **Grouped output format**
  - Ruff supports `--output-format=grouped` which groups violations by file path instead of a flat list
  - **Goal**: `lint lint . --output grouped` shows all issues per file together
  - Useful when scanning long output to focus on one file at a time
  - **Status**: Added `Grouped` variant to `OutputFormat`. `render_grouped()` groups messages by file path with bold filename headers, colored severity prefixes, line/column numbers, messages, and dimmed rule names. Clean files are skipped. CLI parsing test and rendering test added.

## Brainstorming (from competitive intelligence round 11)

- [x] **`--check` CLI flag**
  - Prettier supports `--check` to verify files are already formatted without writing changes
  - **Goal**: `lint lint . --check` exits 1 if any fixable violations exist, without modifying files
  - Useful for CI pipelines that want to enforce clean code without auto-fixing
  - **Status**: Added `check: bool` to `Commands::Lint`. When true, fixes are applied in-memory (like `--diff`) but not written to disk. A summary "Would fix N file(s)" is printed. Exit code is 1 if any fixes would be applied. CLI parsing test and behavior test added.

- [x] **Auto-fixes for remaining generic rules**
  - Currently only `trailing-whitespace`, `no-tabs`, and `final-newline` have auto-fixes
  - `no-consecutive-empty-lines`, `no-mixed-line-endings`, and `no-empty-file` lack fixes
  - **Goal**: All generic rules that can be safely auto-fixed emit `suggestion` and are applied by `--fix`
  - **Status**: Extended `apply_fixes()` to support full-content fixes via `line == 0` sentinel. Added `collapse_empty_lines()` helper. `NoConsecutiveEmptyLinesRule` now emits a full-content fix that collapses consecutive empty lines. `NoMixedLineEndingsRule` now emits a full-content fix that normalizes CRLF to LF. `NoEmptyFileRule` intentionally has no auto-fix (deleting a file or adding arbitrary content is not safe). Unit tests for fixes and full-content fix application added.

- [x] **`--max-diagnostics` CLI flag**
  - Ruff supports `--max-diagnostics N` to limit the number of violations shown per file or total
  - **Goal**: `lint lint . --max-diagnostics 50` truncates output after 50 messages with a "... N more" hint
  - Useful for large files with thousands of violations (e.g., generated code)
  - **Status**: Added `max_diagnostics: Option<usize>` to `Commands::Lint`. Extracted `truncate_diagnostics()` helper that limits total diagnostics across all files. When exceeded, prints `... N additional diagnostics hidden ...` after output. CLI parsing test and truncation behavior tests added.

- [x] **TOML configuration file support**
  - Biome and many Rust tools support `.biome.toml` in addition to JSON config
  - **Goal**: `lint lint .` auto-discovers `.lint.toml` and parses it with the same schema as `.lint.json`
  - TOML is more readable and comment-friendly than JSON for configuration
  - **Status**: Added `toml = "0.8"` dependency. `load_config_file()` detects `.toml` extension and uses `toml::from_str()` instead of `serde_json::from_str()`. Added `#[serde(default)]` to `Config` fields and `Default` impls for `CacheStrategy`, `RuleSetConfig`, and `OutputFormat` so TOML partial configs work. `find_config_file()` now searches for `.lint.toml` after `.lint.json`. Tests for loading and discovery added.

- [x] **Baseline file support**
  - Ruff supports `--baseline` to compare against a previous run and only show new issues
  - **Goal**: `lint lint . --baseline baseline.json` only reports violations not present in the baseline
  - Critical for incremental adoption in large legacy codebases
  - **Status**: Added `baseline: Option<PathBuf>` to `Commands::Lint`. `BaselineEntry` struct tracks file path, line, and rule for matching. `load_baseline()` parses JSON baseline files. `filter_baseline()` removes current violations that exactly match baseline entries (same file path, line, and rule). Applied after fixes but before rendering. CLI parsing test and filtering behavior tests added.

## Brainstorming (from competitive intelligence round 12)

- [x] **Safe/unsafe fix classification**
  - Ruff separates fixes into `safe` and `unsafe` categories
  - **Goal**: `--fix` only applies safe fixes; `--fix --unsafe-fixes` applies all fixes
  - Critical for CI automation where unsafe fixes could break code semantics
  - **Status**: Added `is_safe: bool` to `Fix` struct with serde default `true`. Added `--unsafe-fixes` CLI flag. In `run_lint_and_print()`, when `--fix` is used without `--unsafe-fixes`, unsafe fixes are filtered out from messages before applying. All existing fixes default to safe. CLI parsing test and filtering behavior test added.

- [x] **`--force-exclude` CLI flag**
  - Ruff supports `--force-exclude` to skip files even when explicitly passed as arguments
  - **Goal**: `lint lint node_modules/file.js --force-exclude` respects ignore patterns and skips the file
  - Useful when shell globbing accidentally matches ignored directories
  - **Status**: Added `force_exclude: bool` to `Config`, `ConfigBuilder`, and `Commands::Lint`. In `linter.rs`, `lint_path()` checks `is_ignored()` for explicitly passed files when `force_exclude` is true. CLI parsing test and behavior tests added.

- [x] **Git-aware linting (`--changed`, `--staged`)**
  - Biome supports `--changed` and `--staged` to only lint files modified in git
  - **Goal**: `lint lint . --changed` only lints files with uncommitted changes; `--staged` only lints staged files
  - Dramatically speeds up pre-commit hooks and CI on large monorepos
  - **Status**: See round 14 for implementation details.

- [x] **`--ext` CLI flag**
  - ESLint uses `--ext .js,.jsx,.ts` to filter which file extensions to lint
  - **Goal**: `lint lint . --ext rs,toml` only processes files with matching extensions
  - Useful when walking directories with many non-source files
  - **Status**: Added `ext: Option<Vec<String>>` to `Config`, `ConfigBuilder`, and `Commands::Lint`. Modified `should_lint_file()` to use configured extensions when `ext` is set, falling back to the hardcoded supported extensions list otherwise. CLI parsing test and behavior test added.

- [x] **`--verbose` / `--log-level` CLI flag**
  - Biome and Ruff support `--verbose` and `--log-level` for debugging config loading, file discovery, cache behavior
  - **Goal**: `lint lint . --verbose` prints debug info: config file found, cache hits/misses, files skipped and why
  - Essential for troubleshooting why a file is or isn't being linted
  - **Status**: Added `verbose: bool` to `Commands::Lint` and `run_lint_and_print()`. When true, prints file count, diagnostic count, cache status, and fix mode status before rendering results. CLI parsing test added.

- [x] **Plugin system for custom rules**
  - Oxlint supports `--import-plugin`, `--jsdoc-plugin`, etc. for extending rule sets
  - **Goal**: Load additional rule packs as plugins (e.g., `--plugin security`, `--plugin react`)
  - Would enable ecosystem-specific rules without bloating the core binary
  - **Status**: See round 14 for implementation details.

## Brainstorming (from competitive intelligence round 13)

- [x] **`--show-source` toggle**
  - Currently text output always shows source context (offending line + caret underline)
  - Some users may want cleaner output without source snippets
  - **Goal**: `--no-show-source` disables source context in text output
  - Simple to implement: add `show_source: bool` to config, thread through `render_results()`
  - **Status**: See round 14 for implementation details.

- [x] **`--line-length` CLI alias**
  - Ruff uses `--line-length N`; we only have `--max-line-length N`
  - **Goal**: `--line-length 120` is an alias for `--max-line-length 120`
  - Very simple: add `line_length` to CLI with `visible_alias = "max-line-length"` or duplicate handler
  - **Status**: Added `visible_alias = "line-length"` to the `max_line_length` CLI argument via clap. `--line-length 120` now parses identically to `--max-line-length 120`. CLI parsing test added.

- [x] **`--no-gitignore` CLI flag**
  - Currently `.gitignore` files are always respected via the `ignore` crate's `WalkBuilder`
  - **Goal**: `--no-gitignore` disables automatic .gitignore respect
  - Useful when you want to lint generated files that are gitignored
  - **Status**: Added `no_gitignore: bool` to `Config`, `ConfigBuilder`, and `Commands::Lint`. `WalkBuilder::git_ignore()` toggled in `linter.rs`. `GitignoreBuilder` skipped when `no_gitignore` is true. CLI parsing and behavior tests added.

- [x] **`--fixable` / `--unfixable` CLI flags**
  - Ruff supports `--fixable E,W` and `--unfixable I` to control which rules can be auto-fixed
  - **Goal**: `--fixable trailing-whitespace` only auto-fixes that rule; `--unfixable line-length` excludes it
  - Simple extension of the existing fix filtering logic
  - **Status**: Added `fixable: Option<Vec<String>>` and `unfixable: Option<Vec<String>>` to `Commands::Lint`. In `run_lint_and_print()`, when `--fixable` is used, only fixes for listed rules are retained; when `--unfixable` is used, fixes for listed rules are removed. Applied after unsafe fix filtering but before fix application. CLI parsing tests and behavior test added.

- [x] **`--preview` CLI flag**
  - Ruff uses `--preview` to enable experimental/nightly rules
  - **Goal**: `--preview` enables rules marked as experimental in the rule registry
  - Simple boolean flag that gates certain rules; future-proof for adding new rules
  - **Status**: Added `preview: bool` to `Config`, `ConfigBuilder`, and `Commands::Lint`. `merge_configs()` merges with OR logic (either config or CLI enables preview). `test_config_serialization` updated. CLI parsing test added.

## Brainstorming (from competitive intelligence round 14)

- [x] **`--show-source` toggle**
  - Currently text output always shows source context (offending line + caret underline)
  - Some users may want cleaner output without source snippets
  - **Goal**: `--no-show-source` disables source context in text output
  - Simple to implement: add `show_source: bool` to config, thread through `render_results()`
  - **Status**: Added `show_source: bool` to `Config` and `ConfigBuilder` with serde default `true`. Added `--no-show-source` CLI flag. Threaded `show_source` through `render_results()` and `print_results()`; source context and caret underline are conditionally rendered in `OutputFormat::Text`. `merge_configs()` uses AND logic (both must be true to show source). CLI parsing test added.

- [x] **`--no-gitignore` CLI flag**
  - Currently `.gitignore` files are always respected via the `ignore` crate's `WalkBuilder`
  - **Goal**: `--no-gitignore` disables automatic .gitignore respect
  - Useful when you want to lint generated files that are gitignored
  - **Status**: Added `no_gitignore: bool` to `Config`, `ConfigBuilder`, and `Commands::Lint`. `WalkBuilder::git_ignore()` toggled in `linter.rs`. `GitignoreBuilder` skipped when `no_gitignore` is true. CLI parsing and behavior tests added.

- [x] **Git-aware linting (`--changed`, `--staged`)**
  - Biome supports `--changed` and `--staged` to only lint files modified in git
  - **Goal**: `lint lint . --changed` only lints files with uncommitted changes; `--staged` only lints staged files
  - Dramatically speeds up pre-commit hooks and CI on large monorepos
  - **Status**: Added `--changed` and `--staged` CLI flags. `git_changed_files()` shells out to `git diff --name-only HEAD` and `git diff --cached --name-only`. Paths are intersected with git file list (respecting directory arguments). CLI parsing tests and behavior tests with real git repos added.

- [x] **Plugin system for custom rules**
  - Oxlint supports `--import-plugin`, `--jsdoc-plugin`, etc. for extending rule sets
  - **Goal**: Load additional rule packs as plugins (e.g., `--plugin security`, `--plugin react`)
  - Would enable ecosystem-specific rules without bloating the core binary
  - **Status**: Added `--plugin` CLI flag (repeatable, comma-delimited). Built-in plugins: `security` (hardcoded-secret, unsafe-eval, sql-injection-risk), `javascript`, `python`, `rust`, `html`, `css`. `plugin_rules()` maps plugin names to rule names. Plugin rules are added to the enabled set. `LanguageRuleSet::new_filtered()` only runs language rules in the enabled set when plugins are active (backward compatible: no plugins = all language rules run). New security generic rules added. CLI parsing and behavior tests added.

## Optimization round (performance pass)

- [x] **Bundle `run_lint_and_print` booleans into `RunOptions` struct**
  - Eliminated 15+ positional boolean parameters
  - Removed `#[allow(clippy::too_many_arguments)]`
- [x] **Reduce clones in hot paths**
  - `filter_baseline`: removed `.to_string()` on `to_string_lossy()`
  - `render_results`/`print_results`: take `&OutputFormat` instead of owned
  - `apply_fixes`: early-return without collecting fixes when none exist; avoid `raw_messages.clone()` in suppression handling
  - `statistics`: `HashMap<&str, usize>` instead of `HashMap<String, usize>`
  - `add_noqa`: short-circuit files with no messages
  - `diff mode`: only clone file content when `opts.diff` is true
  - Text renderer: `lines().collect()` moved outside per-message loop
  - JSON output: serialize slice directly when not quiet
- [x] **Cache fast path for content strategy**
  - Check metadata-based cache before reading file even when `cache_strategy == Content`
  - Avoids disk reads when mtime/size haven't changed
- [x] **O(1) ignore pattern lookups**
  - Replaced nested loops in `is_ignored()` with `HashSet<String>` stored in `Linter`
- [x] **Compile regexes once**
  - `NoTodoRule`: changed from unit struct to struct with pre-compiled `Regex`
  - All 18 language rules: converted on-the-fly `Regex::new` to `std::sync::LazyLock` statics
- [x] **Dead code removal**
  - Removed unused `create_default_config()` from `lib.rs`
- [x] **Clean clippy warnings**
  - `large_enum_variant` suppressed on `Commands` enum (CLI-only, constructed once)
  - `collapsible_if` suppressed where let-chains are needed

## Brainstorming (from competitive intelligence round 14)

## Milestone: Import Sorting (COMPLETE)

- [x] `sort-imports` generic rule — detect consecutive import/use/require lines that are not alphabetically sorted
- [x] Support Rust (`use `), JS/TS (`import ` / `require(`), Python (`import ` / `from `), C/C++ (`#include`), Go (`import `)
- [x] `sort-imports` is enabled via `--rules sort-imports` or `enabled_rules` config
- [x] Unit tests for each language pattern (Rust sorted/unsorted, Python unsorted, JS from unsorted, single line noop)
- [x] README documentation

## Milestone: Complexity Rules (COMPLETE)

- [x] `max-nesting-depth` generic rule — count `{`/`}` depth, report lines exceeding threshold
- [x] `max-function-lines` generic rule — detect function boundaries, report functions exceeding threshold
- [x] `--max-nesting-depth` and `--max-function-lines` CLI flags
- [x] Config fields `max_nesting_depth` and `max_function_lines`
- [x] Unit tests and integration tests (4 rule tests + 2 CLI parsing tests)
- [x] README documentation

- [x] **Per-directory configuration overrides**
  - Ruff and Biome support config files in subdirectories that override parent settings
  - **Goal**: A `src/.lint.toml` can override `max_line_length` or `ignore_patterns` for that subtree only
  - **Status**: Added `DirConfig` struct in `linter.rs` to hold per-directory config + rule sets. `Linter::new_with_per_dir_configs()` builds base + per-directory configs. `discover_per_dir_configs()` in `main.rs` walks paths looking for `.lint.toml`/`.lint.json`, loads, merges with base, and passes to Linter. `lint_file()` uses `effective_config()` and `effective_rule_sets()` to look up nearest directory config. Supports overrides for `max_line_length`, `enabled_rules`, `plugins`, `ignore_patterns`, `per_file_ignores`, `severity_overrides`. Two unit tests added.

- [x] **Rule category enable/disable (`--select`, `--ignore` by category)**
  - Ruff supports `--select E,W` to enable by category code; Biome uses `--linter.enabled-rules` groups
  - **Goal**: `lint lint . --select style,correctness` enables only those categories; `--ignore security` disables security rules
  - **Status**: Added `category_rules()` and `is_category()` to `rules.rs`. `expand_rule_names()` in `main.rs` expands category names to their constituent rule names before rule selection. `--select style` adds all style rules; `--ignore security` removes all security rules. Works alongside existing per-rule `--select` and `--ignore`. Unit tests for helpers and behavior tests for expansion added.

- [x] **LSP / IDE integration (`lint-lsp` binary)**
  - Ruff provides `ruff-lsp` for real-time diagnostics in VS Code, Neovim, etc.
  - **Goal**: A `lint-lsp` binary that implements the Language Server Protocol, streaming diagnostics as files change
  - **Status**: Added `lint-lsp` binary using raw JSON-RPC 2.0 over stdin/stdout (no new dependencies). `LspServer::run()` reads Content-Length prefixed messages, dispatches to method handlers. Supports: `initialize`, `initialized`, `textDocument/didOpen`, `textDocument/didChange`, `textDocument/didClose`, `shutdown`, `exit`. On open/change, lints content via `Linter::lint_content()` and sends `textDocument/publishDiagnostics`. Refactored `linter.rs` to extract `lint_content()` for in-memory linting. README updated with setup instructions.

- [x] **Rule descriptions for all rules**
  - `explain` and `list-rules` commands show descriptions for rules
  - **Status**: Added `description()` implementations to all 11 generic rules and all 18 language-specific rules. Refactored `explain_rule()` and `list_rules()` in `main.rs` to use the actual `Linter` rule sets instead of hardcoded lists, so new rules automatically appear. Added `rule_set()` and `language_rule_set()` getters to `Linter`.

- [x] **Pre-commit hook support**
  - Ruff and Biome publish official `.pre-commit-hooks.yaml` files
  - **Goal**: A `pre-commit` hook config that runs `lint lint --changed --staged` on commit
  - **Status**: Added `.pre-commit-hooks.yaml` with `id: lint`, `entry: lint lint`, `types_or: [text]`, `pass_filenames: true`. README updated with basic usage, `--changed`, and `--staged` examples.

- [x] **Progress indicator for large runs**
  - Biome shows a progress bar when scanning thousands of files
  - **Goal**: `lint lint . --progress` shows a progress bar with file count and elapsed time
  - **Status**: Added `--progress` CLI flag. Uses `indicatif` crate. Before linting, counts files via `Linter::list_files()` and shows a spinner-style progress bar with `{spinner} [{elapsed}] Linting files {pos}/{len}`. Bar finishes with "Linting complete" after results return. CLI parsing test added.

- [x] **Config validation mode (`--show-config` with errors)**
  - Ruff validates config and prints helpful error messages for unknown rules or invalid severity values
  - **Goal**: `lint lint --validate-config` exits non-zero if config is invalid, printing specific errors
  - **Status**: Added `--validate-config` CLI flag. `validate_config_errors()` checks: (1) all rules in `enabled_rules` are known, (2) all keys in `severity_overrides` are known rules, (3) all plugins are known. Prints each error to stderr and exits 1 if any found; prints "Config is valid." and exits 0 otherwise. `known_rules()` and `known_plugins()` added to `rules.rs`. 4 unit tests and 1 CLI parsing test added.

- [x] **Import sorting / organization**
  - Ruff's `isort` equivalent and Biome's formatter both organize imports
  - **Goal**: A new generic rule `sort-imports` that reorders `use`/`import`/`require` statements alphabetically/grouped
  - **Status**: Added `SortImportsRule` that detects consecutive import blocks across Rust (`use `), JS/TS (`import ` / `require(`), Python (`import ` / `from `), C/C++ (`#include`). Extracts sort keys per language and reports out-of-order lines. 5 unit tests. Enabled via `--rules sort-imports`.

- [x] **Complexity rules (nesting depth, function length)**
  - ESLint has `max-nested-callbacks`, `max-lines-per-function`; Ruff has `C901` (McCabe complexity)
  - **Goal**: Generic rules `max-nesting-depth`, `max-function-lines`, `max-cyclomatic-complexity`
  - **Status**: Added `MaxNestingDepthRule` (counts `{`/`}` depth, default threshold 4) and `MaxFunctionLinesRule` (detects function boundaries via `fn`/`def`/`func`/`function`/`{`+`(` patterns, default threshold 50 lines). CLI flags `--max-nesting-depth` and `--max-function-lines`. Config fields `max_nesting_depth` and `max_function_lines`. 6 tests.

## Brainstorming / Competitive Intelligence

- [x] **`--select ALL` to enable all rules**
  - Ruff supports `--select ALL` to enable every rule; our `--select` only accepts categories or individual rule names
  - **Goal**: `--select ALL` expands to every known generic and language rule
  - **Status**: `expand_rule_names()` now handles `ALL` (case-insensitive) by returning all generic and language rules via `known_rules()`. `--select-all` flag refactored to use `expand_rule_names(vec!["ALL"])` instead of a stale hardcoded list, so it automatically includes new rules. `LanguageRuleSet::known_rules()` added. 2 unit tests: CLI parsing and expansion verification.

- [x] **Rule codes / IDs (E501, W001 style)**
  - Ruff, Clippy, ESLint all use short letter-number codes for rules (e.g., E501 = line too long)
  - Makes referencing rules in suppressions, configs, and conversations much easier
  - **Goal**: Assign a unique `code: &str` to every rule; display in output; support `# lint: ignore=E501`
  - **Status**: Added `code()` to `Rule` and `LanguageRule` traits with a default implementation using `code_from_name()`. Created `code_from_name()` and `name_from_code()` lookup functions. Assigned codes: W### for style, E### for correctness/error, S### for security, L### for language-specific. Updated `expand_rule_names()` to accept codes (e.g., `--select W001` resolves to `line-length`). Updated `validate_config_errors()` to accept codes. Updated suppression system (`is_line_suppressed`, block suppressions, file-level ignores, unused suppression detection) to match both rule names and codes. Updated text output to display `[code]` before severity. Updated `list-rules` and `explain` to show codes. 8 tests: code mapping, round-trip, unknown fallback, rule method, expansion, config validation, and suppression by code.

- [x] **Auto-fix for `sort-imports`**
  - Currently only trailing-whitespace, no-tabs, final-newline, no-consecutive-empty-lines, and no-mixed-line-endings have fixes
  - **Goal**: `sort-imports` fix reorders the import block alphabetically
  - **Status**: `SortImportsRule::check()` now attaches a `Fix { line: 0, replacement: sorted_content }` to the first violation message. `sort_all_imports()` finds every consecutive import block, sorts it alphabetically using the existing `extract_sort_key()` per-language logic, and preserves all non-import lines. 2 unit tests verify block reordering and non-import line preservation.

- [x] **`has_fix()` on Rule and LanguageRule traits**
  - `list-rules` hardcoded a broken heuristic (`rule.name() == "line-length"`) for showing auto-fix availability
  - **Goal**: Rules self-report whether they support auto-fix; `list-rules` and `explain` show accurate info
  - **Status**: Added `fn has_fix(&self) -> bool` to both `Rule` and `LanguageRule` traits with default `false`. Overridden to `true` for: `TrailingWhitespaceRule`, `NoTabsRule`, `FinalNewlineRule`, `NoConsecutiveEmptyLinesRule`, `NoMixedLineEndingsRule`, `SortImportsRule`. Refactored `list_rules()` to use `rule.has_fix()`. Refactored `explain_rule()` to DRY the lookup and print `Auto-fix: yes/no`. 2 tests verify fixable and non-fixable rule sets.

- [x] **Multiple ignore patterns per suppression**
  - Currently `// lint: ignore=W001` only accepts a single rule; some lines violate multiple rules
  - **Goal**: Support comma-separated lists like `// lint: ignore=W001,W002`
  - **Status**: Updated all suppression parsers (`is_line_suppressed`, `parse_block_suppressions`, `parse_file_level_ignore`, `parse_suppression_directives`) to split by comma and check each rule. `lint: ignore=W001,W002`, `lint: disable=W001,W002`, `lint: enable=W001,W002`, and `lint: ignore-file=W001,W002` all work. Unused suppression detection correctly evaluates each rule in the list independently. 1 test (`test_multiple_inline_ignore_patterns`).

- [x] **Per-rule default severity**
  - All rules currently default to Warning; security rules like `hardcoded-secret` should arguably be Error by default
  - **Goal**: Rules self-report a default severity; `severity_overrides` can upgrade or downgrade
  - **Status**: Added `fn default_severity(&self) -> Severity` to `Rule` and `LanguageRule` traits with default `Severity::Warning`. Overridden for: `NoTodoRule` (Info), `SortImportsRule` (Info), `HardcodedSecretRule` (Error), `UnsafeEvalRule` (Error), `SqlInjectionRiskRule` (Error), `PythonStyleRule` (Info), `JavaStyleRule` (Error), `KotlinStyleRule` (Error), `CSharpStyleRule` (Error), `RubyStyleRule` (Info), `SqlSelectStarRule` (Info), `RPrintRule` (Info), `HtmlInlineStyleRule` (Info), `CssImportantRule` (Info). All `check()` methods now use `self.default_severity()` instead of hardcoded values. `explain` output shows default severity. 2 tests (`test_default_severity_values` in both `rules.rs` and `language_rules.rs`).

- [x] **Rule documentation URL in `explain` output**
  - Ruff and ESLint provide URLs to rule docs; users often want to read more
  - **Goal**: Add `url()` method to `Rule`/`LanguageRule`; `explain` prints it
  - **Status**: Added `fn url(&self) -> &str` to both `Rule` and `LanguageRule` traits with default `""`. `explain_rule()` now includes `r.url()` in the lookup tuple and prints `URL: ...` only when the URL is non-empty. All existing rules default to empty string; future rules can override with real documentation links.

- [x] **Lint timing / `--profile` flag**
  - Large codebases want to know which rules are slow
  - **Goal**: `--profile` prints per-rule and per-file timing after linting
  - **Status**: Added `--profile` CLI flag. Added `profile: bool`, `rule_times: Arc<Mutex<HashMap<String, Duration>>>`, and `file_times: Arc<Mutex<Vec<(PathBuf, Duration)>>>}` to `Linter`. Modified `lint_content()` to time each generic rule check, and `lint_file()` to time the whole file lint. `language_rule_set.check()` is timed as a single bucket labeled `__language_rules__`. `run_lint_and_print()` prints rule timing (sorted by duration) and file timing (top 10 + total) when `--profile` is enabled.

- [x] **SARIF output format**
  - GitHub Advanced Security, Azure DevOps, and many CI systems consume SARIF for code scanning dashboards
  - **Goal**: `lint lint --format sarif` produces SARIF 2.1.0 JSON
  - **Status**: Already implemented. `OutputFormat::Sarif` variant exists. `render_sarif()` produces SARIF 2.1.0 JSON with runs, rules, and results arrays.

- [x] **Parallel linting with `rayon`**
  - Ruff and Biome both lint files in parallel; our linter processes sequentially
  - **Goal**: Use `rayon::par_iter` over file list for multi-core speedup
  - **Status**: Already implemented. `Linter::run()` uses `self.config.paths.par_iter().map(|path| self.lint_path(path)).collect()` via `rayon::prelude::*`. `rayon` dependency already in `Cargo.toml`.

- [x] **`--statistics` flag**
  - ESLint has `--stats` to show violation counts by rule; Ruff shows per-rule counts in summary
  - **Goal**: `lint lint --statistics` prints a table of rule -> count -> severity
  - **Status**: Already implemented. `RunOptions` includes `statistics: bool`. After linting, counts violations per rule into a `HashMap<&str, usize>`, sorts by count descending, and prints `Statistics:` table. CLI parsing test exists.

- [x] **`--baseline` mode (only new violations)**
  - Biome supports `--changed` for staged files; some teams want "only show violations I introduced"
  - **Goal**: `lint lint --baseline baseline.json` filters out known violations from a JSON baseline file
  - **Status**: Already implemented. `--baseline PATH` CLI flag loads a JSON array of `{file, line, rule}` entries and removes matching violations from results before output. `BaselineEntry` struct, `load_baseline()`, `filter_baseline()` helpers. CLI parsing and behavior tests exist.

- [x] **TOML config support**
  - Ruff reads `pyproject.toml`; Biome reads `biome.json` or `biome.toml`
  - **Goal**: Support `.lint.toml` as an alternative to `.lint.json`
  - **Status**: Already implemented. `load_config_file()` detects `.toml` extension and uses `toml::from_str()` instead of `serde_json::from_str()`. `find_config_file()` searches for both `.lint.json` and `.lint.toml`. `toml` crate is in `Cargo.toml` dependencies.

- [x] **`add_noqa` should use rule codes**
  - Currently inserts `// lint: ignore=trailing-whitespace` which is verbose
  - **Goal**: Use short codes like `// lint: ignore=W002` when a known code exists
  - **Status**: `add_noqa` logic now looks up `code_from_name(&msg.rule)`. If the code differs from the rule name, it inserts the shorter code (e.g., `W002` instead of `trailing-whitespace`). Falls back to the rule name for custom/unknown rules.

- [x] **Context lines in text output**
  - Ruff and Clippy show 1-2 lines of surrounding context around each violation
  - **Goal**: `--show-source` displays the violating line plus one line before and after
  - **Status**: Enhanced `show_source` block in text output renderer. Now prints the previous line (dimmed), the violating line with caret, and the next line (dimmed) when `--show-source` is enabled.

- [x] **Cache invalidation on config / rule changes**
  - Current cache uses file mtime/size or content hash, but does NOT consider rule or config changes
  - If you lint with rules A+B, then add rule C to config, the cache may return stale results
  - **Goal**: Include a hash of the effective lint configuration in the cache key so that any rule/config change invalidates cached results
  - **Status**: Added `config_hash: Option<String>` to `CacheEntry`. `lint_file()` now computes a config hash from `enabled_rules` (sorted), `max_line_length`, `max_nesting_depth`, `max_function_lines`, `severity_overrides` (sorted), and `ignore_suppressions`. Both `get()` and `get_by_hash()` now accept an optional `config_hash` parameter and validate it against stored entries. `insert()` and `insert_with_hash()` store the config hash. Cache tests updated. 1 new test (`test_cache_miss_config_hash_changed`).

- [x] **Show rule codes in SARIF / GitHub / JUnit / GitLab output**
  - Machine-readable formats currently only emit rule names; codes make integration with dashboards easier
  - **Goal**: Include `code` field in SARIF rules and GitHub/JUnit/GitLab annotations
  - **Status**: Updated all machine-readable renderers to use short codes. SARIF `ruleId` and rules `id` now use codes (e.g., `W001`) with `shortDescription` containing both code and name. GitHub `title` now shows `W001 (line-length)`. JUnit `type` and body bracket now use codes. GitLab `check_name` now uses codes.

- [x] **`/* block comment */` suppression support**
  - Currently only `// line comments` are parsed for `lint: ignore/disable/enable`
  - **Goal**: Support `/* lint: ignore=W001 */` and `/* lint: disable=W002 */ ... /* lint: enable=W002 */`
  - **Status**: Updated all suppression parsers (`is_line_suppressed`, `parse_block_suppressions`, `parse_file_level_ignore`, `parse_suppression_directives`) to strip trailing `*/` after extracting the directive, enabling single-line block comment suppressions. 4 tests: inline, block, file-level, and block-comment-block suppression.