- Zig 99.7%
- Nix 0.3%
| .github/workflows | ||
| src | ||
| test | ||
| .envrc | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| treefmt.nix | ||
| zippy.zon | ||
zippy
Clippy for Zig. A linter that, like cargo clippy, drives your build system and
lints exactly what actually compiles.
Requires Zig 0.16.0.
What it does
Run zippy with no arguments inside a project that has a build.zig. Zippy runs
your build with an embedded custom build runner, walks the real compile graph, and
lints exactly the source files that are actually compiled (following
@import/@embedFile reachability from the real roots). It also:
- surfaces the Zig compiler's own errors (type errors, undeclared identifiers,
unused locals) merged inline with the lints, tagged
[zig], so one run gives you both, - flags
.zigfiles present in the tree but part of no compilation (dead_file), - ships as a single static binary (the build runner is embedded, written to a temp file at run time, so there is no sidecar to install).
Output reads like zig build errors, so editors and CI that already parse Zig
diagnostics keep working.
Usage
zippy # builder mode: build the project and lint what compiles
zippy --fix # also apply any automatic fixes to the working tree
zippy lsp # run as a Language Server over stdio
zippy --help # full flag list
Flags:
--color=auto|always|nevercontrols ANSI color (autocolors only on a TTY withNO_COLORunset).--fixapplies each diagnostic's automatic fix, if it has one. It refuses to touch a file with uncommitted git changes unless--allow-dirtyis also given, and writes each file atomically.--allow-dirtylets--fixedit files with uncommitted git changes.
Building from source:
zig build # produces zig-out/bin/zippy
zig build test # runs the test suite
Lints
Every lint belongs to a group. A per-lint override beats a per-group override
beats the group default, resolved in that order (most specific wins).
correctness and memory default to error; style, complexity, and
unused default to warn.
Many lints go beyond matching the syntax tree: Zippy builds a per-file semantic
model (scopes, symbols, and reference lists) so a lint can ask "what does this
name actually resolve to" and "is this declaration ever used". That is what lets
no_print ignore your own fn print, unused_decl see through shadowing, and
the memory lints follow an allocation through the function that owns it.
style (default warn)
line_width: a source line longer than the configured maximum.naming_convention: functions should be camelCase, type declarations TitleCase.inline_container_type: a large anonymous struct/union/enum written inline where a named type would be clearer.param_ordering: leading parameters should follow a convention:self, then anio(std.Io), then the allocator, then the rest. Theio/allocator positions are configurable.no_print:std.debug.printin non-test code (usestd.log). Shadow-aware, so it never flags your ownfn printor a differently-boundprint.useless_error_return: a function that returns!Tbut whose body can never actually produce an error. Drop the!so callers stop handling a non-existent error.
complexity (default warn)
too_many_params: a function with more than the configured number of parameters.redundant_else: anelseafter an if-branch that always returns, breaks, or continues. Fixable:--fixremoves theelseand dedents its body.duplicate_case: twoswitchprongs with structurally identical bodies (commutativity-aware, sox + 1and1 + xcount as the same).
correctness (default error)
discarded_error:_ = call();throws away a call's result, which may be an unhandled error.suppressed_errors: acatch {}orcatch unreachablethat silently swallows an error. Exemptions for tests, writer calls, anddeferare configurable.homeless_try:tryused where the enclosing function does not return an error union (and it is not atest), which the compiler rejects anyway.
memory (default error)
return_stack_local:return &xwherexis a function-local variable yields a pointer into a stack frame that is destroyed on return.hidden_allocator: reaching for a global allocator (std.heap.page_allocatorand friends) instead of taking anstd.mem.Allocatorparameter. Onlymainmay originate a root allocator.use_after_free: a use of an allocation after it was freed, or a double free, within one function.unsafe_undefined:= undefinedoutside the cases where it is idiomatic. Array var-decls,testblocks, and destructors (deinit/destroy/reset) are each exempt by a configurable toggle;x == undefinedalways flags.leaked_allocation: a local allocation that is used in place and never freed or handed off. Defaults towarn(a leak is a resource bug, not unsafety), even though its group defaults toerror.
unused (default warn)
unused_decl: a private (non-pub) top-level declaration nothing in its file references. Fixable:--fixdeletes it.dead_file: a.zigfile in the tree that no compilation reaches (builder mode only).
The memory lints are deliberately conservative: they follow allocations only within a single function and stay silent the moment ownership could leave through a path they cannot prove, so they miss cases rather than raise a false alarm.
Zig compiler errors also appear, tagged [zig], and are always shown.
Configuration
Zippy reads the nearest zippy.zon, walking up from the current directory. With
none found, built-in defaults apply. A per-lint override beats a per-group
override beats the group default.
.{
.default_severity = .warn,
.groups = .{
.style = .warn,
.memory = .@"error",
},
.lints = .{
.line_width = .{ .max = 100 },
.too_many_params = .{ .max = 5 },
// some lints take options beyond a severity:
.param_ordering = .{ .io = 0, .allocator = 1 },
.unsafe_undefined = .{ .allow_tests = true, .allow_destructors = true },
// silence one lint everywhere without disabling its whole group
.dead_file = .{ .severity = .off },
},
}
Each severity is off, warn, or error. A run exits 1 if any
error-severity diagnostic fired, 0 otherwise, and 2 if Zippy itself failed
(a bad zippy.zon, a build that did not build, or an internal error).
Inline suppression
Mute a lint at a specific spot with a // zippy:ignore comment. A standalone
comment mutes the next line. A trailing comment mutes its own line. A bare
directive with no names mutes every lint on the target line.
// zippy:ignore hidden_allocator
const gpa = std.heap.page_allocator;
const gpa = std.heap.page_allocator; // zippy:ignore hidden_allocator
// zippy:ignore discarded_error too_many_params
Zig compiler errors and parse errors are never suppressible. A real build error should not be silenceable.
Editor integration
zippy lsp runs a Language Server over stdio, giving live diagnostics on every
edit, hover text with each lint's rationale, and quick-fix code actions (the same
fixes --fix applies, offered per diagnostic).
Status
Early and moving fast (version 0.1.0). The lint set, fix coverage, and
memory-safety analysis are growing. Correctness bias throughout: a lint prefers a
missed case over a false positive.