affogato is a suite of tools for the Nix language, written in Java:

  • a formatter, a port of nixfmt that leaves all of nixpkgs unchanged;

  • a linter, with rules drawn from deadnix, statix and nixd, many with an automatic fix;

  • a syntax highlighter, which works from the lexer alone and so copes with syntax errors;

  • an evaluator on GraalVM, as a Truffle AST interpreter, which evaluates nixpkgs unmodified and computes store paths and .drv paths in memory, without writing anything to /nix/store;

  • a playground that runs the formatter, linter and highlighter in the browser, compiled to JavaScript.

Getting started

Build it with Nix, either on the JVM or as a static native binary (linked against musl), which starts in milliseconds rather than half a second:

nix build .#affogato-truffle          # on OpenJDK 25
nix build .#affogato-truffle-native   # native image, built with GraalVM

To format and lint only, affogato and affogato-native provide a affogato command with only format, check and fix, without the evaluator and Truffle.

Each package installs bash and zsh completions; affogato generate-completion prints the script, to source in a shell where it is not installed.

To try the formatter and linter in a browser, open the playground; they run in the page, compiled to JavaScript. To build it locally, then open result/index.html in a browser:

nix build .#playground

Evaluate an expression:

$ ./result/bin/affogato eval --strict -E 'builtins.map (x: x * 2) [ 1 2 3 ]'
[ 2 4 6 ]

Compute the derivation path of a nixpkgs attribute:

./result/bin/affogato drv-path -I nixpkgs=/path/to/nixpkgs -E 'import <nixpkgs> {}' -A hello

Command line

Each command has its own page, generated from the command line itself; -h or --help after any command prints the same.

Synopsis

affogato [-h] [COMMAND]

Options

-h, --help

Show this help and exit.

Commands

format

Format Nix files in place, as nixfmt does.

check

Report the linter’s findings in Nix files without changing them.

fix

Apply the linter’s fixes to Nix files in place, then format them.

eval

Evaluate an expression and print the result, like nix-instantiate --eval.

drv-path

Evaluate an expression to a derivation and print its drvPath.

treefmt

format and fix follow the treefmt formatter spec:

[formatter.affogato]
command = "affogato"
options = ["fix"]
includes = ["*.nix"]

Status

Checked against Nix 2.34.9 and nixpkgs 31ecbaca1bde on x86_64-linux.

Language tests

affogato passes NixOS/nix’s tests/functional/lang suite (303 tests) and Tvix’s eval/src/tests/tvix_tests, apart from the cases listed with reasons in language/src/test/java/lang-skip.txt.

Top-level packages

For every top-level nixpkgs attribute (scripts/toplevel-drvpaths.nix), affogato and nix-instantiate produce 24,268 identical drvPaths. 680 attributes fail in both, and none differ or fail in only one evaluator. The run takes 1 min 40 s on the JVM, against 2 min 08 s for Nix.

Heavier attributes

scripts/parity.sh finds identical drvPaths for firefox, chromium, libreoffice, texliveFull, haskellPackages.pandoc, nixosTests.firefox, pkgsCross.aarch64-multiplatform.hello and pkgsStatic.hello.

Not supported

  • Fetchers (fetchurl, fetchTarball, fetchGit, fetchTree) and flakes.

  • Import from derivation.

  • Content-addressed and impure derivations.

  • --xml output.

  • The --lint-* flags.

  • Rejection of NUL bytes in strings, and UTF-8 validation in toJSON.

Architecture

Each top-level folder is a Java module.

parser/

A hand-written lexer and parser. It builds a concrete syntax tree that keeps every token, then lowers it to the evaluator’s AST.

store/

NAR hashing, Nix base32 and store-path computation.

language/

The Truffle language:

  • Translator: static scope resolution into Truffle nodes.

  • nodes/ and runtime/: the evaluator and its values.

  • builtins/: the builtins.

  • corepkgs/: Nix’s derivation.nix and fetchurl.nix, vendored from NixOS/nix.

launcher/

The command line, built on the polyglot API, plus its native-image build.

format/

The formatter.

lint/

The linter.

Formatter

org.affogato.format.Formatter ports nixfmt 1.5.0 (MPL-2.0) to the parser’s concrete syntax tree. Formatter.format(src) formats with nixfmt’s defaults. format(src, width, indent, strict) sets the line width, the indentation and nixfmt’s strict mode.

Linter

org.affogato.lint.Linter runs checks in the spirit of deadnix (unused bindings), statix (simpler forms) and nixd (undefined variables, unknown builtins, unused with and rec). Linter.lint(src) returns the diagnostics, and Linter.fix(src) applies the fixes that come with most of them. Both take an optional set of Rule values to restrict the checks. Lint rules lists the rules.

Tests

language/src/test/java/

The evaluator’s test module. Its data are packaged with it, so the tests rerun whenever the data change.

  • lang/: NixOS/nix’s tests/functional/lang (LGPL-2.1).

  • tvix/: Tvix’s eval/src/tests/tvix_tests.

  • affogato/: affogato’s own regression tests.

format/src/test/java/nixfmt/

nixfmt’s own test suite.

lint/src/test/java/cases/

The linter’s cases.

The upstream suites are kept in sync with vendir. To follow a new Nix release, change ref in vendir.yml, run vendir sync, and update lang-symlinks.txt if upstream added symlinks (the jar cannot hold them).

Lint rules

Each rule shows a case from the linter’s tests: the original code, what the rule’s fixes make of it, and what the rule reports, with [fix] after the findings that come with a fix.

Rule Severity Description

syntax-error

❌ error

The code does not parse, or breaks a rule Nix checks while parsing, such as defining an attribute twice. No other rule runs then.

unused-let-binding

⚠️ warning

A let binding that the body never reaches. A binding used only by itself, or by other unused bindings, counts as unused too. The fix removes it.

unused-lambda-arg

⚠️ warning

A function argument that the body never uses. The fix renames it to , unless the body uses an outer .

unused-formal

⚠️ warning

An attribute of a function’s pattern that the body never uses. It is not reported when the @ binding is used, as that may pass it on. The fix removes it only when the pattern has ..., so that callers passing it still work.

unused-at-pattern

⚠️ warning

An @ binding that the body never uses. The fix removes it.

bool-comparison

⚠️ warning

A comparison with true or false. There is no fix: v == true is also a type test, where v alone fails on other types.

bool-simplification

⚠️ warning

!(a == b), which is a != b, and !(a != b), which is a == b.

empty-let-in

⚠️ warning

A let with no bindings.

collapsible-let-in

⚠️ warning

A let right in the body of another. The fix merges them unless the outer bindings use or bind a name of the inner ones.

legacy-let-syntax

⚠️ warning

The deprecated let { ...; body = ...; }. The fix turns it into rec { ...; body = ...; }.body.

empty-inherit

⚠️ warning

An inherit with no names.

empty-pattern

⚠️ warning

A function taking { }. There is no fix: _: would also accept arguments that are not an empty attribute set.

redundant-pattern-bind

⚠️ warning

{ ... }@x: body, which can be written x: body.

manual-inherit

⚠️ warning

a = a; in an attribute set that is not recursive, which is inherit a;.

manual-inherit-from

⚠️ warning

a = e.a;, which is inherit (e) a;.

eta-reduction

💡 hint

x: f x, which is f when f is a name or an attribute path that does not use x.

useless-parens

⚠️ warning

Parentheses around a simple expression, or around a whole binding value, let body, interpolation or file.

unquoted-uri

⚠️ warning

A URI literal, which is deprecated. The fix quotes it.

deprecated-is-null

⚠️ warning

isNull e, which is e == null.

deprecated-to-path

⚠️ warning

builtins.toPath, which is deprecated. There is no fix.

