Polyglot ubiquitous language linter. Reads class, function, method, and type names across 306 languages and enforces them against a domain vocabulary — banned terms, non-canonical synonyms, and one concept spelled multiple ways.
Works with any language tree-sitter recognises, without per-language queries. Slots into pre-commit hooks, CI pipelines, and coding agent loops via a non-zero exit code on findings. Ships with an LSP server for inline editor diagnostics and rename code actions.
uv add dddlint# lint current directory (looks for dddlint.yaml)
dddlint lint
# lint a specific path
dddlint lint src/
# open an interactive language graph in the browser
dddlint html
# start the LSP server (stdio)
dddlint lspPlace dddlint.yaml at the project root. When a path is passed to lint or html, the config is looked up in that directory first, then falls back to the current working directory.
similarity_threshold: 0.85 # Jaccard threshold for drift detection
enforce_canonical: true # flag alias terms in addition to forbidden ones
# terms that must never appear in a definition name
forbidden:
- util
- helper
- manager
# canonical terms and their aliases
synonyms:
- canonical: customer
aliases: [client, user, accountholder]
- canonical: order
aliases: [purchase, transaction]
# high-level business domains
domains:
- name: commerce
include: ["**/commerce/**"]
synonyms:
- canonical: order
aliases: [purchase]
# bounded contexts — same structure as domains, applied after (context wins on conflict)
contexts:
- name: billing
include: ["**/billing/**"]
forbidden: [discount]
synonyms:
- canonical: invoice
aliases: [bill, statement]Global rules apply everywhere. Domain rules apply to matching paths. Context rules apply after domains, so a context can override a domain synonym.
| Rule | Severity | Description |
|---|---|---|
forbidden |
error | A definition name contains a banned term |
alias |
warning | A definition uses a non-canonical synonym — includes a rename suggestion |
drift |
info | The same concept is spelled multiple ways across the codebase |
config:forbidden-canonical-clash |
error | A term is both forbidden and a canonical synonym |
config:alias-conflict |
warning | The same alias maps to different canonicals in different scopes |
config:duplicate-name |
info | Two domains or contexts have suspiciously similar names |
Config rules are checked against dddlint.yaml itself on every run.
The LSP server publishes diagnostics on file open and save, scanning the entire workspace each time so cross-file drift is always caught. Alias findings include a code action to rename the identifier to the canonical term with case preserved (ClientRepo → CustomerRepo, get_client → get_customer).
Neovim — add to init.lua:
vim.api.nvim_create_autocmd("BufReadPost", {
callback = function(args)
local root = vim.fs.root(args.buf, { "dddlint.yaml" })
if root then
vim.lsp.start({
name = "dddlint",
cmd = { "dddlint", "lsp" },
root_dir = root,
}, { bufnr = args.buf })
end
end,
})The autocmd fires on every buffer, attaches only when dddlint.yaml is found, and is language-agnostic — no filetype list required.
VS Code — via a generic LSP client extension:
{
"lsp.servers": {
"dddlint": {
"command": ["uvx", "dddlint", "lsp"],
"filetypes": ["*"]
}
}
}Helix — .helix/languages.toml:
[language-server.dddlint]
command = "dddlint"
args = ["lsp"]Extraction is powered by tree-sitter-language-pack, which covers 306 languages including:
Ada · Agda · Arduino · Bash · C · C++ · C# · Clojure · COBOL · Crystal · CSS · D · Dart · Dockerfile · Elixir · Elm · Erlang · F# · Fortran · GDScript · GLSL · Go · GraphQL · Groovy · Hack · Haskell · HCL · HTML · Java · JavaScript · Julia · Kotlin · Lean · Lua · MATLAB · Mojo · Nix · OCaml · Odin · Pascal · Perl · PHP · PowerShell · Prolog · Python · R · Racket · Ruby · Rust · Scala · Scheme · Solidity · SQL · Svelte · Swift · Terraform · TLA+ · TOML · TypeScript · V · VHDL · Vim · Vue · WebAssembly · XML · YAML · Zig — and 243 more.
# .github/workflows/dddlint.yml
- run: uvx dddlint lintExit code is 0 when clean, 1 when findings exist.
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: dddlint
name: dddlint
entry: dddlint lint
language: python
pass_filenames: falseThe language pack downloads parsers on first use. For CI or air-gapped runs, warm the cache in the image:
python -c "import tree_sitter_language_pack as t; t.download_all()"