Lints
A lint is a diagnostic that a project can turn off, report at a different
severity, or turn into an error. Every warning and every info diagnostic in the
catalogue is a lint: each has a stable kebab-case name, and its catalogue
severity is its default level, unless the table at the end of this page gives
its default as allow. A lint that is allow by default is reported only when
a project sets its level. An error diagnostic is never a lint, and its severity
cannot be changed.
The lint name appears in every report of a lint diagnostic. The JSON report of
ridl check --format json carries it in the
lint field, and the text report adds a note after the snippet:
warning[TYPL-102]: `float` without both a range and a `step`
┌─ ./demo.ridl:3:15
│
3 │ type Broken : km/h [250.0..0.0]
│ ^^^^
│
= lint: `unbounded-float` (set its level in `[lints]` in ridl.toml)
Levels
A lint has one of four levels:
allowremoves the diagnostic from the report.inforeports the diagnostic as an info.warnreports the diagnostic as a warning.denyreports the diagnostic as an error. Inridl check,ridl build,ridlc checkandridlc build, an error makes the command exit 1, so a lint atdenyfails the run the same way any other error does.
No lint defaults to deny.
The [lints] table
A level is set in the [lints] table of a ridl.toml. Each key is a lint name
and each value is one of the strings allow, info, warn or deny:
[lints]
missing-timing = "deny"
shared-error-type = "allow"
A package manifest and a workspace manifest both accept the table. There is no command-line flag that changes a level and no source attribute that changes a level: a CI that wants stricter checks commits the levels to the manifest, so the editor, the command line and an agent see the same levels for the same entry point.
The level of a lint for a package is resolved key by key, and a later source replaces an earlier one:
- the default level from the table below;
- the
[lints]table of the workspace root manifest, when the package is a workspace member; - the package's own
[lints]table.
A diagnostic takes the levels of the package whose directory contains the file
of its primary span. In a workspace, the root's [lints] table covers the
whole directory tree of the root: a file under the workspace root that no
member contains, and the root ridl.toml itself, take the root's levels. A
file outside the directory tree of the entry point (the standard library, a
fetched import, an editor buffer outside the project) uses the default levels,
so a project's [lints] never applies to code outside its directory. When there
is no manifest at all, for example ridl check on a single file outside any
package, the default levels apply.
A member loads its workspace. Running ridl check on a workspace member
or on a file inside a member, opening an editor on a member, or passing a
member as the path of the MCP tool ridl_check loads the workspace whose
members lists the member. The workspace root's [lints] table applies to the
member, and the member's own table is applied over it, as in a check from the
root. The [defaults] keys and [imports] behave the same way. The command
reports only the diagnostics of files under the member; check from the
workspace root to see every member's diagnostics. A package that no workspace
lists stays standalone, and the search for a workspace stops at the first
[workspace] manifest and at a directory that holds .git.
Known limitations of the language server. The language server reads the
[lints] tables once, when it loads the workspace: at start, or when the first
file is opened. An edit to a [lints] table takes effect in the editor after
the server restarts. The language server publishes the diagnostics of every
loaded member, also when the editor is opened on one member.
Where levels apply
Levels apply where diagnostics are reported to a person or an agent:
ridl check, including the desk check against a baseline;ridl build, which stops before it writes any artifact when a lint is atdeny;ridlc checkandridlc build;- the language server;
- the MCP tool
ridl_check.
Every other command reports the severities the emit sites chose: ridl diff,
ridl test, ridl baseline, ridl lock, ridl vendor, the MCP tool
ridl_diff and the MCP lookup tools. A lint at deny never makes one of them
fail. The text note that names the lint still appears in their output. A lint
that is allow by default is left out of their output, as it is everywhere
else.
Mistakes in [lints]
A mistake in the table never stops a check. Each of these raises MANI-010
(unknown-lint, a warning) on the entry, and the entry is ignored:
- the key is not a lint name, which includes a diagnostic code written as a key
(
RIDL-100 = "deny") and the name of an error code, which has none; - the value is not a string;
- the value is a string other than
allow,info,warnordeny, which includes a level with the wrong case ("Deny"); lintsis not a table.
MANI-010 is itself a lint, so the table can set its level.
The lints
| Lint | Code | Default | Summary |
|---|---|---|---|
unused-import | TYPL-007 | warn | unused import |
unneeded-import-alias | TYPL-008 | warn | import alias without an actual collision |
unbounded-integer | TYPL-101 | warn | integer without a range constraint |
unbounded-float | TYPL-102 | warn | float without both a range and a step |
unbounded-length | TYPL-103 | warn | string/bytes without explicit bounds |
no-init-value | TYPL-115 | info | type has no derivable init value and no declared = value |
bound-above-transfer | TYPL-116 | warn | string/bytes bound above the advised transfer limit |
duplicate-reserved | TYPL-211 | warn | duplicate reserved entry |
inconsistent-unit | TYPL-222 | info | one field name used with different units |
duplicate-shape | TYPL-224 | info | duplicate declaration shape |
broken-doc-link | TYPL-401 | warn | doc link or @see target that does not resolve |
detached-doc-comment | TYPL-404 | warn | blank line between a doc comment and its carrier |
deprecated-without-reason | TYPL-405 | warn | @deprecated doc tag without a reason string |
missing-docs | TYPL-406 | warn | item without a doc comment |
misplaced-doc-comment | TYPL-407 | warn | doc comment in a position that is not a carrier |
unknown-doc-tag | TYPL-408 | warn | doc tag other than @see, @since, @deprecated and @labels |
malformed-doc-tag | TYPL-409 | warn | @see or @since with a missing or malformed value |
doc-comment-style | TYPL-410 | allow | doc comment written as /** */ |
missing-timing | RIDL-100 | warn | signal or event without a timing annotation |
degenerate-timing-range | RIDL-108 | warn | degenerate timing range @[X..X] |
missing-response-bound | RIDL-112 | warn | command or query takes the default response bound |
payload-above-ceiling | RIDL-150 | warn | payload above the payload ceiling or with no finite bound |
podbuf-padding-order | RIDL-156 | info | struct field order wastes podbuf padding |
error-typed-parameter | RIDL-304 | warn | error-typed or result-union parameter on a command or query |
ensure-without-result | RIDL-305 | warn | ensure clause that never references result |
contract-error-name-in-enum | RIDL-307 | warn | contract-error category name declared in an error enum |
named-result-union-in-query | RIDL-308 | warn | named result union in query return position |
query-named-like-mutation | RIDL-404 | warn | query named like a mutation |
shared-error-type | RIDL-405 | info | one error type shared across unrelated failure domains |
redeclared-envelope-metadata | RIDL-406 | info | payload struct re-declares envelope metadata |
ordinal-changed | RIDL-407 | warn | interaction, struct field, or union arm ordinal changed against the published baseline |
low-cohesion-interface | RIDL-414 | info | interface members form disconnected type-sharing groups |
lock-in-subdirectory | RIDL-416 | warn | interfaces.lock in a subdirectory of a unit is not read |
unsupported-source-file | RIDL-417 | warn | .rxdl or .rmdl file is not compiled: its profile is not supported |
redundant-provider-set | RSDL-409 | warn | a requires resolves to a redundant provider set |
unclaimed-backend-key | RSDL-804 | warn | a backend key whose namespace no configured backend claims |
depth-below-bound | RSDL-805 | warn | a declared depth is below ceil(max / min) for an event a covered link consumes |
depth-underivable | RSDL-806 | warn | an event whose contract bound is not derivable is consumed by a link with no declared depth |
link-encoding-not-emitted | RSDL-807 | warn | a link of the selected deployment carries an encoding the emitted backend's encodings list does not name |
unknown-manifest-key | MANI-005 | warn | unknown manifest key |
unknown-lint | MANI-010 | warn | [lints] entry names no lint, or its value is not a level |
vendor-source-not-loaded | MANI-018 | warn | a source file under the root's vendor/ is not loaded |
vendor-source-not-loaded (MANI-018) is raised in standalone mode only; a
workspace has no such warning.
bound-above-transfer (TYPL-116) reports a string bound above 4,032
characters or a bytes bound above 16,128 bytes, on the bound.
payload-above-ceiling (RIDL-150) reports a payload whose largest encoded size,
in an encoding that the workspace manifest enables, is above 16,256 bytes or has no finite
bound. It reads the encodings from every [backend.<name>] table of the
workspace manifest, as one set, and checks FlatBuffers when that set is empty,
which is the case for no table and for tables whose encodings lists are all
empty. It reports on the
interaction that carries the payload, so a type that no interaction carries is
not reported. A fixed is provisioned and never in flight, so it is not
checked. A query whose reply is a fallible T | E is checked through both
types, and the larger problem is reported. Both messages say that the payload
ceiling is 16,256 bytes and that the codecs do not yet enforce it
(driftsys/ridl#996). Both default to warn, and a package can set either to
deny. The limits are stated in
ADR-0030.
[limits] in a unit's ridl.toml sets a payload limit below the ceiling for an
interaction. RIDL-151 (a payload above its declared limit or with no finite
bound) and RIDL-152 (a [limits] entry that names no interaction of its unit)
are errors, so they have no lint name and no level in [lints]. RIDL-151 runs
only where an entry gives the limit; where none does, payload-above-ceiling
alone applies. The table and the codes are described in the
CLI reference and in
ADR-0030.
payload-above-ceiling is raised by ridl check and ridl build. The language
server does not run it, so it does not appear in the editor. It skips a package
that has an error that stops ridl build writing artifacts, or that references
a package that has one, and still reports on the other packages of the
workspace. An RSDL-7xx error does not count, because a build still writes code
after it. An error in no package source file, such as a manifest error, skips
every package.
podbuf-padding-order (RIDL-156) reports a struct whose declared field order
lays out in a larger podbuf head than the same fields sorted by descending
alignment, with declaration order kept among fields of equal alignment. The
message names both head sizes and the saving order. It runs only when a
[backend.<name>] table of the workspace manifest lists podbuf in its
encodings, and only in ridl check, because it reads the published baseline
and ridlc does not. A struct is released when a snapshot under
.ridl/baseline/ (or under the --baseline path) declares it, and a released
struct is never reported, because its order is part of its wire format. With no
baseline, every struct is unreleased. The order is reported only when it is
strictly smaller than the declared order, so a struct that no order improves
draws nothing. The level is info by default.
Setting ordinal-changed to allow makes ridl check --baseline report
nothing for ordinal changes, so the baseline desk check no longer warns about
them.
link-encoding-not-emitted has a per-backend way to silence it, which the
[lints] level does not: a [backend.<name>] table that names no encoding —
an empty encodings list, or no encodings key at all — declares that the
backend emits no codec, and that backend's links are not compared. A backend
with no table at all is still compared. See
ridl build for the rule in full.
How the design lints' levels were set
inconsistent-unit, duplicate-shape and low-cohesion-interface are
workspace design lints: each reports a property of the whole checked workspace
rather than of one declaration. Their default levels and thresholds were not
chosen by hand. Each check was run over the evaluation corpus under evals/,
three workspaces translated from public interface definitions, and every
finding was labelled, blind, by two labellers, one of them a rule-based script,
with the maintainer deciding every disagreement. A check's precision is the
share of its findings that the labels accept:
- at least 80 %: the lint defaults to
warn; - from 50 % up to 80 %: the lint defaults to
info; - below 50 %: the check does not ship as a lint.
A check with fewer than ten findings on the corpus defaults to info at most,
because the sample is too small for warn. A threshold, such as the number of
groups low-cohesion-interface reports from, is the least strict value whose
precision still meets the level; it is a constant beside the check and is not
set in [lints]. Two candidate checks, one on inconsistent abbreviations and
one on a package's number of dependencies, fell below the floor and are not
lints; the package coupling numbers stay available through the MCP tool
ridl_metrics.
The derivation tables, with the precision at every candidate threshold and the
recall against the corpus review rubrics, are in
evals/calibration/summary.md;
the method, the outcome and the recall mapping are recorded in
evals/calibration/notes.md.
A test pins the number of findings each design lint reports on each corpus
workspace, so a change that moves a count is visible in review.