empty-list-concat

⚠️ warning

A concatenation with [ ].

useless-has-attr

⚠️ warning

if x ? a then x.a else d, which is x.a or d.

faster-builtin

💡 hint

lib.groupBy and lib.zipAttrsWith, which the builtins of the same names implement natively.

repeated-keys

💡 hint

Three or more bindings that start with the same attribute, such as a.b, a.c and a.d, which could form one nested attribute set. The fix moves them into a = { ... }; in place of the first, with the comments on their lines; a.b.c, a.b.d and a.b.e go into a.b = { ... };. It is withheld when the set also binds a whole, or when bindings over several lines do not each start a line.

undefined-variable

❌ error

A variable that no binding defines, outside any with. Nix rejects the file before evaluating it.

unknown-builtin

❌ error

builtins.x where x is not a builtin, unless the file tests builtins ? x or gives an or fallback.

prefixed-builtin

⚠️ warning

__x for the builtin builtins.x.

shadowed-builtin

⚠️ warning

A binding that hides a global such as map, true or builtins. Attributes of a rec set and names inherited from an expression are not reported.

extra-rec

⚠️ warning

A rec attribute set whose attributes never refer to each other.

extra-with

⚠️ warning

A with that no variable can come from.

missing-phase-hooks

⚠️ warning

A phase such as installPhase set to a string that does not run its hooks: nixpkgs expects runHook preInstall first and runHook postInstall last, so that preInstall and postInstall still work. The fix adds the missing calls to strings of several lines.

missing-final-attrs

⚠️ warning

A derivation built from a rec set whose attributes refer to each other, such as stdenv.mkDerivation rec { ... }. With finalAttrs: { ... }, an override of version through overrideAttrs also reaches the attributes that use it. The fix rewrites the references to finalAttrs.version and the like. For builders other than stdenv.mkDerivation, finalAttrs holds the attributes the builder made, so the fix is only offered when the references are to pname, version and src.

missing-structured-attrs

💡 hint

A derivation that does not set structuredAttrs, which nixpkgs expects to be true for new packages. The builders that default it to true, such as those for Python, are not reported. The fix adds structuredAttrs = true; after pname and version; it changes the build, since phases then see lists as Bash arrays, so check that the package still builds.

missing-strict-deps

💡 hint

A derivation that does not set strictDeps, which nixpkgs expects to be true for new packages. The builders that set it already, such as those for Python, Go and Rust, are not reported. The fix adds strictDeps = true; after pname and version, or under __structuredAttrs; it changes the build, since tools must then be in nativeBuildInputs and libraries in buildInputs, so check that the package still builds.

deprecated-replace

⚠️ warning

substituteInPlace and the other substitute functions called with --replace, which is deprecated and behaves as --replace-warn: a pattern that no longer matches only prints a warning. The fix uses --replace-fail, which makes such a build fail instead, so check that the package still builds.

sri-hash

⚠️ warning

A fetcher given sha256, where nixpkgs uses hash with an SRI hash such as sha256-…​=. The fix renames it and converts a literal in Nix base32, hex or base64; lib.fakeSha256 becomes lib.fakeHash. Fetchers that take only sha256, such as fetchHex, are not reported.

env-variable

⚠️ warning

A NIX_ variable such as NIX_CFLAGS_COMPILE set beside the attributes of a derivation: with __structuredAttrs, only env is exported, so the compiler wrapper would not see it. The fix moves it to env, through toString unless it is a string already. It is withheld when env is not an attribute set written in place, or the file refers to the variable elsewhere, as finalAttrs.NIX_LDFLAGS.

Syntax errors

syntax-error

Severity: ❌ error. The code does not parse, or breaks a rule Nix checks while parsing, such as defining an attribute twice. No other rule runs then.

Original
{
  a = 1;
  a = 2;
}
Diagnostics
syntax-error.nix:3:3: error[syntax-error]: attribute 'a' already defined

Dead code

Bindings that nothing uses, as deadnix finds them. Names starting with _ are never reported.

unused-let-binding

Severity: ⚠️ warning. A let binding that the body never reaches. A binding used only by itself, or by other unused bindings, counts as unused too. The fix removes it.

Original
{ f }:
let
  used = 1;
  unused = 2;
  self = x: self x;
  a = b;
  b = a;
  _ignored = 3;
  inherit (f) kept dropped;
  inherit (f) gone;
  nested.x = 1;
  nested.y = 2;
  # Only `dead` is reported: the uses of `ver` and `pkg` are inside it too.
  dead = ver: pkg: pkg.${ver};
in
[
  used
  kept
]
Fixed
{ f }:
let
  used = 1;
  _ignored = 3;
  inherit (f) kept;
  # Only `dead` is reported: the uses of `ver` and `pkg` are inside it too.
in
[
  used
  kept
]
Diagnostics
unused-let-binding.nix:4:3: warning[unused-let-binding]: unused let binding 'unused' [fix]
unused-let-binding.nix:5:3: warning[unused-let-binding]: unused let binding 'self' [fix]
unused-let-binding.nix:6:3: warning[unused-let-binding]: unused let binding 'a' [fix]
unused-let-binding.nix:7:3: warning[unused-let-binding]: unused let binding 'b' [fix]
unused-let-binding.nix:9:20: warning[unused-let-binding]: unused let binding 'dropped' [fix]
unused-let-binding.nix:10:15: warning[unused-let-binding]: unused let binding 'gone' [fix]
unused-let-binding.nix:11:3: warning[unused-let-binding]: unused let binding 'nested' [fix]
unused-let-binding.nix:14:3: warning[unused-let-binding]: unused let binding 'dead' [fix]

unused-lambda-arg

Severity: ⚠️ warning. A function argument that the body never uses. The fix renames it to , unless the body uses an outer .

Original
let
  ignored = x: 1;
  underscore = _x: 1;
  used = x: x;
  # Renaming `y` to `_` would capture the outer `_`.
  capture = _: y: _;
in
[
  ignored
  underscore
  used
  capture
]
Fixed
let
  ignored = _: 1;
  underscore = _x: 1;
  used = x: x;
  # Renaming `y` to `_` would capture the outer `_`.
  capture = _: y: _;
in
[
  ignored
  underscore
  used
  capture
]
Diagnostics
unused-lambda-arg.nix:2:13: warning[unused-lambda-arg]: unused lambda argument 'x' [fix]
unused-lambda-arg.nix:6:16: warning[unused-lambda-arg]: unused lambda argument 'y'

unused-formal

Severity: ⚠️ warning. An attribute of a function’s pattern that the body never uses. It is not reported when the @ binding is used, as that may pass it on. The fix removes it only when the pattern has ..., so that callers passing it still work.

Original
[
  ({ a, b, ... }: a)
  # Without `...`, removing `b` would change which arguments are accepted.
  ({ a, b }: a)
  # `args` may pass `b` on.
  ({ a, b, ... }@args: args)
  ({ a, b ? a, ... }: b)
  (
    {
      a,
      b,
      c,
      ...
    }:
    c
  )
]
Fixed
[
  ({ a, ... }: a)
  # Without `...`, removing `b` would change which arguments are accepted.
  ({ a, b }: a)
  # `args` may pass `b` on.
  ({ a, b, ... }@args: args)
  ({ a, b ? a, ... }: b)
  (
    {
      c,
      ...
    }:
    c
  )
]
Diagnostics
unused-formal.nix:2:9: warning[unused-formal]: unused formal argument 'b' [fix]
unused-formal.nix:4:9: warning[unused-formal]: unused formal argument 'b'
unused-formal.nix:10:7: warning[unused-formal]: unused formal argument 'a' [fix]
unused-formal.nix:11:7: warning[unused-formal]: unused formal argument 'b' [fix]

