convention-lint 0.3.0

File-naming convention linter configurable via Cargo.toml metadata — use as `cargo convention-lint` or embed as a library
Documentation

convention-lint

A file-naming convention linter for Rust projects. Configure it once in Cargo.toml, run it as a Cargo subcommand.

Uses the ignore crate for parallel directory traversal (same as ripgrep), so it respects .gitignore and skips hidden files out of the box. Errors are printed in rustc/clippy style.


Installation

cargo install convention-lint

The binary is named cargo-convention-lint, so Cargo picks it up automatically as a subcommand:

cargo convention-lint


Configuration

Each check is a [[...checks]] entry under package.metadata.convention-lint (or workspace.metadata.convention-lint). Every entry specifies which directories to scan, which files to include/exclude, and what naming convention to enforce.

Basic example

[[package.metadata.convention-lint.checks]]

dirs    = ["src"]

include = ["*.rs"]

format  = "snake_case"

Fields

Field Required Default Description
dirs yes Directories to scan. Supports glob patterns (*, **, ?) Cannot be empty..
include no all files Glob patterns for files to check (e.g. ["*.rs", "*.py"]).
exclude no none Glob patterns for files to skip (takes priority over include).
format yes Naming convention to enforce (see supported conventions).
recursive no true When false, only direct children of each dir are checked.

Note on Filtering: If a file matches both an include and an exclude pattern, it will be skipped.

Include / exclude filtering

Only include — check only .rs files:

[[package.metadata.convention-lint.checks]]

dirs    = ["some/dir", "../../some/dir2"]

include = ["*.rs"]

format  = "camelCase"

Include + exclude — check .py and .sh but skip __init__.py:

[[package.metadata.convention-lint.checks]]

dirs    = ["other/dir"]

include = ["*.py", "*.sh"]

exclude = ["__init__.py"]

format  = "PascalCase"

Only exclude — check everything except specific files:

[[package.metadata.convention-lint.checks]]

dirs    = ["other/dir3"]

exclude = ["**/the-only-exclude.txt"]

format  = "PascalCase"

Pattern Matching Rules

Patterns are matched against the full relative path from the project root. To ensure your filters work as expected, follow these rules:

Pattern Match Type Description
*.rs Extension Any .rs file in any directory.
generated.rs Exact File Only generated.rs in the project root.
**/generated.rs Floating File Any file named generated.rs anywhere in the project.
**/tests/** Directory Entire directory and all its contents (recursive).
core/cli/src/** Subtree Everything inside a specific path.

Pro Tip: If you want to exclude a whole folder, always end the pattern with /**. A pattern like exclude = ["path/to/dir"] matches only the directory itself, not the files inside it.

Example: Excluding a module

To skip an entire crate or a specific deep directory:

[[workspace.metadata.convention-lint.checks]]

dirs = ["**/*/*src/"]

# This ensures every file inside this path is ignored

exclude = ["**/core/cli/src/**"]

format = "snake_case"



### Globbed directories



The `dirs` field supports glob patterns so you can target many directories with a single rule:



```toml

# Single-level wildcard — matches packages/foo/tests, packages/bar/tests, etc.

[[package.metadata.convention-lint.checks]]

dirs   = ["packages/*/tests"]

include = ["*.rs"]

format  = "snake_case"



# Recursive wildcard — matches src/flow, src/a/flow, src/a/b/flow, etc.

[[package.metadata.convention-lint.checks]]

dirs   = ["src/**/flow"]

include = ["*.rs"]

format  = "snake_case"

Non-recursive search

By default every directory is scanned recursively. Set recursive = false to check only the direct children:

[[package.metadata.convention-lint.checks]]

dirs      = ["dir1/tests"]

recursive = false

format    = "snake_case"

# dir1/tests/foo.rs        — checked

# dir1/tests/sub/bar.rs    — skipped

Workspace config

Put the config in the root Cargo.toml under workspace.metadata:

[[workspace.metadata.convention-lint.checks]]

dirs    = ["crates/*/src"]

include = ["*.rs"]

format  = "snake_case"



[[workspace.metadata.convention-lint.checks]]

dirs    = ["proto"]

include = ["*.proto"]

format  = "snake_case"

Then run:

cargo convention-lint

# or point at a specific manifest:

cargo convention-lint --manifest-path path/to/Cargo.toml

Exit code is 0 if everything passes, 1 if there are violations.

Note Rule Merging: If you define checks in both workspace.metadata and package.metadata, the linter will combine them. This allows you to set global rules for the whole project and specific rules for individual crates.


Supported conventions

Name Example
snake_case my_service
CamelCase MyService
camelCase myService
SCREAMING_SNAKE_CASE MY_CONSTANT
kebab-case my-service

PascalCase is accepted as an alias for CamelCase.


Output

error[convention]: `src/idl/MyService.idl` — stem `MyService` does not follow snake_case convention
error[convention]: `src/idl/badName.idl` — stem `badName` does not follow snake_case convention

convention-lint: found 2 naming violation(s)

Testing

tests/fixtures/ contains two small projects you can run against directly:

tests/fixtures/
├── pass/          ← all files conform → exit 0
│   ├── Cargo.toml
│   ├── idl/
│   │   ├── my_service.idl
│   │   └── order_processor.idl
│   └── src/
│       └── my_module.rs
└── fail/          ← intentional violations → exit 1
    ├── Cargo.toml
    ├── idl/
    │   ├── my_service.idl    ✓
    │   ├── MyService.idl     ✗  (should be snake_case)
    │   └── another_Bad.idl   ✗
    └── src/
        ├── OrderProcessor.rs ✓
        └── bad_module.rs     ✗  (should be CamelCase)
cargo run -- convention-lint --manifest-path tests/fixtures/pass/Cargo.toml

cargo run -- convention-lint --manifest-path tests/fixtures/fail/Cargo.toml

Full test suite:

cargo test


CI

GitHub Actions

- name: Install convention-lint
  uses: taiki-e/install-action@v2
  with:
    tool: convention-lint

- name: Check naming conventions
  run: cargo convention-lint

Pre-commit hook

#!/bin/sh

cargo convention-lint || exit 1


Library usage

The crate also works as a library if you want to embed it in a build script or another tool:

[dependencies]

convention-lint = "0.3"

use convention_lint::{config::load_config, lint::run};
use std::path::Path;

fn main() {
    let cfg = load_config(Path::new("Cargo.toml")).expect("failed to load config");
    let violations = run(&cfg, Path::new("."));

    for v in &violations {
        eprintln!("{v}");
    }

    if !violations.is_empty() {
        std::process::exit(1);
    }
}

Public API:

Item Description
convention_lint::Convention enum of supported conventions
convention_lint::Error error variants from config loading
convention_lint::Violation a single naming violation
convention_lint::config::load_config parse config from a Cargo.toml path
convention_lint::lint::run walk the filesystem and collect violations

Full docs on docs.rs/convention-lint.


License