Linting

The ubCode language server has a linting feature built in and detects a broad range of RST issues. The linting feature is enabled by default. Specific rules can be deactivated in the ubproject.toml file.

Lint errors appear in the editor as well as in the problems tab. The same rules also run on the command line through ubc check, the canonical project linter for CI and pre-commit use.

Supported lint rules:

Identifier

Description

source.tab_in_line

Warns on tabs in a line, which can degrade performance of source mapping.

block.blank_line

Warns on missing blank lines between syntax blocks.

block.title_line

Warns on issues with title under/over lines.

block.title_disallowed

Warns on unexpected titles in a context where they are not allowed.

block.paragraph_indentation

Warns on unexpected indentation of a paragraph line.

block.literal_no_content

Warns on literal blocks with no content.

block.target_malformed

Warns on malformed hyperlink targets.

block.substitution_malformed

Warns on malformed substitution definition.

block.table_malformed

Warns on malformed tables.

block.inconsistent_title_level

Warns on inconsistent title levels, e.g. a level 1 title style followed by a level 3 style.

block.directive_unknown

Warns on unknown directives.

block.directive_indented_options

Warns if the second line of a directive starts with an indented :.

block.directive_malformed

Warns on malformed directives.

block.directive_no_content

Warns on directives with missing required content.

block.directive_content_not_permitted

Warns on a directive given a body when its definition permits none (for example .. rubric:: or .. image::). The body is discarded.

block.directive_no_argument

Warns on directives with a missing required argument.

inline.no_closing_marker

Warns on inline markup with no closing marker.

inline.role_malformed

Warns on malformed inline roles.

The rules above are reported by the reStructuredText parser. The Markdown parser reports its own, smaller set of parse-level codes, named without the block. prefix because Markdown directives are fences rather than explicit-markup blocks:

Identifier

Description

directive.unknown

Warns on unknown directives.

directive.malformed

Warns on a directive whose option block cannot be read (invalid YAML, or a malformed :key: value line), and on a repeated option key.

directive.no_content

Warns on directives with missing required content.

directive.content_not_permitted

Warns on a directive given a fence body when its definition permits none (for example {rubric} or {image}). The body is discarded, as it is in reStructuredText, where the same case is reported as block.directive_content_not_permitted.

directive.no_argument

Warns on directives with a missing required argument.

Markup-semantics diagnostics

Added in version 0.31.0.

In addition to the parse-level rules above, ubCode analyses the meaning of the parsed markup (directive options, diagram directives, document structure) and reports the issues it finds. Most apply to reStructuredText and Markdown documents alike — a row says so when it does not — and they are reported in the editor, by ubc check, and in the ubc build diagnostics log.

The rules in this table are warnings unless a row says otherwise. Their source label is ubcode-markup for most rules and ubcode-syntax for some, so an editor filter on the source column should include both.

Identifier

Description

directive.invalid_option_value

Warns when a directive option value cannot be interpreted as its declared type.

directive.invalid_option_choice

Warns when a directive option value is outside its declared set of choices (e.g. :align: on an image).

directive.diagram_content_and_file

Warns when a diagram directive (e.g. mermaid) has both inline content and a file argument; the file argument is ignored.

directive.unknown_role

Warns when a default-role directive names an unknown interpreted text role (the default role is left unchanged, as in docutils). Carries the source label ubcode-syntax.

directive.invalid_role

Warns when a role directive’s arguments are not valid role names, or its base role (.. role:: name(base)) is unknown — the role is not defined. Carries the source label ubcode-syntax.

directive.role_redefinition

Warns when a role directive redefines a role already defined earlier in the same document (the last definition wins, as in docutils). Carries the source label ubcode-syntax.

directive.class_invalid_argument

Warns when an rst-class / cssclass argument contains a token that normalises to nothing (.. rst-class:: 123). The directive renders as a plain directive block and no class is attached, where docutils reports an error and drops it. Carries the source label ubcode-syntax.

directive.class_no_target

Warns when an rst-class / cssclass with no content has no element after it to attach to. Nothing is rendered, where docutils renders an error block quoting the directive’s source. Carries the source label ubcode-syntax.

directive.class_unsupported_target

Warns when the element an rst-class / cssclass would attach to is not one ubCode can render a class attribute on (a transition, a footnote definition, a sphinx-design component, an .. include::). The message names the element. No class is attached — the alternative would be storing a class that never appears and never warns. See CSS classes for the elements a class CAN land on.