unused-at-pattern

Severity: ⚠️ warning. An @ binding that the body never uses. The fix removes it.

Original
[
  (args@{ a, ... }: a)
  ({ a, ... } @ args: a)
  (args@{ a, ... }: args)
]
Fixed
[
  ({ a, ... }: a)
  ({ a, ... }: a)
  (args@{ a, ... }: args)
]
Diagnostics
unused-at-pattern.nix:2:4: warning[unused-at-pattern]: unused pattern binding 'args' [fix]
unused-at-pattern.nix:3:17: warning[unused-at-pattern]: unused pattern binding 'args' [fix]

Simpler forms

Code with a simpler or more idiomatic form, as statix finds it.

bool-comparison

Severity: ⚠️ warning. A comparison with true or false. There is no fix: v == true is also a type test, where v alone fails on other types.

Original
{ x, y }:
[
  (x == true)
  (false != y)
  (x == y)
]
Diagnostics
bool-comparison.nix:3:4: warning[bool-comparison]: comparison with 'true'; if the other side is always a boolean, use it directly
bool-comparison.nix:4:4: warning[bool-comparison]: comparison with 'false'; if the other side is always a boolean, use it directly

bool-simplification

Severity: ⚠️ warning. !(a == b), which is a != b, and !(a != b), which is a == b.

Original
{ a, b }:
[
  (!(a == b))
  (!(a != b) || a)
  (!(a && b))
]
Fixed
{ a, b }:
[
  (a != b)
  (a == b || a)
  (!(a && b))
]
Diagnostics
bool-simplification.nix:3:4: warning[bool-simplification]: negated comparison; use '!=' [fix]
bool-simplification.nix:4:4: warning[bool-simplification]: negated comparison; use '==' [fix]

empty-let-in

Severity: ⚠️ warning. A let with no bindings.

Original
let
in
{
  a = let in 1;
}
Fixed
{
  a = 1;
}
Diagnostics
empty-let-in.nix:1:1: warning[empty-let-in]: 'let' with no bindings [fix]
empty-let-in.nix:4:7: warning[empty-let-in]: 'let' with no bindings [fix]

collapsible-let-in

Severity: ⚠️ warning. A let right in the body of another. The fix merges them unless the outer bindings use or bind a name of the inner ones.

Original
{
  merged =
    let
      a = 1;
    in
    let
      b = a;
    in
    b;
  # Merged, the inner `b` would replace the argument in `a = b;`.
  capture =
    b:
    let
      a = b;
    in
    let
      b = 2;
    in
    a + b;
}
Fixed
{
  merged =
    let
      a = 1;
      b = a;
    in
    b;
  # Merged, the inner `b` would replace the argument in `a = b;`.
  capture =
    b:
    let
      a = b;
    in
    let
      b = 2;
    in
    a + b;
}
Diagnostics
collapsible-let-in.nix:6:5: warning[collapsible-let-in]: this 'let' can be merged into the outer one [fix]
collapsible-let-in.nix:16:5: warning[collapsible-let-in]: this 'let' can be merged into the outer one

legacy-let-syntax

Severity: ⚠️ warning. The deprecated let { ...; body = ...; }. The fix turns it into rec { ...; body = ...; }.body.

Original
let {
  x = 1;
  body = x;
}
Fixed
rec {
  x = 1;
  body = x;
}.body
Diagnostics
legacy-let-syntax.nix:1:1: warning[legacy-let-syntax]: legacy 'let { }' syntax; use 'let ... in' [fix]

empty-inherit

Severity: ⚠️ warning. An inherit with no names.

Original
{
  a = let inherit; in 1;
  inherit;
  b = 2;
}
Fixed
{
  a = let in 1;
  b = 2;
}
Diagnostics
empty-inherit.nix:2:11: warning[empty-inherit]: 'inherit' with no names [fix]
empty-inherit.nix:3:3: warning[empty-inherit]: 'inherit' with no names [fix]

empty-pattern

Severity: ⚠️ warning. A function taking { }. There is no fix: _: would also accept arguments that are not an empty attribute set.

Original
[
  ({ }: 1)
  ({ ... }: 1)
]
Diagnostics
empty-pattern.nix:2:4: warning[empty-pattern]: empty pattern; use '_' if the argument is not needed

redundant-pattern-bind

Severity: ⚠️ warning. { ... }@x: body, which can be written x: body.

Original
[
  ({ ... }@args: args)
  (args @ { ... }: args)
  ({ a, ... }@args: args)
]
Fixed
[
  (args: args)
  (args: args)
  ({ a, ... }@args: args)
]
Diagnostics
redundant-pattern-bind.nix:2:4: warning[redundant-pattern-bind]: '{ ... }@args' is the same as 'args' [fix]
redundant-pattern-bind.nix:3:4: warning[redundant-pattern-bind]: '{ ... }@args' is the same as 'args' [fix]

manual-inherit

Severity: ⚠️ warning. a = a; in an attribute set that is not recursive, which is inherit a;.

Original
{ a, b }:
{
  plain = {
    a = a;
  };
  # In a `rec`, `b = b;` refers to itself.
  recursive = rec {
    b = b;
  };
}
Fixed
{ a, b }:
{
  plain = {
    inherit a;
  };
  # In a `rec`, `b = b;` refers to itself.
  recursive = rec {
    b = b;
  };
}
Diagnostics
manual-inherit.nix:4:5: warning[manual-inherit]: 'a = a;' can be written 'inherit a;' [fix]

manual-inherit-from

Severity: ⚠️ warning. a = e.a;, which is inherit (e) a;.

Original
{ pkgs, lib }:
{
  hello = pkgs.hello;
  name = pkgs.stdenv.hostPlatform.name;
  options = (lib.evalModules { modules = [ ]; }).options;
  lib = lib.lib;
  other = pkgs.notOther;
  fallback = pkgs.fallback or null;
}
Fixed
{ pkgs, lib }:
{
  inherit (pkgs) hello;
  inherit (pkgs.stdenv.hostPlatform) name;
  inherit (lib.evalModules { modules = [ ]; }) options;
  inherit (lib) lib;
  other = pkgs.notOther;
  fallback = pkgs.fallback or null;
}
Diagnostics
manual-inherit-from.nix:3:3: warning[manual-inherit-from]: can be written 'inherit (...) hello;' [fix]
manual-inherit-from.nix:4:3: warning[manual-inherit-from]: can be written 'inherit (...) name;' [fix]
manual-inherit-from.nix:5:3: warning[manual-inherit-from]: can be written 'inherit (...) options;' [fix]
manual-inherit-from.nix:6:3: warning[manual-inherit-from]: can be written 'inherit (...) lib;' [fix]

eta-reduction

Severity: 💡 hint. x: f x, which is f when f is a name or an attribute path that does not use x.

Original
{ f, lib }:
[
  (x: f x)
  (x: lib.length x)
  (x: f x x)
  # Partial applications are kept.
  (x: lib.map f x)
  (map (x: f x) [ ])
]
Fixed
{ f, lib }:
[
  (f)
  (lib.length)
  (x: f x x)
  # Partial applications are kept.
  (x: lib.map f x)
  (map (f) [ ])
]
Diagnostics
eta-reduction.nix:3:4: hint[eta-reduction]: 'x: f x' can be written 'f' [fix]
eta-reduction.nix:4:4: hint[eta-reduction]: 'x: f x' can be written 'f' [fix]
eta-reduction.nix:8:9: hint[eta-reduction]: 'x: f x' can be written 'f' [fix]

useless-parens

Severity: ⚠️ warning. Parentheses around a simple expression, or around a whole binding value, let body, interpolation or file.

