chore(nix): remove statix and deadnix from fixer
Drop the statix and deadnix packages from the fix app now that the repository has a local Nix style linter path. Remove the obsolete statix configuration, document the evaluated rule sets and architectures, and keep vendored third-party files out of treefmt.
This commit was merged in pull request #62.
This commit is contained in:
1 parent
2ddd39e7a1
commit
f98af6ad3f
7 files changed
+1364
-8
No files matched your search
@@ -0,0 +1,261 @@
|
||||
# Statix Architecture
|
||||
|
||||
## Purpose
|
||||
|
||||
This note explains how `statix` works as a Nix linter and fixer.
|
||||
It focuses on architecture, not the value of each rule.
|
||||
|
||||
Use this note when deciding how complete our repository-local Nix linter should become.
|
||||
|
||||
## What Statix Is
|
||||
|
||||
`statix` is a syntax-tree linter for Nix.
|
||||
It parses source text into a Nix AST, walks that tree, and runs rules on matching nodes.
|
||||
|
||||
It does not evaluate Nix.
|
||||
It does not import files.
|
||||
It does not know the final value of an attribute set.
|
||||
It sees source structure, not runtime semantics.
|
||||
|
||||
That design makes `statix` fast and deterministic.
|
||||
It also limits it to rules that the parser can prove from local syntax.
|
||||
|
||||
## Minimal Linter Vocabulary
|
||||
|
||||
- Source text is the bytes in a Nix file.
|
||||
- A parser converts source text into a syntax tree.
|
||||
- A syntax tree records nodes such as lambdas, lists, strings, and operators.
|
||||
- A token is a leaf item such as an identifier, comment, or URI literal.
|
||||
- A rule inspects one syntax element and optionally returns a diagnostic.
|
||||
- A diagnostic reports a span and a message.
|
||||
- A suggestion adds a replacement for that span.
|
||||
- A fixer applies suggestions back to source text.
|
||||
|
||||
## Component Map
|
||||
|
||||
The upstream architecture document names four major components.
|
||||
|
||||
### CLI component
|
||||
|
||||
The CLI owns operator interaction.
|
||||
It parses subcommands and options, loads configuration, discovers files, and prints output.
|
||||
|
||||
It supports these user flows:
|
||||
|
||||
- `check`: report diagnostics.
|
||||
- `fix`: apply all safe suggestions.
|
||||
- `single`: apply one suggestion at a position.
|
||||
- `explain`: print the long explanation for a rule code.
|
||||
- `dump`: print a sample configuration file.
|
||||
- `list`: print all available rules.
|
||||
|
||||
### Library component
|
||||
|
||||
The library owns lint rules and the data structures that rules return.
|
||||
It defines `Report`, `Diagnostic`, `Suggestion`, `Rule`, `Metadata`, `Explain`, and `Lint`.
|
||||
|
||||
Each rule is a small unit.
|
||||
It receives one syntax element and returns zero or one report.
|
||||
|
||||
The library also contains helpers that build replacement syntax.
|
||||
Those helpers parse small replacement fragments back into AST nodes.
|
||||
This makes fixes use valid Nix syntax instead of raw string assembly.
|
||||
|
||||
### Virtual filesystem component
|
||||
|
||||
The virtual filesystem stores file paths and file contents in memory.
|
||||
It assigns each file a small `FileId`.
|
||||
|
||||
The CLI reads files into this store before linting.
|
||||
Diagnostics then carry `FileId` references instead of path strings.
|
||||
|
||||
The VFS supports sequential iteration and parallel iteration.
|
||||
`statix check` uses parallel iteration across files.
|
||||
|
||||
### Macro component
|
||||
|
||||
The macro component reduces rule boilerplate.
|
||||
A `lint` macro generates rule metadata, explanation support, and the static rule object.
|
||||
|
||||
A rule author writes the rule structure, metadata, documentation, and validation logic.
|
||||
The macro wires that rule into the common linter interface.
|
||||
|
||||
## Check Pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Parse CLI options] --> B[Discover configuration]
|
||||
B --> C[Build ignore set]
|
||||
C --> D[Walk Nix files]
|
||||
D --> E[Read files into VFS]
|
||||
E --> F[Build active lint map]
|
||||
F --> G[Parse each file with rnix]
|
||||
G --> H[Walk syntax tree]
|
||||
H --> I[Run rules matching each syntax kind]
|
||||
I --> J[Collect reports]
|
||||
J --> K[Render diagnostics]
|
||||
K --> L[Exit 1 when reports exist]
|
||||
```
|
||||
|
||||
The key optimization is the active lint map.
|
||||
`statix` groups rules by `SyntaxKind` before it walks a file.
|
||||
During traversal, it looks up only rules that declared interest in the current node kind.
|
||||
|
||||
This avoids running every rule on every node.
|
||||
It also keeps each rule narrow.
|
||||
|
||||
## Rule Registration
|
||||
|
||||
Rules are built into the binary.
|
||||
There is no dynamic rule loading.
|
||||
|
||||
A registry macro imports each rule module and builds a static `LINTS` vector.
|
||||
The CLI filters that vector with configuration.
|
||||
Disabled rule names disappear from the active lint map.
|
||||
|
||||
This model has a simple trust boundary:
|
||||
|
||||
- all rule code ships with the tool;
|
||||
- configuration can disable rules;
|
||||
- configuration cannot define new rules.
|
||||
|
||||
## Rule Execution Model
|
||||
|
||||
Each rule declares the syntax kinds it wants to inspect.
|
||||
For example, one rule can ask for binary-operation nodes.
|
||||
Another rule can ask for URI tokens.
|
||||
|
||||
The traversal visits tree entries with `preorder_with_tokens`.
|
||||
That includes both nodes and tokens.
|
||||
Rules can therefore inspect structured AST nodes and raw tokens.
|
||||
|
||||
A rule returns a `Report` when it finds a problem.
|
||||
The report contains one or more diagnostics.
|
||||
Each diagnostic has a source range and a message.
|
||||
A diagnostic can also include a `Suggestion`.
|
||||
|
||||
Rules do not share state with each other.
|
||||
They also do not run a whole-program analysis.
|
||||
A rule sees the syntax element passed to it and whatever children or parents it chooses to inspect.
|
||||
|
||||
## Parse Errors
|
||||
|
||||
`statix` turns parser errors into reports.
|
||||
Those reports use code `0` and severity `Error`.
|
||||
|
||||
Parse errors flow through the same output machinery as lint findings.
|
||||
This lets the CLI render syntax errors and lint diagnostics with one reporter.
|
||||
|
||||
## Output Model
|
||||
|
||||
`statix` has several output formats.
|
||||
|
||||
The human format uses `ariadne` to render annotated source snippets.
|
||||
The `errfmt` format prints one compact line per diagnostic.
|
||||
The JSON format exists behind a feature flag.
|
||||
|
||||
The output layer receives reports and the VFS.
|
||||
It converts byte ranges into line and column positions at render time.
|
||||
|
||||
This separates rule logic from display logic.
|
||||
Rules do not need to know how the terminal or editor consumes diagnostics.
|
||||
|
||||
## Fix Pipeline
|
||||
|
||||
A fixable diagnostic contains a `Suggestion`.
|
||||
A suggestion contains the range to replace and replacement syntax.
|
||||
|
||||
`statix fix` uses the same parser and rule walk as `check`, but filters for reports that contain suggestions.
|
||||
It then sorts and filters suggestions to avoid overlapping edits.
|
||||
|
||||
The fixer applies selected reports to a copy of the source.
|
||||
Then it parses and scans the changed source again.
|
||||
It repeats this process until no more suggestions appear.
|
||||
|
||||
That iterative design matters because one edit can expose a later edit.
|
||||
It also avoids applying stale ranges after source text changes.
|
||||
|
||||
## Single Fix Pipeline
|
||||
|
||||
The `single` command applies one fix at a requested line and column.
|
||||
It converts the line and column to a byte offset.
|
||||
Then it finds the first fixable report whose diagnostic range contains that offset.
|
||||
|
||||
This shape supports editor workflows.
|
||||
An editor can request a fix for the diagnostic under the cursor.
|
||||
|
||||
## Configuration Model
|
||||
|
||||
Configuration is small.
|
||||
A `statix.toml` file can disable rules by name.
|
||||
It can also add ignore globs.
|
||||
|
||||
The CLI discovers configuration by walking ancestors from the configured path.
|
||||
If no config exists, it uses defaults.
|
||||
|
||||
The command also respects `.gitignore` unless unrestricted mode is enabled.
|
||||
Ignore handling happens before parsing.
|
||||
|
||||
## Architectural Strengths
|
||||
|
||||
`statix` has a clean separation between these concerns:
|
||||
|
||||
- file discovery;
|
||||
- parsing;
|
||||
- rule selection;
|
||||
- rule execution;
|
||||
- reporting;
|
||||
- fixing.
|
||||
|
||||
Rules stay small because the framework handles traversal and output.
|
||||
Fixes are safer because replacement syntax comes from parsed Nix fragments.
|
||||
The lint map avoids unnecessary rule calls.
|
||||
Parallel file traversal improves throughput for large trees.
|
||||
|
||||
The static rule registry is easy to audit.
|
||||
It fits a repository-local linter well when we want deterministic behavior.
|
||||
|
||||
## Architectural Limits
|
||||
|
||||
`statix` does not evaluate Nix.
|
||||
It cannot know imported module results, overlays, option merges, or generated attributes.
|
||||
|
||||
Most rules must remain local syntax rules.
|
||||
A rule that needs semantic knowledge can easily become wrong.
|
||||
|
||||
Fixes rely on byte ranges in source text.
|
||||
They must avoid overlapping ranges.
|
||||
They must rerun after edits because previous ranges become stale.
|
||||
|
||||
The macro system reduces rule boilerplate, but it hides generated behavior.
|
||||
That tradeoff is useful for many rules, but it can make debugging harder.
|
||||
|
||||
## Lessons for Our Linter
|
||||
|
||||
Start with a clear rule interface.
|
||||
A rule should declare its name, matching syntax kinds, and diagnostic output.
|
||||
|
||||
Keep parsing and diagnostics separate from output.
|
||||
The same diagnostic should support terminal output, editor output, and tests.
|
||||
|
||||
Use syntax-kind indexing when rules are local.
|
||||
It prevents unnecessary calls and makes rule ownership explicit.
|
||||
|
||||
Treat autofix as a second contract.
|
||||
A diagnostic can exist without a safe fix.
|
||||
A fix must define exact replacement ranges and avoid overlap.
|
||||
|
||||
Rerun analysis after applying a batch of fixes.
|
||||
Source ranges become invalid after edits.
|
||||
|
||||
Do not add Nix evaluation accidentally.
|
||||
If a rule needs evaluated values, design that as a separate analysis layer.
|
||||
|
||||
## Open Design Questions for Our Linter
|
||||
|
||||
- Should our linter support fixes, or stay report-only for now?
|
||||
- Should rules register by syntax kind like `statix`?
|
||||
- Should our suppression model live in the parser layer or rule layer?
|
||||
- Should parse errors use the same diagnostic type as style rules?
|
||||
- Should editor-friendly output be part of the first complete architecture?
|
||||
- Should future semantic rules use a separate pass from local syntax rules?
|
||||
Reference in new issue
Block a user