Skip to content

Linter Rules ​

badness lint runs a set of built-in rules over each file’s parse tree and reports a diagnostic for every finding. Each section describes one rule, identified by its stable rule id. The id appears in diagnostics and selects the rule in [lint] settings, --select and --ignore flags, and % badness-lint skip <id> comments.

Most rules are on by default. Each rule’s section states whether it is enabled by default. Use select to enable an opt-in rule, or use select and ignore in the [lint] table to narrow the default set. See the Configuration reference.

Some rules offer an autofix when a rewrite is unambiguous. badness lint --fix applies safe fixes, shown below under “After applying the fix”. An unsafe fix may change output, such as by inserting a tie that prevents a line break. It applies only with --unsafe-fixes or 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 cataloged in BibTeX Linter Rules.

abbreviation-spacing ​

Flag incorrect sentence and interword spacing around abbreviations and acronyms (ChkTeX 12/13). Outside \frenchspacing, TeX widens the space after ., ?, or ! unless the punctuation follows an uppercase letter. A lowercase abbreviation (e.g., i.e., etc., et al.) therefore gets too much space, which \ corrects (e.g.\ foo). An uppercase acronym ending a sentence (USA.) gets too little space, which \@ corrects (USA\@.).

The rule flags a lowercase abbreviation only before a lowercase word, where the sentence clearly continues. It flags an acronym only when two or more capitals precede the period and an uppercase word follows it, indicating a new sentence. Initials (J.), dotted forms (U.S.A.), and mid-sentence acronyms are left alone. Both fixes are unsafe because they change typeset spacing, so --fix leaves them alone; --unsafe-fixes and editor code actions apply them. The rule is silent under \frenchspacing and never touches comments, verbatim content, or math.

This rule is enabled by default.

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

tex
We tried several methods, e.g. gradient descent.
text
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:

tex
The rover reached the USA. Then it stopped.
text
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 produces a \par token, and a keyval processor reads its entries with macros that are not \long, so the call aborts. TeX’s error names the processor rather than the command the author wrote: \hypersetup yields “Paragraph ended before \kv@processor@default was complete”. This rule points to the source of that error.