Original
{ f, a }:
{
  value = (f a);
  atom = f (a);
  select = (a.b).c;
  fallback = f (a.b or null);
  call = (f a) a;
  interpolation = "${(f a)}";
  string = f ("x");
  nested = f ((a));
  tight = f(a);
  tighter = f((a));
  path = f(./x);
  operator = (a + a) * 2;
}
Fixed
{ f, a }:
{
  value = f a;
  atom = f a;
  select = a.b.c;
  fallback = f (a.b or null);
  call = (f a) a;
  interpolation = "${f a}";
  string = f "x";
  nested = f a;
  tight = f a;
  tighter = f a;
  path = f ./x;
  operator = (a + a) * 2;
}
Diagnostics
useless-parens.nix:3:11: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:4:12: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:5:12: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:8:22: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:9:14: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:10:15: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:11:12: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:12:15: warning[useless-parens]: useless parentheses [fix]
useless-parens.nix:13:11: warning[useless-parens]: useless parentheses [fix]

unquoted-uri

Severity: ⚠️ warning. A URI literal, which is deprecated. The fix quotes it.

Original
{
  url = https://example.org/x?y=1;
}
Fixed
{
  url = "https://example.org/x?y=1";
}
Diagnostics
unquoted-uri.nix:2:9: warning[unquoted-uri]: unquoted URI; URI literals are deprecated [fix]

deprecated-is-null

Severity: ⚠️ warning. isNull e, which is e == null.

Original
{ x }:
[
  (isNull x)
  (builtins.isNull x)
  (!isNull x)
]
Fixed
{ x }:
[
  (x == null)
  (x == null)
  (!(x == null))
]
Diagnostics
deprecated-is-null.nix:3:4: warning[deprecated-is-null]: 'isNull' is deprecated; use '== null' [fix]
deprecated-is-null.nix:4:4: warning[deprecated-is-null]: 'isNull' is deprecated; use '== null' [fix]
deprecated-is-null.nix:5:5: warning[deprecated-is-null]: 'isNull' is deprecated; use '== null' [fix]

deprecated-to-path

Severity: ⚠️ warning. builtins.toPath, which is deprecated. There is no fix.

Original
{ x }:
[
  (builtins.toPath x)
  (__toPath x)
]
Diagnostics
deprecated-to-path.nix:3:4: warning[deprecated-to-path]: 'builtins.toPath' is deprecated
deprecated-to-path.nix:4:4: warning[deprecated-to-path]: '__toPath' is deprecated

empty-list-concat

Severity: ⚠️ warning. A concatenation with [ ].

Original
{ x }:
[
  ([ ] ++ x)
  (x ++ [ ])
  ([ ] ++ [ ])
]
Fixed
{ x }:
[
  (x)
  (x)
  ([ ])
]
Diagnostics
empty-list-concat.nix:3:4: warning[empty-list-concat]: concatenation with an empty list [fix]
empty-list-concat.nix:4:4: warning[empty-list-concat]: concatenation with an empty list [fix]
empty-list-concat.nix:5:4: warning[empty-list-concat]: concatenation with an empty list [fix]

useless-has-attr

Severity: ⚠️ warning. if x ? a then x.a else d, which is x.a or d.

Original
{ x, d }:
[
  (if x ? a then x.a else d)
  (if x.y ? a.b then x.y.a.b else d + 1)
  (if x ? a then x.b else d)
]
Fixed
{ x, d }:
[
  (x.a or d)
  (x.y.a.b or (d + 1))
  (if x ? a then x.b else d)
]
Diagnostics
useless-has-attr.nix:3:4: warning[useless-has-attr]: can be written 'x.a or ...' [fix]
useless-has-attr.nix:4:4: warning[useless-has-attr]: can be written 'x.y.a.b or ...' [fix]

faster-builtin

Severity: 💡 hint. lib.groupBy and lib.zipAttrsWith, which the builtins of the same names implement natively.

Original
{ lib }:
[
  lib.groupBy
  lib.attrsets.zipAttrsWith
  lib.mapAttrs
  (builtins: lib.groupBy)
]
Fixed
{ lib }:
[
  builtins.groupBy
  builtins.zipAttrsWith
  lib.mapAttrs
  (builtins: lib.groupBy)
]
Diagnostics
faster-builtin.nix:3:3: hint[faster-builtin]: 'lib.groupBy' can be 'builtins.groupBy' [fix]
faster-builtin.nix:4:3: hint[faster-builtin]: 'lib.attrsets.zipAttrsWith' can be 'builtins.zipAttrsWith' [fix]
faster-builtin.nix:6:14: hint[faster-builtin]: 'lib.groupBy' can be 'builtins.groupBy'

repeated-keys

Severity: 💡 hint. Three or more bindings that start with the same attribute, such as a.b, a.c and a.d, which could form one nested attribute set. The fix moves them into a = { ... }; in place of the first, with the comments on their lines; a.b.c, a.b.d and a.b.e go into a.b = { ... };. It is withheld when the set also binds a whole, or when bindings over several lines do not each start a line.

Original
{
  a.b = 1;
  a.c = 2; # This comment moves with its binding.
  x = 0;

  # So does this one, above its binding.
  a.d = {
    e = ''
      kept
    '';
    # Its second line is part of the string, so it stays.
    f = "not
    moved";
  };

  f = { g.h = 1; g.i = 2; g.j = 3; };

  # Grouped under the prefix they share.
  k.l.m = 1;
  k.l.n = 2;
  k.l.o = 3;

  # Not fixed: `p` is also set whole.
  p = { };
  p.q = 1;
  p.r = 2;
  p.s = 3;

  w.a = 1;

  w.b = 2;

  w.c = 3;
  y = 0;

  t.u = 1;
  t.v = 2;
}
Fixed
{
  a = {
    b = 1;
    c = 2; # This comment moves with its binding.

    # So does this one, above its binding.
    d = {
      e = ''
        kept
      '';
      # Its second line is part of the string, so it stays.
      f = "not
    moved";
    };
  };
  x = 0;

  f = { g = { h = 1; i = 2; j = 3; }; };

  # Grouped under the prefix they share.
  k.l = {
    m = 1;
    n = 2;
    o = 3;
  };

  # Not fixed: `p` is also set whole.
  p = { };
  p.q = 1;
  p.r = 2;
  p.s = 3;

  w = {
    a = 1;

    b = 2;

    c = 3;
  };
  y = 0;

  t.u = 1;
  t.v = 2;
}
Diagnostics
repeated-keys.nix:2:3: hint[repeated-keys]: 'a' is set 3 times; group its attributes in one attribute set [fix]
repeated-keys.nix:16:9: hint[repeated-keys]: 'g' is set 3 times; group its attributes in one attribute set [fix]
repeated-keys.nix:19:3: hint[repeated-keys]: 'k.l' is set 3 times; group its attributes in one attribute set [fix]
repeated-keys.nix:25:3: hint[repeated-keys]: 'p' is set 3 times; group its attributes in one attribute set
repeated-keys.nix:29:3: hint[repeated-keys]: 'w' is set 3 times; group its attributes in one attribute set [fix]

Semantics

Checks that need name resolution, as nixd makes them.

undefined-variable

Severity: ❌ error. A variable that no binding defines, outside any with. Nix rejects the file before evaluating it.

Original
{ known }:
[
  known
  unknown
  true
  __curPos
  <nixpkgs>
  # `with` may bind it.
  (with known; maybe)
]
Diagnostics
undefined-variable.nix:4:3: error[undefined-variable]: undefined variable 'unknown'

unknown-builtin

Severity: ❌ error. builtins.x where x is not a builtin, unless the file tests builtins ? x or gives an or fallback.

