Files
dev/docs/00 INBOX/ai/nix-statix-deadnix-rules.md
alexion ddc7952399 feat(xatu): scaffold project
Add the Xatu Rust package as a minimal hello-world skeleton with Cargo workspace metadata and Bazel library, binary, and test targets.

Include the Statix and Deadnix evaluation notes so the new linter work starts with the prior rule and architecture research.
2026-09-17 00:26:12 -04:00

12 KiB

Nix Statix and Deadnix Rules

Purpose

This note rewrites the current statix and deadnix findings as possible nix-style rules. Use the Decision line on each rule for keep, discard, update, or need-more-info.

Source Baseline

  • statix rule names come from nix run --inputs-from . nixpkgs#statix -- list.
  • deadnix checks come from nix run --inputs-from . nixpkgs#deadnix -- --help.
  • deadnix reports categories instead of named rules.
  • At collection time, src/dotfiles/apps.nix ran statix fix . and deadnix --edit in nix run .#fix.
  • At collection time, statix.toml disabled repeated_keys.

Statix Rules

bool_comparison

Decision: keep

Source: statix W01. Status at collection time: enabled. Autofix: yes.

Rule

Do not compare a boolean expression to true or false. Use the expression directly for true checks. Use ! for false checks.

Avoid:

x == true
x == false

Use:

x
!x

Rationale

Boolean literal comparisons add noise without changing behavior. Direct predicates make conditions easier to scan.

empty_let_in

Decision: keep

Source: statix W02. Status at collection time: enabled. Autofix: yes when the let body has no comments.

Rule

Do not keep a let expression that declares no bindings. Replace it with its body.

Avoid:

let in value

Use:

value

Rationale

An empty let adds syntax without naming a dependency. It usually remains after editing or debugging.

manual_inherit

Decision: Keep

Source: statix W03. Status at collection time: enabled. Autofix: yes.

Rule

Use inherit when an attribute assignment repeats the same name as its value.

Avoid:

{
  package = package;
}

Use:

{
  inherit package;
}

Rationale

inherit states that the attribute comes from the current scope. It removes duplicated names.

manual_inherit_from

Decision:

Source: statix W04. Status at collection time: enabled. Autofix: yes.

Rule

Use inherit (source) when an assignment selects an attribute with the same name.

Avoid:

package = pkgs.package;

Use:

inherit (pkgs) package;

Rationale

inherit (source) states that the value comes from an attribute set. It avoids repeated selector names.

legacy_let_syntax

Decision: keep

Source: statix W05. Status at collection time: enabled. Autofix: yes.

Rule

Do not use legacy let { ... } syntax. Use documented Nix expressions.

Avoid:

let {
  body = x + y;
  x = 2;
  y = 3;
}

Use:

rec {
  body = x + y;
  x = 2;
  y = 3;
}.body

Rationale

Legacy let syntax is undocumented. Documented forms are clearer to tools and readers.

collapsible_let_in

Decision: keep

Source: statix W06. Status at collection time: enabled. Autofix: yes.

Rule

Collapse a let expression when its body is another let expression. Put both binding groups in one let when scope permits it.

Avoid:

let
  a = 1;
in
let
  b = 2;
in
  a + b

Use:

let
  a = 1;
  b = 2;
in
  a + b

Rationale

Nested let expressions add structure when they do not add a scope boundary. A single binding group shows the local dependencies together.

eta_reduction

Decision: keep

Source: statix W07. Status at collection time: enabled. Autofix: yes.

Rule

Remove a lambda that only passes its argument to another function. Keep the target function name when that preserves behavior.

Avoid:

map (x: double x) values

Use:

map double values

Rationale

The wrapper lambda adds no behavior. The direct function reference exposes the operation.

useless_parens

Decision: keep

Source: statix W08. Status at collection time: enabled. Autofix: yes.

Rule

Remove parentheses around expressions when they do not affect parsing.

Avoid:

let
  double = (x: x * 2);
in
  (value)

Use:

let
  double = x: x * 2;
in
  value

Rationale

Redundant parentheses make expressions harder to scan. Required precedence parentheses remain valid.

empty_pattern

Decision: keep

Source: statix W10. Status at collection time: enabled. Autofix: yes.

Rule

Use _ instead of an empty variadic function pattern when the argument is ignored. Do not apply this rule to NixOS-style modules that declare imports.

Avoid:

{ ... }: value

Use:

_: value

Rationale

_ directly marks an ignored argument. An empty variadic pattern looks like partial destructuring.

redundant_pattern_bind

Decision: keep

Source: statix W11. Status at collection time: enabled. Autofix: yes.

Rule

Do not bind a variadic pattern when the pattern captures no fields. Use a single argument name instead.

Avoid:

args @ { ... }: args.nixpkgs

Use:

args: args.nixpkgs

Rationale

The empty pattern adds no destructuring value. A named argument shows that the whole argument is used.

unquoted_uri

Decision: keep

Source: statix W12. Status at collection time: enabled. Autofix: yes.

Rule

Quote URI literals. Do not use Nix's special unquoted URI syntax.

Avoid:

url = github:owner/repository;

Use:

url = "github:owner/repository";

Rationale

Quoted strings work for all URI forms. They avoid ambiguity with punctuation after the URI.

empty_inherit

Decision: keep

Source: statix W14. Status at collection time: enabled. Autofix: yes.

Rule

Remove inherit statements that inherit no names.

Avoid:

inherit;

Use no replacement.

Rationale

