modum 0.2.0

Workspace lint tool for Rust naming and API-shape policy
Documentation

modum

modum enforces consistent module naming, import style, and public API paths across a Rust workspace.

It is a lint tool. It reports diagnostics. It does not rewrite code.

Why It Exists

These API shapes are legal Rust, but they add noise:

use http::Client;
use user::UserRepository;

pub use partials::error::Error;

These read more clearly:

use http;
use user;

pub use partials::Error;

And these public paths usually read better:

user::Repository
user::error::InvalidEmail
partials::Error

instead of:

user::UserRepository
user::error::InvalidEmailError
partials::error::Error

modum enforces that style across an entire workspace.

Mental Model

modum follows three rules:

  1. Keep namespace context visible at call sites.
  2. Keep public paths meaningful without redundancy.
  3. Use modules for domain boundaries, not file organization.

Quick Usage

cargo run -p modum -- check --root .
cargo run -p modum -- check --root . --mode warn
cargo run -p modum -- check --root . --format json

If you already built the binary:

target/debug/modum check --root .

Environment:

MODUM=off|warn|deny

Default mode is deny.

Exit Behavior

  • 0: clean, or warnings allowed via --mode warn
  • 2: warning-level policy violations found in deny mode
  • 1: hard errors, including parse/configuration failures and error-level policy violations such as api_organizational_submodule_flatten

Configuration

Configure the lints in any workspace with Cargo metadata:

[workspace.metadata.modum]
generic_nouns = ["Id", "Repository", "Service", "Error", "Command", "Request", "Response"]
weak_modules = ["storage", "transport", "infra", "common", "misc", "helpers", "helper", "types", "util", "utils"]
catch_all_modules = ["common", "misc", "helpers", "helper", "types", "util", "utils"]
organizational_modules = ["error", "errors"]
namespace_preserving_modules = ["auth", "command", "email", "error", "http", "page", "partials", "policy", "query", "repo", "store", "storage", "transport", "infra"]

Use [package.metadata.modum] inside a member crate to override workspace defaults for that package.

Tuning guide:

  • generic_nouns: generic leaves like Repository, Error, or Request
  • namespace_preserving_modules: modules that should stay visible at call sites, such as http, email, or query
  • organizational_modules: modules that should not leak into the public API surface, such as error

Lint Categories

Import Style

These warn when imports or re-exports flatten a namespace that should stay visible.

  • namespace_flat_use
  • namespace_flat_use_preserve_module
  • namespace_flat_use_redundant_leaf_context
  • namespace_flat_pub_use
  • namespace_flat_pub_use_preserve_module
  • namespace_flat_pub_use_redundant_leaf_context

Examples:

  • use storage::Repository;
  • use http::Client;
  • use user::UserRepository;

Public API Paths

These warn when public leaves are too generic for a weak parent, or when the path repeats context it already has.

  • api_weak_module_generic_leaf
  • api_redundant_leaf_context
  • api_redundant_category_suffix

Examples:

  • storage::Repository
  • user::UserRepository
  • user::error::InvalidEmailError

Module Boundaries

These catch weak or redundant public module structure.

  • api_catch_all_module
  • api_repeated_module_segment

Examples:

  • helpers
  • error::error

Structural Errors

This is an error-level rule, not a warning.

  • api_organizational_submodule_flatten

Example:

  • partials::error::Error should usually be partials::Error

What It Does Not Check

Some naming-guide rules stay advisory because they are too semantic to lint reliably without compiler-grade context.

Examples:

  • choosing the best public path among several plausible domain decompositions
  • deciding when an internal long name plus pub use ... as ... is the right tradeoff
  • deciding whether a new module level adds real meaning or only mirrors the file tree in edge cases

Scope

Default discovery:

  • package root: scans <root>/src
  • workspace root: scans each member crate's src

Override discovery with --include:

modum check --root . --include crates/api/src --include crates/domain/src

False Positives And False Negatives

The broader import-style lints only inspect module-scope use items. They do not scan local block imports inside functions or tight test scopes, because those scopes often benefit from flatter imports.

To reduce false negatives:

  • extend namespace_preserving_modules for domain modules like user, billing, or tenant
  • keep generic_nouns aligned with the generic leaves your API actually uses
  • keep organizational_modules configured so partials::error::Error-style paths stay blocked