Original
[
  builtins.map
  builtins.mapAttrs'
  (builtins.fetchClosure or null)
  # Tested first, so the file works where Nix has it.
  (if builtins ? getFlakeAttr then builtins.getFlakeAttr else null)
  (
    let
      builtins = { };
    in
    builtins.custom
  )
]
Diagnostics
unknown-builtin.nix:3:3: error[unknown-builtin]: unknown builtin 'builtins.mapAttrs''

prefixed-builtin

Severity: ⚠️ warning. __x for the builtin builtins.x.

Original
[
  (__add 1 2)
  __curPos
  (
    let
      builtins = { };
    in
    __sub builtins 1
  )
]
Fixed
[
  (builtins.add 1 2)
  __curPos
  (
    let
      builtins = { };
    in
    __sub builtins 1
  )
]
Diagnostics
prefixed-builtin.nix:2:4: warning[prefixed-builtin]: '__add' is 'builtins.add' [fix]
prefixed-builtin.nix:8:5: warning[prefixed-builtin]: '__sub' is 'builtins.sub'

shadowed-builtin

Severity: ⚠️ warning. A binding that hides a global such as map, true or builtins. Attributes of a rec set and names inherited from an expression are not reported.

Original
{ map, lib }:
let
  inherit (lib) toString;
  true = false;
in
rec {
  import = 1;
  x = [
    map
    toString
    true
    import
  ];
}
Diagnostics
shadowed-builtin.nix:1:3: warning[shadowed-builtin]: 'map' shadows a builtin
shadowed-builtin.nix:4:3: warning[shadowed-builtin]: 'true' shadows a builtin

extra-rec

Severity: ⚠️ warning. A rec attribute set whose attributes never refer to each other.

Original
{
  plain = rec {
    a = 1;
    b = 2;
  };
  used = rec {
    a = 1;
    b = a;
  };
}
Fixed
{
  plain = {
    a = 1;
    b = 2;
  };
  used = rec {
    a = 1;
    b = a;
  };
}
Diagnostics
extra-rec.nix:2:11: warning[extra-rec]: 'rec' is not needed: no attribute refers to another [fix]

extra-with

Severity: ⚠️ warning. A with that no variable can come from.

Original
{ lib, pkgs }:
{
  unused = with lib; [ ];
  used = with pkgs; [ hello ];
  # Either may bind `hello`.
  both = with lib; with pkgs; [ hello ];
  shadowed = x: with lib; x;
}
Fixed
{ lib, pkgs }:
{
  unused = [ ];
  used = with pkgs; [ hello ];
  # Either may bind `hello`.
  both = with lib; with pkgs; [ hello ];
  shadowed = x: x;
}
Diagnostics
extra-with.nix:3:12: warning[extra-with]: unused 'with': no variable comes from it [fix]
extra-with.nix:7:17: warning[extra-with]: unused 'with': no variable comes from it [fix]

Packaging

Conventions of nixpkgs for derivations.

missing-phase-hooks

Severity: ⚠️ warning. A phase such as installPhase set to a string that does not run its hooks: nixpkgs expects runHook preInstall first and runHook postInstall last, so that preInstall and postInstall still work. The fix adds the missing calls to strings of several lines.

Original
{ stdenv }:
stdenv.mkDerivation {
  pname = "example";
  version = "1.0";

  installPhase = ''
    mkdir -p $out/bin
    cp example $out/bin/
  '';

  # Only the missing call is added.
  buildPhase = ''
    runHook preBuild
    make
  '';

  checkPhase = ''
    runHook preCheck
    make check
    runHook postCheck
  '';

  # Not fixed: the calls need lines of their own.
  configurePhase = "./configure --prefix=$out";
  fixupPhase = ''true'';
}
Fixed
{ stdenv }:
stdenv.mkDerivation {
  pname = "example";
  version = "1.0";

  installPhase = ''
    runHook preInstall

    mkdir -p $out/bin
    cp example $out/bin/

    runHook postInstall
  '';

  # Only the missing call is added.
  buildPhase = ''
    runHook preBuild
    make

    runHook postBuild
  '';

  checkPhase = ''
    runHook preCheck
    make check
    runHook postCheck
  '';

  # Not fixed: the calls need lines of their own.
  configurePhase = "./configure --prefix=$out";
  fixupPhase = ''true'';
}
Diagnostics
missing-phase-hooks.nix:6:3: warning[missing-phase-hooks]: 'installPhase' does not run 'runHook preInstall' and 'runHook postInstall' [fix]
missing-phase-hooks.nix:12:3: warning[missing-phase-hooks]: 'buildPhase' does not run 'runHook postBuild' [fix]
missing-phase-hooks.nix:24:3: warning[missing-phase-hooks]: 'configurePhase' does not run 'runHook preConfigure' and 'runHook postConfigure'
missing-phase-hooks.nix:25:3: warning[missing-phase-hooks]: 'fixupPhase' does not run 'runHook preFixup' and 'runHook postFixup'

missing-final-attrs

Severity: ⚠️ warning. A derivation built from a rec set whose attributes refer to each other, such as stdenv.mkDerivation rec { ... }. With finalAttrs: { ... }, an override of version through overrideAttrs also reaches the attributes that use it. The fix rewrites the references to finalAttrs.version and the like. For builders other than stdenv.mkDerivation, finalAttrs holds the attributes the builder made, so the fix is only offered when the references are to pname, version and src.

Original
{
  stdenv,
  fetchurl,
  buildPythonPackage,
  fetchPypi,
  lib,
}:
{
  hello = stdenv.mkDerivation rec {
    pname = "hello";
    version = "2.12";
    src = fetchurl {
      url = "mirror://gnu/hello/${pname}-${version}.tar.gz";
      hash = "sha256-AAAA";
    };
    passthru = {
      inherit lib version;
    };
    meta.changelog = "https://example.org/${version}";
  };

  # Other builders give `finalAttrs` what they made of `meta`, so only `pname`, `version` and `src` are rewritten.
  python = buildPythonPackage rec {
    pname = "example";
    version = "1.0";
    src = fetchPypi { inherit pname version; };
    passthru.homepage = meta.homepage;
    meta.homepage = "https://example.org";
  };

  fixable = buildPythonPackage rec {
    pname = "other";
    version = "2.0";
    src = fetchPypi { inherit pname version; };
  };

  # Not fixed: the inner `finalAttrs` would capture `pname` and `version`.
  nested = stdenv.mkDerivation rec {
    pname = "nested";
    version = "1.0";
    modules = stdenv.mkDerivation (finalAttrs: {
      pname = "${pname}-modules";
      inherit version;
    });
  };

  # Left to `extra-rec`: no attribute refers to another.
  plain = stdenv.mkDerivation rec {
    pname = "plain";
  };
}
Fixed
{
  stdenv,
  fetchurl,
  buildPythonPackage,
  fetchPypi,
  lib,
}:
{
  hello = stdenv.mkDerivation (finalAttrs: {
    pname = "hello";
    version = "2.12";
    src = fetchurl {
      url = "mirror://gnu/hello/${finalAttrs.pname}-${finalAttrs.version}.tar.gz";
      hash = "sha256-AAAA";
    };
    passthru = {
      inherit lib; inherit (finalAttrs) version;
    };
    meta.changelog = "https://example.org/${finalAttrs.version}";
  });

  # Other builders give `finalAttrs` what they made of `meta`, so only `pname`, `version` and `src` are rewritten.
  python = buildPythonPackage rec {
    pname = "example";
    version = "1.0";
    src = fetchPypi { inherit pname version; };
    passthru.homepage = meta.homepage;
    meta.homepage = "https://example.org";
  };

  fixable = buildPythonPackage (finalAttrs: {
    pname = "other";
    version = "2.0";
    src = fetchPypi { inherit (finalAttrs) pname version; };
  });

  # Not fixed: the inner `finalAttrs` would capture `pname` and `version`.
  nested = stdenv.mkDerivation rec {
    pname = "nested";
    version = "1.0";
    modules = stdenv.mkDerivation (finalAttrs: {
      pname = "${pname}-modules";
      inherit version;
    });
  };

  # Left to `extra-rec`: no attribute refers to another.
  plain = stdenv.mkDerivation rec {
    pname = "plain";
  };
}
Diagnostics
missing-final-attrs.nix:9:31: warning[missing-final-attrs]: use 'finalAttrs: { ... }' instead of 'rec { ... }' [fix]
missing-final-attrs.nix:23:31: warning[missing-final-attrs]: use 'finalAttrs: { ... }' instead of 'rec { ... }'
missing-final-attrs.nix:31:32: warning[missing-final-attrs]: use 'finalAttrs: { ... }' instead of 'rec { ... }' [fix]
missing-final-attrs.nix:38:32: warning[missing-final-attrs]: use 'finalAttrs: { ... }' instead of 'rec { ... }'