An empty inherit has no effect. It usually remains after a refactor.

deprecated_to_path

Decision: keep

Source: statix W17. Status at collection time: enabled. Autofix: no.

Rule

Do not use builtins.toPath or unqualified toPath. Use explicit path construction instead.

Avoid:

builtins.toPath "/path"

Use:

/. + "/path"
./. + "/relative-path"

Rationale

toPath is deprecated. Explicit path construction states whether the result is absolute or relative.

bool_simplification

Decision: keep

Source: statix W18. Status at collection time: enabled. Autofix: yes.

Rule

Use != instead of negating an equality comparison.

Avoid:

!(x == y)

Use:

x != y

Rationale

The direct operator expresses inequality without nested boolean syntax.

useless_has_attr

Decision: keep

Source: statix W19. Status at collection time: enabled. Autofix: yes.

Rule

Use or when an if expression only checks for an attribute before selecting it.

Avoid:

if x ? a then x.a else default

Use:

x.a or default

Rationale

The or form is the Nix operator for attribute defaults. It avoids repeating the attribute path.

repeated_keys

Decision: update so that it doesn't conflict with the custom module convention.

Source: statix W20. Status at collection time: disabled in statix.toml. Autofix: no.

Rule

Group repeated attribute path prefixes in a nested attribute set. Apply this rule only when at least three sibling assignments share the same first key. Do not apply it inside rec attribute sets.

Avoid:

{
  services.a = 1;
  services.b = 2;
  services.c = 3;
}

Use:

{
  services = {
    a = 1;
    b = 2;
    c = 3;
  };
}

Rationale

Grouping repeated prefixes reduces visual repetition. The local config disables this rule, so it needs an explicit decision before promotion.

empty_list_concat

Decision: keep

Source: statix W23. Status at collection time: enabled. Autofix: yes.

Rule

Remove list concatenation with an empty list.

Avoid:

[] ++ values
values ++ []

Use:

values

Rationale

Concatenating [] changes no values. It usually remains after conditional list construction changed.

Deadnix Checks

Unused let bindings

Decision: keep

Source: deadnix default check. Status at collection time: active in nix run .#fix. Autofix: yes.

Rule

Remove let bindings when no live expression uses their names. Do not keep a binding only because another dead binding uses it. Use # deadnix: skip only for intentional unused declarations.

Avoid:

let
  unused = 1;
in
  value

Use:

value

Rationale

Unused bindings hide the dependencies that matter. They also make refactors look unfinished.

Unused inherit names

Decision: keep

Source: deadnix default check. Status at collection time: active in nix run .#fix. Autofix: yes.

Rule

Remove names from inherit statements when no live expression uses those names. Remove the whole inherit statement when it becomes empty.

Avoid:

let
  inherit (builtins) unused;
in
  value

Use:

value

Rationale

Unused inherited names create the same noise as unused let bindings. An empty result also overlaps with statix empty_inherit.

Unused lambda arguments

Decision: keep

Source: deadnix default check. Status at collection time: active in nix run .#fix. Autofix: yes.

Rule

Mark an unused single lambda argument with _. Do not keep a meaningful name for an ignored argument.

Avoid:

arg: value

Use:

_: value

Rationale

_ states that the argument is intentionally ignored. A named unused argument implies missing logic.

Unused lambda pattern names

Decision: keep

Source: deadnix default check. Status at collection time: active in nix run .#fix. Autofix: yes.

Rule

Remove unused names from lambda attribute patterns. Keep only names used by the body or defaults. Use ... when callers can pass extra attributes.

Avoid:

{ used, unused, ... }: used

Use:

{ used, ... }: used

Rationale

Pattern names usually declare dependencies. Unused names overstate the function's inputs.

Unused lambda pattern aliases

Decision: keep

Source: deadnix default check. Status at collection time: active in nix run .#fix. Autofix: yes.

Rule

Remove a lambda pattern alias when the body does not use the alias. Keep the alias only when code reads the whole argument set.

Avoid:

args @ { used, ... }: used

Use:

{ used, ... }: used

Rationale

The alias says the function needs the complete input set. Keeping it unused makes the function contract look wider than it is.

Ignored underscore bindings

Decision: keep

Source: deadnix --no-underscore option. Status at collection time: inactive; current fixer does not pass --no-underscore. Autofix: no separate rule.

Rule

Treat bindings whose names start with _ as intentionally ignored. Optionally skip unused diagnostics for those names.

Avoid, when the skip policy is active:

let
  _unused = expensiveValue;
in
  value

Use only for intentional placeholders.

Rationale

An underscore prefix is a common marker for intentionally unused names. Skipping all underscore bindings can hide real dead code.

Used underscore bindings

Decision: keep

Source: deadnix --warn-used-underscore option. Status at collection time: inactive; current fixer does not pass --warn-used-underscore. Autofix: no.

Rule

Do not use a binding whose name starts with _. Rename it to a normal name when code references it.

Avoid:

let
  _package = pkgs.hello;
in
  _package

Use:

let
  package = pkgs.hello;
in
  package

Rationale

The underscore prefix communicates that the name is intentionally unused. Using it contradicts that signal.

Notes for Decisions

  • statix rules are mostly syntax rewrites.
  • deadnix rules need scope analysis and dead-code reachability.
  • deadnix does not report rec attribute-set bindings as removable findings.
  • statix repeated_keys is present in the installed rule list but disabled locally.
  • Rules that change module argument patterns need extra care around NixOS modules and callPackage functions.