What gnarl measures

  1. What it counts
  2. What runs at compile time
  3. Fan-out and fan-in

Each function and test block, from the compiler’s own parser alone. Every number here can be taken apart with gnarl --explain.

Metric What it counts
cyclomatic the paths through the function
cognitive how hard it is to follow: each structure once, more the deeper it sits
lines logical lines: no blanks, comment-only lines or lone closing brackets
nesting how many levels deep the code goes
params parameters, a leading self, Allocator and Io left out
weighted the same, a type parameter and an anytype counted twice

What it counts

Cyclomatic complexity is 1, and 1 for each if (so each else if, and an if written as a value), while, for, and, or, orelse, catch with a handler, and switch prong but a plain else or a lone _. An inline else counts once. Nothing for a try, which only passes an error on; a payload capture (|v|), which names a value on a branch already counted; an else; a labeled break or continue; or a catch unreachable, catch {} or orelse unreachable, which have no other path.

Cognitive complexity is how hard the function is to follow. A structure costs once however many ways it branches, and more the deeper it sits: an if, while, for, switch, or a catch or orelse whose handler is a block or a switch, costs 1 and 1 more for each level it is in. An else if, an else and a loop’s else cost 1, but not the else of an if whose branches are both expressions. Each run of and or of or costs 1, as do a catch or orelse with a value or a jump for its handler, a labeled block used as a value, a return inside a loop or a labeled switch, a labeled break or continue that ends a loop or block on its way, and an errdefer. Nothing for a try, a defer, a capture or a guard clause’s return. --explain shows each cost a function was charged and why, and the table below is checked against gnarl itself.

Code cyclomatic cognitive
fn f(x: E) u8 { return switch (x) { .a => 1, .b => 2, .c => 3, else => 0 }; } 4 1
fn f(xs: []const u8) void { for (xs) \|x\| { if (x > 0) { if (x < 9) use(x); } } } 4 6
fn f(x: i8) i8 { if (x > 0) { return 1; } else if (x < 0) { return -1; } else { return 0; } } 3 3
fn f(c: bool) u8 { return if (c) 1 else 2; } 2 1
fn f(a: bool, b: bool, c: bool) bool { return a and b and c; } 3 1
fn f(xs: []const u8) ?u8 { for (xs) \|x\| { if (x == 0) return x; } return null; } 3 4
fn f() !void { errdefer undo(); try work(); } 1 1

Nesting depth is how many levels deep the code goes. An if, while, for or switch opens a level for its branches, as do a labeled block, a container declared in the function, and a catch or orelse whose handler is a block with something in it. An else if stays on its if’s level, and a block that is a branch’s body is that branch’s level.

Code cyclomatic nesting
fn f() !void { try a(); } 1 0
fn f(c: bool) void { if (c) a() else b(); } 2 1
fn f(x: u8) void { if (x == 0) a() else if (x == 1) b() else c(); } 3 1
fn f(c: bool, d: bool) void { if (c and d) a(); } 3 1
fn f(c: bool) void { if (c) { if (c) a(); } } 3 2
fn f(o: ?u8) void { if (o) \|v\| use(v); } 2 1
fn f(xs: []const u8) void { for (xs) \|x\| use(x); } 2 1
fn f(o: ?u8) u8 { return o orelse 0; } 2 0
fn f(o: ?u8) u8 { return o orelse unreachable; } 1 0
fn f() u8 { return parse() catch 0; } 2 0
fn f() void { work() catch {}; } 1 0
fn f(x: E) void { switch (x) { .a => {}, .b, .c => {}, else => {} } } 3 1
fn f(x: E) void { switch (x) { .a => {}, inline else => {} } } 3 1
fn f(c: bool) u8 { return blk: { if (c) break :blk 1; break :blk 2; }; } 2 2
fn f() void { const S = struct { fn g(d: bool) void { if (d) {} } }; _ = S; } 1 1
fn F(comptime T: type) type { return struct { const n = if (@sizeOf(T) > 8) 2 else 1; x: T }; } 1 0

The last two rows: a function declared inside another is measured on its own (here as f.S.g, cyclomatic 2), and adds nothing to the one around it but the container’s level. A container a function returns, as a generic type such as F does, is the type it makes, not code it runs: like a container at the top of a file, it is in no function’s numbers. It opens no level, nothing in it counts, and its methods are measured on their own. What the function does before it returns, such as checking its parameters, counts as any code.

Logical lines are the lines from the signature to the closing brace that hold code: not a blank line, a comment-only line, or a line of nothing but closing brackets and the ; or , after them. A nested function’s lines are its own, and the fields and declarations of a type a function returns are no function’s.

Parameters leave out what is context rather than input: a leading self (a first parameter typed as the container, @This(), or a name the container gives @This()), and each Allocator or Io. Weighted parameters count a type parameter and an anytype twice: each is a contract the signature does not spell out.

Code params weighted
const S = struct { fn f(self: *S, gpa: Allocator, n: u8) void {} }; 1 1
fn f(comptime T: type, x: anytype, n: usize) void {} 3 5
fn f(io: std.Io, w: *std.Io.Writer) void {} 1 1

The cognitive rules in full, each with a worked example the tests are held to, are on Cognitive complexity.

What runs at compile time

--explain tallies what of a function runs at compile time: each comptime block or expression, inline loop or prong, reflection builtin (@typeInfo, @Type, @hasDecl, @hasField, @FieldType), comptime and anytype parameter, and whether it returns a type. What the source alone cannot show it names as unknown rather than leaving out: the members of an inferred error set, how many times a generic function is instantiated and with what, and what each anytype must provide.

Fan-out and fan-in

gnarl --files lists files rather than functions: the files each imports (fan-out), the files read that import it (fan-in), and the modules it imports (std, build options, named modules), which gnarl names but does not follow. A file many import is risky to change; one that imports many knows too much.

$ gnarl --files --top 3 .
fan-out  fan-in  modules  file
     27       0        4  src/main.zig
     16       2        1  src/explain.zig
      1      15        1  src/functions.zig