Linters

When Lins reads the input file, the text first passes through a parser tasked with piecing together segments of text which are then passed on to the linting stage. The linting stage enforces the rules defined by the user by seaching for and reporting any violations in the set of text segments.

Currently, two different parsers—and by extension, two different linters—are available:

Note

You can manually select which linter to use with the --linter option.

Plain text

The plain text parser emits a text segment for each paragraph. Two consecutive line breaks is interpreted as a paragraph break. For example, this text:

A Section Title
---------------

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras porta
odio non efficitur pharetra. Phasellus venenatis accumsan quam, ut
lacinia magna ornare iaculis.

Integer porttitor vel dolor accumsan sodales. Etiam est sem,
ullamcorper sit amet viverra eu, tempus sed nisi. Nulla scelerisque
purus nunc. Sed consectetur nunc in est euismod, aliquet volutpat
ante luctus. Nam feugiat quam a tortor ultricies tristique.

is split up into the following text segments:

A Section Title
---------------
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras porta
odio non efficitur pharetra. Phasellus venenatis accumsan quam, ut
lacinia magna ornare iaculis.
Integer porttitor vel dolor accumsan sodales. Etiam est sem,
ullamcorper sit amet viverra eu, tempus sed nisi. Nulla scelerisque
purus nunc. Sed consectetur nunc in est euismod, aliquet volutpat
ante luctus. Nam feugiat quam a tortor ultricies tristique.

Scopes

Since a text segment corresponds to a paragraph, that becomes the granularity of the plain text linter. Thus, two scopes are available:

  • text—spanning the entire document and

  • paragraph—spanning a paragraph.

Scopes are used when defining rule files.

LaTeX

The LaTeX parser follows the rules of the TeX language; identifying control symbols, control words, comments and the text to be typeset. The parser does not perform expansion or symbol lookup. However, control symbols are replaced with the character that they would generate in the typeset text, i.e. \_ inserts an underscore, \% a percent sign, \& an ampersand and so on.

The stream of TeX tokens is later interpreted in the LaTeX sense, identifying environments, option capture groups and special font styling macros like \emph, \textbf etc.

Just like the plain text parser, the text in the target file is passed on to the linting stage in segments. However, these segments—apart from containing only the text that will be typset in the document—also contain meta information such as the scope and the context.

The Examples section breaks down a few real-world examples and demonstrates how the parser works. Lins can be instructed to dump the parser output with the --parser-output option.

Font Styling Macros

The font styling macros defined by LaTeX such as \textbf and \emph are handled differently from regular control sequences. Instead of triggering a new segment and adding to the scope chain, the text within is expanded into the text of the parent segment. This is done to allow rules to target expressions consisting of more than one word where the author has decided to style one of the words differently from the others. For example, say we want to warn the author about the existence of “bad expression”. This behavior results in

This is a sentence containing the \emph{bad} expression.

being emitted as one single segment with the complete sentence as the text to be typeset, i.e. \emph is discarded—allowing the rule searching for “bad expression” to trigger a violation.

The only exception to this behavior is \texttt which is not expanded into the text of the parent segment. This is because of the assumption that this particular font style is used for identifiers and do not tie in to the surronding text in the same way. More importantly, this offers the ability to create rules to target identifiers written in snake case or camel case, triggering a warning that \texttt should be used to typeset this word.

Scopes

Scopes play a key role in the LaTeX linter and can be leveraged by rules to great effect. The scope is defined as the chain of the control sequences and environments enclosing the text segment. Each link in the chain is a scope entry. For example,

\section{The Section Title}

would be emitted as a segment containing “The Section Title” as the text to be typeset and section (control sequence) as the attached scope.

The same goes for environments,

\begin{foo}
This is the first
paragraph.

This is the second paragraph.
\end{foo}

would be emitted as two segments:

  • one containing “This is the first paragraph.” and

  • one containing “This is the second paragraph.”,

both with the scope: foo (environment). Segments are emitted when the \par token (which is implied by two newline characters) is processed and when enclosures end. A single newline character inserts a space—all according to the rules of TeX.

Context

The context is a property attached to a scope entry. It holds 20 characters from the raw text leading up to the start of the enclosure. For example, in

01234567890123456789\foo{Inside the foo control sequence.}

the context of the \foo scope entry would hold the characters 01234567890123456789. This feature may be used to define rules enforcing the use of specific TeX source code before a particular control sequence. For example, a rule requiring any reference macro like \ref or \eqref to be preceded by the tie character ~ (non-breaking space) is straight-forward to define using contexts.

This is a reference to Table \ref{tab:my_table}.

is emitted as two segments:

  • one containing the text leading up to \ref (plus the last . character) and

  • one containing the text “tab:my_table” where the scope entry for \ref has the context

     reference to Table 

Defining the rule discussed above involves simply specifying (?<!~)$ as the value for the leading field for a \ref scope entry in the LaTeX section of the rule file.

Examples

