Skip to content

An extendable and customisable Zig linter that is integrated from source into your build.zig.

License

Notifications You must be signed in to change notification settings

KurtWagner/zlinter

Repository files navigation

Zlinter icon

Zlinter - Linter for Zig

Zig support linux windows Coverage Status License: MIT

An extendable and customizable Zig linter (with AST explorer) that is integrated from source into yourbuild.zig.

A linter is a tool that automatically checks source code for style issues, bugs, or patterns that may lead to errors,
helping developers write cleaner and more reliable code.


Screenshot

Table of contents

Getting started

zlinter is not a standalone binary - it's built into your projects build.zig. This makes it flexible to each projects needs. Simply add the dependency and hook it up to a build step, like zig build lint:

1. Save dependency to your zig project:

For 0.14.x:

zig fetch --save git+https://github.com/kurtwagner/zlinter#0.14.x

For 0.15.x:

zig fetch --save git+https://github.com/kurtwagner/zlinter#0.15.x

For master (0.16.x-dev):

zig fetch --save git+https://github.com/kurtwagner/zlinter#master

2. Configure lint step in your build.zig:

 const zlinter = @import("zlinter");
 // ...
 const lint_cmd = b.step("lint", "Lint source code.");
 lint_cmd.dependOn(step: {
     // Swap in and out whatever rules you see fit from RULES.md
     var builder = zlinter.builder(b, .{});
     builder.addRule(.{ .builtin = .field_naming }, .{});
     builder.addRule(.{ .builtin = .declaration_naming }, .{});
     builder.addRule(.{ .builtin = .function_naming }, .{});
     builder.addRule(.{ .builtin = .file_naming }, .{});
     builder.addRule(.{ .builtin = .switch_case_ordering }, .{});
     builder.addRule(.{ .builtin = .no_unused }, .{});
     builder.addRule(.{ .builtin = .no_deprecated }, .{});
     builder.addRule(.{ .builtin = .no_orelse_unreachable }, .{});
     break :step builder.build();
 });

3. Run linter:

Keep in mind the first run will be slower as the cache isn't warmed:

zig build lint

You can also be specific with paths (see command-line arguments for more options):

zig build lint -- --include src/ file.zig

Alternative: Enable all built in rules

If you just want to test out zlinter, you can also enable all rules and then selectively run rules from the command line. A lot of rules are quite pedantic so this is not recommended outside of testing zlinters rules for your project:

  1. Enable all built in rules in build.zig
const zlinter = @import("zlinter");
const lint_cmd = b.step("lint", "Lint source code.");
lint_cmd.dependOn(step: {
    var builder = zlinter.builder(b, .{});
    inline for (@typeInfo(zlinter.BuiltinLintRule).@"enum".fields) |f| {
        builder.addRule(.{ .builtin = @enumFromInt(f.value) }, .{});
    }
    break :step builder.build();
});
  1. Selectively run rules:
zig build lint -- --rule no_unused no_deprecated

Autofix

Some linter rules support auto fixing some problems.

Important

Auto fixing is an experimental feature so only use it if you use source control - always back up your code first!

For example, to auto fix unused declarations and field ordering issues, assuming your project has these rules configured:

# First ensure you're working branch is clean (or back up your code!)
$ git status

# Then run the fix command (you may need to run this multiple times)
$ zig build lint -- --rule field_ordering --rule no_unused --fix

It can sometimes require a multiple runs to completely resolve all fixable issues. i.e., run with --fix until it reports 0 fixes applied.

Custom rules

Bespoke rules can be added to your project. For example, maybe you really don't like cats, and refuse to let any cats exist in any identifier. See example rule no_cats, which is then integrated like builtin rules in your build.zig:

builder.addRule(b, .{ 
  .custom = .{
    .name = "no_cats",
    .path = "src/no_cats.zig",
  },
}, .{});

Alternatively, take a look at https://github.com/KurtWagner/zlinter-custom-rule-example, which is a minimal custom rule example with accompanying zig project.

Configuration

Configure paths

The builder used in build.zig has a method addPaths, which can be used to add included and excluded paths. For example,

builder.addPaths(.{
    .include = &.{ b.path("engine-src/"), b.path("src/") },
    .exclude = &.{ b.path("src/android/"), b.path("engine-src/generated.zig") },
});

would lint zig files under engine-src/ and src/ except for engine-src/generated.zig and any zig files under src/android/.

Configure Rules

addRule accepts an anonymous struct representing the Config of rule being added. For example,

builder.addRule(.{ .builtin = .field_naming }, .{
  .enum_field = .{ .style = .snake_case, .severity = .warning },
  .union_field = .off,
  .struct_field_that_is_type = .{ .style = .title_case, .severity = .@"error" },
  .struct_field_that_is_fn = .{ .style = .camel_case, .severity = .@"error" },
});
builder.addRule(.{ .builtin = .no_deprecated }, .{
  .severity = .warning,
});

where Config struct are found in the rule source files no_deprecated.Config and field_naming.Config.

Disable with comments

Disable next line

Disable all rules or an explicit set of rules for the next source code line.

Syntax:

zlinter-disable-next-line [rule_1] [rule_n] [- comment]`

For example,

// zlinter-disable-next-line no_deprecated - not updating so safe
const a = this.is.deprecated();

Disable current line

Disable all rules or an explicit set of rules for the current source code line.

Syntax:

zlinter-disable-current-line [rule_1] [rule_n] [- comment]

For example,

const a = this.is.deprecated(); // zlinter-disable-current-line

Disable multiple lines

Disable all rules or an explicit set of rules for multiple source code lines.

Syntax:

zlinter-disable [rule_1] [rule_n] [- comment]
zlinter-enable [rule_1] [rule_n] [- comment]

For example, to disable multiple lines for a given set of rules:

// zlinter-disable rule_a rule_b - rationale
var something = doSomethin();
var something_else = doSomethingElse();
// zlinter-disable rule_a rule_b

For example, to disable multiple lines for all rules:

// zlinter-disable - rationale
var something = doSomethin();
var something_else = doSomethingElse();
// zlinter-disable

If you omit zlinter-enable, all lines until EOF will be disabled.

Command-Line Arguments

zig build lint -- [--include <path> ...] [--exclude <path> ...] [--filter <path> ...] [--rule <name> ...] [--fix]
  • --include run the linter on these path ignoring the includes and excludes defined in the build.zig forcing these paths to be resolved and linted (if they exist).
  • --exclude exclude these paths from linting. This argument will be used in conjunction with the excludes defined in the build.zig unless used with --include.
  • --filter used to filter the run to a specific set of already resolved paths. Unlike --include this leaves the includes and excludes defined in the build.zig as is.
  • --fix used to automatically fix some issues (e.g., removal of unused container declarations) - Only use this feature if you use source control as it can result loss of code!

For example

zig build lint -- --include src/ android/ --exclude src/generated.zig --rule no_deprecated no_unused
  • Will resolve all zig files under src/ and android/ but will exclude linting src/generated.zig; and
  • Only rules no_deprecated and no_unused will be ran.

Configure Optimization

zlinter.builder accepts .optimize (defaults to .Debug). For example,

var builder = zlinter.builder(b, .{.optimize = .ReleaseFast });

If your project is large it may be worth setting optimize to .ReleaseFast - keep in mind the first run may be slower as it builds the the modules for the first time with the new optimisation.

Supported zig versions

The plan is to support master (mostly because its an important exercise in keeping up to date with whats changing in zig) and the latest previous version.

Currently, 0.14.x and master.

Fixes and improvements to rules may be cherry-picked to older versions if there's no API compatibility issues.

This may change once zig hits 1.x.

Milestones

Background

zlinter was written to be used across my personal projects. The main motivation was to have it integrated from source through a build step so that it can be

  1. customized at build time (e.g., byo rules); and
  2. versioned with your projects source control (no separate binary to juggle)

I'm opening it up incase it's more generally useful, and happy to let it organically evolve around needs, if there's value in doing so.

It uses zls (an awesome project, go check it out if you haven't already) and std.zig to build and analyze zig source files.

Current limitations

zlinter currently analyzes the Zig AST, which has limited context without trying to re-implement the Zig compiler (not doing).

A more accurate approach could be to integrate more closely with the Zig build system and compiler (e.g., the proposed Zig compiler server), but for now, using the AST should be sufficient for most cases, and maybe one day zlinter can use newer Zig Compiler APIs as they become available. The milestones will help inform this.


  1. [done] Rough implementaton of 20 diverse linter rules - this is important to understanding limitations (e.g., the AST and design patterns to a stable API.)

  2. [in-progress] Run and review the results on at least 5 large open source Zig projects - this is to discover unknown unknowns to populate caveats and limitations of current approach.

  3. [pending] To be informed by (1) and (2) - could be that AST is good enough for enough cases to provide value providing adequate documentation, AND/OR, could be that it's worth contributing time into Zigs efforts around "multibuild" and zig compiler server.

Versioning

zlinter will:

  • follow the same semantic versioning as zig;
  • use branch master for zig master releases; and
  • use branch 0.14.x for zig 0.14.x releases.

This may change, especially when zig is "stable" at 1.x. If you have opinions on this, feel free to comment on #20.

Contributing

Contributions

Contributions and new rules or formatters are very welcome.

Rules are per project configurable so I don't see any problems if new opinionated ones are added (assuming they're not completely bespoke).

If you notice breaking changes in zig that will not be picked up by a Deprecated: comment then consider contributing to the no_deprecated.zig rule, with a specific check for the change. For example, zig removed usingnamespace in 0.15 so no_deprecated.zig will explicitly check and report the usage of usingnamespace keyword in 0.14 runs.

Dependencies

Zlinter avoids dependencies. It's just too much of a burden right now to depend on something written for Zig when Zig isn't 1.x.

The one exception is ZLS, as it's well maintained and doesn't appear to be going anywhere. More often than not I've wasted hours implementing a method to find a very similar method already exists in ZLS, which makes sense, as ZLS analyses Zig code using the AST like this linter currently does.

The AST Explorer provided with Zlinter will be similar and aims to be minimal. Ideally no build system, no dependencies, just plain JS and CSS targetting modern browers as the target audience should all have access to such things.

Run tests

Unit tests:

zig build unit-test

Integration tests:

zig build integration-test

All tests:

zig build test

Run lint on self

zig build lint

Regenerate documentation

zig build docs

Build and serve website (with AST explorer)

zig build website && npx http-server -c-1 zig-out/website

You don't need to use npx, its just static content in zig-out/website. You may decide to use python -m http.server instead.

About

An extendable and customisable Zig linter that is integrated from source into your build.zig.

Topics

Resources

License

Stars

Watchers

Forks

Languages