directive.replace_single_paragraph

Warns when a replace substitution body is not a single paragraph (the definition is left empty, as in docutils). Carries the source label ubcode-syntax.

directive.invalid_character_code

Warns when a unicode substitution code point is out of range (the definition is left empty, as in docutils).

directive.invalid_date_format

Warns when a date substitution format string cannot be interpreted. Carries the source label ubcode-syntax.

directive.nesting_too_deep

Warns when directives are nested more than 64 levels deep. Reported once per crossing point — the first directive past the cap — so two over-deep chains side by side warn twice, and a single chain 300 levels deep still warns once. That directive and everything inside it render as plain directive blocks instead of being interpreted. Their own body still renders; a directive that pulls in or generates content (for example include) contributes nothing at that depth, and a need declared there is not indexed.

heading.inconsistent_level

Warns on non-consecutive Markdown heading levels, e.g. a # heading followed by ### (all Markdown flavours).

title.cross_reference

Notes that a cross-reference role (:ref:, :doc:, :term:, :external:…) or a MyST link is written inside a page or section title — an informational message, not a warning. Carries the source label ubcode-syntax. A title is re-used outside the page body, and a link cannot nest inside the link a navigation or contents entry already is, so the reference is shown there as plain text. In the bare form (:ref:`target`) a document title’s chrome also shows the raw target name rather than the target’s title. Move the reference into the body, or — for a role — write the explicit-text form (:ref:`text <target>`). No rewrite is suggested for a MyST link: its destination as written is not recoverable once the link is parsed, so any spelled-out form could point somewhere else. A Sphinx build accepts the construct silently, so this is info-level and never fails ubc check. Need roles (:need:, :need_part:) are deliberately excluded.

role.invalid_pep_number

Warns when a :pep: role’s number is not a value from 0 to 9999.

role.invalid_rfc_number

Warns when an :rfc: role’s number is not a value greater than or equal to 1.

role.raw_no_format

Warns when a raw role is used without an output format — either the bare :raw: form or a role derived from it with no :format: option. Nothing is rendered. docutils refuses the same construct with an error; write .. role:: raw-html(raw) with :format: html and use that instead.

role.raw_disabled

Warns when a raw (or raw-derived) role is used while raw_enabled is switched off (see Parsing). Nothing is rendered. The companion code for the directive is directive.raw_disabled; the two are separate so either can be silenced on its own.

download.not_readable

Warns when a :download: role’s target names no readable file. The role renders its text with no link (as a Sphinx build does), and nothing is published for it. Reported at index time — while you type — where a Sphinx build reports it only when the site is built. The companion build-time code is build.download_read_failed, for a file that disappears during a build. Carries the source label ubcode-syntax.

md.html_image_no_src

Warns when an <img> tag has a missing or empty src attribute, with the MyST html_image extension enabled (see Parsing). The whole HTML block is left raw — every tag in it, converted or not — so the markup still reaches the page. Only Markdown documents can raise it.

needs.variant

Warns when a :variant: role cannot be resolved, because no variant data is configured or the expression does not resolve.

topmatter.malformed_yaml

Warns when a Markdown document’s YAML front matter cannot be parsed. The front matter is ignored and the document still builds.

topmatter.not_a_mapping

Warns when a Markdown document’s YAML front matter parses but is not a mapping of keys to values (a bare list or scalar). The front matter is ignored and the document still builds.

topmatter.invalid_tocdepth

Warns when a document’s tocdepth topmatter value is not an integer. The value is treated as 0 (unlimited depth), as in Sphinx, which does this silently.

attrs.invalid_value

Warns when a MyST attribute group gives a value the key cannot read — an image width or height that is not a measure, or a lineno-start that is not a whole number. The attribute is ignored and the rest of the group still applies. MyST warns for width/height written in full, crashes the build for the w/h aliases, and is silent for lineno-start.

attrs.invalid_choice

Warns when a MyST attribute group gives a value outside the key’s fixed set — an image align that is not left/center/right, or an ordered-list style that is not one of the five enumeration names. The attribute is ignored. MyST is silent for style.

attrs.no_target

Warns when a block attribute group has no block below it to attach to (the end of the file, or the end of a list item or block quote). The group is ignored, as it is in MyST, which says nothing.

attrs.unsupported_target