missing-structured-attrs

Severity: 💡 hint. A derivation that does not set structuredAttrs, which nixpkgs expects to be true for new packages. The builders that default it to true, such as those for Python, are not reported. The fix adds structuredAttrs = true; after pname and version; it changes the build, since phases then see lists as Bash arrays, so check that the package still builds.

Original
{
  stdenv,
  buildGoModule,
  buildPythonPackage,
  buildHomeAssistantComponent,
  pname,
  version,
}:
{
  plain = stdenv.mkDerivation {
    pname = "plain";
    version = "1.0";

    strictDeps = true;
  };

  final = stdenv.mkDerivation (finalAttrs: {
    inherit pname version;
    meta.description = "${finalAttrs.pname} example";
  });

  go = buildGoModule {
    # The comment stays with `src`.
    src = ./.;
    vendorHash = null;
  };

  set = stdenv.mkDerivation {
    pname = "set";
    __structuredAttrs = true;
  };

  # Deliberately off.
  off = stdenv.mkDerivation {
    pname = "off";
    __structuredAttrs = false;
  };

  # Defaults to `true`.
  python = buildPythonPackage {
    inherit pname version;
  };

  # So does Home Assistant's builder.
  component = buildHomeAssistantComponent {
    inherit pname version;
  };
}
Fixed
{
  stdenv,
  buildGoModule,
  buildPythonPackage,
  buildHomeAssistantComponent,
  pname,
  version,
}:
{
  plain = stdenv.mkDerivation {
    pname = "plain";
    version = "1.0";

    strictDeps = true;
    __structuredAttrs = true;
  };

  final = stdenv.mkDerivation (finalAttrs: {
    inherit pname version;

    __structuredAttrs = true;
    meta.description = "${finalAttrs.pname} example";
  });

  go = buildGoModule {
    __structuredAttrs = true;

    # The comment stays with `src`.
    src = ./.;
    vendorHash = null;
  };

  set = stdenv.mkDerivation {
    pname = "set";
    __structuredAttrs = true;
  };

  # Deliberately off.
  off = stdenv.mkDerivation {
    pname = "off";
    __structuredAttrs = false;
  };

  # Defaults to `true`.
  python = buildPythonPackage {
    inherit pname version;
  };

  # So does Home Assistant's builder.
  component = buildHomeAssistantComponent {
    inherit pname version;
  };
}
Diagnostics
missing-structured-attrs.nix:10:11: hint[missing-structured-attrs]: set '__structuredAttrs = true;' [fix]
missing-structured-attrs.nix:17:11: hint[missing-structured-attrs]: set '__structuredAttrs = true;' [fix]
missing-structured-attrs.nix:22:8: hint[missing-structured-attrs]: set '__structuredAttrs = true;' [fix]

missing-strict-deps

Severity: 💡 hint. A derivation that does not set strictDeps, which nixpkgs expects to be true for new packages. The builders that set it already, such as those for Python, Go and Rust, are not reported. The fix adds strictDeps = true; after pname and version, or under __structuredAttrs; it changes the build, since tools must then be in nativeBuildInputs and libraries in buildInputs, so check that the package still builds.

Original
{
  stdenv,
  buildGoModule,
  buildDotnetModule,
  pname,
  version,
}:
{
  # The two fixes put both flags together.
  both = stdenv.mkDerivation {
    pname = "both";
    version = "1.0";

    src = ./.;
  };

  structured = stdenv.mkDerivation (finalAttrs: {
    inherit pname version;

    __structuredAttrs = true;

    meta.description = "${finalAttrs.pname} example";
  });

  set = stdenv.mkDerivation {
    pname = "set";
    __structuredAttrs = true;
    strictDeps = true;
  };

  # Deliberately off.
  off = stdenv.mkDerivation {
    pname = "off";
    __structuredAttrs = true;
    strictDeps = false;
  };

  # Go's builder sets it already.
  go = buildGoModule {
    __structuredAttrs = true;
    src = ./.;
  };

  dotnet = buildDotnetModule {
    inherit pname version;
    __structuredAttrs = true;
  };
}
Fixed
{
  stdenv,
  buildGoModule,
  buildDotnetModule,
  pname,
  version,
}:
{
  # The two fixes put both flags together.
  both = stdenv.mkDerivation {
    pname = "both";
    version = "1.0";

    strictDeps = true;

    src = ./.;
  };

  structured = stdenv.mkDerivation (finalAttrs: {
    inherit pname version;

    __structuredAttrs = true;
    strictDeps = true;

    meta.description = "${finalAttrs.pname} example";
  });

  set = stdenv.mkDerivation {
    pname = "set";
    __structuredAttrs = true;
    strictDeps = true;
  };

  # Deliberately off.
  off = stdenv.mkDerivation {
    pname = "off";
    __structuredAttrs = true;
    strictDeps = false;
  };

  # Go's builder sets it already.
  go = buildGoModule {
    __structuredAttrs = true;
    src = ./.;
  };

  dotnet = buildDotnetModule {
    inherit pname version;
    __structuredAttrs = true;
    strictDeps = true;
  };
}
Diagnostics
missing-strict-deps.nix:10:10: hint[missing-strict-deps]: set 'strictDeps = true;' [fix]
missing-strict-deps.nix:17:16: hint[missing-strict-deps]: set 'strictDeps = true;' [fix]
missing-strict-deps.nix:44:12: hint[missing-strict-deps]: set 'strictDeps = true;' [fix]

deprecated-replace

Severity: ⚠️ warning. substituteInPlace and the other substitute functions called with --replace, which is deprecated and behaves as --replace-warn: a pattern that no longer matches only prints a warning. The fix uses --replace-fail, which makes such a build fail instead, so check that the package still builds.

