Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Linter Rules

badness lint runs a set of built-in rules over each file’s parse tree and reports a diagnostic for every finding. This page is the catalogue: one section per rule, keyed by its stable rule id. That id is what appears in a diagnostic, what [lint] select/ignore (and --select/--ignore) target, and what a % badness-lint skip <id> comment suppresses.

Most rules are on by default. Each rule’s section states its default; enable an opt-in rule with select, or narrow the default set with select/ignore in the [lint] table (see the Configuration reference). Where a rewrite is unambiguous a rule carries an auto-fix: a safe fix (shown below as “After applying the fix”) is applied by badness lint --fix; an unsafe fix, one that may change output such as inserting a line-breaking tie, is applied only with --unsafe-fixes or as an editor code action, so it has no “after” block here.

Each example below is linted live to produce its diagnostic and fixed output, so this page never drifts from the rules’ actual behavior.

This page covers the LaTeX linter. BibTeX files have a parallel set of rules (a separate BibRule registry under src/bib/linter/), selectable through the same [lint] config and catalogued in BibTeX Linter Rules.

abbreviation-spacing

Flag TeX’s sentence-vs-interword spacing going wrong around abbreviations and acronyms (ChkTeX 12/13). Outside \frenchspacing, TeX widens the space after ./?/! unless the punctuation follows an uppercase letter. Two shapes defeat that: a lowercase abbreviation (e.g., i.e., etc., et al.) gets a too-wide space, fixed with \ (e.g.\ foo); and an uppercase acronym ending a sentence (USA.) gets a too-narrow space, fixed with \@ (USA\@.). To stay conservative the first fires only before a lowercase word (the sentence clearly continues) and the second only for a run of two or more capitals before the period and before an uppercase word (a new sentence), so initials (J.), dotted forms (U.S.A.), and mid-sentence acronyms are left alone. Both fixes are unsafe – they change the typeset spacing – so --fix leaves them alone while --unsafe-fixes and the editor code action apply them. The rule is silent under \frenchspacing, and never touches comments, verbatim, or math.

This rule is enabled by default.

A lowercase abbreviation followed by more text takes an interword space:

We tried several methods, e.g. gradient descent.
warning: abbreviation-spacing
 --> example.tex:1:31
  |
1 | We tried several methods, e.g. gradient descent.
  |                               ^ `e.g.` is an abbreviation, not a sentence end; use an interword space `\ ` (`e.g.\ `) so TeX does not widen the gap

An acronym ending a sentence takes intersentence spacing:

The rover reached the USA. Then it stopped.
warning: abbreviation-spacing
 --> example.tex:1:26
  |
1 | The rover reached the USA. Then it stopped.
  |                          ^ capital before sentence-ending punctuation suppresses intersentence spacing; use `\@` (`Word\@.`) to restore it

blank-line-in-keyval

