feat(lang): Nix language support (outline, semantic indexing, ast-grep)

KI
9d ago
+1569-186

Approved issue

Approved issue: cortexkit/aft#492 (design-approved)

What changed

Adds Nix as a supported language for:

  • Outline — the entry is the file's top-level attrset: a lambda body, a let … in body (its let bindings are listed at file level too), or an attrset reached through with/assert/parentheses or by applying a function to an attrset. Only an application's final argument is traversed; earlier arguments and list/if/// expressions contribute nothing. The same rule applies to nested values, so config = lib.mkIf cond { … } nests its children. The outline lists the entry bindings, then exactly one level of nested attrsets or lambda bodies returning attrsets. Bindings are named by their dotted attrpath (config.services.demo.enable); function bindings carry a one-line header as the signature; inherit clauses are skipped; two top-level bindings with identical full names are both kept without expanding their children.
  • Zoom — resolves the same dotted paths, including paths through an enclosing outlined binding.
  • AST search/replace — AstGrepLang::Nix on tree-sitter-nix 0.3.0. Patterns are written with $NAME, which the _ expando rewrites to _NAME because the grammar rejects µ. Complete Nix expressions match through the normal path; a standalone binding or inherit clause is not an expression, so compile_pattern falls back to a contextual pattern that selects the sole binding-set member's own kind (binding, inherit, inherit_from). This keeps sibling bindings intact on replace (a whole-attrset pattern would delete them), and a wrapper mixing members — two bindings, or a binding plus an inherit — is rejected rather than matched piecemeal. Because _ is also a legal Nix identifier start, a key written _ plus an all-uppercase name (_FOO) is read as a metavariable; _Foo, __functor, and _module.args stay literal.
  • Semantic indexing — .nix is in the extension policy and the chunk census label map.
  • Formatter — nixfmt is the only formatter Nix resolves, and only through an explicit formatter.nix key (no auto-detection; formatter.nix = "none" still disables formatting). nixfmt is accepted by the Rust config resolver, both plugin config schemas, and the generated JSON schema. Checker validation stays unsupported.
  • Agent surface — nix added to both harness plugin AST enums and the regenerated subc_tool_schemas.json + tool-provider catalog goldens.
  • Registry sweep — indent (two spaces), inspect labels, callgraph-store labels (including join::language_id, which the views plane requires even though Nix has no call edges), views lazy-read label map.
  • Docs — README language row and Nix caveat, the formatter.nix key in docs/config.md, the Config::formatter doc comment, and accepted-language lists plus error messages in the AST tools now naming every canonical language.

Unsupported for now (approved scope): imports, refactor, call graph. The call graph reports no Nix edges.

Dependency

tree-sitter-nix = "0.3.0" (nix-community, MIT, ~45 KB, ABI 13, external scanner.c). numtide has no crates.io release yet, so this is the published pin the issue approved; switch when a numtide crate publishes. 0.3.0 predates the |> pipe operator and bare inherit;, so a file using them parses with ERROR nodes and may outline partially. The workspace's tree-sitter 0.26.7 has a minimum compatible ABI of 13, so the grammar loads at the floor.

Checks

Re-run on 1cd536049:

  • cargo test -p agent-file-tools --lib nix_ — 29 passed, of which 14 are Nix tests (the filter also matches 15 unrelated unix* tests): detection, wrapper table, dotted outline, signatures, depth stop, inherit, applied-entry shapes, nested application values, full-name collisions, ast-grep contextual patterns (bindings, inherit clauses, multi-binding rejection), formatter candidates, indent, duplicates exclusion, complexity skip, semantic chunks, config resolver.
  • cargo test -p agent-file-tools --test integration nix_test — 7 passed (outline/zoom/language label on a schema-valid flake fixture, ast search/replace sibling preservation, unsupported import/refactor, empty call-graph annotations, validation skip, views callgraph payload, dotted provider symbols).
  • cargo fmt --all -- --check — clean.

Passed on the first commit 02104292b and unaffected by the follow-up diff, which touches only Nix paths and documentation; CI re-runs them:

  • cargo test -p agent-file-tools --test integration config_parity and is_semantic_indexed_extension — passed.
  • cargo test -p agent-file-tools --lib full_catalog_goldens_and_digest_only_have_exact_identity — passed after regenerating the catalog fixtures.
  • cargo test -p agent-file-tools --features semantic-chunk-census --test semantic language_label — passed.
  • cargo test -p agent-file-tools --test integration format_integration_nixfmt_config_runs_nixfmt — passed (a configured nixfmt stub runs on write, so the formatter scenario covers invocation, not only candidate resolution).
  • Manual smoke check: printf '%s\n' '{"id":"o","command":"outline","file":"flake.nix"}' | aft → symbol list; swap "command":"ast_search","pattern":"$NAME = true;","lang":"nix","paths":["."] for structural search.
  • bun run typecheck (root, all four packages) and bun scripts/build-tool-schemas.ts --check (opencode-plugin) — passed.

Corpus validation

Against a local NixOS configuration corpus (121 .nix files, 494,563 bytes, 13,929 lines), re-measured after the nested-application change:

MeasureResult
Files parsed with no ERROR/MISSING nodes121 of 121
Files whose files-mode outline entry reports language nix120 of 121 (files mode honours .gitignore)
Outline symbols2,344 (2,119 before nested application expansion)
Files with at least one nested symbol114 of 120 (113 before)
Files whose entry is an applied function66 of 121 parsed files (entry discovery untouched)
Files with zero symbols0 of 120 outlined files
Structural-search matches; dry-run rewrites succeeded335; 335 of 335
Zoom on a nested dotted symbolresolved the expected line
Write with validate: "full"Succeeds; validate_skipped_reason = unsupported_language

Only aggregate counts and size are reported. Corpus paths, file names, and content stay on the machine it was found on.

Release binary size

target/release/aft, rustc 1.95.0, cargo build --release -p agent-file-tools:

Buildtarget/release/aft bytes
main at 7ca30b526130,206,840
branch at 1cd536049130,333,816
delta+126,976 (+0.10%)

Notes

  • The cfg-gated aft_views_lazy_benchmark build has a pre-existing type error at views/lazy_read_benchmark.rs:318 (&str vs String); the Nix arm there compiles once that is patched. Not introduced here.
  • An existing integration-test helper deploys the binary via /bin/cat, which is absent on the NixOS host used for local verification; tests were run in a mount namespace with /bin/cat provided. CI is unaffected.