The scope follows compilation checks: a blank line nested inside a value’s brace group (\tikzset{aa/.style={draw,\n\nthick}}) compiles cleanly and is not flagged. An unclosed { is left to the parser’s diagnostic. The rule consults only hand-curated signatures. The safe autofix removes the blank line and preserves the following indentation. It changes only whitespace, and ContentKind::Keyval establishes that the processor strips spaces around entries.

This rule is enabled by default.

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

tex
\hypersetup{colorlinks=true,

linkcolor=blue}
text
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:

tex
\hypersetup{colorlinks=true,
linkcolor=blue}

duplicate-label ​

Flag a label key defined more than once in the same label namespace, either within one file or across files that share a document when a project view is available. LaTeX warns but 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. The rule recognizes \if...\else...\fi and common macros with complete braced arguments, including \ifthenelse, \iftoggle, and \IfFileExists. It does not evaluate predicates or combine coverage across branches. Cross-file checks use label namespaces. No autofix is offered because the author must choose whether to rename or delete a definition.

This rule is enabled by default.

The same key defined twice in one file:

tex
\section{One}\label{sec:x}
\section{Two}\label{sec:x}
text
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 \...series, \...shape, and \...family declarations. The rule leaves \em alone because it remains the supported emphasis switch. If the file redefines a name (\renewcommand{\sl}{…}, \def\rm{…}), the rule treats it as the user’s macro and does not flag it anywhere.

The autofix replaces only the control word (\bf → \bfseries), leaving following text untouched. 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:

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

After applying the fix:

tex
{\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:

tex
% badness-ignore deprecated-command: legacy source
{\bf text}
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:

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

missing-nonbreaking-space ​

Flag a plain space 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 TeX tie (~) keeps the reference on the same line. Self-describing references (\autoref, \cref) and textual citations (\textcite, \citet) are not flagged because they supply their own noun.

The rule flags both a same-line space and a single source line break before the command. A blank line starts a new paragraph and is not flagged. For a same-line space, the fix is unsafe because inserting a tie changes line breaking. --fix leaves it alone; --unsafe-fixes and editor code actions apply it. A line break is reported without a fix: replacing it with ~ would join the lines, a reflow the formatter owns.

This rule is enabled by default.

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

tex
see Figure \ref{fig:plot}
text
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 superseded math environments and name the modern replacement in the diagnostic. The canonical case is eqnarray, which amsmath replaced with align decades ago because it spaces relations incorrectly. It is also a recurring l2tabu warning. The autofix renames the \begin and \end pair in place and leaves the body untouched, so it is correct by construction.

This rule is enabled by default.

The superseded eqnarray environment:

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

After applying the fix:

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

primitive-command ​

Flag raw plain-TeX primitives discouraged in LaTeX source and name the LaTeX construct that supersedes each one (ChkTeX 41, lacheck, l2tabu). The related deprecated-command rule covers obsolete font switches. Most primitives are reported without a fix because their replacements restructure arguments: a \over b becomes \frac{a}{b}, and \centerline{x} becomes a \centering declaration or a center environment. No single textual edit can perform those rewrites correctly by construction.

A few primitives carry a Safe autofix that replaces one control word with a single token of identical meaning (\sb and \sp become _ and ^). The edit replaces only the control word, preserving losslessness and meaning. It is withheld where the primitive is merely referenced (\let\x\sp, \ifx\sp\y). If the file redefines a name (\renewcommand\sp{…}), the rule treats it as the user’s macro and does not flag it anywhere. The rule also leaves \bgroup and \egroup alone because replacing implicit braces 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):

tex
$a \over b$
text
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 _:

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

After applying the fix:

tex
$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:

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

After applying the fix:

tex
\[a + b = c\]

ellipsis ​

Flag a literal run of three or more periods (...) where an ellipsis command belongs. Literal periods set tight full stops, while LaTeX’s ellipsis commands set correctly spaced dots. In text, the fix is a safe replacement with \dots. It adds a space before a following letter so the control word cannot merge with the next word.

In math, \ldots (baseline dots for comma lists) and \cdots (centered dots for operator chains) are not interchangeable. The fix is therefore unsafe: it chooses \cdots beside an operator or relation and \ldots otherwise. It applies only with --unsafe-fixes or an editor code action. The rule never touches comments or verbatim content.

This rule is enabled by default.

Literal dots in text:

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

After applying the fix:

tex
See Chapter 2, 3, \dots for details.

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

tex
$a_1 + ... + a_n$
text
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:

tex
\ExplSyntaxOn
\msg_new:nnn { demo } { bad-value } { Invalid~value:~#5 }
\ExplSyntaxOff
text
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:

tex
\ExplSyntaxOn
\prg_new_protected_conditional:Nnn \demo_ready: { p, TF }
  { \prg_return_true: }
\ExplSyntaxOff
text
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:

tex
\ExplSyntaxOn
\cs_generate_variant:Nn \demo_use:n { nn }
\ExplSyntaxOff
text
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:

tex
\ExplSyntaxOn
\cs_generate_variant:Nn \demo_use:Nn { nn }
\ExplSyntaxOff
text
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:

tex
\begin{tabular}{ll}
  a & b & c \\
\end{tabular}
text
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:

tex
\begin{align}
  a &= b \\
\end{align}
text
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:

tex
\begin{align*}
  a &= b \\
  \intertext{and therefore}\\
  c &= d
\end{align*}
text
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 possible hard-coded cross-reference in prose, such as Figure 3, Table~1, or Section 2 (textidote sh:hcfig/hctab/hcsec). If the reference points into this document, use \ref or \cref to a \label so the number tracks its target and the reference can carry a hyperlink.

The rule is opt-in because prose alone cannot distinguish local references from valid references to external work, such as Bob's Lemma~1 or Lemma~1 in Bob's paper. No autofix is offered because the correct rewrite needs a label that the text does not supply.

The rule flags only a capitalized reference word (Figure, Table, Section, Eq., …) matched as a whole word and followed by an Arabic number across one space or a tie (~). It leaves plurals, lowercase words, Figure~\ref{x}, and Figure three alone. It also skips citation locators (\cite[Section~8.1]{...}, which refers to external work), environment titles (\begin{thm}[Conway's Theorem 0], a proper name), and description-list captions (\item[Part 3.]). It never touches math, comments, or verbatim content.

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

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

tex
See Figure 3 for the results.
text
warning: hard-coded-reference
 --> example.tex:1:5
  |
1 | See Figure 3 for the results.
  |     ^^^^^^^^ possible hard-coded reference `Figure 3`; if this refers to this document, use `\ref` or `\cref`

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

tex
Table~1 lists the parameters.
text
warning: hard-coded-reference
 --> example.tex:1:1
  |
1 | Table~1 lists the parameters.
  | ^^^^^^^ possible hard-coded reference `Table~1`; if this refers to this document, use `\ref` or `\cref`

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:

tex
 %<*package>
\ProvidesPackage{example}
 %</package>
text
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:

tex
% badness-lint on deprecated-command
{\bf text}
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 %:

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

After applying the fix:

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

straight-quotes ​

Flag a literal ASCII double quote (") used for quotation. In LaTeX, a straight " sets a closing double quote, so an opening one comes out backward. The correct forms are `` (two backticks) to open and '' (two apostrophes) to close. The rule reports a quotation once, spanning both quotes, and rewrites the pair in one atomic edit so an editor code action can repair it from either end. A quote with no closer before the paragraph ends is reported on its own.

The fix is unsafe because it infers direction from context. A quote opens when preceded by whitespace, a line break, an opening delimiter ((, [, {), a backtick, or the start of the document; otherwise, it closes. This guess can change the typeset glyph, so the fix applies only with --unsafe-fixes or an editor code action. Single straight quotes (') are left alone because they can be apostrophes. The rule never touches comments, verbatim content, math, TeX hex constants ("2D), or \pdfmapline font maps.

This rule is enabled by default.

Straight ASCII double quotes around a phrase:

tex
He said "hello world" to me.
text
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:

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

swallowed-space ​

Flag a text-producing control word followed directly by a space that TeX discards, joining the macro’s output to the next word (\LaTeX is renders as “LaTeXis”) (ChkTeX 1). TeX discards spaces after a control word during tokenization, so they never reach the output.

The rule checks only a curated set of TeX-family logos that take no arguments (\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 left alone. The fix inserts {} after the control word (\LaTeX{} is), ending the macro name so the space survives. It is unsafe because it changes typeset output. --fix leaves it alone; --unsafe-fixes and editor code actions apply it.

This rule is enabled by default.

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

tex
We used \LaTeX to typeset this.
text
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 follow the preceding word without a gap: \footnote, \footnotemark, \index, or \label (ChkTeX 24/42). A space before \footnote produces an unwanted space before the footnote mark (word \footnote{x} → “word ¹”). A space before a zero-width \index or \label leaves an interword gap that can shift the recorded page. The fix deletes the space. It is unsafe because it changes typeset spacing, so --fix leaves it alone; --unsafe-fixes and editor code actions apply it.

The rule flags only the same-line WORD SPACE \cmd shape. It leaves spaces at line starts or after braces alone and skips math, where inter-token spaces are insignificant, including $…$ and environments such as equation and align. For zero-width \index and \label, the fix is withheld unless whitespace, a newline, or paragraph end follows the group. Otherwise, the leading space separates the preceding word from the following content.

This rule is enabled by default.

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

tex
This is important \footnote{See the appendix.}
text
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 ---. A range between two numbers takes an en dash, so the rule flags 5-10 and 5---10 with an unsafe fix to --. The fix changes the typeset glyph, and a hyphen between numbers is occasionally intentional.

Between two words, an en dash (--) is almost always a mistake, but the choice between a hyphen and an em dash is ambiguous, so no fix is offered. Coordinate proper names (Barzilai--Borwein, Newton--Raphson) are exempt: an uppercase first letter on either side suppresses the finding.

The rule checks only a word containing exactly one dash run with content on both sides. It leaves dates (2020-01-15), ISBNs, spaced dashes, and option flags (--verbose) alone. It also skips column spans (\cline{1-3}, \cmidrule(lr){2-3}) and key arguments (\label{fig:1-3}, \cite{smith2020-1}), which are specifications or opaque identifiers. The same applies to angle-delimited command and environment specifications such as Beamer’s \item<1-2> and \begin{onlyenv}<2-3>. The rule never touches comments, verbatim content, or math.

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

A hyphen where a number range wants an en dash:

tex
See pages 5-10 for the proof.
text
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):

tex
A well--known result.
text
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 it as an italic letter rather than the \times cross. The rule flags only a whole word of the form digits x digits: one lowercase x with ASCII digits on both sides and nothing else. It leaves ordinary words (matrix), spaced products (n x m), hex literals (0xFF, 0x12), and opaque key arguments such as \label{fig:3x3} or \ref{fig:3x3} alone.

The fix is unsafe because a bare x between numbers is usually a cross but occasionally a variable. In math, it replaces x with \times; in text, it uses $\times$ so the result still compiles. --fix leaves it alone; --unsafe-fixes and editor code actions apply it.

This rule is enabled by default.

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

tex
A 640x200 pixel image.
text
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):

tex
The grid is $640x200$ cells.
text
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 function name from the LaTeX and amsmath set (sin, cos, log, lim, and others) written in math mode without its backslash (ChkTeX 35). TeX sets the name as italic variables rather than an upright operator such as \sin with the correct spacing. The rule matches a name at the start of a WORD and ending at a word boundary, catching both $sin x$ and $sin(x)$. It leaves words such as since alone and prefers the longest match (sinh over sin).

The rule checks only math mode and skips subscripts and superscripts, where max in x_{max} is almost always a label. It also skips text-domain and unknown arguments. The fix inserts the backslash (sin → \sin). It is unsafe because it changes the glyph and operator spacing, and a bare sin can be a product. --fix leaves it alone; --unsafe-fixes and editor code actions apply it.

This rule is enabled by default.

A bare function name typesets as italic variables:

tex
$sin x + cos x = 1$
text
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:

tex
The limit $lim(x)$ diverges.
text
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 name containing @ (\foo@bar, \p@, \@ifnextchar) used outside a \makeatletter and \makeatother region. There, @ has its ordinary catcode and cannot be part of a control word. TeX reads \foo@bar as \foo followed by the text @bar, rather than as the internal macro \foo@bar. This usually means the enclosing \makeatletter and \makeatother were forgotten.

The formatter’s lexer tracks \makeatletter state, so the rule distinguishes these cases exactly. A name inside the region lexes as one token and is never flagged. Only the split form outside the region is flagged: a control word abutting an @-word, or \@ abutting a letter-word. No autofix is offered because wrapping the use in \makeatletter and \makeatother requires more than a local edit. The end-of-sentence \@ in NASA\@. is left alone.

This rule is enabled by default.

An internal @ macro used without \makeatletter:

tex
\my@command
text
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:

tex
\@ifstar{\StarredForm}{\PlainForm}
text
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, such as \section followed directly by \subsubsection, skipping \subsection (textidote’s sh:secskip). The heading hierarchy follows the document class: it includes \chapter only for classes known to provide it or when the source uses it. Unknown classes conservatively omit it. The rule treats \paragraph and \subparagraph as transparent because technical papers commonly use them as run-in labels.

Only downward jumps are flagged. Returning to a higher level or repeating a heading at the same level is normal. The comparison is with the previous structural heading rather than an absolute top level. No autofix is offered because repairing a skip requires the author to choose the structure.

This rule is enabled by default.

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

tex
\section{Introduction}
\subsubsection{Details}
text
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 with fewer {…} groups than its curated built-in signature requires (ChkTeX warning 14, checked through the parse tree and signature database rather than line heuristics). TeX also accepts unbraced single-token arguments (\frac12), so the rule stays silent when a following token could supply the missing argument. It flags the command 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.

The rule skips contexts where a bare command may be deliberate: macro-definition bodies (\newcommand{\bold}{\textbf}), arguments of unknown commands, standalone {…} scope groups, \let-style aliases, and names redefined in the file. Curated environment-local signatures take precedence over global signatures: inside parts, exam’s \part takes only optional points. No autofix is offered because the author must supply the missing argument’s content.

This rule is enabled by default.

A fraction missing its denominator:

tex
$\frac{1}$
text
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:

tex
\emph{see \textbf}
text
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. The check is sound only when the label namespace is complete, so the rule stays silent unless the project view is closed (every include resolves to an analyzed file) and rooted. It is inactive on stdin or wherever cross-file label resolution is unavailable. No autofix is offered.

This rule is enabled by default.

A reference to a label defined nowhere in the document:

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

undefined-citation ​

Flag a \cite-family key that matches no entry in the document’s bibliography, analogous to undefined-ref. The check is sound only over a closed, rooted namespace where every .bib resource resolves to an analyzed file. Resource lookup honors BibTeX’s BIBINPUTS and TEXBIB search paths. A \nocite{*} wildcard, which marks every key as used, suppresses the rule entirely. The rule is inactive without cross-file citation resolution. No autofix is offered.

This rule is enabled by default.

A citation of a key that matches no bibliography entry:

tex
\cite{knuth:1984}
text
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 any \ref-family command 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. This includes literal included files with an unambiguous source order. Manual tags, suppressed numbers, and counter changes stop inference. A reference to a subequations group label also uses the labels in its enclosed math environments.

Like undefined-ref, the check is sound only when the label namespace is complete. The rule stays silent unless the project view is closed (every include resolves to an analyzed file) and rooted. It is inactive on stdin or wherever cross-file label resolution is unavailable. No autofix is offered because removing the unused label and adding a reference are both valid choices.

This rule is enabled by default.

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

tex
\section{Intro}\label{sec:intro}
text
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 scans line by line to \end{verbatim}, then discards the rest of that line, so \end{verbatim} foo silently drops foo.

The rule identifies verbatim-like environments from an opaque VERBATIM_BODY in the parse tree or a curated built-in verbatim name when the body is empty. Ordinary environments do not discard the rest of their closing line and are not checked. A trailing % comment is treated as trivia and is not flagged. No autofix is offered because the author must choose whether to move or delete the text.

This rule is enabled by default.

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

tex
\begin{verbatim}
sample
\end{verbatim} and more
text
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 or \RequirePackage, which share one package namespace. LaTeX loads a package only once. A second load is redundant and produces an option-clash error when its options disagree.

A warning requires a prior load in the same conditional branch or an enclosing context, including an unconditional prior load. Separate conditional tests are treated as uncertain and do not trigger a warning. The rule recognizes \if...\else...\fi and common macros with complete braced arguments, including \ifthenelse, \iftoggle, and \IfFileExists. It does not evaluate predicates or combine coverage across branches. No autofix is offered because removing a load can discard options the remaining load lacks. Class loads (\documentclass and \LoadClass) are not flagged.

This rule is enabled by default.

The same package loaded twice:

tex
\usepackage{amsmath}
\usepackage{amsmath}
text
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 or .cls) that never identifies itself with the matching \ProvidesPackage or \ProvidesClass. A well-formed package declares its identity so LaTeX can log it and honor date-based compatibility checks. A .sty file containing only \ProvidesClass still lacks the matching declaration.

The rule is inactive for other extensions: a .tex file has nothing to provide, and a .dtx file hides its declaration inside guarded macrocode. No autofix is offered because the author must choose the declaration’s placement, date, and version.

This rule is enabled by default.

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

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

unknown-option ​

Flag a \usepackage or \RequirePackage option that the loaded package never declares with \DeclareOption. LaTeX reports this as an “Unknown option” error at compile time. The rule checks only analyzed project packages, such as a sibling .sty file; it ships no option data for system packages.

The package’s declared option set must be 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 because an unknown class option becomes an unused global option rather than an error. No autofix is offered because the author must choose whether to remove or rename the option.

This rule is enabled by default.

With a sibling mypkg.sty:

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

Loading the sibling package with an option it never declares:

tex
\usepackage[final]{mypkg}
text
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 subscript or superscript argument, which _ and ^ can bind without braces (x^{2} is x^2). The autofix deletes the two braces and leaves the inner token untouched.

The fix is withheld when removing the braces would let a following character merge with the argument and change meaning. For example, x^{2}-3 stays braced because x^2-3 would re-lex 2-3 as one token, and y_{\alpha}b stays braced because \alphab is one control word. Standard named math operators also remain 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:

tex
$x^{2}$ and $y_{\alpha}$
text
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:

tex
$x^2$ and $y_\alpha$

unclosed-math-delimiter ​

Flag a math opener the parser demoted to a plain token because no closer was reachable: a $ without a matching $, a \[ or \( without a matching \] or \), or a \left without a matching \right. These shapes can be data in macro code (>{$} array columns, \expandafter\@tempa\[\@nil), so the parser accepts them without a diagnostic. In prose, they almost always indicate a missing closer.

The rule reports only openers in document prose. It stays silent inside brace groups or optional arguments (\newcommand{...}{$}, the >{$} column specification), expl3 regions, and macrocode bodies. No autofix is offered because the author may need to insert a closer or delete a stray opener.

This rule is enabled by default.

An inline-math $ with no matching $:

tex
Let $x = 1 be the base case.
text
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 \]:

tex
The bound \[ x + y follows immediately.
text
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:

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

label-before-caption ​

Flag a \label 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 a standard numbered enumerate list. In these positions, \label captures the previous \@currentlabel, usually an enclosing section number, so \ref silently prints an unrelated number. LaTeX gives no warning.

The list check covers only 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 check 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. It is Unsafe because it infers the author’s intent and deliberately changes what \ref prints.

This rule is enabled by default.

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

tex
\begin{figure}
  \includegraphics{plot}
  \label{fig:plot}
  \caption{A plot.}
\end{figure}
text
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:

tex
\begin{enumerate}
  \label{item:first}
  \item First
\end{enumerate}
text
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:

tex
\begin{minipage}{\textwidth}
  \label{fig:plot}
  \captionof{figure}{A plot.}
\end{minipage}
text
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:

tex
\begin{document}
\item A
\end{document}
text
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:

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

The verb determines the scope. skip covers the next construct, off and on delimit a region, and skip-file covers the whole file wherever the directive appears:

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

The <id> is optional. Omitting it suppresses every rule over the same span. The : <reason> 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.