Warns when the element below a block attribute group cannot carry attributes at all — a comment, a target, a block break, a thematic break, an HTML block, a math block, a footnote definition or an {eval-rst} fence. A link reference definition is not one of them: both engines skip it and the group attaches to the block below, so nothing is reported. The message names the element. The group is ignored, as it is in MyST.

attrs.unsupported_key

Informational. Reports a key the element it landed on does not support — {width=10px} on a paragraph, {start=3} on a bullet list. The message names the element and lists the keys it does support. The key is dropped, as it is in MyST, which says nothing; it is graded as information rather than a warning because the attribute grammar accepts keys that are not even valid HTML attribute names, so a dropped key is often inert rather than wrong.

These codes are governed by the same lint configuration as every other rule: ignore, per-file-ignores, message-ignores, and the --extend-ignore flag of ubc check all apply.

Toctree diagnostics

Added in version 0.31.0.

ubCode expands the project’s toctree directives into a single navigation tree, rooted at the configured root document, and reports the structural problems it finds along the way. These are project-level diagnostics — they depend on which documents exist and how the toctrees reference one another — and surface in the editor, by ubc check, and in the ubc build diagnostics log.

Identifier

Description

toctree.nonexisting_document

Warns when a toctree entry references a document that does not exist (including an entry that names its own containing document).

toctree.duplicate_entry

Warns when a document is listed more than once within a single toctree; the duplicate entry is kept.

toctree.circular

Warns when a circular toctree reference is detected and pruned.

toctree.empty_glob

Warns when a toctree :glob: pattern matches no documents.

toctree.not_included

Warns when a document is not reachable from any toctree; the root document and documents pulled in via include are exempt.

toctree.multiple_toc_parents

Notes that a document is referenced from more than one toctree — an informational message, not a warning.

toctree.missing_root_document

Warns when the configured root document ([project].root_doc) does not exist.

toctree.variant_excluded

Notes that a toctree entry names a document this variant’s source rules removed — an informational message, not a warning, and the message names the rule that removed it.

toctree.no_title

Warns when a toctree entry names a document that has no title and holds no toctree of its own. Such an entry generates no link, exactly as in a Sphinx build, and the document keeps its place in the previous/next chain. Silencing the warning does not restore the link.

Like every other rule, these codes are governed by the lint configuration (ignore, per-file-ignores, message-ignores, and the --extend-ignore flag of ubc check).

Imported-needs diagnostics

Everything a .. needimport:: reports is anchored at the directive and governed by the lint configuration. Most of it arrives under one umbrella code; three causes have their own, so they can be graded and silenced independently of genuine read failures.

Identifier

Description

needs.import

The umbrella code: a source that could not be read or parsed, a need that could not be built, and every construct refused inside imported content (see What works inside imported need content).

needs.import.filter_empty

Notes that a :filter: selected none of the needs its source offered, so the import contributed nothing — an informational message, not a warning, because a filter can legitimately match nothing today and something tomorrow. Never reported for a source that was empty to begin with.

needs.import.ids_unmatched

Warns that an :ids: entry names a need the source does not contain, naming the unmatched ids (a handful, then a count of the rest). A warning rather than a note, unlike filter_empty above: a filter is a predicate that may legitimately match nothing, while an id is a name that either exists in the source or is a typo.

needs.import.doctype_ambiguous

Warns that two or more configured parsers claim an imported need’s doctype with the same implied suffix. The content is parsed with the first parser in name order; give the parsers include globs with different suffixes to choose deliberately.

Because a bare code in ignore matches exactly (see Code matching and .* wildcards), ignore = ["needs.import"] silences the umbrella code only — it does not reach the three codes above, and silencing any of those does not hide a source that failed to read. To silence the umbrella and all three at once, use the family wildcard ignore = ["needs.import.*"].

Linting on the command line

Added in version 0.31.0.

The ubc check command is the canonical project linter, in the mould of ruff check and cargo check. It reports the same project-wide diagnostics the editor shows, so a passing ubc check and a clean Problems view mean the same thing.

There are three scopes:

  • ubc check (no paths) checks the whole project, discovered by walking up from the current directory to the nearest ubproject.toml. Every diagnostic family runs — the parse, markup-semantics, toctree, reference, need, and schema families above — and both reStructuredText and Markdown documents are included.

  • ubc check <paths> still resolves the whole project, so cross-file diagnostics such as duplicate need IDs and broken references stay correct, but reports only the diagnostics located under the named paths. Project-level configuration problems are always reported, whatever the paths.

  • ubc check --per-file <paths> is the fast, project-free path: each named file is parsed in isolation, so only the per-file (parse and markup) families run — no cross-file, reference, toctree, or schema checks — and non-RST files such as Markdown are skipped.

