dylint_linting

Macros and config helpers for authoring custom Dylint lint libraries without boilerplate.

Library
Cargo
v6.0.4
644stars
MIT OR Apache-2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
89/100Excellent
Development Activity100
Maintenance100
Community68
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
79/100Good
Architecture86
Code Quality74
Innovation80
Learning Curve75

dylint_linting is the authoring toolkit inside the Dylint project (Trail of Bits’ framework for running Rust lints from dynamic libraries). Where a raw Dylint library has to hand-wire the FFI entry point, register a LintPass with rustc’s LintStore, and declare the lint itself, dylint_linting collapses that into a handful of macros: dylint_library! for the exported dylint_version symbol, declare_late_lint!/declare_early_lint!/declare_pre_expansion_lint! for single-lint libraries, and the impl_*_lint! variants when the LintPass struct needs custom construction (for example, to hold parsed configuration).

The crate also owns Dylint’s per-workspace configuration story. Libraries that need settings read from a target repository’s dylint.toml file call config_or_default, config, or config_toml, all built on init_config/try_init_config plus TOML deserialization via dylint_internal::config. A constituent feature flag changes what the macros expand to: with it off, a lint compiles as a standalone cdylib Dylint can load directly; with it on, the same source drops the #[no_mangle] FFI plumbing so it can be folded into a larger aggregate library (which is exactly how the examples/general and examples/supplementary libraries in the parent monorepo are structured).

Because it links against rustc’s internal rustc_driver, rustc_lint, and rustc_session crates, dylint_linting only builds on the nightly toolchain with rustc-dev and llvm-tools-preview components — the same driver Dylint itself uses to run the compiled libraries, so a library written with dylint_linting and dylint-driver stay compiler-version-locked to each other.

What You Get

  • dylint_library! - expands to the #[no_mangle] extern "C" fn dylint_version() export that lets Dylint’s driver identify and load the compiled cdylib at runtime.
  • declare_late_lint! / declare_early_lint! / declare_pre_expansion_lint! - one-macro path for single-lint libraries: declares the lint, generates its LintPass, and registers it, mirroring rustc_session::declare_lint! plus declare_lint_pass!.
  • impl_late_lint! / impl_early_lint! / impl_pre_expansion_lint! - same registration wiring as the declare_* macros but takes a caller-supplied LintPass value instead of an empty struct, needed when the pass carries state such as parsed configuration.
  • constituent feature - toggles whether the macros emit the FFI entry point and #[no_mangle] attribute, letting one lint’s source compile either standalone or as part of a combined library alongside other lints.
  • config_or_default / config / config_toml - read and deserialize a named entry from the target workspace’s dylint.toml file, with config_or_default falling back to T::default() when the file or key is absent.
  • init_config / try_init_config - must run (via the generated register_lints function, or manually for multi-lint libraries) before any of the config-reading functions can resolve the target workspace’s dylint.toml path.

Common Use Cases

  • Writing a single custom lint - a team uses cargo dylint new to scaffold a library, then declare_late_lint! or impl_late_lint! to wire the generated LintPass into register_lints without hand-writing the FFI export.
  • Building a configurable lint - a library author exposes tunables (e.g. a work_limit or an allowlist) by deserializing a Config struct from dylint.toml via config_or_default, read once when the LintPass is constructed.
  • Bundling many lints into one aggregate library - the constituent feature lets each lint in a collection (as done in Dylint’s own examples/general library) compile as a standalone crate for testing and also compile mangle-free when combined into a single shared library.
  • Porting Clippy-style lints into a private collection - teams that want organization-specific restriction lints without maintaining a Clippy fork use dylint_linting’s macros to keep new lints structurally close to rustc_session::declare_lint! idioms, minimizing the delta from upstream lint-writing docs.

Under The Hood

Architecture dylint_linting is a macro-heavy, thin wrapper around rustc’s own lint-registration APIs rather than an independent runtime. Its macros (dylint_library!, the declare_*_lint! family, and the impl_*_lint! family) expand at the call site into: an FFI-exported dylint_version function, an extern crate rustc_lint/rustc_session declaration, a #[no_mangle] register_lints function that calls init_config and registers the pass with rustc’s LintStore, and a rustc_session::declare_lint!/declare_lint_pass! or impl_lint_pass! call for the pass itself. The constituent feature is implemented via two paired macro variants, __maybe_exclude! and __maybe_mangle! (selected through #[cfg(feature = "constituent")]), which conditionally drop the FFI plumbing so the exact same lint source can be compiled as either a standalone cdylib or folded into a larger aggregate library — this is the mechanism the parent repository’s examples/general and examples/supplementary libraries depend on. Configuration reading is layered separately: config_toml walks up from the target workspace to find dylint.toml, parses it once via dylint_internal::config, and caches it behind init_config/try_init_config so later config/config_or_default calls are cheap lookups rather than repeated file reads.

Tech Stack The crate targets Rust’s 2024 edition and only builds on the nightly toolchain (pinned via rust-toolchain.toml with the rustc-dev and llvm-tools-preview components), because it links directly against rustc’s internal rustc_driver, rustc_lint, rustc_session, rustc_data_structures, and rustc_span crates behind the rustc_private feature gate. Runtime dependencies are deliberately small: dylint_internal (shared config/env/path utilities within the Dylint monorepo), toml and serde for dylint.toml deserialization, paste for generating CamelCase struct identifiers from lint names inside the macros, rustversion for conditionally compiling around a specific rustc release date, and thiserror for the crate’s ConfigError type. A build.rs script (using cargo_metadata and toml) runs at compile time. Dev-dependencies (assert_cmd, rustc_version, tempfile) back a small integration-test suite that mostly asserts the toolchain channel is nightly and that the crate still builds under --cfg docsrs.

Code Quality The crate is exhaustively doc-commented — nearly every public macro and function carries doc-comments explaining preconditions (for example, that init_config must run before config_or_default), which the project also re-exports as its published README via cargo-rdme. Its own automated tests are thin (an integration test file asserting the nightly channel and one --cfg docsrs build check, the latter currently #[ignore]d due to an upstream nightly regression), but the crate’s real correctness coverage comes from the parent dylint monorepo’s broader CI matrix (ci.yml runs across ubuntu/macos/windows and exercises the example lint libraries, which are themselves built with these macros) rather than unit tests local to this crate. Error handling for config parsing goes through a typed ConfigError (via thiserror) surfaced as a ConfigResult, and the workspace enforces #[lints.rust.unexpected_cfgs] at deny level to keep the dylint_lib cfg-based conditional-compilation trick from silently breaking.

What Makes It Unique Most lint-authoring helpers either fork an existing linter’s internals or require reimplementing rustc’s LintPass/LintStore wiring by hand each time. dylint_linting’s differentiator is the constituent feature: a single flag flips the same lint source between “standalone loadable library” and “folded into a shared aggregate library” without source-level changes, which is what lets the sibling examples/ directory in the Dylint repository maintain dozens of individually testable lints that also ship as one combined library for end users. Combining that with first-class, per-target-workspace dylint.toml configuration (rather than compile-time cargo features) gives lint authors a way to ship tunable lints to consumers who never touch the lint’s own source tree.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search