Original
{ stdenv, python3 }:
stdenv.mkDerivation {
  pname = "example";
  version = "1.0";

  __structuredAttrs = true;
  strictDeps = true;

  postPatch = ''
    # Don't let this apostrophe open a quote, nor this one: "''${out}".
    substituteInPlace Makefile --replace /usr/bin/env env
    substituteInPlace $(pwd)/config.mk --replace gcc cc
    substituteInPlace setup.py \
      --replace-fail "version='0'" "version='1.0'" \
      --replace "python" "${python3.interpreter}"
    substitute config.in config --replace @out@ $out
    # Quotes and interpolations over several lines are part of the same call.
    substituteInPlace tagtime.pl \
      --replace-fail '$logf = 1;' \
        '$logf = 2;
    $more = 3;' \
      --replace '"__ED__"' '"vi"'
    substituteInPlace latex.py --replace-fail "pdflatex" "${
      python3
    }/bin/pdflatex" \
      --replace "pdf2svg" "x"

    # Not a substitute call.
    patchelf --replace-needed libfoo.so libbar.so example
    ./configure --replace
  '';
}
Fixed
{ stdenv, python3 }:
stdenv.mkDerivation {
  pname = "example";
  version = "1.0";

  __structuredAttrs = true;
  strictDeps = true;

  postPatch = ''
    # Don't let this apostrophe open a quote, nor this one: "''${out}".
    substituteInPlace Makefile --replace-fail /usr/bin/env env
    substituteInPlace $(pwd)/config.mk --replace-fail gcc cc
    substituteInPlace setup.py \
      --replace-fail "version='0'" "version='1.0'" \
      --replace-fail "python" "${python3.interpreter}"
    substitute config.in config --replace-fail @out@ $out
    # Quotes and interpolations over several lines are part of the same call.
    substituteInPlace tagtime.pl \
      --replace-fail '$logf = 1;' \
        '$logf = 2;
    $more = 3;' \
      --replace-fail '"__ED__"' '"vi"'
    substituteInPlace latex.py --replace-fail "pdflatex" "${
      python3
    }/bin/pdflatex" \
      --replace-fail "pdf2svg" "x"

    # Not a substitute call.
    patchelf --replace-needed libfoo.so libbar.so example
    ./configure --replace
  '';
}
Diagnostics
deprecated-replace.nix:11:32: warning[deprecated-replace]: '--replace' is deprecated; use '--replace-fail' [fix]
deprecated-replace.nix:12:40: warning[deprecated-replace]: '--replace' is deprecated; use '--replace-fail' [fix]
deprecated-replace.nix:15:7: warning[deprecated-replace]: '--replace' is deprecated; use '--replace-fail' [fix]
deprecated-replace.nix:16:33: warning[deprecated-replace]: '--replace' is deprecated; use '--replace-fail' [fix]
deprecated-replace.nix:22:7: warning[deprecated-replace]: '--replace' is deprecated; use '--replace-fail' [fix]
deprecated-replace.nix:26:7: warning[deprecated-replace]: '--replace' is deprecated; use '--replace-fail' [fix]

sri-hash

Severity: ⚠️ warning. A fetcher given sha256, where nixpkgs uses hash with an SRI hash such as sha256-…​=. The fix renames it and converts a literal in Nix base32, hex or base64; lib.fakeSha256 becomes lib.fakeHash. Fetchers that take only sha256, such as fetchHex, are not reported.

Original
{
  lib,
  fetchurl,
  fetchFromGitHub,
  fetchHex,
  sha256,
}:
# Each hash is sha256-LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ= in another encoding.
[
  (fetchurl {
    url = "https://example.org/a.tar.gz";
    sha256 = "094qif9n4cq4fdg459qzbhg1c6wywawwaaivx0k0x8xhbyx4vwic";
  })
  (fetchurl {
    url = "https://example.org/b.tar.gz";
    sha256 = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
  })
  (fetchFromGitHub {
    owner = "example";
    repo = "c";
    rev = "v1";
    sha256 = "sha256:LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
  })
  (fetchFromGitHub {
    owner = "example";
    repo = "d";
    rev = "v1";
    sha256 = lib.fakeSha256;
  })
  # Not fixed: only a literal can be converted.
  (fetchurl {
    url = "https://example.org/e.tar.gz";
    inherit sha256;
  })
  (fetchurl {
    url = "https://example.org/f.tar.gz";
    sha256 = sha256;
  })
  # Not fixed: the url refers to it.
  (fetchurl rec {
    url = "https://example.org/i_${sha256}.deb";
    sha256 = "094qif9n4cq4fdg459qzbhg1c6wywawwaaivx0k0x8xhbyx4vwic";
  })
  # A fetcher of the file's own, which may take only sha256.
  (
    let
      fetchurl = { url, sha256 }: builtins.fetchurl { inherit url sha256; };
    in
    fetchurl {
      url = "https://example.org/h.tar.gz";
      sha256 = "094qif9n4cq4fdg459qzbhg1c6wywawwaaivx0k0x8xhbyx4vwic";
    }
  )
  # fetchHex takes only sha256.
  (fetchHex {
    pkg = "g";
    version = "1";
    sha256 = "094qif9n4cq4fdg459qzbhg1c6wywawwaaivx0k0x8xhbyx4vwic";
  })
]
Fixed
{
  lib,
  fetchurl,
  fetchFromGitHub,
  fetchHex,
  sha256,
}:
# Each hash is sha256-LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ= in another encoding.
[
  (fetchurl {
    url = "https://example.org/a.tar.gz";
    hash = "sha256-LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
  })
  (fetchurl {
    url = "https://example.org/b.tar.gz";
    hash = "sha256-LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
  })
  (fetchFromGitHub {
    owner = "example";
    repo = "c";
    rev = "v1";
    hash = "sha256-LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
  })
  (fetchFromGitHub {
    owner = "example";
    repo = "d";
    rev = "v1";
    hash = lib.fakeHash;
  })
  # Not fixed: only a literal can be converted.
  (fetchurl {
    url = "https://example.org/e.tar.gz";
    inherit sha256;
  })
  (fetchurl {
    url = "https://example.org/f.tar.gz";
    sha256 = sha256;
  })
  # Not fixed: the url refers to it.
  (fetchurl rec {
    url = "https://example.org/i_${sha256}.deb";
    sha256 = "094qif9n4cq4fdg459qzbhg1c6wywawwaaivx0k0x8xhbyx4vwic";
  })
  # A fetcher of the file's own, which may take only sha256.
  (
    let
      fetchurl = { url, sha256 }: builtins.fetchurl { inherit url sha256; };
    in
    fetchurl {
      url = "https://example.org/h.tar.gz";
      sha256 = "094qif9n4cq4fdg459qzbhg1c6wywawwaaivx0k0x8xhbyx4vwic";
    }
  )
  # fetchHex takes only sha256.
  (fetchHex {
    pkg = "g";
    version = "1";
    sha256 = "094qif9n4cq4fdg459qzbhg1c6wywawwaaivx0k0x8xhbyx4vwic";
  })
]
Diagnostics
sri-hash.nix:12:5: warning[sri-hash]: use 'hash' with an SRI hash instead of 'sha256' [fix]
sri-hash.nix:16:5: warning[sri-hash]: use 'hash' with an SRI hash instead of 'sha256' [fix]
sri-hash.nix:22:5: warning[sri-hash]: use 'hash' with an SRI hash instead of 'sha256' [fix]
sri-hash.nix:28:5: warning[sri-hash]: use 'hash' with an SRI hash instead of 'sha256' [fix]
sri-hash.nix:33:13: warning[sri-hash]: use 'hash' with an SRI hash instead of 'sha256'
sri-hash.nix:37:5: warning[sri-hash]: use 'hash' with an SRI hash instead of 'sha256'
sri-hash.nix:42:5: warning[sri-hash]: use 'hash' with an SRI hash instead of 'sha256'

env-variable

Severity: ⚠️ warning. A NIX_ variable such as NIX_CFLAGS_COMPILE set beside the attributes of a derivation: with __structuredAttrs, only env is exported, so the compiler wrapper would not see it. The fix moves it to env, through toString unless it is a string already. It is withheld when env is not an attribute set written in place, or the file refers to the variable elsewhere, as finalAttrs.NIX_LDFLAGS.