Cache. The whole-project and scoped modes read and warm the on-disk .ub_cache, exactly like ubc build index, so repeated runs are fast; pass --no-cache for a run that neither reads nor writes it. --per-file never touches the cache.

Scoped details. A named path that does not exist on disk is an error, so a typo in a CI recipe fails rather than passing green; a path that exists but matches no source file is a warning on stderr. Configuration-less files that span unrelated directory trees are refused (use --per-file instead of walking the whole disk).

Exit policy. By default any warning-or-worse finding fails the command (exit 1); info-level findings alone do not. --deny <none|info|warning|error> moves the failure threshold (none never fails the threshold), and --max-warnings <N> caps the number of warnings — when both are given the stricter one wins. ubc build html applies the same policy over both of its warning streams, and can declare the threshold in configuration — see Building for production. An invalid flag value is a usage error (exit 2).

License. Like ubc build index, ubc check runs without a license on small projects (up to five files) and fails with an error above that; --per-file requires an active license for any non-open-source project.

Human output. Each finding prints as a block headed by its severity and its code — error[needs.schema_definition_violation], warning[image.not_found], info[toctree.multiple_toc_parents] — followed by its source locations and its message. The label is plain text, so it survives a pipe, a CI log and a file: grep '^warning\[' over a captured run lists every warning. On a terminal the header is coloured by severity, red for errors and yellow for warnings, and an informational block is dimmed throughout; when the output is piped or redirected (unless colour is explicitly forced), or NO_COLOR is set to a non-empty value, the escapes are omitted and the words remain. The same block is printed by ubc build index, ubc build html and ubc build linkcheck, so one finding reads the same way whichever command reports it.

While it works. Every command that indexes a project for a person — ubc check, every ubc build, ubc report, ubc schema and ubc diff — says what it is about to do — Building index for: <config> and, when there is anything to parse, Processing N files — and then, on a terminal, shows one progress bar redrawn in place until the pass ends:

indexing 412/1001 [████████░░░░░░░░░░░░] 4s

The count is the files being parsed this run, not the project’s file count: a warm run reuses everything that has not changed, so a project of a thousand files with one edit reports Processing 1 file and finishes at 1/1. The bar is cleared the moment the last file is parsed, so every line the run prints afterwards — the remaining pipeline lines, the findings, the summary — starts on a clean row; on ubc build html the build’s own bars come next, and each of those is cleared the same way. It is written only when standard error is a terminal: a pipe, a file or a CI log gets the opening lines and then nothing until the result, so a captured log stays greppable and carries no carriage returns. Setting CI suppresses it even on a runner that allocates a terminal, as do TERM=dumb, an unset TERM and --quiet. --verbose replaces it with one parsed <path> line per file as each is parsed — the line and the bar are mutually exclusive, since both want the same row.

ubc build html shows one more bar for each of the build’s three main phases: rendering the pages, assembling them into the site, and finishing with the assets.

rendering 412/1001 [████████░░░░░░░░░░░░] 4s

They appear in that order, each cleared before the next one opens, and the last cleared before the build’s summary line — so a build that used to spend seconds saying nothing now says which phase it is in and how far through. Every rule above applies unchanged: a terminal only, nothing under --quiet, CI, TERM=dumb or an unset TERM, and under --verbose the Timings: line reports each phase’s duration instead.

Machine-readable output. --output-format json writes a single JSON document to stdout — the diagnostics and a summary severity tally — with every other line on stderr, so stdout is safe to pipe into a JSON consumer:

$ ubc check --output-format json
{
  "diagnostics": [
    {
      "code": "image.not_found",
      "severity": "warning",
      "message": "Image file not found: 'missing.png'",
      "sources": [
        {"stype": "local", "path": "index.rst", "line": 4, "column": 12, "label": null}
      ]
    }
  ],
  "summary": {"errors": 0, "warnings": 1, "infos": 0}
}

The item shape matches the editor’s project-wide diagnostics (code, message, and a sources list tagged by stype), with a severity field added. The exit code and the --deny / --max-warnings policy are unchanged by the format.