lastlint: LaTeX strict linter
A linter for Evan Chen's LaTeX style guide.
Usage
Pre-commit hook
Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/cjquines/lastlint
rev: v0.5.1 # x-release-please-version
hooks:
- id: lastlint-fix
To only report errors rather than auto-fixing them,
use the lastlint hook id.
Standalone
Install from PyPI. Using uv (recommended):
uv tool install lastlint
lastlint path/to/file.tex another.tex
To run without installing: uvx lastlint path/to/file.tex.
With pip: pip install lastlint.
Multiple files are accepted, and glob patterns are expanded.
Pass --fix to auto-fix supported rules in place.
Pass --ignore E001,E013 to skip both reporting and fixing.
On a terminal, output is grouped per file and colorized;
when piped or redirected it uses a plain
file:line:col: EXXX: message
format parseable by editors.
Force either mode with --color always or --color never
(also honors NO_COLOR and FORCE_COLOR).
Editor
An optional language server exposes lastlint to compatible text editors.
Install it with the lsp extra:
uv tool install 'lastlint[lsp]'
# or: pip install 'lastlint[lsp]'
The server lastlint-lsp, over stdio,
publishes diagnostics and implements formatting,
so format-on-save will auto-fix LaTeX files.
Pass ignored rules through initializationOptions,
e.g. {"ignore": ["E001", "E013"]}.
The setups below assume lastlint-lsp is globally installed.
To skip a global install, replace the command with
uvx --from 'lastlint[lsp]' lastlint-lsp.
Neovim
0.11+, native vim.lsp:
vim.lsp.config('lastlint', {
cmd = { 'lastlint-lsp' },
filetypes = { 'tex', 'plaintex' },
root_markers = { '.git' },
})
vim.lsp.enable('lastlint')
Sublime Text
The LSP package, in LSP.sublime-settings:
{
"clients": {
"lastlint": {
"enabled": true,
"command": ["lastlint-lsp"],
"selector": "text.tex.latex"
}
}
}
Emacs
Eglot (built
in since 29) — then run M-x eglot in a TeX buffer:
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'((latex-mode LaTeX-mode) . ("lastlint-lsp"))))
Suppression
Add an inline comment to skip rules on a single line:
This text has "intentional" quotes. % lastlint: disable=E002
Rules
Fixable rules are marked with 🔧.
| ID | Rule from style guide |
|---|---|
| E001 | Line length must be ≤ 120 |
| E002 🔧 | No literal " |
| E003 🔧 | Operators (\sin etc.) must be commands |
| E004 🔧 | No \ldots, \cdots, or ... |
| E005 🔧 | No grammatical punctuation inside inline math |
| E006 🔧 | No extraneous space before punctuation |
| E007 | Spacing around = must be symmetric |
| E009 🔧 | No bad math operators (| etc.) |
| E010 🔧 | No $$ ... $$ |
| E011 | No adjacent \[ ... \] blocks |
| E012 | \begin{align*} / \end{align*} must have own line |
| E013 🔧 | Env contents must be indented |
| E014 🔧 | No trailing whitespace |
| E015 | No \\\\ paragraph break |
| E017 🔧 | No : for function signatures |
Rules not implemented:
- Rule 8 (balanced delimiters): hard to lint reliably.
- Rule 16 (variables in prose): requires natural-language understanding.
E001
E001 skips two kinds of unfixable long lines:
those containing a URL (http:// or https://),
and fully-commented lines (only whitespace before the %).
Lines with a long trailing comment after real content are still flagged.
I recommend SemBr for automatically adding line breaks.
E004
E004 picks the right \dots variant from context
(\dotsc after a comma, \dotsb after an operator)
and leaves a \cdots unresolved when neither side determines spacing.
E013
E013 checks every env except a small denylist:
document, center, quote,
and the verbatim envs (asy, verbatim, lstlisting, …).
See NO_INDENT_ENVS in lastlint.py to adjust.
The --fix pads under-indented lines inside indent envs to 2 * depth spaces
(already-correctly-indented or deeper lines are left alone).
Development
pip install -e .[dev]
pytest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lastlint-0.5.1.tar.gz.
File metadata
- Download URL: lastlint-0.5.1.tar.gz
- Upload date:
- Size: 19.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b82de73478e386e869ab4780d0fc509fc9232f7a61ef5129d258303c6f2f5e13
|
|
| MD5 |
aed7ec2f2c1a7e6d54fe526fc8e5fb76
|
|
| BLAKE2b-256 |
d48942ef5812fc3c2303e49ca129679029a76eb4f65bf7231ae9e934410dff3c
|
Provenance
The following attestation bundles were made for lastlint-0.5.1.tar.gz:
Publisher:
release-please.yml on cjquines/lastlint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lastlint-0.5.1.tar.gz -
Subject digest:
b82de73478e386e869ab4780d0fc509fc9232f7a61ef5129d258303c6f2f5e13 - Sigstore transparency entry: 2374894780
- Sigstore integration time:
-
Permalink:
cjquines/lastlint@4d5331a8e31fac6a00e15dded7dd7ea70120c1d0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cjquines
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@4d5331a8e31fac6a00e15dded7dd7ea70120c1d0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file lastlint-0.5.1-py3-none-any.whl.
File metadata
- Download URL: lastlint-0.5.1-py3-none-any.whl
- Upload date:
- Size: 16.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a306b747a0f727a18afff0bcd47e35c9b21ee1e2a112b2f8dcb6843d5877f32
|
|
| MD5 |
284c80a4194e6133f074a9bb13f4a3b2
|
|
| BLAKE2b-256 |
72294656ebecc3e519dc46c3049c7e59bb9f6d511ef1d596df31a5c3cef25699
|
Provenance
The following attestation bundles were made for lastlint-0.5.1-py3-none-any.whl:
Publisher:
release-please.yml on cjquines/lastlint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lastlint-0.5.1-py3-none-any.whl -
Subject digest:
5a306b747a0f727a18afff0bcd47e35c9b21ee1e2a112b2f8dcb6843d5877f32 - Sigstore transparency entry: 2374894855
- Sigstore integration time:
-
Permalink:
cjquines/lastlint@4d5331a8e31fac6a00e15dded7dd7ea70120c1d0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cjquines
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-please.yml@4d5331a8e31fac6a00e15dded7dd7ea70120c1d0 -
Trigger Event:
push
-
Statement type: