ZLint v0.10.0
Two months of work, 24 PRs, and a few things I've wanted to fix for a while. The headline items are custom rules, control flow analysis, better config behavior, and a builtin update command.
There are also three breaking changes, and one of them will almost certainly
require you to edit your zlint.json. Those are first.
Breaking Changes
zlint.json no longer defaults to every rule being off
This is the big one. Previously, the mere presence of a zlint.json turned all
rules off, and you had to opt back in to each one individually:
{
"rules": {} // before v0.10.0: nothing runs at all
}
This was to prevent upgrades from unexpectedly introducing new rules into your
CI. This ended up being bad DX and surprised basically everyone who tried it.
Now rules run at their default severities, and zlint.json will override/adjust
them.
{
"rules": {
// everything else has default behavior.
"unsafe-undefined": "error",
"homeless-try": "warn",
"unused-decls": "off"
}
}
What to do: if your config was an explicit allowlist, you'll now get
diagnostics from rules you never enabled. Set the ones you don't want to "off".
Run zlint once after upgrading and see what shows up - it's usually a short
list.
ignore patterns are globs now
ignore used to match with "starts with" semantics. It now takes real glob
patterns, which means a bare folder name matches the folder and not everything
inside it:
{
"ignore": [
// before
"src/codegen",
// after
"src/codegen/**",
],
}
Add /** to your existing entries and you're done. In exchange you get patterns
that were previously impossible:
{
"ignore": ["src/codegen/**", "src/**/*_test.zig", "**/*.gen.zig"]
}
GlobSet also supports negation now, so you can carve exceptions back out of a
broad pattern. And while I was in there: a zlint.json with any ignore key at
all used to panic with exit code 134. That's fixed too ([#358]).
Custom rules
Custom rules are now a proper build option. Add them when you declare the dependency:
const zlint = @import("zlint");
const zlint_dep = b.dependency("zlint", .{
.custom_rules = &[_]std.Build.LazyPath{
b.path("./src/your_custom_rule.zig"),
},
});
const lint_step = b.step("lint", "run zlint");
lint_step.dependOn(&zlint.addRunLint(b, zlint_dep).step);
There's a matching addRunCustomLintRulesTest for running your rules' tests. See
the custom rules docs for the full API.
CLI arguments are paths, not globs
Positional arguments are now treated as file or directory paths. This mostly means the obvious thing finally works:
zlint . # same as `zlint`
zlint src
zlint ./src/
zlint src/main.zig src/root.zig
If you were passing glob patterns on the command line, let your shell expand
them, or move the pattern into ignore.
Control Flow Analysis
Semantic now builds a control flow graph. This is groundwork only, as no rules
consume it yet. CFG-based rules will be added and released in the near future.
You can look at it today:
zlint --print-cfg src/main.zig | dot -Tsvg > cfg.svg
zlint --print-cfg src/main.zig --cfg-decls
I'm hoping to add rules to check for dead code, inits without deinits, and maybe even integrate with an SMT solver for some really cool lints.
--config and zlint update
By default ZLint walks up from your cwd looking for a zlint.json. You can now
skip that search entirely:
zlint --config ./ci/zlint.strict.json
zlint -c ../shared/zlint.json src
Handy for CI, or for keeping a stricter ruleset next to a looser local one.
And upgrading is now a command instead of a curl pipeline:
zlint update # aliases: `upgrade`, `up`
It downloads and installs the latest stable release in place.
Better looking diagnostics
Multiline spans used to underline every line edge-to-edge, which was noisy and genuinely hard to read when two spans sat next to each other. They're now drawn with left-hand arrows that bracket the region:
𝙭 duplicate-case: Switch statement has duplicate cases
╭─[duplicate-case.zig:3:10]
2 │ const x = switch (bar) {
3 │ ╭─▶ 1 => {
4 │ │ if (cond) {
5 │ │ const y = x + 2;
6 │ │ return y;
7 │ │ }
8 │ │ return 0;
9 │ ╰─▶ },
10 │ ╭─▶ 2 => {
11 │ │ if (cond) {
12 │ │ const y = x + 2;
13 │ │ return y;
14 │ │ }
15 │ │ return 0;
16 │ ╰─▶ },
17 │ else => 0
╰────
There's also a new ASCII formatter for terminals that can't render the box drawing characters. ZLint sniffs your terminal's Unicode support and falls back automatically, but you can force it:
zlint --format ascii
Rule fixes
homeless-try understands type aliases. It used to look only at the syntax
of the return type node, so a named alias for an error union looked like an
infallible function:
const E = error{A};
const Result = E!void;
fn bar() E!void { return error.A; }
pub fn foo() Result {
try bar(); // v0.9.1: "`try` cannot be used in functions that do not return errors."
}
Alias chains are now followed, and return types that can't be classified are assumed fallible and stay quiet. Aliases that definitively resolve to a concrete type are still reported.
duplicate-case compares more of the AST. The structural comparator
understood a fairly narrow slice of Zig, which cut both ways: equivalent branches
were missed, and token-level differences were sometimes treated as equal.
function(value) // now correctly equal to `function(value,)`
if (opt) |*left| left else null
if (opt) |*right| left else null // and these are still correctly different
unused-decls doesn't leave doc comments behind. The --fix-dangerously
autofix deleted the declaration but not its /// comment, which left you with
an "unattached documentation comment" error - or worse, silently glued the
comment onto whatever declaration came next. The deletion span now extends
backward over consecutive doc comment tokens. Module-level //! comments are
deliberately left alone.
Performance and housekeeping
- The parser no longer tokenizes each file twice. Free speed.
- Summary statistics update monotonically, so the counters no longer appear to jump backwards while a large run is in progress.
- The static library artifact is now named
zlint-lib. - Config reading and resolution got a good simplification pass.
- CLI tests were silently not running in CI. They are now, which is how the argument-handling change above got its coverage.
