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:

  • allow removes the diagnostic from the report.
  • info reports the diagnostic as an info.
  • warn reports the diagnostic as a warning.
  • deny reports the diagnostic as an error. In ridl check, ridl build, ridlc check and ridlc build, an error makes the command exit 1, so a lint at deny fails 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:

  1. the default level from the table below;
  2. the [lints] table of the workspace root manifest, when the package is a workspace member;
  3. 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 at deny;
  • ridlc check and ridlc 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, warn or deny, which includes a level with the wrong case ("Deny");
  • lints is not a table.

MANI-010 is itself a lint, so the table can set its level.

The lints

LintCodeDefaultSummary
unused-importTYPL-007warnunused import
unneeded-import-aliasTYPL-008warnimport alias without an actual collision
unbounded-integerTYPL-101warninteger without a range constraint
unbounded-floatTYPL-102warnfloat without both a range and a step
unbounded-lengthTYPL-103warnstring/bytes without explicit bounds
no-init-valueTYPL-115infotype has no derivable init value and no declared = value
bound-above-transferTYPL-116warnstring/bytes bound above the advised transfer limit
duplicate-reservedTYPL-211warnduplicate reserved entry
inconsistent-unitTYPL-222infoone field name used with different units
duplicate-shapeTYPL-224infoduplicate declaration shape
broken-doc-linkTYPL-401warndoc link or @see target that does not resolve
detached-doc-commentTYPL-404warnblank line between a doc comment and its carrier
deprecated-without-reasonTYPL-405warn@deprecated doc tag without a reason string
missing-docsTYPL-406warnitem without a doc comment
misplaced-doc-commentTYPL-407warndoc comment in a position that is not a carrier
unknown-doc-tagTYPL-408warndoc tag other than @see, @since, @deprecated and @labels
malformed-doc-tagTYPL-409warn@see or @since with a missing or malformed value
doc-comment-styleTYPL-410allowdoc comment written as /** */
missing-timingRIDL-100warnsignal or event without a timing annotation
degenerate-timing-rangeRIDL-108warndegenerate timing range @[X..X]
missing-response-boundRIDL-112warncommand or query takes the default response bound
payload-above-ceilingRIDL-150warnpayload above the payload ceiling or with no finite bound
podbuf-padding-orderRIDL-156infostruct field order wastes podbuf padding
error-typed-parameterRIDL-304warnerror-typed or result-union parameter on a command or query
ensure-without-resultRIDL-305warnensure clause that never references result
contract-error-name-in-enumRIDL-307warncontract-error category name declared in an error enum
named-result-union-in-queryRIDL-308warnnamed result union in query return position
query-named-like-mutationRIDL-404warnquery named like a mutation
shared-error-typeRIDL-405infoone error type shared across unrelated failure domains
redeclared-envelope-metadataRIDL-406infopayload struct re-declares envelope metadata
ordinal-changedRIDL-407warninteraction, struct field, or union arm ordinal changed against the published baseline
low-cohesion-interfaceRIDL-414infointerface members form disconnected type-sharing groups
lock-in-subdirectoryRIDL-416warninterfaces.lock in a subdirectory of a unit is not read
unsupported-source-fileRIDL-417warn.rxdl or .rmdl file is not compiled: its profile is not supported
redundant-provider-setRSDL-409warna requires resolves to a redundant provider set
unclaimed-backend-keyRSDL-804warna backend key whose namespace no configured backend claims
depth-below-boundRSDL-805warna declared depth is below ceil(max / min) for an event a covered link consumes
depth-underivableRSDL-806warnan event whose contract bound is not derivable is consumed by a link with no declared depth
link-encoding-not-emittedRSDL-807warna link of the selected deployment carries an encoding the emitted backend's encodings list does not name
unknown-manifest-keyMANI-005warnunknown manifest key
unknown-lintMANI-010warn[lints] entry names no lint, or its value is not a level
vendor-source-not-loadedMANI-018warna 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.