Docent is a code quality linter for Zig, focused on documentation, complexity and style. It’s available as a CLI and library.
About this documentation
This documentation covers both the CLI tool and the public library API. The CLI docs are aimed at end users; the library docs target developers embedding Docent programmatically.
-
The CLI documentation, available as well as a PDF for offline reading.
Concepts and terminology
Some key terms for better understanding taken from the compilation model:
- Zig module
-
A Zig package or compilation is made up of one or more modules.
- Zig source file
-
A Zig source file is a file with the
.zigextension. A module is defined by a collection of these. - Manifest
-
The project’s
build.zig.zonsource file. - Build system script
-
The
build.zigsource file, which works similar to an executable, so it’s basically an module root with the reservedbuild()entry point. - Module root
-
The designated top-level file of a module’s source tree, declared in
build.zig. Serves as the scan origin for the compiler, linter, and doc system. For libraries, it is also the import surface resolved by@import(). Not to be confused with entry point, which is execution-specific and only applies to executables and the test runner. By convention usually namedroot.zig, but this is not a strict requirement. - Module types
-
- Libraries or modules per-se
-
Modules that are meant to be imported by other modules, these don’t have a
main()public function, and by convention are usually namedroot.zig, but this is not a strict requirement. When scanned, the linter will crawl around all the public reachable declarations from the module root, this behavior is shared across all module types. - Executables
-
Executables are those modules that have the special public function
main(), a source file with that function is strictly considered an executable, treated as a module root. - Test module
-
Test modules are similar to libraries, any
testdeclaration presence can mark the module as a test module, Zig allows to define special test modules too.
- Doc comments
-
Documentation comments.
///-
See [doc-comments].
//!-
See [top-level-doc-comments]. As well, these are marked as an urgent proposal for removal.
- Doctests
-
See [doctests]. As well, there’s an open proposal to extend the syntax by Mitchell Hashimoto. It aims to allow multiple doctest declarations for the same identifier but each one having its own description.
Scanning strategy
- Public API surface
-
Depends on a module root, and all exposed public declarations reaachable from it. Similar to how one would use a module as a dependency.
- Reachability traversal
-
Similar to public API surface, but it includes non-public declarations, so the surface area is larger.
- Syntactic traversal
-
This doesn’t depend on a module root, it just traverses all the syntax of the files being scanned. This is similar to how formatters work, which might include orphan files that are not reachable from any module.
Default scanning without path arguments
Whenever the program is run without any path arguments, the current working directory is scanned and it’s expected to be a project. The scanning strategy will attempt to find both build system script and manifest to determine the project structure.
Project discovery
Project discovery is performed by looking at both script and manifest files, considering. It’ll attempt find the nearest script either in the current working directory or any of its parent directories, and then look for a manifest to determine for complementary information, such as: local dependencies and their paths, bundled paths, project name, etc.
-
If there are local dependencies, they can be linted optionally by passing
--deps. -
Paths listed are entirely for supporting information, as sometimes paths or even the manifest isn’t present, or there are no paths listed, so the project discovery is still valid and strictly focused on the build script first.
Based on the build script discovery, the behavior defaults to only public modules being scanned. Executables and test modules are discovered but excluded by default.
-
Traversal is recursive across imported files, so multi-hop public chains are included.
-
Imports reachable only through non-public declarations are excluded.
-
Package imports (for example
@import("std")) are not treated as local lint targets. -
build.zigand files underbuild/are ignored by default.-
Instead of files within the build directory, it’s mostly all of those files that are used/imported by the main build script (
build.zig).
-
-
This avoids false-positive API checks from build tooling paths that are commonly present in
build.zig.zon.paths. -
CLI users can opt in with
--include-build-scripts.
Scanning with arguments
When explicit paths are passed, each argument is treated as a file or directory target.
-
A single file.
-
A single directory.
-
A mix of files and directories.
When explicit paths are provided, the default project discovery is disabled and only the specified targets are scanned.
-
Explicit paths are validated strictly by default.
-
If a path does not exist, cannot be accessed, or refers to a non-Zig source file, an error is emitted and the process exits with a non-zero status.
-
If a directory is valid but does not yield any Zig lint targets, a warning is emitted and that directory is skipped.
-
If all explicit targets are valid but none yield any Zig lint targets, the process exits with a non-zero status.
-
If the target is a Zig source file, it is treated as a module root candidate.
-
If the target is a directory, module roots are resolved within that directory.
-
For each resolved source file or directory, the module kind is determined:
-
A file containing a public
main()is treated as an executable root module. -
A file containing a public
build()is treated as a build script root module. -
Otherwise, it is treated as a library source file.
-
-
In all cases, the publicly reachable API is traversed starting from the resolved module root.
Linting rules
To check the rules documentation, check library documentation rules namespace for the actual implemented rules.
Test suite
Please refer to the tests README.
References
-
[dcm-dev] https://dcm.dev/.
-
[doctests] Doctests in the Zig documentation — https://ziglang.org/documentation/0.16.0/#Doctests
-
[top-level-doc-comments] Top-level documentation comments in the Zig documentation — https://ziglang.org/documentation/0.16.0/#Top-Level-Doc-Comments
-
[doc-comments] Doc comments in the Zig documentation — https://ziglang.org/documentation/0.16.0/#Doc-Comments
-
[mccabe-guidance] NIST/McCabe guidance (Structured Testing: A Testing Methodology Using the Cyclomatic Complexity Metric):
-
Section 2.5: Limiting cyclomatic complexity to 10.
-
-
[sonar-cyclomatic] Discussing Cyclomatic Complexity — https://www.sonarsource.com/blog/discussing-cyclomatic-complexity/
-
[sonar-paper] Cognitive Complexity, a Sonar exclusive metric formulated to more accurately measure the relative understandability of methods — https://www.sonarsource.com/resources/cognitive-complexity/
-
[sonar-reasoning] Sonar’s reasoning for defaulting cognitive complexity to 15 — https://community.sonarsource.com/t/s3776-reason-for-the-current-default-value-of-15/127103
-
[rustdoc-lints] The Rustdoc Book — §5. Rustdoc-specific lints.
-
[rustc-lints] The Rustc Book — §4. Lints.
-
[rustc-missing-docs] The Rustc Book — Missing docs lint.
-
[clippy-lints] Clippy lints (filter by
docordocumentation).
-
[godoc-comments] Go Doc Comments.
-
[godoc-comment-pkg] Go standard library —
go/doc/comment. -
[godoc-lint] Godoc-Lint.
-
[golangci-lint] Golangci-lint linter suite.
Acknowledgments and prior art
Mainly Rust/Cargo’s documentation (and probably Clippy too) and linter checks, while also taking inspiration from Go’s linting tooling. As well as Scott Matheina’s Ephor