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
The binary is named cargo-convention-lint, so Cargo picks it up automatically as a subcommand:
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
[[]]
= ["src"]
= ["*.rs"]
= "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:
[[]]
= ["some/dir", "../../some/dir2"]
= ["*.rs"]
= "camelCase"
Include + exclude — check .py and .sh but skip __init__.py:
[[]]
= ["other/dir"]
= ["*.py", "*.sh"]
= ["__init__.py"]
= "PascalCase"
Only exclude — check everything except specific files:
[[]]
= ["other/dir3"]
= ["**/the-only-exclude.txt"]
= "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 likeexclude = ["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:
[[]]
= ["**/*/*src/"]
# This ensures every file inside this path is ignored
= ["**/core/cli/src/**"]
= "snake_case"
### Globbed directories
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.
[[]]
= ["packages/*/tests"]
= ["*.rs"]
= "snake_case"
# Recursive wildcard — matches src/flow, src/a/flow, src/a/b/flow, etc.
[[]]
= ["src/**/flow"]
= ["*.rs"]
= "snake_case"
Non-recursive search
By default every directory is scanned recursively. Set recursive = false to check only the direct children:
[[]]
= ["dir1/tests"]
= false
= "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:
[[]]
= ["crates/*/src"]
= ["*.rs"]
= "snake_case"
[[]]
= ["proto"]
= ["*.proto"]
= "snake_case"
Then run:
# or point at a specific manifest:
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)
Full test suite:
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
||
Library usage
The crate also works as a library if you want to embed it in a build script or another tool:
[]
= "0.3"
use ;
use Path;
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.