Flag a blank line at the top level of a key=value argument. A blank line is a \par token and a keyval processor walks its entries with macros that are not \long, so the call aborts – and the error TeX reports names the processor rather than the command the author wrote (\hypersetup yields “Paragraph ended before \kv@processor@default was complete”), which is what makes the finding worth more than the compiler’s own message. Scoped by measurement: a blank line nested inside a value’s brace group (\tikzset{aa/.style={draw,\n\nthick}}) compiles clean and is not flagged, an unclosed { is left to the parse error it already draws, and only the hand-curated signature tier is consulted. The autofix drops the blank line and keeps the following indentation; it is safe by construction, since it edits only whitespace and ContentKind::Keyval is exactly the claim that the processor strips spaces around entries.

This rule is enabled by default.

A blank line separating two keys, which aborts the call:

\hypersetup{colorlinks=true,

linkcolor=blue}
error: blank-line-in-keyval
 --> example.tex:1:29
  |
1 |   \hypersetup{colorlinks=true,
  |  _____________________________^
2 | |
3 | | linkcolor=blue}
  | |_^ blank line in `\hypersetup`'s key-value argument; the `\par` aborts the call

After applying the fix:

\hypersetup{colorlinks=true,
linkcolor=blue}

duplicate-label

Flag a label key defined more than once in the same label namespace – within one file, or across files that share a document when a project view is available. LaTeX itself only warns and silently keeps the last definition. Within a file, a warning requires a prior definition in the same conditional branch or an enclosing context. Separate conditional tests are treated as uncertain and do not trigger a warning. Recognizes \if...\else...\fi and common macros with complete braced arguments, including \ifthenelse, \iftoggle, and \IfFileExists. Predicates are not evaluated, and coverage across branches is not combined. Cross-file checks use label namespaces. No autofix: resolving a collision (rename vs delete) is the author’s call.

This rule is enabled by default.

The same key defined twice in one file:

\section{One}\label{sec:x}
\section{Two}\label{sec:x}
warning: duplicate-label
 --> example.tex:2:14
  |
1 | \section{One}\label{sec:x}
  |                     ----- first definition of `sec:x`
2 | \section{Two}\label{sec:x}
  |              ^^^^^^^^^^^^^ label `sec:x` is defined more than once

deprecated-command

Flag the obsolete two-letter font switches (\bf, \it, \rm, \sf, \tt, \sc, \sl) that LaTeX 2e superseded with the \...series/\...shape/\...family declarations. \em is not flagged; it is still the supported emphasis switch. A name the file redefines (\renewcommand{\sl}{…}, \def\rm{…}) is the user’s macro, not the switch, so it is not flagged anywhere. The autofix swaps just the control word (\bf -> \bfseries), leaving any following text untouched, so it is correct by construction; it is withheld where the switch is merely referenced (\let\x\rm, \ifx\rm\y).

This rule is enabled by default.

An obsolete two-letter font switch:

{\bf important}
warning: deprecated-command
 --> example.tex:1:2
  |
1 | {\bf important}
  |  ^^^ `\bf` is deprecated; use `\bfseries`

After applying the fix:

{\bfseries important}

deprecated-suppression-syntax

Flag the retired % badness-ignore <rule> and % badness-ignore-file [<rule>] suppression spellings, which remain accepted for compatibility but are no longer documented. The Safe autofix rewrites only the family and verb to % badness-lint skip <rule> or % badness-lint skip-file [<rule>]; the selector and reason remain byte-for-byte unchanged, and the edit stays entirely inside a comment. This meta diagnostic is not silenced by the retired directive it reports; use [lint].ignore to disable the rule deliberately.

This rule is enabled by default.

A retired suppression directive:

% badness-ignore deprecated-command: legacy source
{\bf text}
warning: deprecated-suppression-syntax
 --> example.tex:1:3
  |
1 | % badness-ignore deprecated-command: legacy source
  |   ^^^^^^^^^^^^^^ retired suppression syntax; use `% badness-lint skip` instead

After applying the fix:

% badness-lint skip deprecated-command: legacy source
{\bf text}

missing-nonbreaking-space

Flag a plain space where a TeX tie (~) belongs, before a command whose output a line break would orphan: a bare-number reference (Figure \ref{x}, \eqref, \pageref) or a bracketed citation (see \cite{a}, \parencite, \autocite). A tie keeps the reference on the same line. Self-describing references (\autoref, \cref) and textual citations (\textcite, \citet) are not flagged – they emit their own noun, so a break orphans nothing. Both a same-line space and a single source line break before the command are flagged (a blank line is not – that starts a new paragraph). For a same-line space the fix is unsafe – inserting a tie changes line breaking – so --fix leaves it alone; --unsafe-fixes and the editor code action apply it. A line break is report-only: rewriting the newline to ~ would join the two lines, a reflow the formatter owns.

This rule is enabled by default.

A plain space where a tie belongs before a cross-reference:

see Figure \ref{fig:plot}
warning: missing-nonbreaking-space
 --> example.tex:1:11
  |
1 | see Figure \ref{fig:plot}
  |           ^ missing non-breaking space before `\ref`; use a tie `~` so the reference stays on the same line

obsolete-environment

Flag math environments the community has superseded, naming the modern replacement in the message. The canonical case is eqnarray, which amsmath replaced with align decades ago (it mis-spaces relations and is a perennial l2tabu warning). The autofix renames the \begin/\end pair in place, leaving the body untouched, so it is correct by construction.

This rule is enabled by default.

The superseded eqnarray environment:

\begin{eqnarray}
  a &=& b
\end{eqnarray}
warning: obsolete-environment
 --> example.tex:1:7
  |
1 | \begin{eqnarray}
  |       ^^^^^^^^^^ `eqnarray` is obsolete; use `align`

After applying the fix:

\begin{align}
  a &=& b
\end{align}

primitive-command

Flag raw plain-TeX primitives discouraged in LaTeX source, naming the LaTeX construct that supersedes each one (ChkTeX 41, lacheck, l2tabu). A sibling of deprecated-command, which covers the obsolete font switches. Most primitives are reported only: their LaTeX replacement restructures arguments (a \over b becomes \frac{a}{b}, \centerline{x} becomes a \centering declaration or a center environment), so no single textual edit can rewrite them correctly by construction. A few carry a Safe autofix — a 1:1 control-word swap for a primitive whose LaTeX form is a single meaning-identical token (\sb/\sp become _/^); the swap replaces just the control word, so it stays lossless and meaning-preserving, and is withheld where the primitive is merely referenced (\let\x\sp, \ifx\sp\y). A name the file redefines (\renewcommand\sp{…}) is the user’s macro, not the primitive, so it is not flagged anywhere. The implicit braces \bgroup and \egroup are not flagged: replacing them with literal braces can change macro argument and definition boundaries.

This rule is enabled by default.

A plain-TeX fraction primitive (report-only; the LaTeX form restructures its operands):

$a \over b$
warning: primitive-command
 --> example.tex:1:4
  |
1 | $a \over b$
  |    ^^^^^ `\over` is a raw TeX primitive; use `\frac{...}{...}`

The plain-TeX subscript alias, carrying a safe swap to _:

$x\sb2$
warning: primitive-command
 --> example.tex:1:3
  |
1 | $x\sb2$
  |   ^^^ `\sb` is a raw TeX primitive; use `_`

After applying the fix:

$x_2$

dollar-display-math

Flag plain-TeX $$...$$ display math. $$ is a TeX primitive that bypasses amsmath spacing hooks and breaks fleqn/\everydisplay, so LaTeX steers users to \[...\]. The autofix swaps the delimiters in place and leaves the body untouched, so it parses and stays lossless; it is withheld when the display math is unclosed.

This rule is enabled by default.

Plain-TeX display math:

$$a + b = c$$
warning: dollar-display-math
 --> example.tex:1:1
  |
1 | $$a + b = c$$
  | ^^ `$$…$$` is plain-TeX display math; use `\[…\]`

After applying the fix:

\[a + b = c\]

ellipsis

Flag a literal run of three or more periods (...) where a real ellipsis command belongs. ... sets three tight full stops; LaTeX’s ellipsis commands set correctly spaced dots. In text the fix is a safe swap to \dots (a space is added before a following letter so the control word cannot glue onto the next word). In math \ldots (baseline, for comma lists) and \cdots (centered, for operator chains) are not interchangeable, so the fix is unsafe: it guesses from the neighboring atoms – an operator or relation picks \cdots, otherwise \ldots – and applies only under --unsafe-fixes or as an editor code action. Comments and verbatim are never touched.

This rule is enabled by default.

Literal dots in text:

See Chapter 2, 3, ... for details.
warning: ellipsis
 --> example.tex:1:19
  |
1 | See Chapter 2, 3, ... for details.
  |                   ^^^ literal `...` ellipsis; use `\dots`

After applying the fix:

See Chapter 2, 3, \dots for details.

Literal dots in a math sum (an operator neighbor picks \cdots):

$a_1 + ... + a_n$
warning: ellipsis
 --> example.tex:1:8
  |
1 | $a_1 + ... + a_n$
  |        ^^^ literal `...` ellipsis; use `\cdots` in math (`\ldots` for lists, `\cdots` for operator chains)

expl3-invalid-message-parameter

Flag #5 through #9 in either text argument of a literal expl3 msg_new, msg_set, or msg_gset definition. Messages accept only #1 through #4. Escaped hashes and parameters belonging to an enclosing function definition are distinguished from message parameters. Checks cover recognized executable calls and unexpanded function bodies; stored token lists, expanded text arguments, and unresolved calls stay silent. Report-only: the intended message argument is unknown.

This rule is enabled by default.

A message refers to a fifth parameter:

\ExplSyntaxOn
\msg_new:nnn { demo } { bad-value } { Invalid~value:~#5 }
\ExplSyntaxOff
warning: expl3-invalid-message-parameter
 --> example.tex:2:54
  |
2 | \msg_new:nnn { demo } { bad-value } { Invalid~value:~#5 }
  |                                                      ^^ invalid expl3 message parameter `#5`; messages accept only `#1` through `#4`

expl3-protected-predicate

Flag a protected expl3 conditional definition whose literal condition list requests a p predicate. Predicates must be expandable, which protection prevents. The new, set, and gset families are checked in recognized executable code, including unexpanded function bodies. Computed condition lists and unresolved calls stay silent. Report-only: choosing between protection and the predicate changes the function’s API or meaning.

This rule is enabled by default.

A protected conditional requests a predicate:

\ExplSyntaxOn
\prg_new_protected_conditional:Nnn \demo_ready: { p, TF }
  { \prg_return_true: }
\ExplSyntaxOff
warning: expl3-protected-predicate
 --> example.tex:2:51
  |
2 | \prg_new_protected_conditional:Nnn \demo_ready: { p, TF }
  |                                                   ^ a protected expl3 conditional cannot define an expandable `p` predicate

expl3-variant-type

Flag incompatible or deprecated argument-type conversions in literal expl3 variant-generation calls. A shorter variant inherits the original suffix. Unchanged letters are valid, N may become c, and n may become o, V, v, f, e, or x. Conversions between these two families are deprecated; other changes are incompatible. Checks cover recognized executable calls and unexpanded function bodies, not stored token lists or unresolved expansion. Report-only: the intended signature is the author’s decision.

This rule is enabled by default.

A variant cannot add arguments:

\ExplSyntaxOn
\cs_generate_variant:Nn \demo_use:n { nn }
\ExplSyntaxOff
warning: expl3-variant-type
 --> example.tex:2:39
  |
2 | \cs_generate_variant:Nn \demo_use:n { nn }
  |                                       ^^ incompatible expl3 variant conversion from `n` to `nn`

Converting a single-token argument to a token-list argument is deprecated:

\ExplSyntaxOn
\cs_generate_variant:Nn \demo_use:Nn { nn }
\ExplSyntaxOff
warning: expl3-variant-type
 --> example.tex:2:40
  |
2 | \cs_generate_variant:Nn \demo_use:Nn { nn }
  |                                        ^^ deprecated expl3 variant conversion from `Nn` to `nn`

extra-alignment-tab

Flags a row in a built-in tabular, tabular*, or array environment that consumes more columns than its column preamble declares. LaTeX cannot place the overflowing cell and reports an extra alignment tab. Short rows are valid and are not flagged. Custom column types and dynamic \multicolumn spans are left alone when their width cannot be established statically. No autofix is offered because either the row or the preamble may be wrong.

This rule is enabled by default.

A row that exceeds the declared table width:

\begin{tabular}{ll}
  a & b & c \\
\end{tabular}
error: extra-alignment-tab
 --> example.tex:2:9
  |
1 | \begin{tabular}{ll}
  |                 -- table preamble declares 2 columns
2 |   a & b & c \\
  |         ^ row uses at least 3 columns, but the table preamble declares 2

extra-math-linebreak

Flag a plain \\ at the end of an align, alignat, flalign, gather, or multline environment, including their starred forms, or immediately after \intertext{...} or \shortintertext{...} in an environment that supports intertext. These breaks add an empty row, increasing vertical space and potentially adding an equation number. Breaks before intertext, starred breaks, explicit spacing arguments, subsidiary environments such as aligned, and locally redefined environments or intertext commands are left alone. The fix deletes only the offending \\, preserving comments and surrounding whitespace. It is unsafe because it changes typeset spacing and potentially numbering; use --fix --unsafe-fixes or an explicit editor action.

This rule is enabled by default.

A final linebreak adds an empty equation row:

\begin{align}
  a &= b \\
\end{align}
warning: extra-math-linebreak
 --> example.tex:2:10
  |
2 |   a &= b \\
  |          ^^ final linebreak adds an empty math row

Intertext already separates the surrounding equation rows:

\begin{align*}
  a &= b \\
  \intertext{and therefore}\\
  c &= d
\end{align*}
warning: extra-math-linebreak
 --> example.tex:3:28
  |
3 |   \intertext{and therefore}\\
  |                            ^^ linebreak after `\intertext` adds an empty math row

hard-coded-reference

Flag a literal cross-reference written in prose – Figure 3, Table~1, Section 2 – instead of \ref/\cref to a \label (textidote sh:hcfig/hctab/hcsec). Hard-coding the number defeats LaTeX’s automatic numbering: renumbering a float or reordering sections silently breaks the reference and drops the hyperlink. The rule is report-only – the correct rewrite needs the label the number refers to, which is not in the text, so no autofix is offered. To stay conservative it fires only for a capitalized reference word (Figure, Table, Section, Eq., …) matched as a whole word and directly followed, across one space or a tie ~, by an arabic number; plurals, lowercase, Figure~\ref{x}, and Figure three are left alone. It also skips a citation locator (\cite[Section~8.1]{...}, a reference into external work), an environment title (\begin{thm}[Conway's Theorem 0], a proper name), and an \item[label] description-list caption (\item[Part 3.]). It never touches math, comments, or verbatim.

This rule is enabled by default.

A hard-coded figure number instead of a cross-reference:

See Figure 3 for the results.
warning: hard-coded-reference
 --> example.tex:1:5
  |
1 | See Figure 3 for the results.
  |     ^^^^^^^^ hard-coded reference `Figure 3`; use `\ref`/`\cref` to a `\label` so the number stays in sync

Even tied with ~, the number is still hard-coded:

Table~1 lists the parameters.
warning: hard-coded-reference
 --> example.tex:1:1
  |
1 | Table~1 lists the parameters.
  | ^^^^^^^ hard-coded reference `Table~1`; use `\ref`/`\cref` to a `\label` so the number stays in sync

indented-docstrip-guard

Flag a syntactically complete %<…> marker in a .dtx file when it is preceded only by horizontal whitespace on its physical line. Docstrip recognizes guards only at column zero, so an indented near match is an ordinary comment and does not select or delimit generated code. No autofix is offered because activating a guard can change generated files.

This rule is enabled by default.

A docstrip guard indented by one space:

 %<*package>
\ProvidesPackage{example}
 %</package>
warning: indented-docstrip-guard
 --> example.dtx:1:2
  |
1 |  %<*package>
  |  ^^^^^^^^^^^ docstrip guards are recognized only at column zero
warning: indented-docstrip-guard
 --> example.dtx:3:2
  |
3 |  %</package>
  |  ^^^^^^^^^^^ docstrip guards are recognized only at column zero

inert-suppression

Flag a suppression directive that cannot take effect: skip with no following construct, on with no matching off, or a directive written on a .dtx documentation-margin line, where % is typeset prose rather than a comment. Also flag an off region left open at EOF; it currently suppresses through the end of the file, but the missing closer is usually accidental. Report-only: moving, deleting, or closing the directive requires knowing the boundary the author intended. Inline suppressions cannot hide this meta diagnostic; use [lint].ignore to disable the rule deliberately.

This rule is enabled by default.

An on directive with no matching open region does nothing:

% badness-lint on deprecated-command
{\bf text}
warning: inert-suppression
 --> example.tex:1:1
  |
1 | % badness-lint on deprecated-command
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `on` has no matching `off`, so this directive closes no region

invalid-macrocode-frame

Flag a .dtx macrocode or macrocode* closing frame unless exactly four spaces separate its column-one % from \end{…}. The doc package scans for that literal physical delimiter, so a near match does not close the code chunk even though it looks like an ordinary environment to Badness. The safe autofix replaces only the malformed horizontal space with the required four spaces.

This rule is enabled by default.

A macrocode closer with only three spaces after %:

%    \begin{macrocode}
\def\example{value}
%   \end{macrocode}
error: invalid-macrocode-frame
 --> example.dtx:3:2
  |
3 | %   \end{macrocode}
  |  ^^^ `macrocode` closing frame requires exactly four spaces after `%`

After applying the fix:

%    \begin{macrocode}
\def\example{value}
%    \end{macrocode}

straight-quotes

Flag a literal ASCII double quote (") used for quotation. In LaTeX a straight " always sets a closing double quote, so an opening one comes out backwards; the correct forms are `` (two backticks) to open and '' (two apostrophes) to close. A quotation is reported once, spanning both quotes, and its fix rewrites the pair in one atomic edit – so a single editor code action repairs it from either end. A quote left unpaired (no closer before the paragraph ends) reports on its own. The fix is unsafe: it infers direction from context – a quote preceded by whitespace, a line break, an opening delimiter ((, [, {), a backtick, or the start of the document opens, anything else closes – and applies only under --unsafe-fixes or as an editor code action, since the guess can flip the typeset glyph. Single straight quotes (') are left alone (they are legitimately apostrophes), and comments, verbatim, math, TeX hex constants ("2D), and \pdfmapline font maps are never touched.

This rule is enabled by default.

Straight ASCII double quotes around a phrase:

He said "hello world" to me.
warning: straight-quotes
 --> example.tex:1:9
  |
1 | He said "hello world" to me.
  |         ^^^^^^^^^^^^^ straight double quotes; use `` `` `` (opening) and `''` (closing)

An opening quote after a parenthesis:

("quoted")
warning: straight-quotes
 --> example.tex:1:2
  |
1 | ("quoted")
  |  ^^^^^^^^ straight double quotes; use `` `` `` (opening) and `''` (closing)

swallowed-space

Flag a text-producing control word directly followed by a space that TeX eats, gluing the macro’s output to the next word (\LaTeX is renders “LaTeXis”) (ChkTeX 1). When TeX tokenizes a control word it discards following spaces, so the space never reaches the output. To stay conservative the rule fires only for a curated set of argument-less TeX-family logos (\LaTeX, \TeX, \BibTeX, …), only in text mode, and only when the next token is a word beginning with an alphanumeric character – a following period (\LaTeX . -> “LaTeX.”) is what the author wanted. The fix inserts {} after the control word (\LaTeX{} is), ending the macro name so the space survives; it is unsafe because it changes the typeset output, so --fix leaves it alone while --unsafe-fixes and the editor code action apply it.

This rule is enabled by default.

A logo swallows the following space, gluing it to the next word:

We used \LaTeX to typeset this.
warning: swallowed-space
 --> example.tex:1:15
  |
1 | We used \LaTeX to typeset this.
  |               ^ `\LaTeX` swallows the following space; add `{}` (`\LaTeX{}`) or `\ ` so it prints

space-before-command

Flag a plain space directly before a command that should hug the preceding word – \footnote, \footnotemark, \index, \label (ChkTeX 24/42). A space before \footnote sets a spurious space before the footnote mark (word \footnote{x} -> “word ¹”); a space before a zero-width \index/\label leaves a stray inter-word gap that can shift the recorded page. The fix deletes the space. It is unsafe – removing the space changes the typeset spacing – so --fix leaves it alone while --unsafe-fixes and the editor code action apply it. To stay conservative only the same-line WORD SPACE \cmd shape is flagged (a space at line start or after a brace is left alone), and math is skipped (an inter-token space is insignificant there), covering both $…$ and math environments like equation/align. For the zero-width \index/\label the fix is withheld unless the group is trailed by whitespace, a newline, or paragraph end, since otherwise the leading space is a real interword space to the following content.

This rule is enabled by default.

A space before a footnote sets a spurious space before the mark:

This is important \footnote{See the appendix.}
warning: space-before-command
 --> example.tex:1:18
  |
1 | This is important \footnote{See the appendix.}
  |                  ^ spurious space before `\footnote`; delete it so no stray space is typeset before the command

dash-length

Flag a dash of the wrong length for its context (ChkTeX 8). LaTeX sets a hyphen from -, an en dash from --, and an em dash from ---. Between two numbers a range takes an en dash, so 5-10 or 5---10 is flagged with an unsafe fix to -- (unsafe because it changes the typeset glyph and a hyphen between numbers is occasionally intentional). Between two words an en dash (--) is almost always a mistake, but whether a hyphen or an em dash was meant is ambiguous, so it is reported without a fix – except when it joins coordinate proper names (Barzilai--Borwein, Newton--Raphson), detected by an uppercase first letter on either flank, where the en dash is correct and the finding is suppressed. To stay conservative the rule only inspects a dash run that sits inside a single word with content on both sides and is the only dash run in that word, so dates (2020-01-15), ISBNs, spaced dashes, and option flags (--verbose) are left alone. Column spans in rule commands (\cline{1-3}, \cmidrule(lr){2-3}) and key arguments (\label{fig:1-3}, \cite{smith2020-1}) are specs and opaque identifiers rather than typeset ranges, so they are skipped too. The same applies to angle-delimited command and environment specifications such as Beamer’s \item<1-2> and \begin{onlyenv}<2-3>. Comments, verbatim, and math are never touched.

This rule is disabled by default; enable it with select.

A hyphen where a number range wants an en dash:

See pages 5-10 for the proof.
warning: dash-length
 --> example.tex:1:12
  |
1 | See pages 5-10 for the proof.
  |            ^ hyphen between numbers; use an en dash `--` for a number range

An en dash between words (ambiguous, so reported without a fix):

A well--known result.
warning: dash-length
 --> example.tex:1:7
  |
1 | A well--known result.
  |       ^^ en dash `--` between words; use a hyphen `-` for a compound or an em dash `---` for a break

times-variable

Flag a literal x used as a multiplication sign between two numbers, such as 640x200 or 3x3 (ChkTeX 29). TeX sets that x as an italic letter rather than the \times cross, so it reads wrong. The rule only fires when the whole word is digits x digits – one lowercase x with ASCII digits on both sides and nothing else – so ordinary words (matrix), spaced products (n x m), hex literals (0xFF, 0x12), and key arguments such as \label{fig:3x3} or \ref{fig:3x3} (where the x is part of an opaque identifier) are left alone. The fix is unsafe (a bare x between numbers is usually a cross but occasionally a real variable): inside math it rewrites the x to \times, and in text it wraps it as $\times$ so the result still compiles. So --fix leaves it alone; --unsafe-fixes and the editor code action apply it.

This rule is enabled by default.

A literal x as a multiplication sign in text (fixed to $\times$):

A 640x200 pixel image.
warning: times-variable
 --> example.tex:1:6
  |
1 | A 640x200 pixel image.
  |      ^ literal `x` as a multiplication sign between numbers; use `\times` for a cross

The same inside math mode (fixed to \times):

The grid is $640x200$ cells.
warning: times-variable
 --> example.tex:1:17
  |
1 | The grid is $640x200$ cells.
  |                 ^ literal `x` as a multiplication sign between numbers; use `\times` for a cross

math-operator-name

Flag a bare log-like function name (sin, cos, log, lim, and the rest of the LaTeX/amsmath set) written in math mode without its backslash, so TeX sets it as italic variables instead of the upright \sin operator with correct spacing (ChkTeX 35). It fires when the name starts a WORD and ends at a word boundary, catching both $sin x$ and the glued $sin(x)$, while leaving words that merely begin with one (since) alone and preferring the longest match (sinh over sin). To stay conservative it only fires inside math mode, never in a subscript or superscript, where max in x_{max} is almost always a label, and never inside a text-domain or unknown argument. The fix inserts the backslash (sin -> \sin); it is unsafe because it changes the typeset output (upright glyph and operator spacing) and a bare sin is occasionally a real product, so --fix leaves it alone while --unsafe-fixes and the editor code action apply it.

This rule is enabled by default.

A bare function name typesets as italic variables:

$sin x + cos x = 1$
warning: math-operator-name
 --> example.tex:1:2
  |
1 | $sin x + cos x = 1$
  |  ^^^ bare `sin` in math typesets as italic variables; use `\sin`
warning: math-operator-name
 --> example.tex:1:10
  |
1 | $sin x + cos x = 1$
  |          ^^^ bare `cos` in math typesets as italic variables; use `\cos`

It fires through the glued f(x) form too:

The limit $lim(x)$ diverges.
warning: math-operator-name
 --> example.tex:1:12
  |
1 | The limit $lim(x)$ diverges.
  |            ^^^ bare `lim` in math typesets as italic variables; use `\lim`

makeat-macro

Flag a macro whose name contains @ (\foo@bar, \p@, \@ifnextchar) used outside a \makeatletter/\makeatother region. There @ has its ordinary catcode, so it cannot be part of a control word: \foo@bar is read as \foo followed by the text @bar, not as a call to the internal macro \foo@bar. Usually the enclosing \makeatletter/\makeatother was forgotten. Because the formatter’s lexer already tracks \makeatletter state, this is decided exactly – an in-region name lexes as one token and is never flagged; only the split out-of-region form (control word abutting an @-word, or \@ abutting a letter-word) is. Report-only: a correct fix would mean wrapping the use in \makeatletter/\makeatother, not a tight local edit, so no autofix is offered. The end-of-sentence \@ (as in NASA\@.) is not flagged.

This rule is enabled by default.

An internal @ macro used without \makeatletter:

\my@command
warning: makeat-macro
 --> example.tex:1:1
  |
1 | \my@command
  | ^^^^^^^^^^^ `\my@command` uses `@` in a macro name outside a `\makeatletter` region; `@` is not a letter here, so this reads as `\my` followed by the text `@command`

A leading-@ macro (a \@-prefixed internal) outside a region:

\@ifstar{\StarredForm}{\PlainForm}
warning: makeat-macro
 --> example.tex:1:1
  |
1 | \@ifstar{\StarredForm}{\PlainForm}
  | ^^^^^^^^ `\@ifstar` uses `@` in a macro name outside a `\makeatletter` region; `@` is not a letter here, so this reads as `\@` followed by the text `ifstar`

sectioning-level-jump

Flag a structural heading that descends more than one level below the preceding structural heading – \section straight to \subsubsection, skipping \subsection (textidote’s sh:secskip). The active ladder follows the document class: \chapter is included only for classes known to provide it or when the source uses it, while unknown classes conservatively omit it. \paragraph and \subparagraph are transparent because technical papers commonly use them as run-in labels rather than outline subdivisions. Only downward jumps are flagged – climbing back up and repeated headings at one level are normal. The comparison is relative to the previous structural heading, never an absolute top level. Report-only: repairing a skip is a structural choice for the author, not a correct-by-construction edit.

This rule is enabled by default.

A heading that drops two levels at once (skipping \subsection):

\section{Introduction}
\subsubsection{Details}
warning: sectioning-level-jump
 --> example.tex:2:1
  |
2 | \subsubsection{Details}
  | ^^^^^^^^^^^^^^ `\subsubsection` skips a sectioning level after `\section` (expected `\subsection`)

missing-required-argument

Flag a command invoked with fewer {…} groups than the required arity in its curated built-in signature (ChkTeX warning 14, decided on the parse tree and signature database rather than line heuristics). TeX also accepts unbraced single-token arguments (\frac12), so the rule stays silent whenever a following token could still supply the missing argument and fires only at a hard boundary: the end of the enclosing group, math shell, or environment, an alignment &, a \\ line break, a blank line, or the end of the file. Contexts where a bare command is deliberate are skipped – macro-definition bodies (\newcommand{\bold}{\textbf}), arguments of unknown commands, standalone {…} scope groups, \let-style alias forms, and names the file itself redefines. Curated environment-local signatures take precedence over global signatures: inside parts, exam’s \part takes only optional points. Report-only: the missing argument’s content is the author’s to write, so no fix is correct by construction.

This rule is enabled by default.

A fraction missing its denominator:

$\frac{1}$
warning: missing-required-argument
 --> example.tex:1:2
  |
1 | $\frac{1}$
  |  ^^^^^ `\frac` is missing 1 of its 2 required arguments

A command left bare at the end of a group, with nothing to take:

\emph{see \textbf}
warning: missing-required-argument
 --> example.tex:1:11
  |
1 | \emph{see \textbf}
  |           ^^^^^^^ `\textbf` is missing its required argument

undefined-ref

Flag a \ref-family reference to a label defined nowhere in the document. Sound only when the label namespace is complete, so it stays silent unless the project view is closed (every include resolves to an analyzed file) and rooted. Inert on stdin or wherever no cross-file label resolution is available. No autofix.

This rule is enabled by default.

A reference to a label defined nowhere in the document:

\ref{sec:intro}
warning: undefined-ref
 --> example.tex:1:1
  |
1 | \ref{sec:intro}
  | ^^^^^^^^^^^^^^^ reference to undefined label `sec:intro`

undefined-citation

Flag a \cite-family key matching no entry in the document’s bibliography – the bibliographic analog of undefined-ref. Sound only over a closed, rooted namespace where every .bib resource resolves to an analyzed file; resource lookup honors BibTeX’s BIBINPUTS/TEXBIB search path. Suppressed entirely by a \nocite{*} wildcard (which marks every key as used). Inert without cross-file citation resolution. No autofix.

This rule is enabled by default.

A citation of a key that matches no bibliography entry:

\cite{knuth:1984}
warning: undefined-citation
 --> example.tex:1:1
  |
1 | \cite{knuth:1984}
  | ^^^^^^^^^^^^^^^^^ citation of undefined key `knuth:1984`

unreferenced-label

Flag a label definition unused by a \ref-family command anywhere in the document. A \eqref{A}--\eqref{D} range also uses labels between A and D when they occur in consecutive, singly labeled equation environments or numbered align and gather rows, including through literal included files with an unambiguous source order. Manual tags, suppressed numbers, and counter changes stop inference. Referencing a subequations group label also uses the labels in its enclosed math environments. The mirror of undefined-ref, and sound only when the label namespace is complete, so it stays silent unless the project view is closed (every include resolves to an analyzed file) and rooted. Inert on stdin or wherever no cross-file label resolution is available. Report-only: removing the dead label or adding a reference are both valid, so there is no autofix.

This rule is enabled by default.

A label that no \ref-family command ever targets:

\section{Intro}\label{sec:intro}
warning: unreferenced-label
 --> example.tex:1:16
  |
1 | \section{Intro}\label{sec:intro}
  |                ^^^^^^^^^^^^^^^^^ label `sec:intro` is never referenced

verbatim-trailing-text

Flag non-whitespace text after a verbatim-like environment’s \end{…} on the same line (ChkTeX warning 31). LaTeX closes a verbatim environment by scanning line by line to \end{verbatim} and then gobbling the rest of that line, so \end{verbatim} foo silently drops foo. Scoped to verbatim-like environments — read off the parse tree (an opaque VERBATIM_BODY, or a curated built-in verbatim name for the empty-body case) — because ordinary environments do not gobble their \end line. A trailing % comment is treated as trivia, not flagged. Report-only: whether to move or delete the swallowed text is the author’s call, so no fix is correct by construction.

This rule is enabled by default.

Text after \end{verbatim} is silently discarded by LaTeX:

\begin{verbatim}
sample
\end{verbatim} and more
warning: verbatim-trailing-text
 --> example.tex:3:16
  |
3 | \end{verbatim} and more
  |                ^^^^^^^^ text after `\end{verbatim}` on the same line is silently discarded

duplicate-package

Flag a package loaded more than once in the same file with \usepackage/\RequirePackage (which share one package namespace). LaTeX loads a given package only once; a second load is redundant and, when the options disagree, an option-clash error. A warning requires a prior load in the same conditional branch or an enclosing context (including an unconditional prior). Separate conditional tests are treated as uncertain and do not trigger a warning. Recognizes \if...\else...\fi and common macros with complete braced arguments, including \ifthenelse, \iftoggle, and \IfFileExists. Predicates are not evaluated, and coverage across branches is not combined. No autofix: removing a load can drop options the survivor lacks, and which load to keep is the author’s call. Class loads (\documentclass/\LoadClass) are a separate concern and are not flagged.

This rule is enabled by default.

The same package loaded twice:

\usepackage{amsmath}
\usepackage{amsmath}
warning: duplicate-package
 --> example.tex:2:1
  |
2 | \usepackage{amsmath}
  | ^^^^^^^^^^^^^^^^^^^^ package `amsmath` is loaded more than once

missing-provides

Flag a package or class source (.sty/.cls) that never identifies itself with the matching \ProvidesPackage/\ProvidesClass. Every well-formed package declares its identity so LaTeX can log it and honor date-based compatibility checks; a .sty carrying only \ProvidesClass (wrong kind) still counts as missing. The rule is inert for any other extension – a .tex has nothing to provide, and a .dtx hides its declaration inside guarded macrocode. No autofix: writing a correct \Provides… line (placement, date, version) is the author’s call.

This rule is enabled by default.

A package source with no self-identification (the docs are rendered against a .sty path):

\NeedsTeXFormat{LaTeX2e}
\RequirePackage{xcolor}
warning: missing-provides
 --> example.sty:1:1
  |
1 | \NeedsTeXFormat{LaTeX2e}
  | ^^^^^^^^^^^^^^^ package file lacks `\ProvidesPackage`

unknown-option

Flag a \usepackage/\RequirePackage option that the loaded package never declares with \DeclareOption, which LaTeX reports as an “Unknown option” error at compile time. Checked only against packages that are analyzed project files (a sibling .sty) — no option data ships for system packages — and only when the package’s declared set is trustworthy: a \DeclareOption* default handler, a key-value option processor (kvoptions, \ProcessKeyOptions, …), option forwarding, or an \input in the package silences the rule, as does a key=value option. Class loads (\documentclass) are not checked: an unknown class option is not an error, it becomes an unused global option. No autofix: dropping or renaming the option is the author’s call.

This rule is enabled by default.

With a sibling mypkg.sty:

\ProvidesPackage{mypkg}[2026/01/01 v1.0 Demo package]
\DeclareOption{draft}{}
\ProcessOptions\relax

Loading the sibling package with an option it never declares:

\usepackage[final]{mypkg}
warning: unknown-option
 --> example.tex:1:13
  |
1 | \usepackage[final]{mypkg}
  |             ^^^^^ unknown option `final` for package `mypkg`

redundant-script-braces

Flag braces around a single-token sub/superscript argument, which ^/_ bind without them (x^{2} is x^2). The autofix deletes the two braces and leaves the inner token untouched. It is withheld when dropping the braces would let the following character glue onto the argument and change meaning (x^{2}-3 stays braced — unspaced x^2-3 would re-lex 2-3 as one token; y_{\alpha}b stays braced — \alphab is one control word). It also leaves standard named math operators braced because commands such as \max are not valid unbraced script fields.

This rule is enabled by default.

Redundant braces around a single-token script argument:

$x^{2}$ and $y_{\alpha}$
help: redundant-script-braces
 --> example.tex:1:4
  |
1 | $x^{2}$ and $y_{\alpha}$
  |    ^^^ redundant braces around a single-token script argument
help: redundant-script-braces
 --> example.tex:1:16
  |
1 | $x^{2}$ and $y_{\alpha}$
  |                ^^^^^^^^ redundant braces around a single-token script argument

After applying the fix:

$x^2$ and $y_\alpha$

unclosed-math-delimiter

Flag a math opener the parser silently demoted to a plain token because no closer was reachable – a $ with no matching $, a \[/\( with no \]/\), or a \left with no \right. Such a shape is routine data in macro code (>{$} array columns, \expandafter\@tempa\[\@nil), so the parser tolerates it without a diagnostic; in prose it is almost always a dropped closer. To stay clear of the macro-code cases the rule is conservative: it reports only an opener in document prose, staying silent when it sits inside a brace group or optional argument (\newcommand{...}{$}, the >{$} column spec), an expl3 region, or a macrocode body. No autofix: the correction (insert a closer, or delete a stray opener) is ambiguous.

This rule is enabled by default.

An inline-math $ with no matching $:

Let $x = 1 be the base case.
warning: unclosed-math-delimiter
 --> example.tex:1:5
  |
1 | Let $x = 1 be the base case.
  |     ^ `$` has no matching `$` (unclosed inline math)

A display-math \[ with no matching \]:

The bound \[ x + y follows immediately.
warning: unclosed-math-delimiter
 --> example.tex:1:11
  |
1 | The bound \[ x + y follows immediately.
  |           ^^ `\[` has no matching `\]` (unclosed display math)

A \left with no matching \right:

$a + \left( b + c$
warning: unclosed-math-delimiter
 --> example.tex:1:6
  |
1 | $a + \left( b + c$
  |      ^^^^^ `\left` has no matching `\right`

label-before-caption

Flag a \label placed before the statement that establishes its intended counter: the outer \caption in a curated float (figure, table, and their starred forms), an explicit \captionof in a curated caption container (minipage), or the first \item in the standard numbered enumerate list. In either position, \label captures the previous \@currentlabel—usually an enclosing section number—so \ref silently prints an unrelated number. LaTeX gives no warning. The list case is limited to statement-level labels before the first item; labels after an item may belong to it, while itemize and description items do not step a reference counter. Attached custom item labels and complete Beamer overlay markers remain intact. The float case likewise skips labels nested in command arguments, and classifies nested counter steps conservatively. The fix moves the label just after the proven caption or item marker, and is Unsafe because it intentionally changes what \ref prints from an inferred intent.

This rule is enabled by default.

A \label above its \caption picks up the section counter, not the figure number:

\begin{figure}
  \includegraphics{plot}
  \label{fig:plot}
  \caption{A plot.}
\end{figure}
warning: label-before-caption
 --> example.tex:3:3
  |
3 |   \label{fig:plot}
  |   ^^^^^^^^^^^^^^^^ `\label` before the outer `\caption` in this `figure` does not capture the float number

A \label before the first \item has not seen the item counter step:

\begin{enumerate}
  \label{item:first}
  \item First
\end{enumerate}
warning: label-before-caption
 --> example.tex:2:3
  |
2 |   \label{item:first}
  |   ^^^^^^^^^^^^^^^^^^ `\label` before the first `\item` in this `enumerate` does not capture the item number

A \label above \captionof in a minipage likewise precedes the explicit counter step:

\begin{minipage}{\textwidth}
  \label{fig:plot}
  \captionof{figure}{A plot.}
\end{minipage}
warning: label-before-caption
 --> example.tex:2:3
  |
2 |   \label{fig:plot}
  |   ^^^^^^^^^^^^^^^^ `\label` before `\captionof` in this `minipage` does not capture the caption number

lonely-item

Flag \item written directly in the document environment, where LaTeX reports a lonely item because there is no list. The rule leaves items inside other environments, command arguments, low-level \list/\trivlist pairs, and standalone fragments alone because their list context may come from a custom definition or an including file. Report-only: the intended list type and boundaries cannot be inferred from the item.

This rule is enabled by default.

An item directly in the document body has no list:

\begin{document}
\item A
\end{document}
error: lonely-item
 --> example.tex:2:1
  |
2 | \item A
  | ^^^^^ `\item` has no enclosing list environment

Suppression

To suppress a rule at a single site, use a comment directive:

% badness-lint skip deprecated-command: legacy code, leave as-is
{\bf here}

The verb carries the scope. skip covers the next construct, off and on delimit a region, and skip-file covers the whole file wherever it sits:

% badness-lint off deprecated-command: legacy chapter
{\bf here}
{\it and here}
% badness-lint on deprecated-command

Naming the <id> is optional; leaving it out suppresses every rule over that same span. The : <reason> tail is optional everywhere.

% badness skip / off / on / skip-file do the same and turn off the formatter at the same time; see Formatting for the layout-only % badness-format spellings.

Parse diagnostics (rule id parse) are never suppressed by select/ignore.