How checking works¶
A run is a straight pipeline: discover files, extract names, check names, report. This page explains each stage and the design choices behind it.
graph LR
A[walk ROOT] --> B[detect language]
B --> C[extract definitions]
C --> D[check rules]
D --> E[findings + exit code]
1. Discovery¶
dddlint lint walks ROOT recursively, skipping generated and vendored
directories (.git, .venv, node_modules, __pycache__, target, dist,
build). Every remaining file is offered to language detection.
2. Language detection¶
Each file's suffix is mapped to a tree-sitter language via
tree-sitter-language-pack. Files with no recognised language are skipped
silently, so a repository can mix 306 languages and plain text without
configuration. The grammar is downloaded on first use and cached.
3. Extraction¶
The recognised source is parsed and flattened into a list of Definition
records — one per class, function, method, struct, interface, enum, trait,
variable, or constant. dddlint deliberately reads only names (plus their
location), never bodies. That is what keeps it language-agnostic: it never
needs a per-language query, only the universal notion of "a named definition".
4. Tokenisation¶
Every name is split into lowercase tokens on case and separator boundaries.
OrderManager, order_manager, and ORDER_MANAGER all tokenise to
("order", "manager"). Rules match on tokens, which is why they are
case-insensitive and naming-convention-agnostic by construction.
5. Rule evaluation¶
Two independent passes run over the definitions:
- Per-definition — for each name, check its tokens against the forbidden set and the alias map that apply to that file's path.
- Cross-codebase drift — group every name by its sorted token set; any group with more than one distinct spelling is a drift finding.
The config file is also checked against itself in the same run, so a contradictory vocabulary surfaces before it can mask real findings.
Scope precedence¶
Forbidden terms and alias maps are assembled per file in a fixed order:
- Global rules — always included.
- Domain rules — merged in when the file path matches the domain's
includeglobs. - Context rules — merged in last, so a context can override a domain.
Because the layers are merged, a domain or context can only ever add rules or override an alias mapping for its files — it can never switch off a global rule. "Context wins" falls out of ordering: the last writer of an alias mapping is the one that takes effect.
Why scan the whole workspace every time¶
Drift is a property of the whole codebase, not a single file — you cannot know
that getUser and get_user disagree by looking at either one alone. So both
dddlint lint and the LSP server always scan the
entire tree, even when the editor only asked about one open file. It costs more
per run and guarantees drift is never missed.
The exit code as an interface¶
Everything the CLI does reduces to one bit at the boundary: 0 for clean, 1
for findings. That single contract is what lets the same binary drop into a
CI job, a pre-commit hook, and a
coding agent's feedback loop without any of them knowing anything about DDD.