Original
{
  lib,
  stdenv,
  buildGoModule,
}:
{
  string = stdenv.mkDerivation {
    pname = "string";
    NIX_CFLAGS_COMPILE = "-Wno-error";
    NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-framework Foundation";
    # A list, which env does not take as such.
    NIX_CFLAGS_LINK = [
      "-lm"
      "-lpthread"
    ];
    NIX_DEBUG = lib.optionals stdenv.cc.isClang [ "1" ];
    NIX_ENFORCE_PURITY = 0;
    NIX_HARDENING_ENABLE = lib.optionalString stdenv.isLinux "pie" + " fortify";
    # Shell code reads it from env as well.
    preBuild = ''
      export NIX_CFLAGS_COMPILE+=" -DNDEBUG"
    '';
  };

  # Merged with the env written in place.
  merged = buildGoModule (finalAttrs: {
    pname = "merged";
    env = {
      CGO_ENABLED = 0;
    };
    NIX_LDFLAGS = "-lz";
  });

  # Not fixed: env is not written in place.
  computed = stdenv.mkDerivation {
    pname = "computed";
    env = lib.optionalAttrs stdenv.hostPlatform.isDarwin { CC = "clang"; };
    NIX_LDFLAGS = "-lz";
  };

  # Not fixed: the file refers to it.
  referred = stdenv.mkDerivation (finalAttrs: {
    pname = "referred";
    NIX_HARDENING_ENABLE = "fortify";
    passthru.hardening = finalAttrs.NIX_HARDENING_ENABLE;
  });
}
Fixed
{
  lib,
  stdenv,
  buildGoModule,
}:
{
  string = stdenv.mkDerivation {
    pname = "string";
    env.NIX_CFLAGS_COMPILE = "-Wno-error";
    env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-framework Foundation";
    # A list, which env does not take as such.
    env.NIX_CFLAGS_LINK = toString [
      "-lm"
      "-lpthread"
    ];
    env.NIX_DEBUG = toString (lib.optionals stdenv.cc.isClang [ "1" ]);
    env.NIX_ENFORCE_PURITY = 0;
    NIX_HARDENING_ENABLE = lib.optionalString stdenv.isLinux "pie" + " fortify";
    # Shell code reads it from env as well.
    preBuild = ''
      export NIX_CFLAGS_COMPILE+=" -DNDEBUG"
    '';
  };

  # Merged with the env written in place.
  merged = buildGoModule (finalAttrs: {
    pname = "merged";
    env = {
      CGO_ENABLED = 0;
    };
    env.NIX_LDFLAGS = "-lz";
  });

  # Not fixed: env is not written in place.
  computed = stdenv.mkDerivation {
    pname = "computed";
    env = lib.optionalAttrs stdenv.hostPlatform.isDarwin { CC = "clang"; };
    NIX_LDFLAGS = "-lz";
  };

  # Not fixed: the file refers to it.
  referred = stdenv.mkDerivation (finalAttrs: {
    pname = "referred";
    NIX_HARDENING_ENABLE = "fortify";
    passthru.hardening = finalAttrs.NIX_HARDENING_ENABLE;
  });
}
Diagnostics
env-variable.nix:9:5: warning[env-variable]: set 'NIX_CFLAGS_COMPILE' in 'env', or it is not exported with __structuredAttrs [fix]
env-variable.nix:10:5: warning[env-variable]: set 'NIX_LDFLAGS' in 'env', or it is not exported with __structuredAttrs [fix]
env-variable.nix:12:5: warning[env-variable]: set 'NIX_CFLAGS_LINK' in 'env', or it is not exported with __structuredAttrs [fix]
env-variable.nix:16:5: warning[env-variable]: set 'NIX_DEBUG' in 'env', or it is not exported with __structuredAttrs [fix]
env-variable.nix:17:5: warning[env-variable]: set 'NIX_ENFORCE_PURITY' in 'env', or it is not exported with __structuredAttrs [fix]
env-variable.nix:18:5: warning[env-variable]: set 'NIX_HARDENING_ENABLE' in 'env', or it is not exported with __structuredAttrs
env-variable.nix:31:5: warning[env-variable]: set 'NIX_LDFLAGS' in 'env', or it is not exported with __structuredAttrs [fix]
env-variable.nix:38:5: warning[env-variable]: set 'NIX_LDFLAGS' in 'env', or it is not exported with __structuredAttrs
env-variable.nix:44:5: warning[env-variable]: set 'NIX_HARDENING_ENABLE' in 'env', or it is not exported with __structuredAttrs

Building

The build uses Jenesis, vendored in build/jenesis.

nix develop
java build/jenesis/Make.java                       # compile, test, format and render these docs
java build/jenesis/Execute.java eval -E '1 + 1'    # run the launcher on the JVM

The command line comes in two modules. cli holds format, check and fix and its own affogato command with only these. launcher adds eval and drv-path, which need the evaluator and so Truffle. jenesis.properties makes Execute.java run the launcher; -Djenesis.execute.module=cli/src/main/java runs the other. The native profile of cli is native-lite.

Playground

The web module wraps the formatter and linter for JavaScript: Playground exports format, fix and lint, each returning JSON. TeaVM compiles it with its dependencies to affogato.js at the module’s package step, through the project plugin in plugins/teavm, which jenesis.plugins.properties declares and the module’s plugin-teavm.properties switches on. TeaVM’s jars are no modules, so the plugin resolves them onto a class path and runs TeaVM in a JVM of its own. Their pins live in the web declaration: TeaVM’s asm is older than the tools', so they cannot share the bill of materials.

TeaVM’s class library has no HexFormat and no String.lines: code the playground reaches must do without them, or the build fails naming the method.

java build/jenesis/Make.java build/modules/compose/module/package-module-web+src+main+java
nix shell nixpkgs#nodejs -c node web/check.cjs \
  target/build/modules/compose/module/package-module-web+src+main+java/custom/teavm/delegate/teavm/output/javascript/affogato.js \
  format/src/test/java lint/src/test/java/cases   # the formatter and linter test cases, in JavaScript

Documentation

This documentation is written in AsciiDoc in docs/. Files whose names start with _ are only included. The Jenesis Asciidoctor plugin, switched on by build.jenesis/asciidoctor.properties, renders it during the build. Run stage to get the HTML in target/stage/project/output/docs/, or build it with Nix:

nix build .#docs

Dependencies

Dependency versions and checksums live in one bill of materials, build.jenesis/pin-affogato.properties, which every module imports with @jenesis.bom. After changing a dependency, regenerate it:

java build/jenesis/Make.java pin

jenesis.properties points pin at that file and names the groups it holds: the dependencies, and the tools the modules run (palantir-java-format, JaCoCo), which the modules import it into as @jenesis.bom palantir:tool/pin-affogato.properties. A plugin’s own version stays pinned in each declaration, as palantir/module/…​: Jenesis does not read it from a file. The project plugins' tools are pinned in jenesis.plugins.pin.properties.

Nix

nix/package.nix builds offline. nix/pins.nix reads the pins of the module declarations and of both pin files, and fetches each pinned jar and POM, so a new pin needs nothing more.

The flake provides:

affogato

the cli module on OpenJDK 25: format, check and fix only, without Truffle. It is the default package.

affogato-native

the cli module as a static native image.

affogato-truffle

the launcher on OpenJDK 25. It runs the bundle the launcher’s packaging.properties asks for: its jars and the java argument file that names them. Without GraalVM’s compiler, Truffle runs its fallback runtime, which only interprets; runtime compilation is off by default anyway.

affogato-truffle-native

the launcher as a static native image, built with GraalVM.

playground

web/playground/index.html and affogato.js: a page that formats, fixes and lints Nix in the browser, opened as a file, with no server. Its rule selector loads the linter’s cases from examples.js, which PlaygroundExamplesTest regenerates.

docs

this documentation, rendered to HTML.

tests

a check that runs the test suite on the JVM and outputs its JaCoCo report.

playground-tests

a check that runs the formatter and linter test cases against affogato.js in Node.js.