This section presents a few real-world examples of rules specific to the LaTeX linter. Refer to the section on rule files for documentation of the syntax.

Note

Many of these examples exists as rule files in the rules/tex directory in the source repository.

Check that a \ref contains a proper label prefix:

extends: existence
message: "Unknown \\ref label prefix '$1'."
ignorecase: true
nonword: true
level: error
linter:
  - latex
latex:
  - name: ref
    type: control sequence
tokens:
  - ^(?!fig:|tab:|sec:|ch:|itm:|lst:|alg:|app:)\w+

Check that an \eqref contains a proper label prefix:

extends: existence
message: "Unknown \\eqref label prefix '$1'."
ignorecase: true
nonword: true
level: error
linter:
  - latex
latex:
  - name: eqref
    type: control sequence
tokens:
  - ^(?!eq:)\w+

Check that \ref is not used to reference an equation:

extends: existence
message: "Use \\eqref to reference equations."
ignorecase: true
nonword: true
level: error
linter:
  - latex
latex:
  - name: ref
    type: control sequence
tokens:
  - ^eq:\w+

Check that a \ref to a figure is preceded by Fig. or Figs. (IEEE reference styling):

extends: existence
message: "Use 'Fig.' or 'Figs.'."
ignorecase: true
nonword: true
level: error
linter:
  - latex
latex:
  - name: ref
    type: control sequence
    leading: (?<!Fig\.|Figs\.)~$
tokens:
  - 'fig:'

Enforce the rule that a caption to, e.g. a figure or a table should end with a full stop—making it more likely that the author writes a complete sentence:

extends: existence
message: "A caption should end with a full stop '.'."
ignorecase: true
nonword: true
level: error
linter:
  - latex
latex:
  - name: caption
    type: control sequence
    descend: false
tokens:
  - (?<!\.)$

Enforce the rule that a caption should be longer than five words:

extends: existence
message: "A caption should be a full sentence with more than five words."
ignorecase: true
nonword: true
level: error
linter:
  - latex
latex:
  - name: caption
    type: control sequence
    leading: (?<!\.)$
    descend: false
tokens:
  - ^(\b\w+[.!?]?\s*){0,5}$

Warn the user when there are annotations left in the document source code:

message: "'$1' left in text."
extends: existence
ignorecase: false
nonword: true
level: warning
scope:
  - comment
tokens:
  - FIXME
  - TODO
  - NOTE

Enforce typographically correct quotation marks (single):

extends: existence
message: "Unexpected quotation mark, consider using `$1' or using an emphasis macro, e.g. \\emph or \\texttt."
ignorecase: true
level: warning
nonword: true
linter:
  - latex
tokens:
  - (?<=\s')(\w.*?\b)(?=`|'|"|”)
  # Check for correct start but incorrect end.
  - (?<=\s`)(\w.*?\b)(?=`|"|”|'')

Enforce typographically correct quotation marks (double):

extends: existence
message: "Unexpected quotation mark, consider using ``$1'' or using an emphasis macro, e.g. \\emph or \\texttt."
ignorecase: true
level: warning
nonword: true
linter:
  - latex
tokens:
  - (?<="|”|'')(\w.*?\b)(?="|'|”|`)
  # Check for correct start but incorrect end.
  - (?<=``)(\w.*?\b)(?="|”|`|'\s)

Check for camel case identifiers inside the \texttt control sequence and suggest adding two parentheses:

extends: existence
message: "Add two parentheses '()' to the end of the function '$1'."
ignorecase: false
nonword: true
level: error
linter:
  - latex
latex:
  - name: texttt
    type: control sequence
tokens:
  - '(?<!:|\/|\\)([a-z]*)([A-Z]+[a-z0-9][A-za-z0-9]*){2,}+(?!\/)(?!\()'

Check for identifiers that should be typeset using the TeX typewriter font, i.e. enclosed in \texttt:

extends: existence
message: "Use \\texttt to typeset '$1'."
ignorecase: false
nonword: true
level: error
linter:
  - latex
latex:
  - name: document
    type: environment
    logic: and
  - name: texttt
    type: control sequence
    logic: not
  - name: href
    type: control sequence
    logic: not
  - name: cite
    type: control sequence
    logic: not
  - name: lstlisting
    type: environment
    logic: not
  - name: includegraphics
    type: control sequence
    logic: not
  - name: input
    type: control sequence
    logic: not
tokens:
  # Pascal & camel case identifiers, except when preceded by ':', '/' or '\' (paths).
  - (?<!:|\/|\\)([a-z]*)([A-Z]+[a-z0-9][A-za-z0-9]*){2,}
  # Snake case identifiers, except when preceded by ':', '/' or '\' (paths).
  - (?<!:|\/|\\)\b[a-z][a-z0-9]+(_[a-z0-9]+){1,}\b
  # Filenames
  - \b\w{3,}\.[a-z]+\b
exceptions:
  - CentOS
  - OpenSuse