From 9dcbe965e9fa29a1d68c134b6c85c023fc40a553 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Wed, 17 Jun 2026 20:05:34 +0200 Subject: [PATCH 01/24] Implement Python checks --- .github/workflows/check-python.yml | 33 + .github/workflows/check.yml | 3 + .gitignore | 1 + pyrightconfig.json | 14 + source/algorithms.tex | 1 + source/assets/example_01.tex | 1 + source/assets/example_02.tex | 1 + source/assets/example_03.tex | 1 + source/assets/example_04.tex | 1 + source/assets/example_05.tex | 1 + source/assets/example_06.tex | 1 + source/basic.tex | 1 + source/classes.tex | 1 + source/compatibility.tex | 1 + source/concepts.tex | 1 + source/containers.tex | 1 + source/declarations.tex | 1 + source/diagnostics.tex | 1 + source/exceptions.tex | 1 + source/exec.tex | 1 + source/expressions.tex | 1 + source/future.tex | 1 + source/grammar.tex | 1 + source/intro.tex | 4 +- source/iostreams.tex | 1 + source/iterators.tex | 1 + source/layout.tex | 1 + source/lex.tex | 1 + source/limits.tex | 1 + source/macros.tex | 2 + source/memory.tex | 1 + source/meta.tex | 1 + source/modules.tex | 1 + source/numerics.tex | 1 + source/overloading.tex | 1 + source/preprocessor.tex | 1 + source/statements.tex | 1 + source/strings.tex | 1 + source/tables.tex | 1 + source/templates.tex | 1 + source/text.tex | 1 + source/threads.tex | 1 + source/time.tex | 1 + source/uax31.tex | 1 + source/utilities.tex | 1 + tools/check-source.py | 2231 ++++++++++++++++++++++++++++ tools/test-checks.tex | 262 ++++ 47 files changed, 2587 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/check-python.yml create mode 100644 pyrightconfig.json create mode 100644 tools/check-source.py create mode 100644 tools/test-checks.tex diff --git a/.github/workflows/check-python.yml b/.github/workflows/check-python.yml new file mode 100644 index 0000000000..054a7a39da --- /dev/null +++ b/.github/workflows/check-python.yml @@ -0,0 +1,33 @@ +name: check-python + +on: + pull_request: + paths: + - "tools/**.py" + - "pyrightconfig.json" + - ".github/workflows/check-python.yml" + push: + paths: + - "tools/**.py" + - "pyrightconfig.json" + - ".github/workflows/check-python.yml" + +jobs: + pyright: + name: Pyright type check + runs-on: ubuntu-24.04 + + steps: + - name: checkout + uses: actions/checkout@v6 + + - name: setup Node + uses: actions/setup-node@v6 + with: + node-version: "20" + + - name: install pyright + run: npm install -g pyright + + - name: run pyright + run: pyright diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index acd96b5047..ecd5d3c34c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -33,6 +33,9 @@ jobs: - name: check-source.sh run: ../tools/check-source.sh + - name: check-source.py + run: python3 ../tools/check-source.py + - name: update brew if: matrix.cfg.os == 'macos-15' run: brew update diff --git a/.gitignore b/.gitignore index 9286aae13e..6db5d22375 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ tools/sections *.synctex.gz *.synctex* .check.stamp +__pycache__ diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000000..5115227a57 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,14 @@ +{ + "include": [ + "tools/*.py" + ], + "typeCheckingMode": "strict", + "reportMissingTypeStubs": false, + "reportUnknownMemberType": false, + "reportUnknownArgumentType": false, + "reportUnknownVariableType": false, + "reportUnknownParameterType": false, + "reportUnusedImport": true, + "reportUnusedVariable": true, + "pythonVersion": "3.11" +} diff --git a/source/algorithms.tex b/source/algorithms.tex index 95b1a81873..8632047b2c 100644 --- a/source/algorithms.tex +++ b/source/algorithms.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[algorithms]{Algorithms library} \rSec1[algorithms.general]{General} diff --git a/source/assets/example_01.tex b/source/assets/example_01.tex index ffda6ec746..6f0cc8bb76 100644 --- a/source/assets/example_01.tex +++ b/source/assets/example_01.tex @@ -1,3 +1,4 @@ +%NOCHECKBEGIN \documentclass[9pt]{standalone} \usepackage{fontspec} diff --git a/source/assets/example_02.tex b/source/assets/example_02.tex index 0b48a5aacc..b5cdb69f2e 100644 --- a/source/assets/example_02.tex +++ b/source/assets/example_02.tex @@ -1,3 +1,4 @@ +%NOCHECKBEGIN \documentclass[9pt]{standalone} \usepackage{fontspec} diff --git a/source/assets/example_03.tex b/source/assets/example_03.tex index 17479d4b6d..7865217fa2 100644 --- a/source/assets/example_03.tex +++ b/source/assets/example_03.tex @@ -1,3 +1,4 @@ +%NOCHECKBEGIN \documentclass[9pt]{standalone} \usepackage{fontspec} diff --git a/source/assets/example_04.tex b/source/assets/example_04.tex index 414cdf0822..132fc1915f 100644 --- a/source/assets/example_04.tex +++ b/source/assets/example_04.tex @@ -1,3 +1,4 @@ +%NOCHECKBEGIN \documentclass[9pt]{standalone} \usepackage{fontspec} diff --git a/source/assets/example_05.tex b/source/assets/example_05.tex index 109eb45f3f..e8d800756c 100644 --- a/source/assets/example_05.tex +++ b/source/assets/example_05.tex @@ -1,3 +1,4 @@ +%NOCHECKBEGIN \documentclass[9pt]{standalone} \usepackage{fontspec} diff --git a/source/assets/example_06.tex b/source/assets/example_06.tex index f63b99cc89..98c68627aa 100644 --- a/source/assets/example_06.tex +++ b/source/assets/example_06.tex @@ -1,3 +1,4 @@ +%NOCHECKBEGIN \documentclass[9pt]{standalone} \usepackage{fontspec} diff --git a/source/basic.tex b/source/basic.tex index 7fa8b01b1a..9e9180ff0c 100644 --- a/source/basic.tex +++ b/source/basic.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[basic]{Basics} \gramSec[gram.basic]{Basics} diff --git a/source/classes.tex b/source/classes.tex index 2349cbc816..85ddf2d985 100644 --- a/source/classes.tex +++ b/source/classes.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[class]{Classes}% \indextext{class|(} diff --git a/source/compatibility.tex b/source/compatibility.tex index 95ce283019..a4b23ef799 100644 --- a/source/compatibility.tex +++ b/source/compatibility.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \infannex{diff}{Compatibility} \rSec1[diff.cpp26]{\Cpp{} and ISO \CppXXVI{}} diff --git a/source/concepts.tex b/source/concepts.tex index d1e15a6e1a..eacfc43ee5 100644 --- a/source/concepts.tex +++ b/source/concepts.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[concepts]{Concepts library} \rSec1[concepts.general]{General} diff --git a/source/containers.tex b/source/containers.tex index 5e4ba77219..99f58011da 100644 --- a/source/containers.tex +++ b/source/containers.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[containers]{Containers library} \rSec1[containers.general]{General} diff --git a/source/declarations.tex b/source/declarations.tex index d3821debca..5f992716ea 100644 --- a/source/declarations.tex +++ b/source/declarations.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[dcl]{Declarations}% \indextext{declaration|(} diff --git a/source/diagnostics.tex b/source/diagnostics.tex index cd913a8e7e..89aec3148b 100644 --- a/source/diagnostics.tex +++ b/source/diagnostics.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[diagnostics]{Diagnostics library} \rSec1[diagnostics.general]{General} diff --git a/source/exceptions.tex b/source/exceptions.tex index 23e143e137..3338cd26e5 100644 --- a/source/exceptions.tex +++ b/source/exceptions.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[except]{Exception handling}% \indextext{exception handling|(} diff --git a/source/exec.tex b/source/exec.tex index 2fd6f4f240..f1e7dfb3ab 100644 --- a/source/exec.tex +++ b/source/exec.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[exec]{Execution control library} \rSec1[exec.general]{General} diff --git a/source/expressions.tex b/source/expressions.tex index b217285984..4efff5d39d 100644 --- a/source/expressions.tex +++ b/source/expressions.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[expr]{Expressions} \gramSec[gram.expr]{Expressions} diff --git a/source/future.tex b/source/future.tex index 38948be5d0..c9c282aa25 100644 --- a/source/future.tex +++ b/source/future.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \normannex{depr}{Compatibility features} \rSec1[depr.general]{General} diff --git a/source/grammar.tex b/source/grammar.tex index 259dabaedf..53daa81f2b 100644 --- a/source/grammar.tex +++ b/source/grammar.tex @@ -1,3 +1,4 @@ +%NOCHECKBEGIN(lib-*) \infannex{gram}{Grammar summary} \rSec1[gram.general]{General} diff --git a/source/intro.tex b/source/intro.tex index 3e24d06dce..82d7454c75 100644 --- a/source/intro.tex +++ b/source/intro.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \clearpage \bigskip\noindent\textlarger{\textbf{Programming languages --- \Cpp{}}} @@ -325,8 +326,7 @@ behavior, for a \termref{defns.well.formed}{well-formed program}{} construct and correct data, that depends on the implementation and that each implementation documents -\definition{implementation-defined strict total order over pointers} -{defns.order.ptr} +\definition{implementation-defined strict total order over pointers}{defns.order.ptr} \indexdefn{pointer!strict total order}% \defncontext{library} \impldef{strict total order over pointer values} diff --git a/source/iostreams.tex b/source/iostreams.tex index e911e38de6..f45a7b995b 100644 --- a/source/iostreams.tex +++ b/source/iostreams.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[input.output]{Input/output library} \rSec1[input.output.general]{General} diff --git a/source/iterators.tex b/source/iterators.tex index 8c8469c0fa..d0e633d54d 100644 --- a/source/iterators.tex +++ b/source/iterators.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[iterators]{Iterators library} \rSec1[iterators.general]{General} diff --git a/source/layout.tex b/source/layout.tex index 98564d8bd0..f0ab2decef 100644 --- a/source/layout.tex +++ b/source/layout.tex @@ -74,6 +74,7 @@ % Register our intent to number the next paragraph. Don't actually number it % yet, because we might have a paragraph break before we see its contents (for % example, if the paragraph begins with a note or example). +%EXPECTCHECKNEXTLINE(base-pnum-alone) \def\pnum{% \global\def\maybeaddpnum{\global\def\maybeaddpnum{}\parabullnum{Paras}{0pt}}% \everypar=\expandafter{\the\everypar\maybeaddpnum}% diff --git a/source/lex.tex b/source/lex.tex index f96ec6b62c..c4d855e5db 100644 --- a/source/lex.tex +++ b/source/lex.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[lex]{Lexical conventions} \gramSec[gram.lex]{Lexical conventions} diff --git a/source/limits.tex b/source/limits.tex index e0596a320a..10d4d27087 100644 --- a/source/limits.tex +++ b/source/limits.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \infannex{implimits}{Implementation quantities} \pnum diff --git a/source/macros.tex b/source/macros.tex index bbba8134c6..bd76432404 100644 --- a/source/macros.tex +++ b/source/macros.tex @@ -1,4 +1,6 @@ %!TEX root = std.tex +%NOCHECKBEGIN +%NOCHECKEND(text-*) % Definitions and redefinitions of special commands % diff --git a/source/memory.tex b/source/memory.tex index 2c68fbe7ab..6c007493be 100644 --- a/source/memory.tex +++ b/source/memory.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[mem]{Memory management library} \rSec1[mem.general]{General} diff --git a/source/meta.tex b/source/meta.tex index 08052036a3..2b95cb125c 100644 --- a/source/meta.tex +++ b/source/meta.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[meta]{Metaprogramming library} \rSec1[meta.general]{General} diff --git a/source/modules.tex b/source/modules.tex index 9ecef29d77..29618c7c87 100644 --- a/source/modules.tex +++ b/source/modules.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[module]{Modules}% \gramSec[gram.module]{Modules} diff --git a/source/numerics.tex b/source/numerics.tex index e3a90bc456..88606917b1 100644 --- a/source/numerics.tex +++ b/source/numerics.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[numerics]{Numerics library} \rSec1[numerics.general]{General} diff --git a/source/overloading.tex b/source/overloading.tex index d25bf7febf..da1d4b79b2 100644 --- a/source/overloading.tex +++ b/source/overloading.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[over]{Overloading}% \indextext{overloading|(} diff --git a/source/preprocessor.tex b/source/preprocessor.tex index 39cd5e50f2..d3995e7523 100644 --- a/source/preprocessor.tex +++ b/source/preprocessor.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[cpp]{Preprocessing directives}% \indextext{preprocessing directive|(} diff --git a/source/statements.tex b/source/statements.tex index f034493787..9694c9ca63 100644 --- a/source/statements.tex +++ b/source/statements.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[stmt]{Statements}% \indextext{statement|(} diff --git a/source/strings.tex b/source/strings.tex index 1286a2364b..b27746dd7c 100644 --- a/source/strings.tex +++ b/source/strings.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[strings]{Strings library} \rSec1[strings.general]{General} diff --git a/source/tables.tex b/source/tables.tex index ea2f26a2c9..da944fedd9 100644 --- a/source/tables.tex +++ b/source/tables.tex @@ -1,5 +1,6 @@ %!TEX root = std.tex % Definitions of table environments +%NOCHECKBEGIN(base-env-balancing) %%-------------------------------------------------- %% Table environments diff --git a/source/templates.tex b/source/templates.tex index e9dc7b7d19..7a0fb6d457 100644 --- a/source/templates.tex +++ b/source/templates.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \rSec0[temp]{Templates}% \indextext{template|(} diff --git a/source/text.tex b/source/text.tex index b7b6f6ed55..2e6ace0f34 100644 --- a/source/text.tex +++ b/source/text.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[text]{Text processing library} diff --git a/source/threads.tex b/source/threads.tex index a898419ceb..08008a7530 100644 --- a/source/threads.tex +++ b/source/threads.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[thread]{Concurrency support library} \rSec1[thread.general]{General} diff --git a/source/time.tex b/source/time.tex index 85696ba96b..2b339b7b4f 100644 --- a/source/time.tex +++ b/source/time.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[time]{Time library} \rSec1[time.general]{General} diff --git a/source/uax31.tex b/source/uax31.tex index 6574a48637..9f5e9ab268 100644 --- a/source/uax31.tex +++ b/source/uax31.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \infannex{uaxid}{Conformance with \UAX{31}} \rSec1[uaxid.general]{General} diff --git a/source/utilities.tex b/source/utilities.tex index 363841ece6..064b4c6e80 100644 --- a/source/utilities.tex +++ b/source/utilities.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-ranges-*) \rSec0[utilities]{General utilities library} diff --git a/tools/check-source.py b/tools/check-source.py new file mode 100644 index 0000000000..0b3f6b4218 --- /dev/null +++ b/tools/check-source.py @@ -0,0 +1,2231 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import fnmatch +import os +import re +import sys +import unicodedata +from abc import ABC +from collections import defaultdict +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Dict, List, Optional, Pattern, Set, Tuple + + +# ================================================================================================== +# Lookup tables +# ================================================================================================== + +# Remember to keep this in sync with [structure.specifications] and our set of macros. +# The order matters for ElementOrderCheck. +FUNCTION_DESCRIPTORS = [ + "constraints", # Constraints + "mandates", # Mandates + "constantwhen", # Constant When + "expects", # Preconditions + "hardexpects", # Hardened preconditions + "effects", # Effects + "sync", # Synchronization + "ensures", # Postconditions + "returns", # Returns + "throws", # Throws + "complexity", # Complexity + "remarks", # Remarks + "errors", # Error conditions +] + +# All library descriptors. +# The order doesn't matter; this list is used for checking whether e.g. `\recommended` +# is preceded by `\pnum` +PARAGRAPH_DESCRIPTORS = [ + *FUNCTION_DESCRIPTORS, + "recommended", # Recommended practice + "required", # Required behavior + "implimits", # Implementation limits + "returntype", # Return type + "ctype", # Type + "templalias", # Alias template +] + +DIFF_DESCRIPTORS = [ + "change", + "rationale", + "effect", + "difficulty", + "howwide", +] + +KNOWN_COMMANDS = { + "0", + "Annex", + "BODY", + "BnfInc", + "BnfIndent", + "BnfNontermshape", + "BnfReNontermshape", + "BnfRest", + "BnfTermshape", + "BreakableUnderscore", + "CodeBlockSetup", + "CodeStyle", + "CodeStylex", + "CodeStylexGuarded", + "Cpp", + "CppIII", + "CppXI", + "CppXIV", + "CppXVII", + "CppXX", + "CppXXIII", + "CppXXVI", + "D", + "DeclareCaptionLabelSeparator", + "DeclareMathOperator", + "DisableLigatures", + "EXPO", + "ExplSyntaxOff", + "ExplSyntaxOn", + "Fundesc", + "Fundescx", + "Gamma", + "GrammarStylex", + "Head", + "Huge", + "IsoC", + "IsoCUndated", + "IsoFloatUndated", + "IsoPosix", + "IsoPosixUndated", + "LARGE", + "Large", + "N", + "NTS", + "NewDocumentCommand", + "NewEnviron", + "OldTextAsciiTilde", + "Pi", + "RSsmallest", + "Range", + "S", + "Sec", + "Sigma", + "StrClosingbrace", + "StrSubstitute", + "StrTextsc", + "StrTextsmaller", + "The", + "U", + "UAX", + "UNSP", + "UNSPnc", + "W", + "a", + "abovecaptionskip", + "addcontentsline", + "added", + "addednb", + "addtocontents", + "addtocounter", + "addtolength", + "addxref", + "advance", + "afterchapskip", + "afterchapternum", + "afterskip", + "aliaspagestyle", + "allowbreak", + "alpha", + "annexlabel", + "annexnumberlinebox", + "appendix", + "appendixname", + "arabic", + "atsign", + "b", + "backmatter", + "backslash", + "bar", + "baselineskip", + "beforechapskip", + "beforeskip", + "begin", + "begingroup", + "belowcaptionskip", + "beta", + "bfseries", + "bgroup", + "bibitem", + "bibname", + "big", + "bigl", + "bigoh", + "bigr", + "bigskip", + "binom", + "bitand", + "bitor", + "bm", + "bmod", + "bnfindent", + "bnfindentfirst", + "bnfindentinc", + "bnfindentrest", + "bot", + "bottomline", + "br", + "brange", + "break", + "brk", + "c", + "capsep", + "caption", + "captionsetup", + "caret", + "cdot", + "cdots", + "cedef", + "centering", + "cftsetindents", + "change", + "changed", + "changednb", + "changeglossnumformat", + "chapnamefont", + "chapnumfont", + "chapter", + "chaptermark", + "chaptername", + "chapternamenum", + "chapternumberlinebox", + "chapterstyle", + "chaptitlefont", + "char", + "chdr", + "chdrx", + "checkandfixthelayout", + "cite", + "clauselabel", + "clearpage", + "cline", + "clist", + "closeout", + "clubpenalty", + "cname", + "colhdr", + "color", + "columnbreak", + "commentellip", + "complexity", + "constantwhen", + "constraints", + "contentsname", + "continuedcaption", + "copyboxwidth", + "copypagestyle", + "copyright", + "cos", + "countedrange", + "counterwithin", + "counterwithout", + "cppver", + "cptn", + "crange", + "cs", + "ctype", + "customlabel", + "cv", + "cvqual", + "d", + "def", + "default", + "defexposconcept", + "defexposconceptnc", + "definecolor", + "definition", + "deflibconcept", + "defn", + "defnadj", + "defnadjx", + "defncontext", + "defnlibxname", + "defnnewoldconcept", + "defnoldconcept", + "defnx", + "defnxname", + "delta", + "deprxref", + "descr", + "diffdef", + "diffhead", + "difficulty", + "diffref", + "diffrefs", + "discretionary", + "displaystyle", + "do", + "doccite", + "docno", + "documentclass", + "doindexescape", + "doneindexescape", + "dotsb", + "dotsc", + "ecname", + "edef", + "ednote", + "effect", + "effectafteritemize", + "effects", + "egroup", + "ell", + "else", + "emergencystretch", + "emph", + "end", + "endfirsthead", + "endgroup", + "endhead", + "endmulticols", + "ensuremath", + "ensures", + "eqref", + "equiv", + "errors", + "everypar", + "exp", + "expandafter", + "expects", + "expos", + "exposconcept", + "exposconceptnc", + "exposconceptx", + "exposid", + "exposidnc", + "f", + "fakegrammarterm", + "fbox", + "fboxrule", + "fboxsep", + "feetatbottom", + "fi", + "figurerefname", + "fill", + "firstlibchapter", + "fmtgrammarterm", + "fmtnontermdef", + "foo", + "footmarkstyle", + "footnote", + "footnotemark", + "footnotesize", + "footnotetext", + "frac", + "fref", + "frenchspacing", + "frontmatter", + "g", + "gamma", + "ge", + "geq", + "global", + "glossary", + "glossaryname", + "glossitem", + "glue", + "gramSec", + "gramWrite", + "grammarterm", + "grammartermnc", + "gramout", + "gterm", + "hangpara", + "hardexpects", + "hbox", + "hd", + "hdstyle", + "hdwidth", + "hfill", + "hline", + "howwide", + "hrule", + "hsize", + "hspace", + "hss", + "hyperpage", + "hyperref", + "hypersetup", + "hypertarget", + "hyphenation", + "idxCpp", + "idxbfpage", + "idxcode", + "idxconcept", + "idxexposconcept", + "idxgram", + "idxhdr", + "idxmname", + "idxnewoldconcept", + "idxoldconcept", + "idxterm", + "idxxname", + "if", + "ifcase", + "iffalse", + "ifhmode", + "ifinner", + "ifmmode", + "ifnum", + "iftrue", + "ifvmode", + "ifx", + "immediate", + "impdef", + "impdefnc", + "impdefx", + "impldef", + "impldefplain", + "impldefrootname", + "implimits", + "importexample", + "include", + "includegraphics", + "index", + "indexconcept", + "indexcont", + "indexdefn", + "indexescape", + "indexgram", + "indexgrammar", + "indexhdr", + "indexheader", + "indeximpldef", + "indexlibrary", + "indexlibraryboth", + "indexlibraryctor", + "indexlibrarydtor", + "indexlibraryglobal", + "indexlibrarymember", + "indexlibrarymemberexpos", + "indexlibrarymemberx", + "indexlibrarymisc", + "indexlibraryzombie", + "indexname", + "indexoff", + "indexordmem", + "indextext", + "indexunordmem", + "infannex", + "infty", + "input", + "int", + "iref", + "isocopyright", + "itcorr", + "itcorrwidth", + "item", + "itletterwidth", + "itshape", + "kern", + "keyword", + "kill", + "l", + "label", + "labelitemi", + "labelitemii", + "labelitemiii", + "labelitemiv", + "labelsep", + "lambda", + "land", + "large", + "larger", + "lastcorechapter", + "lastlibchapter", + "lbl", + "lceil", + "ldots", + "le", + "leaders", + "leavevmode", + "left", + "leftmargin", + "leftmargini", + "leftmark", + "leftshift", + "leq", + "let", + "lfloor", + "lhdr", + "lhdrx", + "libconcept", + "libconceptx", + "libdeprheaderref", + "libglobal", + "libheader", + "libheaderdef", + "libheaderref", + "libheaderrefx", + "libheaderrefxx", + "libheaderx", + "libmacro", + "libmember", + "libnoheader", + "libreqtabenv", + "libspec", + "libxmacro", + "lim", + "linebreak", + "listing", + "listparindent", + "ln", + "locgrammarterm", + "locnontermdef", + "log", + "logop", + "lor", + "lst", + "lstdefinelanguage", + "lsthk", + "lstnewenvironment", + "lstsaved", + "lstset", + "m", + "mainmatter", + "makeatletter", + "makeatother", + "makebox", + "makechapterstyle", + "makeevenfoot", + "makeevenhead", + "makeglossary", + "makeheadrule", + "makeindex", + "makeoddfoot", + "makeoddhead", + "makepagestyle", + "makepsmarks", + "mandates", + "mapsto", + "markboth", + "mathbin", + "mathit", + "mathop", + "mathrel", + "mathrm", + "mathscr", + "mathsf", + "mathtt", + "max", + "maxsecnumdepth", + "maybeaddpnum", + "mbox", + "meaning", + "meaningbody", + "microtypesetup", + "min", + "mname", + "mod", + "movedxref", + "movedxrefii", + "movedxrefiii", + "movedxrefs", + "mu", + "mulhi", + "mullo", + "multicols", + "multicolumn", + "n", + "nb", + "nbc", + "nc", + "neq", + "newcolumntype", + "newcommand", + "newcounter", + "newenvironment", + "newlabel", + "newlength", + "newline", + "newlist", + "newnoteenvironment", + "newoldconcept", + "newpage", + "newsavebox", + "newsubclausecounter", + "newwrite", + "nobreak", + "nocode", + "nocontentsline", + "nocorr", + "nodiffref", + "noexpand", + "noindent", + "nolinebreak", + "nonfrenchspacing", + "nontermdef", + "normalbaselines", + "normalbaselineskip", + "normalcolor", + "normalfont", + "normalsize", + "normannex", + "notdef", + "noteintro", + "noteoutro", + "ntbs", + "ntcxvis", + "ntcxxxiis", + "ntmbs", + "ntwcs", + "nu", + "numberwithin", + "numconst", + "oBreakableUnderscore", + "oCpp", + "oexposid", + "ogrammarterm", + "ohdr", + "ohdrx", + "oindex", + "oldconcept", + "oldconceptname", + "oldcontentsline", + "oldxref", + "omname", + "onelineskip", + "openout", + "operatorname", + "opt", + "or", + "orange", + "otcode", + "otextup", + "otherwise", + "overline", + "pagebreak", + "pagestyle", + "par", + "parabullnum", + "paragraph", + "paragraphmark", + "parindent", + "parshape", + "parskip", + "partopsep", + "pdfbookmark", + "pdfcolorstack", + "pdfcompresslevel", + "pdfminorversion", + "pdfobjcompresslevel", + "pdfstringdefDisableCommands", + "penalty", + "phantom", + "phantomsection", + "phi", + "pi", + "placeholder", + "placeholdernc", + "pnum", + "preglossaryhook", + "preindexhook", + "prevdocno", + "prime", + "printchapternum", + "printglossary", + "printindex", + "protect", + "protected", + "providecommand", + "qquad", + "quad", + "r", + "rSec", + "rSecindex", + "raggedbottom", + "raggedright", + "raise", + "raisebox", + "range", + "rationale", + "rb", + "rceil", + "realglossitem", + "recommended", + "ref", + "reflexpr", + "refstepcounter", + "regrammarterm", + "relax", + "reldate", + "remarks", + "remitem", + "removed", + "removednb", + "removedxref", + "removelastskip", + "renewcommand", + "renontermdef", + "required", + "result", + "returns", + "returntype", + "rfloor", + "rhdr", + "rhdrx", + "rho", + "right", + "rightarrow", + "rightmargin", + "rightmark", + "rightshift", + "rlap", + "rmfamily", + "rowhdr", + "rowsep", + "s", + "savedallowbreak", + "scriptsize", + "secref", + "section", + "sectionmark", + "seeabove", + "seeabovenc", + "seebelow", + "seebelownc", + "setafterparaskip", + "setaftersecskip", + "setaftersubparaskip", + "setaftersubsecskip", + "setaftersubsubsecskip", + "setbeforeparaskip", + "setbeforesecskip", + "setbeforesubparaskip", + "setbeforesubsecskip", + "setbeforesubsubsecskip", + "setbox", + "setcounter", + "setglobalstyles", + "setheaderspaces", + "setheadfoot", + "setlength", + "setlist", + "setlrmarginsandblock", + "setmarginnotes", + "setparaheadstyle", + "setparaindent", + "setsecheadstyle", + "setsecindent", + "setsubparaheadstyle", + "setsubparaindent", + "setsubsecheadstyle", + "setsubsecindent", + "setsubsubsecheadstyle", + "setsubsubsecindent", + "settowidth", + "setulmarginsandblock", + "sffamily", + "sigma", + "sin", + "small", + "smaller", + "sout", + "space", + "sqrt", + "stage", + "state", + "stepcounter", + "stopindexescape", + "string", + "strip", + "subparagraph", + "subsection", + "subsectionmark", + "subsubsection", + "subsubsectionmark", + "sum", + "supercite", + "swallow", + "sync", + "t", + "tabcolsep", + "tableofcontents", + "tablerefname", + "tabularnewline", + "tcode", + "temp", + "templalias", + "term", + "terminal", + "termref", + "texorpdfstring", + "text", + "textasciitilde", + "textbackslash", + "textbf", + "textbullet", + "textcolor", + "textit", + "textlangle", + "textlarger", + "textmu", + "textnormal", + "textrangle", + "textregistered", + "textrm", + "textsc", + "textsection", + "textsf", + "textsl", + "textsmaller", + "textsuperscript", + "texttt", + "textunderscore", + "textup", + "textwidth", + "the", + "thechapter", + "thepage", + "theparagraph", + "thesection", + "thesubsection", + "thesubsubsection", + "theta", + "thispagestyle", + "throws", + "times", + "tllo", + "to", + "today", + "tokenize", + "topline", + "topskip", + "tref", + "ttfamily", + "twocolglossary", + "u", + "ucode", + "uline", + "uname", + "ungap", + "unhbox", + "unicode", + "unskip", + "unspec", + "unspecalloctype", + "unspecbool", + "unspecnc", + "unspecuniqtype", + "unun", + "upshape", + "url", + "urlstyle", + "usepackage", + "v", + "value", + "vbox", + "vcenter", + "vdots", + "verb", + "verbtocs", + "vfill", + "vrule", + "vskip", + "vspace", + "vtop", + "w", + "widowpenalties", + "write", + "x", + "xA", + "xBB", + "xBF", + "xEF", + "xF", + "xc", + "xdef", + "xname", + "xor", + "xref", + "xrefc", + "z", + "zeta", +} + +# =============================================================================== +# Terminal color support +# =============================================================================== + +ANSI_RESET = "\033[0m" +ANSI_BOLD = "\033[1m" +ANSI_RED = "\033[31m" +ANSI_GREEN = "\033[32m" +ANSI_YELLOW = "\033[33m" +ANSI_GRAY = "\033[90m" + +COLOR = sys.stderr.isatty() and "NO_COLOR" not in os.environ + + +def style(text: str, code: str) -> str: + if not COLOR: + return text + return f"{code}{text}{ANSI_RESET}" + + +# ================================================================================================== +# Data types +# ================================================================================================== + + +class Environment(Enum): + ITEMDESCR = "itemdescr" + CODEBLOCK = "codeblock" + CODEBLOCKTU = "codeblocktu" + NOTE = "note" + FOOTNOTE = "footnote" + EXAMPLE = "example" + + def __init__(self, name: str): + self.begin_pattern: Pattern[str] = re.compile( + r"\\begin\{" + re.escape(name) + r"\}" + ) + self.end_pattern: Pattern[str] = re.compile( + r"\\end\{" + re.escape(name) + r"\}" + ) + + +@dataclass +class Failure: + file: str + """File path relative to the working directory.""" + line: int + """0-based line number.""" + column_start: int + """0-based inclusive start column.""" + column_end: int + """0-based exclusive end column.""" + message: str + """The error message.""" + check_id: str + """The id of the check.""" + + +@dataclass +class ExpectedFailure: + file: str + """File path relative to the working directory.""" + comment_line: int + """0-based line number of the directive comment.""" + check_id: str + """The id of the check that is expected to fail.""" + hit: bool = False + """`true` if a matching failure was reported.""" + + +# ================================================================================================== +# Global state +# ================================================================================================== + +source_dir: Path = Path() +file_locations: Dict[str, Path] = {} +expected_registry: Dict[Tuple[str, int], ExpectedFailure] = {} + +# ================================================================================================== +# Expected-failure tracking +# ================================================================================================== + + +def register_expected(file: str, comment_line: int, check_id: str) -> None: + key = (file, comment_line) + if key not in expected_registry: + expected_registry[key] = ExpectedFailure(file, comment_line, check_id) + + +def consume_expected(file: str, failure_line: int, check_id: str) -> bool: + # Walk backwards through *consecutive* %EXPECTCHECKNEXTLINE directives + # only. A blank line (or any other content) breaks the chain — + # EXPECTCHECKNEXTLINE always refers to the immediately following line. + for offset in range(1, min(failure_line, 20) + 1): + entry = expected_registry.get((file, failure_line - offset)) + if entry is None: + # Not an EXPECTCHECKNEXTLINE line — chain is broken. + return False + if entry.check_id == check_id: + entry.hit = True + return True + # A different EXPECTCHECKNEXTLINE — keep looking (stacked directives). + return False + + +def collect_unexpectedly_not_failed() -> List[ExpectedFailure]: + return [e for e in expected_registry.values() if not e.hit] + + +# ================================================================================================== +# Utilities +# ================================================================================================== + + +COMMENT_PATTERN = re.compile(r"^\s*%") + + +def make_alt_pattern(items: List[str]) -> str: + return "(?:" + "|".join(re.escape(s) for s in items) + ")" + + +def read_file(path: Path) -> List[str]: + try: + with open(path, encoding="utf-8", errors="replace") as f: + return f.read().splitlines() + except OSError: + return [] + + +def format_failure(fail: Failure, lines: List[str]) -> str: + parts: List[str] = [] + message = ( + re.sub(r"`([^`]+)`", f"{ANSI_YELLOW}\\1{ANSI_RESET}", fail.message) + if COLOR + else fail.message + ) + parts.append( + f"{style(fail.file, ANSI_BOLD)}:" + f"{style(str(fail.line + 1), ANSI_BOLD)}:" + f"{style(str(fail.column_start + 1), ANSI_BOLD)}: " + f"{style('error:', ANSI_RED)} {message}" + f" {style(f'[{fail.check_id}]', ANSI_GRAY)}" + ) + if fail.line < len(lines): + src_line = lines[fail.line] + display_line = src_line.replace("\t", " ") + line_num_str = str(fail.line + 1) + num_width = max(6, len(line_num_str)) + pad = " " * (num_width - len(line_num_str)) + col1 = fail.column_start + 1 + col2 = fail.column_end + 1 + parts.append(f"{pad}{line_num_str} | {display_line}") + prefix = display_line[: col1 - 1] + highlight_pad = len(prefix) + highlight_len = max(1, col2 - col1) + if col1 - 1 + highlight_len > len(display_line): + highlight_len = max(1, len(display_line) - (col1 - 1)) + gutter = " " * (num_width + 1) + "| " + hl = style("^" + "~" * (highlight_len - 1), ANSI_GREEN) + parts.append(f"{gutter}{' ' * highlight_pad}{hl}") + return "\n".join(parts) + + +def find_env_ranges( + lines: List[str], + env: Environment, +) -> List[Tuple[int, int]]: + begin_pattern = env.begin_pattern + end_pattern = env.end_pattern + ranges: List[Tuple[int, int]] = [] + stack: List[int] = [] + for idx, line in enumerate(lines): + if begin_pattern.search(line): + stack.append(idx) + if end_pattern.search(line) and stack: + ranges.append((stack.pop(), idx + 1)) + return ranges + + +unexpected_count = 0 + + +def emit_check_failure( + check: Check, + file: str, + line: int, + column_start: int, + column_end: int, + message: str, +) -> None: + """ + Prints a failure immediately (unless consumed by an expected-failure marker). + Line numbers and columns follow the same convention as `Failure`. + """ + global unexpected_count + if consume_expected(file, line, check.id): + return + unexpected_count += 1 + fail = Failure( + file=file, + line=line, + column_start=column_start, + column_end=column_end, + message=message, + check_id=check.id, + ) + # Reading the file from scratch is very slow, + # but we don't care because this is the unhappy path anyway, + # and we usually don't expect failures anyway. + file_path = file_locations[file] + lines = read_file(file_path) + print(format_failure(fail, lines), file=sys.stderr) + print(file=sys.stderr) + + +# ================================================================================================== +# Checks +# ================================================================================================== + + +class Check(ABC): + """Base class for all checks.""" + + id: str = "" + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + """Called before processing any line of `file_path`.""" + self.file_path = file_path + self.lines = lines + + def check_line(self, line_num: int, line: str) -> None: + """Called for each line while the check is active.""" + pass + + def end_file(self, file_path: Path) -> None: + """Called after processing all lines of `file_path`.""" + pass + + def end_checks(self) -> None: + """Called once after all files have been processed.""" + pass + + def fail( + self, + line: int, + column_start: int, + column_end: int, + message: str, + ) -> None: + """Report a failure at `self.file_path`.""" + emit_check_failure( + self, + os.path.relpath(self.file_path), + line, + column_start, + column_end, + message, + ) + + +class BannedPatternCheck(Check): + """Reports a failure on every line matching a banned regex pattern.""" + + def __init__( + self, + check_id: str, + pattern: Pattern[str], + message: str, + ): + self.id = check_id + self.pattern = pattern + self.message = message + + def check_line(self, line_num: int, line: str) -> None: + if COMMENT_PATTERN.match(line): + return + m = self.pattern.search(line) + if m is None: + return + self.fail( + line_num, + m.start(), + m.end(), + self.message, + ) + + +class EnvRanges: + """Pre-computed line ranges for a LaTeX environment within one file.""" + + def __init__(self, lines: List[str], env: Environment): + self.in_range: Set[int] = set() + for start, end in find_env_ranges(lines, env): + self.in_range.update(range(start, end)) + + def contains(self, line_num: int) -> bool: + return line_num in self.in_range + + +class BannedPatternInEnvironmentCheck(Check): + """Checks a pattern only within a given LaTeX environment. + + Environment ranges are pre-computed in ``begin_file`` so they are + always available even if the check is temporarily inactive. + """ + + def __init__( + self, + check_id: str, + env: Environment, + pattern: Pattern[str], + message: str, + ): + self.id = check_id + self.env = env + self.pattern = pattern + self.message = message + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.ranges = EnvRanges(lines, self.env) + + def check_line(self, line_num: int, line: str) -> None: + if not self.ranges.contains(line_num): + return + if COMMENT_PATTERN.match(line): + return + m = self.pattern.search(line) + if m is None: + return + self.fail( + line_num, + m.start(), + m.end(), + self.message, + ) + + +class NonAsciiCheck(Check): + NON_ASCII_PATTERN = re.compile(r"[^\x09\x0a\x0d\x20-\x7e]") + + def __init__(self, check_id: str): + self.id = check_id + + def check_line(self, line_num: int, line: str) -> None: + for m in self.NON_ASCII_PATTERN.finditer(line): + ch, cp = m.group(0), ord(m.group(0)) + name = unicodedata.name(ch, "") + self.fail( + line_num, + m.start(), + m.end(), + f"Non-ASCII character U+{cp:04X} {name} found; " + f"use the appropriate LaTeX macro instead.", + ) + + +class TrailingEmptyLinesCheck(Check): + def __init__(self, check_id: str): + self.id = check_id + + def end_file(self, file_path: Path) -> None: + try: + raw = file_path.read_bytes() + except OSError: + return + if len(raw) >= 2 and raw[-2:] == b"\n\n": + count = sum(1 for b in reversed(raw) if b == 0x0A) + self.fail( + raw.decode(errors="replace").count("\n"), + 0, + 0, + f"File ends with {count} trailing newlines; " + f"files must end with exactly one newline.", + ) + + +class ConsecutivePnumCheck(Check): + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.previous_was_pnum = False + + def check_line(self, line_num: int, line: str) -> None: + if COMMENT_PATTERN.match(line): + return + is_pnum = line == "\\pnum" + if is_pnum and self.previous_was_pnum: + self.fail( + line_num, + 0, + 0, + "Two consecutive `\\pnum` found; remove the duplicate.", + ) + self.previous_was_pnum = is_pnum + + +class TailnoteTailexampleCheck(Check): + def __init__(self, check_id: str): + self.id = check_id + + END_PATTERN = re.compile(r"\\end\{(?:example|note)\}") + TAIL_PATTERN = re.compile(r"- *(?:\\\\|&)") + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.pending: Optional[Tuple[int, int, int]] = None + + def check_line(self, line_num: int, line: str) -> None: + # Check whether the previous line should have used \\tailnote. + if self.pending is not None: + prev_line_num, cs, ce = self.pending + if self.TAIL_PATTERN.search(line): + self.fail( + prev_line_num, + cs, + ce, + "`\\end{note}` or `\\end{example}` appears at the end of a table cell; " + "use `\\tailnote` or `\\tailexample` instead.", + ) + self.pending = None + + m = self.END_PATTERN.search(line) + if m is not None: + self.pending = (line_num, m.start(), m.end()) + + +class BlankLineExampleCodeblockCheck(Check): + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.previous_line: Optional[str] = None + + def check_line(self, line_num: int, line: str) -> None: + if ( + self.previous_line == "\\begin{example}" + and line == "" + and line_num + 1 < len(self.lines) + and self.lines[line_num + 1] == "\\begin{codeblock}" + ): + self.fail( + line_num, + 0, + 0, + "Blank line between `\\begin{example}` and `\\begin{codeblock}`; " + "remove the empty line.", + ) + self.previous_line = line + + +class CommentAlignmentCheck(Check): + def __init__(self, check_id: str): + self.id = check_id + + # This pattern checks for //, with some notable exemptions: + # - If '@' is present anywhere, we don't match the comment because we cannot compute alignment. + # - There needs to be non-whitespace somewhere before `//`, + # otherwise we would match misaligned comments like `// \ref{...}` (in synopses). + # - We require a space before `//` to avoid false positives in string literals, + # which is not perfectly reliable, but works for now. + CHECKED_COMMENT_PATTERN = re.compile(r"^[^@]*[^@\s][^@]*? //") + ENVIRONMENTS = (Environment.CODEBLOCK, Environment.CODEBLOCKTU) + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.in_range: Set[int] = set() + for env in self.ENVIRONMENTS: + for start, end in find_env_ranges(lines, env): + self.in_range.update(range(start, end)) + + def check_line(self, line_num: int, line: str) -> None: + if line_num not in self.in_range: + return + m = self.CHECKED_COMMENT_PATTERN.search(line) + if m is None: + return + column_start = m.end() - 2 + if column_start % 4 != 0: + self.fail( + line_num, + column_start, + m.end(), + f"Comment is preceded by `{column_start}` columns, " + f"which is not a multiple of `4`; move the comment either " + f"`{column_start % 4}` to the left or " + f"`{4 - (column_start % 4)}` columns to the right.", + ) + + +class HangingParagraphsCheck(Check): + SECTION_PATTERN = re.compile(r"^\\rSec([0-9])") + + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.previous_level = 0 + self.previous_line = 0 + self.previous_text = "" + self.has_text = False + + def check_line(self, line_num: int, line: str) -> None: + if line == "\\pnum": + self.has_text = True + return + m = self.SECTION_PATTERN.match(line) + if not m: + return + level = int(m.group(1)) + if self.has_text and level > self.previous_level: + self.fail( + line_num, + 0, + 0, + f"Hanging paragraph: `{self.previous_text.strip()}` has text " + f"but no `\\pnum` before a deeper subclause follows.", + ) + self.previous_level = level + self.previous_line = line_num + self.previous_text = line + self.has_text = False + + +class SubclausesWithoutSiblingsCheck(Check): + SECTION_PATTERN = re.compile(r"^\\rSec([0-9])") + + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.previous_level = 0 + self.sections: Dict[int, int] = {} + self.titles: Dict[int, str] = {} + + def check_line(self, line_num: int, line: str) -> None: + m = self.SECTION_PATTERN.match(line) + if not m: + return + level = int(m.group(1)) + if ( + level < self.previous_level + and self.sections.get(self.previous_level, 0) == 1 + ): + self.fail( + line_num, + 0, + 0, + f"Subclause without siblings: " + f"`{self.titles.get(self.previous_level, '?').strip()}` " + f"is the only subclause at its level.", + ) + self.sections[level] = self.sections.get(level, 0) + 1 + self.titles[level] = line + self.sections[level + 1] = 0 + self.previous_level = level + + +class SectionSelfReferenceCheck(Check): + SECTION_PATTERN = re.compile(r"^\\rSec.\[([^\]]*)\]") + IREF_PATTERN = re.compile(r"\\iref\{([^\}]*)\}") + + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.current_label: Optional[str] = None + + def check_line(self, line_num: int, line: str) -> None: + m = self.SECTION_PATTERN.match(line) + if m: + self.current_label = m.group(1) + return + if self.current_label is None: + return + for im in self.IREF_PATTERN.finditer(line): + if im.group(1) == self.current_label: + self.fail( + line_num, + im.start(), + im.end(), + f"Section self-reference: " + f"`\\iref{{{self.current_label}}}` must not refer to its own section.", + ) + + +class PnumMissingInItemdescrCheck(Check): + ELEMENT_PATTERN = re.compile(r"^\\" + make_alt_pattern(PARAGRAPH_DESCRIPTORS)) + + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.check_file(file_path, lines) + + def check_file(self, file_path: Path, lines: List[str]) -> None: + for start, end in find_env_ranges(lines, Environment.ITEMDESCR): + seen_pnum = False + for idx in range(start, end): + line = lines[idx] + if line == "\\pnum": + seen_pnum = True + continue + if line.startswith("\\index"): + continue + if self.ELEMENT_PATTERN.match(line): + if not seen_pnum: + self.fail( + idx, + 0, + 0, + "Library element descriptor must be preceded by " + "`\\pnum` inside `\\begin{itemdescr}`.", + ) + seen_pnum = False + else: + seen_pnum = False + + +class ClassDefinitionOutsideNamespaceCheck(Check): + CLASS_SECTION_PATTERN = re.compile(r"\\rSec[0-9].*\{Class") + SECTION_PATTERN = re.compile(r"\\rSec") + TEMPLATE_PATTERN = re.compile(r"template<[^>]*>") + CLASS_DEFINITION_PATTERN = re.compile(r"(?:class|struct)\s+[A-Za-z0-9_:]+\s*\{") + NAMESPACE_PATTERN = re.compile(r"^\s*namespace\s", re.MULTILINE) + + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.check_file(file_path, lines) + + def check_file(self, file_path: Path, lines: List[str]) -> None: + in_section = False + in_example = False + in_cb = False + cb_lines: List[str] = [] + cb_start = 0 + for idx, line in enumerate(lines): + if self.CLASS_SECTION_PATTERN.search(line): + in_section = True + continue + if in_section and self.SECTION_PATTERN.match(line): + in_section = False + continue + if not in_section: + continue + if "\\begin{example}" in line: + in_example = True + continue + if "\\end{example}" in line: + in_example = False + continue + if in_example: + continue + if "\\begin{codeblock}" in line: + in_cb = True + cb_lines, cb_start = [], idx + continue + if "\\end{codeblock}" in line: + in_cb = False + cb_text = "\n".join(cb_lines) + stripped = self.TEMPLATE_PATTERN.sub("", cb_text) + if self.CLASS_DEFINITION_PATTERN.search(stripped): + if not self.NAMESPACE_PATTERN.search(stripped): + for ci, cline in enumerate(cb_lines): + cs = self.TEMPLATE_PATTERN.sub("", cline) + if self.CLASS_DEFINITION_PATTERN.search(cs): + self.fail( + cb_start + ci, + 0, + 0, + "Class definition in a `Class` section " + "not wrapped in a `namespace` block.", + ) + break + continue + if in_cb: + cb_lines.append(line) + + +class OutdatedFiguresCheck(Check): + def __init__(self, check_id: str): + self.id = check_id + + def end_checks(self) -> None: + for dot_file in sorted(source_dir.glob("*.dot")): + pdf_file = dot_file.with_suffix(".pdf") + if ( + pdf_file.exists() + and dot_file.stat().st_mtime > pdf_file.stat().st_mtime + ): + emit_check_failure( + self, + os.path.relpath(dot_file), + 0, + 0, + 0, + f"Figure `{dot_file.name}` is newer than " + f"`{pdf_file.name}`; run " + f"`make clean-figures && make figures`.", + ) + + +class FunctionDescriptorOutOfOrderCheck(Check): + element_index: Dict[str, int] = {e: i for i, e in enumerate(FUNCTION_DESCRIPTORS)} + relevant_line_pattern = re.compile(r"^\\" + make_alt_pattern(FUNCTION_DESCRIPTORS)) + + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.check_file(file_path, lines) + + def check_file(self, file_path: Path, lines: List[str]) -> None: + for start, end in find_env_ranges(lines, Environment.ITEMDESCR): + if "% NOCHECK:" in lines[start] and "order" in lines[start]: + continue + prev_name: Optional[str] = None + for idx in range(start, end): + m = self.relevant_line_pattern.match(lines[idx]) + if not m: + continue + name = m.group(0)[1:] + if prev_name is not None: + if self.element_index[name] < self.element_index[prev_name]: + self.fail( + idx, + m.start(), + m.end(), + f"`{name}` must not precede `{prev_name}`.", + ) + prev_name = name + + +class UnbalancedBeginAndEndCheck(Check): + BEGIN_OR_END_PATTERN = re.compile(r"\\(begin|end)\{([^}]+)\}") + + def __init__(self, check_id: str): + self.id = check_id + + def begin_file(self, file_path: Path, lines: List[str]) -> None: + super().begin_file(file_path, lines) + self.stack: List[Tuple[str, int, int, int]] = [] + + def check_line(self, line_num: int, line: str) -> None: + if COMMENT_PATTERN.match(line): + return + for m in self.BEGIN_OR_END_PATTERN.finditer(line): + directive = m.group(1) # begin or end + name = m.group(2) + column_start = m.start() + column_end = m.end() + + if directive == "begin": + self.stack.append((name, line_num, column_start, column_end)) + else: + if not self.stack: + self.fail( + line_num, + column_start, + column_end, + f"`\\end{{{name}}}` has no matching `\\begin{{{name}}}`.", + ) + else: + open_name, open_line_num, _, _ = self.stack.pop() + if open_name != name: + self.fail( + line_num, + column_start, + column_end, + f"`\\end{{{name}}}` does not match " + f"`\\begin{{{open_name}}}` " + f"(opened at line {open_line_num}).", + ) + + def end_file(self, file_path: Path) -> None: + for name, line_num, column_start, column_end in self.stack: + self.fail( + line_num, + column_start, + column_end, + f"`\\begin{{{name}}}` has no matching `\\end{{{name}}}`.", + ) + + +class UnknownCommandCheck(Check): + """ + Report `\\command` not seen anywhere in the codebase. + It is very unlikely that a novel command is used unintentionally at this point, + and much more likely that someone made a typo like `\\tocde` instead of `\\tcode`. + + If this check ever reports a false positive, + add the command to `KNOWN_COMMANDS` above. + """ + + def __init__(self, check_id: str): + self.id = check_id + + COMMAND_PATTERN = re.compile(r"\\([a-zA-Z][a-zA-Z]*)") + + def check_line(self, line_num: int, line: str) -> None: + if COMMENT_PATTERN.match(line): + return + for m in self.COMMAND_PATTERN.finditer(line): + cmd = m.group(1) + if cmd in KNOWN_COMMANDS: + continue + self.fail( + line_num, + m.start(), + m.end(), + f"Unknown command `{cmd}`.", + ) + + +class UseOfUndefinedCheck(Check): + """ + Tracks definitions and uses across all files. + Some of this is already covered by check-output.sh, + but running that script is tremendously expensive and doesn't give pretty error feedback, + so we may as well catch some of these issues here. + + `definition_pattern` and each pattern in `usage_pattern` must capture the name in group 1. + """ + + def __init__( + self, + check_id: str, + definition_pattern: Pattern[str], + usage_pattern: List[Pattern[str]], + ): + self.id = check_id + self.definition_pattern = definition_pattern + self.usage_pattern = usage_pattern + self.defined: Set[str] = set() + self.used: Dict[str, List[Tuple[str, int, int, int]]] = defaultdict(list) + + def check_line(self, line_num: int, line: str) -> None: + for m in self.definition_pattern.finditer(line): + self.defined.add(m.group(1)) + for pattern in self.usage_pattern: + for m in pattern.finditer(line): + for name in m.group(1).split(","): + name = name.strip() + if not name or name.startswith("\\"): + continue + self.used[name].append( + ( + os.path.relpath(self.file_path), + line_num, + m.start(1), + m.end(1), + ) + ) + + def end_checks(self) -> None: + for name, locations in sorted(self.used.items()): + if name not in self.defined: + file_name, line_num, column_start, column_end = locations[0] + emit_check_failure( + self, + file_name, + line_num, + column_start, + column_end, + f"`{name}` has no definition.", + ) + + +class RefUndefinedCheck(Check): + """Checks whether any uses of `\\ref` or `\\iref` are dangling.""" + + SECTION_PATTERN = re.compile(r"\\rSec[0-9]\[([^\]]+)\]") + DEFINITION_PATTERN = re.compile(r"\\definition\{[^\}]*?\}\{(.+?)\}") + ETC_PATTERN = re.compile(r"\\(?:infannex|normannex|label)\{(.+?)\}") + DEFINING_PATTERNS = [SECTION_PATTERN, DEFINITION_PATTERN, ETC_PATTERN] + + # TODO: Excluding `:` means that `tab:...` references are currently unchecked. + # Maybe that could be added in the future, + # but there are lots of different kinds of tables that could be defining. + REF_IREF_PATTERN = re.compile(r"\\i?ref\{([^}\\:]+)\}") + + def __init__(self, check_id: str): + self.id = check_id + self.defined: Set[str] = set() + self.used: Dict[str, List[Tuple[str, int, int, int]]] = defaultdict(list) + + def check_line(self, line_num: int, line: str) -> None: + for defining_pattern in self.DEFINING_PATTERNS: + for m in defining_pattern.finditer(line): + self.defined.add(m.group(1)) + for m in self.REF_IREF_PATTERN.finditer(line): + for usage in m.group(1).split(","): + self.used[usage.strip()].append( + (os.path.relpath(self.file_path), line_num, m.start(1), m.end(1)) + ) + + def end_checks(self) -> None: + for name, locations in sorted(self.used.items()): + if name not in self.defined: + file_name, line_num, column_start, column_end = locations[0] + emit_check_failure( + self, + file_name, + line_num, + column_start, + column_end, + f"`{name}` has no definition.", + ) + + +CHECKS: List[Check] = [ + # -- Text checks ------------------------------------------------------------------------------- + # Such checks run on all files and identify problems like illegal characters, + # trailing whitespace, etc. + # ---------------------------------------------------------------------------------------------- + NonAsciiCheck("text-non-ascii-char"), + BannedPatternCheck( + "text-trailing-ws", + re.compile(r"\s+$"), + "Line has trailing whitespace; remove the extra spaces.", + ), + TrailingEmptyLinesCheck("text-trailing-empty-lines"), + # -- Base checks ------------------------------------------------------------------------------- + # These run in both the core and library TeX sources. + # ---------------------------------------------------------------------------------------------- + BannedPatternCheck( + "base-indented-codeblock", + re.compile(r"(?<=.)\\(?:begin|end)\{codeblock\}"), + "`\\begin{codeblock}` or `\\end{codeblock}` must not be indented.", + ), + BannedPatternCheck( + "base-pnum-alone", + re.compile(r"^[^%].*\\pnum"), + "`\\pnum` must be on its own line; move preceding text to a separate line.", + ), + BannedPatternCheck( + "base-pnum-alone", + re.compile(r"\\pnum(?=\s*.)"), + "`\\pnum` must be on its own line; move trailing text to a separate line.", + ), + ConsecutivePnumCheck("base-consecutive-pnum"), + BannedPatternCheck( + "base-footnote-punct", + re.compile(r"\\end\{footnote\}(?![@%\\]|$)"), + "`\\end{footnote}` must be followed by `@`, `%`, `\\`, or nothing.", + ), + BannedPatternCheck( + "base-opt", + re.compile(r"\\opt(?![{])"), + "`\\opt` must be followed by a brace group `{...}`; write `\\opt{...}`.", + ), + BannedPatternCheck( + "base-opt", + re.compile(r"opt\{\}"), + "`opt{}` is incorrectly used; provide an argument to `\\opt`.", + ), + BannedPatternCheck( + "base-expos", + re.compile(r"//\s+exposition only"), + "Write `\\expos` instead of the literal comment `// exposition only`.", + ), + BannedPatternCheck( + "base-notdef", + re.compile(r"//\s+not defined"), + "Write `\\notdef` instead of the literal comment `// not defined`.", + ), + BannedPatternCheck( + "base-cpp", + re.compile(r'^[^%]*[^{"]C\+\+[^"}]'), + "Write `\\Cpp{}` instead of literally `C++`.", + ), + BannedPatternCheck( + "base-caret", + re.compile(re.escape(r"\^")), + "Write `\\caret` or `\\reflexpr` instead of literally `\\^`.", + ), + BannedPatternCheck( + "base-u-plus", + re.compile(r"U\+"), + "Write `\\unicode{...}`, `\\ucode{...}`, or `\\uname{...}` " + "(with digits and/or lower-case letters) instead of `U+NNNN`.", + ), + BannedPatternCheck( + "base-hex-ucode-case", + re.compile(r"ucode\{[^}]*[A-F][^}]*\}"), + "Hex digits inside `\\ucode{...}` must be lowercase.", + ), + BannedPatternCheck( + "base-hex-unicode-case", + re.compile(r"unicode\{[^}]*[A-F][^}]*\}"), + "Hex digits inside `\\unicode{...}` must be lowercase.", + ), + BannedPatternCheck( + "base-tcode-exposid", + re.compile(r"\\tcode\{\\exposid\{[^\{]*\}\}"), + "Do not write `\\tcode{\\exposid{...}}` use `\\exposid{...}` directly.", + ), + BannedPatternCheck( + "base-ref-in-parens", + re.compile(r"(?<=\()\\ref(?=\{)(?!.*--)"), + "Write `\\iref{...}` instead of `(\\ref{...})`.", + ), + BannedPatternCheck( + "base-iref-location", + re.compile(r"^\\iref"), + "`\\iref` must not appear at the start of the line.", + ), + BannedPatternCheck( + "base-iref-location", + re.compile(r"(?<= )\\iref"), + "`\\iref` must be flush against the preceding word; " + "remove the space in front of it.", + ), + BannedPatternCheck( + "base-xrefc", + re.compile(r"^ISO C [0-9]*(?=\.)"), + "Write `\\xrefc{...}` instead of the literal `ISO C` reference.", + ), + BannedPatternCheck( + "base-diff-marker", + re.compile(r"^\\" + make_alt_pattern(DIFF_DESCRIPTORS) + r"\s.+$"), + "A change marker in (like `\\change`, `\\rationale`, etc.) " + "must not have trailing text on the same line.", + ), + BannedPatternCheck( + "base-note-not-alone", + re.compile(r"^.*[^ ]\s*\\(?:begin|end)\{(?:example|note)\}"), + "`\\begin{note}` / `\\begin{example}` (or their `\\end` forms) " + "must appear alone on their line; move preceding text to a separate line.", + ), + BannedPatternCheck( + "base-note-not-alone", + re.compile(r"\\(?:begin|end)\{(?:example|note)\}(?!%)(.+)$"), + "`\\begin{note}` / `\\begin{example}` (or their `\\end` forms) " + "must appear alone on their line; move trailing text to a separate line.", + ), + TailnoteTailexampleCheck("base-tailnote-needed"), + BlankLineExampleCodeblockCheck("base-blank-example-codeblock"), + CommentAlignmentCheck("base-comment-align"), + BannedPatternCheck( + "base-deleted-param-name", + re.compile(r"&[ 0-9a-z_]+\)\s*=\s*delete"), + "Deleted special member function has a named parameter; " + "remove the parameter name.", + ), + BannedPatternCheck( + "base-bad-label-chars", + re.compile(r"^\\rSec.\[[^\]]*[^a-z\.0-9][^\]]*\]\{"), + "Section label contains an invalid character; use only `[a-z.0-9]` in labels.", + ), + BannedPatternInEnvironmentCheck( + "base-normative-in-note", + Environment.NOTE, + re.compile(r"(?:shall|may|should)(?=[^a-zA-Z])"), + "Neither `shall`, `should`, nor `may` is allowed in notes. Prefer `can` or `cannot`.", + ), + BannedPatternInEnvironmentCheck( + "base-normative-in-footnote", + Environment.FOOTNOTE, + re.compile(r"(?:shall|may|should)(?=[^a-zA-Z])"), + "Neither `shall`, `should`, nor `may` is allowed in footnotes. Prefer `can` or `cannot`.", + ), + BannedPatternCheck( + "base-eg-comma", + re.compile(r"e\.g\.(?!,)"), + "`e.g.` must be followed by a comma.", + ), + BannedPatternCheck( + "base-ie-comma", + re.compile(r"i\.e\.(?!,)"), + "`i.e.` must be followed by a comma.", + ), + BannedPatternCheck( + "base-logop-case", + re.compile(r"\\logop\{[^}]*[^andor\}][^}]*\}"), + "`\\logop` argument must use only lowercase letters `a`, `n`, `d`, `o`, `r`.", + ), + HangingParagraphsCheck("base-hanging-paragraph"), + SubclausesWithoutSiblingsCheck("base-lonely-subclause"), + UnknownCommandCheck("base-unknown-command"), + SectionSelfReferenceCheck("base-self-ref"), + RefUndefinedCheck("base-ref-undef"), + UseOfUndefinedCheck( + "base-grammarterm-undef", + definition_pattern=re.compile(r"\\nontermdef\{([^}]+)\}"), + usage_pattern=[re.compile(r"\\grammarterm\{([^}]+)(? int: + """Run all registered checks. Returns the number of unexpected failures.""" + global unexpected_count + unexpected_count = 0 + file_locations.clear() + + for fp in tex_files: + file_locations[os.path.relpath(fp)] = fp + + all_ids = {c.id for c in CHECKS if c.id} + + for file_path in tex_files: + if not file_path.exists(): + continue + + lines = read_file(file_path) + + # Always call begin_file. + for c in CHECKS: + c.begin_file(file_path, lines) + + # Active-set management — controls whether check_line is called. + active: Set[str] = set(all_ids) + # Per-check next-line skips: check_id → set of line numbers. + skip_next: Dict[str, Set[int]] = defaultdict(set) + + for idx, line in enumerate(lines): + line_num = idx + + # Process %NOCHECK… directives. + m = NO_CHECK_PATTERN.match(line) + if m: + directive = m.group(1) # BEGIN, END, or NEXTLINE + cid = m.group(2) or "*" + if directive == "BEGIN": + if cid == "*": + active.clear() + else: + active = {a for a in active if not fnmatch.fnmatch(a, cid)} + elif directive == "END": + if cid == "*": + active = set(all_ids) + else: + active |= {a for a in all_ids if fnmatch.fnmatch(a, cid)} + elif directive == "NEXTLINE": + if cid == "*": + for c in all_ids: + skip_next[c].add(line_num + 1) + else: + matched = {a for a in all_ids if fnmatch.fnmatch(a, cid)} + for c in matched: + skip_next[c].add(line_num + 1) + + # Call check_line only on active checks (not skipped). + for c in CHECKS: + if c.id in active and line_num not in skip_next.get(c.id, set()): + c.check_line(line_num, line) + + # Always call end_file. + for c in CHECKS: + c.end_file(file_path) + + # After all files, run end-of-checks hooks. + for c in CHECKS: + c.end_checks() + + return unexpected_count + + +def parse_expected_from_files(tex_files: List[Path]) -> None: + """Pre-scan ``.tex`` files for ``%EXPECTCHECKNEXTLINE(id)`` directives. + + The directive must appear alone on its line; *id* is the check that is + expected to fire on the **next** line. + """ + EXPECT_CHECK_PATTERN = re.compile(r"^\s*%EXPECTCHECKNEXTLINE\((\S+)\)\s*$") + + for file_path in tex_files: + lines = read_file(file_path) + for idx in range(len(lines) - 1): + m = EXPECT_CHECK_PATTERN.match(lines[idx]) + if m: + register_expected(os.path.relpath(file_path), idx, m.group(1).strip()) + + +def collect_tex_files_recursively(root_paths: List[Path]) -> List[Path]: + result: List[Path] = [] + for root in root_paths: + if root.is_dir(): + result.extend(sorted(root.rglob("*.tex"))) + elif root.suffix == ".tex": + result.append(root) + return result + + +def find_project_root() -> Path: + """Locate the project root by looking for ``source/std.tex``.""" + script_dir = Path(__file__).resolve().parent + for candidate in (script_dir.parent, Path.cwd()): + if (candidate / "source" / "std.tex").exists(): + return candidate + if (candidate / "std.tex").exists(): + return candidate + return script_dir.parent + + +# ================================================================================================== +# CLI +# ================================================================================================== + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Check LaTeX sources for C++ standard drafting rules." + ) + parser.add_argument( + "files", + nargs="*", + help="Specific .tex files or directories to check (default: all .tex files" + " under the project root).", + ) + args = parser.parse_args() + + global source_dir + project_root = find_project_root() + source_dir = ( + project_root / "source" if (project_root / "source").is_dir() else project_root + ) + + tex_files = ( + collect_tex_files_recursively([Path(f).resolve() for f in args.files]) + if args.files + else collect_tex_files_recursively([project_root]) + ) + if not tex_files: + print("error: no .tex files found", file=sys.stderr) + sys.exit(1) + + parse_expected_from_files(tex_files) + num_failures = run_checks(tex_files) + + unhit = collect_unexpectedly_not_failed() + num_failures += len(unhit) + for entry in unhit: + fp = file_locations.get(entry.file) + if fp is None: + fp = Path.cwd() / entry.file + lines = read_file(fp) + comment_line = entry.comment_line + if comment_line < len(lines): + line_text = lines[comment_line] + column = line_text.find(entry.check_id) + if column < 0: + column = 0 + fail = Failure( + file=entry.file, + line=comment_line, + column_start=column, + column_end=column + len(entry.check_id), + message=f"expected failure `{entry.check_id}` was not triggered", + check_id=entry.check_id, + ) + print(format_failure(fail, lines), file=sys.stderr) + print(file=sys.stderr) + else: + print( + f" {entry.file}:{entry.comment_line + 1}: expected '{entry.check_id}'", + file=sys.stderr, + ) + + if num_failures: + print( + f"{style(str(num_failures), ANSI_RED)} error(s) emitted.", + file=sys.stderr, + ) + + exit_code = 1 if num_failures > 0 else 0 + sys.exit(exit_code) + + +if __name__ == "__main__": + main() diff --git a/tools/test-checks.tex b/tools/test-checks.tex new file mode 100644 index 0000000000..7bb780ccab --- /dev/null +++ b/tools/test-checks.tex @@ -0,0 +1,262 @@ +%EXPECTCHECKNEXTLINE(text-non-ascii-char) +Here is a non-ASCII char: ¶ +%EXPECTCHECKNEXTLINE(text-non-ascii-char) +Another one: → + +%EXPECTCHECKNEXTLINE(text-trailing-ws) +This line has trailing spaces. +%EXPECTCHECKNEXTLINE(text-trailing-ws) +More trailing ws + +%EXPECTCHECKNEXTLINE(base-indented-codeblock) + \begin{codeblock} +%EXPECTCHECKNEXTLINE(base-indented-codeblock) + \end{codeblock} + +%EXPECTCHECKNEXTLINE(base-pnum-alone) +text before \pnum +%EXPECTCHECKNEXTLINE(base-pnum-alone) +\pnum text after + +\pnum +%EXPECTCHECKNEXTLINE(base-consecutive-pnum) +\pnum +%EXPECTCHECKNEXTLINE(base-consecutive-pnum) +\pnum + +%EXPECTCHECKNEXTLINE(base-footnote-punct) +\begin{footnote}Some text.\end{footnote}more text here. +%EXPECTCHECKNEXTLINE(base-footnote-punct) +\begin{footnote}Another\end{footnote}missing punctuation. + +%EXPECTCHECKNEXTLINE(base-opt) +\opt not wrapping braces +%EXPECTCHECKNEXTLINE(base-opt) +\opt{} + +%EXPECTCHECKNEXTLINE(base-expos) +// exposition only +%EXPECTCHECKNEXTLINE(base-expos) +// exposition only text + +%EXPECTCHECKNEXTLINE(base-notdef) +// not defined +%EXPECTCHECKNEXTLINE(base-notdef) +// not defined behavior + +%EXPECTCHECKNEXTLINE(base-cpp) +This is C++ code. +%EXPECTCHECKNEXTLINE(base-cpp) +More C++ here. + +%EXPECTCHECKNEXTLINE(base-caret) +Text with \^ caret. +%EXPECTCHECKNEXTLINE(base-caret) +Another \^ here. + +%EXPECTCHECKNEXTLINE(base-u-plus) +The character U+0041 is 'A'. +%EXPECTCHECKNEXTLINE(base-u-plus) +Code point U+000A is newline. + +%EXPECTCHECKNEXTLINE(base-hex-ucode-case) +\ucode{00A0} +%EXPECTCHECKNEXTLINE(base-hex-ucode-case) +\ucode{00FF} + +%EXPECTCHECKNEXTLINE(base-hex-unicode-case) +\unicode{00A0} +%EXPECTCHECKNEXTLINE(base-hex-unicode-case) +\unicode{00FF} + +%EXPECTCHECKNEXTLINE(base-tcode-exposid) +\tcode{\exposid{snake-case}} +%EXPECTCHECKNEXTLINE(base-tcode-exposid) +\tcode{\exposid{kebak-case}} + +%EXPECTCHECKNEXTLINE(base-ref-in-parens) +See the section.(\ref{expr.const}) +%EXPECTCHECKNEXTLINE(base-ref-in-parens) +Also see (\ref{expr.const}) for details. + +%EXPECTCHECKNEXTLINE(base-iref-location) +\iref{expr.const} at start of line. +%EXPECTCHECKNEXTLINE(base-iref-location) +See \iref{expr.const} for details. + +%EXPECTCHECKNEXTLINE(base-xrefc) +ISO C 7.42.0 specifies this. +%EXPECTCHECKNEXTLINE(base-xrefc) +ISO C 5.2.1 is relevant. + +%EXPECTCHECKNEXTLINE(base-diff-marker) +\change some trailing text +%EXPECTCHECKNEXTLINE(base-diff-marker) +\rationale some trailing text + +%EXPECTCHECKNEXTLINE(base-note-not-alone) +some text\begin{note} +%EXPECTCHECKNEXTLINE(base-note-not-alone) +\begin{note} trailing text + +\end{note} + +\begin{note} +%EXPECTCHECKNEXTLINE(base-tailnote-needed) +\end{note} +- \\ next cell +\begin{example} +%EXPECTCHECKNEXTLINE(base-tailnote-needed) +\end{example} +- & next column + +\begin{codeblock} +%EXPECTCHECKNEXTLINE(base-comment-align) +int x; // misaligned +%EXPECTCHECKNEXTLINE(base-comment-align) +float y; // also misaligned +\end{codeblock} + +%EXPECTCHECKNEXTLINE(base-deleted-param-name) +void func(int& param) = delete; +%EXPECTCHECKNEXTLINE(base-deleted-param-name) +void other(float& value) = delete; + +%EXPECTCHECKNEXTLINE(base-bad-label-chars) +\rSec0[bad label]{Bad label} +%EXPECTCHECKNEXTLINE(base-bad-label-chars) +\rSec0[also bad]{Also bad} + +\begin{note} +%EXPECTCHECKNEXTLINE(base-normative-in-note) +This note shall not use normative language. +\end{note} +\begin{note} +%EXPECTCHECKNEXTLINE(base-normative-in-note) +A note should not be normative either. +\end{note} + +%EXPECTCHECKNEXTLINE(base-normative-in-footnote) +\begin{footnote}This footnote shall not be normative.\end{footnote} +%EXPECTCHECKNEXTLINE(base-normative-in-footnote) +\begin{footnote}This footnote may be problematic.\end{footnote} + +%EXPECTCHECKNEXTLINE(base-eg-comma) +For example e.g. this is wrong. +%EXPECTCHECKNEXTLINE(base-eg-comma) +Another e.g. missing comma. + +%EXPECTCHECKNEXTLINE(base-ie-comma) +That is i.e. this is also wrong. +%EXPECTCHECKNEXTLINE(base-ie-comma) +Another i.e. missing comma. + +%EXPECTCHECKNEXTLINE(base-logop-case) +\logop{NotLowerCase} +%EXPECTCHECKNEXTLINE(base-logop-case) +\logop{ALSOBAD} + +\rSec1[hp.outer]{Outer} +\pnum +Some text +%EXPECTCHECKNEXTLINE(base-hanging-paragraph) +\rSec2[hp.inner]{Inner} +%EXPECTCHECKNEXTLINE(base-lonely-subclause) +\rSec1[hp.outer2]{Outer2} +\pnum +More text +%EXPECTCHECKNEXTLINE(base-hanging-paragraph) +\rSec2[hp.inner2]{Inner2} + +%EXPECTCHECKNEXTLINE(base-lonely-subclause) +\rSec1[lo.a]{A} +\rSec2[lo.a1]{A.1} +%EXPECTCHECKNEXTLINE(base-lonely-subclause) +\rSec1[lo.b]{B} +\rSec1[lo.c]{C} +\rSec2[lo.c1]{C.1} +%EXPECTCHECKNEXTLINE(base-lonely-subclause) +\rSec1[lo.d]{D} + +%EXPECTCHECKNEXTLINE(base-unknown-command) +\thiscommanddoesnotexistanywhere +%EXPECTCHECKNEXTLINE(base-unknown-command) +\nordoesthisone + +\rSec0[test.selfref]{Self ref test} +%EXPECTCHECKNEXTLINE(base-self-ref) +Some text\iref{test.selfref} here. +\rSec0[test.selfref2]{Self ref test 2} +%EXPECTCHECKNEXTLINE(base-self-ref) +More text\iref{test.selfref2} there. + +%EXPECTCHECKNEXTLINE(base-ref-undef) +\ref{lex.whatever} + +%EXPECTCHECKNEXTLINE(base-grammarterm-undef) +\grammarterm{test-nonexistent-grammar-term} +%EXPECTCHECKNEXTLINE(base-grammarterm-undef) +\grammarterm{another-undefined-grammar-term} + +%EXPECTCHECKNEXTLINE(base-env-balancing) +%EXPECTCHECKNEXTLINE(base-footnote-punct) +Some text.\end{footnote}more text here. +%EXPECTCHECKNEXTLINE(base-env-balancing) +\end{itemize} + +%EXPECTCHECKNEXTLINE(lib-template-space) +template +%EXPECTCHECKNEXTLINE(lib-template-space) +template + +%EXPECTCHECKNEXTLINE(lib-keywords-explicit-constexpr) +explicit constexpr void f(); +%EXPECTCHECKNEXTLINE(lib-keywords-explicit-constexpr) +explicit constexpr auto g() -> int; + +%EXPECTCHECKNEXTLINE(lib-keywords-constexpr-static) +constexpr static int x = 0; +%EXPECTCHECKNEXTLINE(lib-keywords-constexpr-static) +constexpr static inline int y = 1; + +%EXPECTCHECKNEXTLINE(lib-using-typename) +using foo = typename bar::type; +%EXPECTCHECKNEXTLINE(lib-using-typename) +using baz = typename qux::other; + +%EXPECTCHECKNEXTLINE(lib-cv-ref-space) +void f() const&; +%EXPECTCHECKNEXTLINE(lib-cv-ref-space) +auto g() const& -> void; + +%EXPECTCHECKNEXTLINE(lib-element-alone) +\effects Some trailing text +%EXPECTCHECKNEXTLINE(lib-element-alone) +\returns Something + +%EXPECTCHECKNEXTLINE(lib-bad-concept-name) +%EXPECTCHECKNEXTLINE(lib-concept-undef) +\libconcept{bad concept} +%EXPECTCHECKNEXTLINE(lib-bad-concept-name) +%EXPECTCHECKNEXTLINE(lib-concept-undef) +\exposconcept{also bad} + +%EXPECTCHECKNEXTLINE(lib-header-undef) +\libheader{test-nonexistent-header} +%EXPECTCHECKNEXTLINE(lib-header-undef) +\libheaderref{another-nonexistent-header} + +%EXPECTCHECKNEXTLINE(lib-concept-undef) +\libconcept{test-nonexistent-concept} +%EXPECTCHECKNEXTLINE(lib-concept-undef) +\exposconcept{another-nonexistent-concept} + +%EXPECTCHECKNEXTLINE(lib-ranges-iterator-indexing) +\indexlibrarymember{some_fn}{ranges::my_view::iterator} +%EXPECTCHECKNEXTLINE(lib-ranges-iterator-indexing) +\indexlibraryctor{ranges::my_view::sentinel} + +%EXPECTCHECKNEXTLINE(lib-ranges-iterator-global-index) +\indexlibraryglobal{ranges::my_view::iterator} +%EXPECTCHECKNEXTLINE(lib-ranges-iterator-global-index) +\indexlibraryglobal{ranges::other_view::sentinel} From 6d4a54cc9e74f20f1be128239943bcf71b2ac9f8 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:22:45 +0200 Subject: [PATCH 02/24] Upgrade pyright to Python 3.12 --- pyrightconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrightconfig.json b/pyrightconfig.json index 5115227a57..84281b1381 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -10,5 +10,5 @@ "reportUnknownParameterType": false, "reportUnusedImport": true, "reportUnusedVariable": true, - "pythonVersion": "3.11" + "pythonVersion": "3.12" } From 9f2e23ee1430f6f414c08bed7fc86de0e6ad96b0 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:23:43 +0200 Subject: [PATCH 03/24] Modernize type annotations --- tools/check-source.py | 105 +++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 0b3f6b4218..6cb21b0953 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -13,8 +13,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import Dict, List, Optional, Pattern, Set, Tuple - +from re import Pattern # ================================================================================================== # Lookup tables @@ -930,8 +929,8 @@ class ExpectedFailure: # ================================================================================================== source_dir: Path = Path() -file_locations: Dict[str, Path] = {} -expected_registry: Dict[Tuple[str, int], ExpectedFailure] = {} +file_locations: dict[str, Path] = {} +expected_registry: dict[tuple[str, int], ExpectedFailure] = {} # ================================================================================================== # Expected-failure tracking @@ -960,7 +959,7 @@ def consume_expected(file: str, failure_line: int, check_id: str) -> bool: return False -def collect_unexpectedly_not_failed() -> List[ExpectedFailure]: +def collect_unexpectedly_not_failed() -> list[ExpectedFailure]: return [e for e in expected_registry.values() if not e.hit] @@ -972,11 +971,11 @@ def collect_unexpectedly_not_failed() -> List[ExpectedFailure]: COMMENT_PATTERN = re.compile(r"^\s*%") -def make_alt_pattern(items: List[str]) -> str: +def make_alt_pattern(items: list[str]) -> str: return "(?:" + "|".join(re.escape(s) for s in items) + ")" -def read_file(path: Path) -> List[str]: +def read_file(path: Path) -> list[str]: try: with open(path, encoding="utf-8", errors="replace") as f: return f.read().splitlines() @@ -984,8 +983,8 @@ def read_file(path: Path) -> List[str]: return [] -def format_failure(fail: Failure, lines: List[str]) -> str: - parts: List[str] = [] +def format_failure(fail: Failure, lines: list[str]) -> str: + parts: list[str] = [] message = ( re.sub(r"`([^`]+)`", f"{ANSI_YELLOW}\\1{ANSI_RESET}", fail.message) if COLOR @@ -1019,13 +1018,13 @@ def format_failure(fail: Failure, lines: List[str]) -> str: def find_env_ranges( - lines: List[str], + lines: list[str], env: Environment, -) -> List[Tuple[int, int]]: +) -> list[tuple[int, int]]: begin_pattern = env.begin_pattern end_pattern = env.end_pattern - ranges: List[Tuple[int, int]] = [] - stack: List[int] = [] + ranges: list[tuple[int, int]] = [] + stack: list[int] = [] for idx, line in enumerate(lines): if begin_pattern.search(line): stack.append(idx) @@ -1080,7 +1079,7 @@ class Check(ABC): id: str = "" - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: """Called before processing any line of `file_path`.""" self.file_path = file_path self.lines = lines @@ -1145,8 +1144,8 @@ def check_line(self, line_num: int, line: str) -> None: class EnvRanges: """Pre-computed line ranges for a LaTeX environment within one file.""" - def __init__(self, lines: List[str], env: Environment): - self.in_range: Set[int] = set() + def __init__(self, lines: list[str], env: Environment): + self.in_range: set[int] = set() for start, end in find_env_ranges(lines, env): self.in_range.update(range(start, end)) @@ -1173,7 +1172,7 @@ def __init__( self.pattern = pattern self.message = message - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) self.ranges = EnvRanges(lines, self.env) @@ -1236,7 +1235,7 @@ class ConsecutivePnumCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) self.previous_was_pnum = False @@ -1261,9 +1260,9 @@ def __init__(self, check_id: str): END_PATTERN = re.compile(r"\\end\{(?:example|note)\}") TAIL_PATTERN = re.compile(r"- *(?:\\\\|&)") - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) - self.pending: Optional[Tuple[int, int, int]] = None + self.pending: tuple[int, int, int] | None = None def check_line(self, line_num: int, line: str) -> None: # Check whether the previous line should have used \\tailnote. @@ -1288,9 +1287,9 @@ class BlankLineExampleCodeblockCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) - self.previous_line: Optional[str] = None + self.previous_line: str | None = None def check_line(self, line_num: int, line: str) -> None: if ( @@ -1322,9 +1321,9 @@ def __init__(self, check_id: str): CHECKED_COMMENT_PATTERN = re.compile(r"^[^@]*[^@\s][^@]*? //") ENVIRONMENTS = (Environment.CODEBLOCK, Environment.CODEBLOCKTU) - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) - self.in_range: Set[int] = set() + self.in_range: set[int] = set() for env in self.ENVIRONMENTS: for start, end in find_env_ranges(lines, env): self.in_range.update(range(start, end)) @@ -1354,7 +1353,7 @@ class HangingParagraphsCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) self.previous_level = 0 self.previous_line = 0 @@ -1389,11 +1388,11 @@ class SubclausesWithoutSiblingsCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) self.previous_level = 0 - self.sections: Dict[int, int] = {} - self.titles: Dict[int, str] = {} + self.sections: dict[int, int] = {} + self.titles: dict[int, str] = {} def check_line(self, line_num: int, line: str) -> None: m = self.SECTION_PATTERN.match(line) @@ -1425,9 +1424,9 @@ class SectionSelfReferenceCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) - self.current_label: Optional[str] = None + self.current_label: str | None = None def check_line(self, line_num: int, line: str) -> None: m = self.SECTION_PATTERN.match(line) @@ -1453,11 +1452,11 @@ class PnumMissingInItemdescrCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) self.check_file(file_path, lines) - def check_file(self, file_path: Path, lines: List[str]) -> None: + def check_file(self, file_path: Path, lines: list[str]) -> None: for start, end in find_env_ranges(lines, Environment.ITEMDESCR): seen_pnum = False for idx in range(start, end): @@ -1491,15 +1490,15 @@ class ClassDefinitionOutsideNamespaceCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) self.check_file(file_path, lines) - def check_file(self, file_path: Path, lines: List[str]) -> None: + def check_file(self, file_path: Path, lines: list[str]) -> None: in_section = False in_example = False in_cb = False - cb_lines: List[str] = [] + cb_lines: list[str] = [] cb_start = 0 for idx, line in enumerate(lines): if self.CLASS_SECTION_PATTERN.search(line): @@ -1568,21 +1567,21 @@ def end_checks(self) -> None: class FunctionDescriptorOutOfOrderCheck(Check): - element_index: Dict[str, int] = {e: i for i, e in enumerate(FUNCTION_DESCRIPTORS)} + element_index: dict[str, int] = {e: i for i, e in enumerate(FUNCTION_DESCRIPTORS)} relevant_line_pattern = re.compile(r"^\\" + make_alt_pattern(FUNCTION_DESCRIPTORS)) def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) self.check_file(file_path, lines) - def check_file(self, file_path: Path, lines: List[str]) -> None: + def check_file(self, file_path: Path, lines: list[str]) -> None: for start, end in find_env_ranges(lines, Environment.ITEMDESCR): if "% NOCHECK:" in lines[start] and "order" in lines[start]: continue - prev_name: Optional[str] = None + prev_name: str | None = None for idx in range(start, end): m = self.relevant_line_pattern.match(lines[idx]) if not m: @@ -1605,9 +1604,9 @@ class UnbalancedBeginAndEndCheck(Check): def __init__(self, check_id: str): self.id = check_id - def begin_file(self, file_path: Path, lines: List[str]) -> None: + def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) - self.stack: List[Tuple[str, int, int, int]] = [] + self.stack: list[tuple[str, int, int, int]] = [] def check_line(self, line_num: int, line: str) -> None: if COMMENT_PATTERN.match(line): @@ -1694,13 +1693,13 @@ def __init__( self, check_id: str, definition_pattern: Pattern[str], - usage_pattern: List[Pattern[str]], + usage_pattern: list[Pattern[str]], ): self.id = check_id self.definition_pattern = definition_pattern self.usage_pattern = usage_pattern - self.defined: Set[str] = set() - self.used: Dict[str, List[Tuple[str, int, int, int]]] = defaultdict(list) + self.defined: set[str] = set() + self.used: dict[str, list[tuple[str, int, int, int]]] = defaultdict(list) def check_line(self, line_num: int, line: str) -> None: for m in self.definition_pattern.finditer(line): @@ -1749,8 +1748,8 @@ class RefUndefinedCheck(Check): def __init__(self, check_id: str): self.id = check_id - self.defined: Set[str] = set() - self.used: Dict[str, List[Tuple[str, int, int, int]]] = defaultdict(list) + self.defined: set[str] = set() + self.used: dict[str, list[tuple[str, int, int, int]]] = defaultdict(list) def check_line(self, line_num: int, line: str) -> None: for defining_pattern in self.DEFINING_PATTERNS: @@ -1776,7 +1775,7 @@ def end_checks(self) -> None: ) -CHECKS: List[Check] = [ +CHECKS: list[Check] = [ # -- Text checks ------------------------------------------------------------------------------- # Such checks run on all files and identify problems like illegal characters, # trailing whitespace, etc. @@ -2047,7 +2046,7 @@ def end_checks(self) -> None: NO_CHECK_PATTERN = re.compile(r"^\s*%NOCHECK(BEGIN|END|NEXTLINE)(?:\((\S*)\))?\s*$") -def run_checks(tex_files: List[Path]) -> int: +def run_checks(tex_files: list[Path]) -> int: """Run all registered checks. Returns the number of unexpected failures.""" global unexpected_count unexpected_count = 0 @@ -2069,9 +2068,9 @@ def run_checks(tex_files: List[Path]) -> int: c.begin_file(file_path, lines) # Active-set management — controls whether check_line is called. - active: Set[str] = set(all_ids) + active: set[str] = set(all_ids) # Per-check next-line skips: check_id → set of line numbers. - skip_next: Dict[str, Set[int]] = defaultdict(set) + skip_next: dict[str, set[int]] = defaultdict(set) for idx, line in enumerate(lines): line_num = idx @@ -2116,7 +2115,7 @@ def run_checks(tex_files: List[Path]) -> int: return unexpected_count -def parse_expected_from_files(tex_files: List[Path]) -> None: +def parse_expected_from_files(tex_files: list[Path]) -> None: """Pre-scan ``.tex`` files for ``%EXPECTCHECKNEXTLINE(id)`` directives. The directive must appear alone on its line; *id* is the check that is @@ -2132,8 +2131,8 @@ def parse_expected_from_files(tex_files: List[Path]) -> None: register_expected(os.path.relpath(file_path), idx, m.group(1).strip()) -def collect_tex_files_recursively(root_paths: List[Path]) -> List[Path]: - result: List[Path] = [] +def collect_tex_files_recursively(root_paths: list[Path]) -> list[Path]: + result: list[Path] = [] for root in root_paths: if root.is_dir(): result.extend(sorted(root.rglob("*.tex"))) From 017ddece48fb2b4c2438a9f60c77befea0459359 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:34:33 +0200 Subject: [PATCH 04/24] Put lookup tables into separate file --- tools/check-source.py | 839 +----------------------------------------- tools/data.py | 834 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 835 insertions(+), 838 deletions(-) create mode 100644 tools/data.py diff --git a/tools/check-source.py b/tools/check-source.py index 6cb21b0953..8b98aee65c 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -15,844 +15,7 @@ from pathlib import Path from re import Pattern -# ================================================================================================== -# Lookup tables -# ================================================================================================== - -# Remember to keep this in sync with [structure.specifications] and our set of macros. -# The order matters for ElementOrderCheck. -FUNCTION_DESCRIPTORS = [ - "constraints", # Constraints - "mandates", # Mandates - "constantwhen", # Constant When - "expects", # Preconditions - "hardexpects", # Hardened preconditions - "effects", # Effects - "sync", # Synchronization - "ensures", # Postconditions - "returns", # Returns - "throws", # Throws - "complexity", # Complexity - "remarks", # Remarks - "errors", # Error conditions -] - -# All library descriptors. -# The order doesn't matter; this list is used for checking whether e.g. `\recommended` -# is preceded by `\pnum` -PARAGRAPH_DESCRIPTORS = [ - *FUNCTION_DESCRIPTORS, - "recommended", # Recommended practice - "required", # Required behavior - "implimits", # Implementation limits - "returntype", # Return type - "ctype", # Type - "templalias", # Alias template -] - -DIFF_DESCRIPTORS = [ - "change", - "rationale", - "effect", - "difficulty", - "howwide", -] - -KNOWN_COMMANDS = { - "0", - "Annex", - "BODY", - "BnfInc", - "BnfIndent", - "BnfNontermshape", - "BnfReNontermshape", - "BnfRest", - "BnfTermshape", - "BreakableUnderscore", - "CodeBlockSetup", - "CodeStyle", - "CodeStylex", - "CodeStylexGuarded", - "Cpp", - "CppIII", - "CppXI", - "CppXIV", - "CppXVII", - "CppXX", - "CppXXIII", - "CppXXVI", - "D", - "DeclareCaptionLabelSeparator", - "DeclareMathOperator", - "DisableLigatures", - "EXPO", - "ExplSyntaxOff", - "ExplSyntaxOn", - "Fundesc", - "Fundescx", - "Gamma", - "GrammarStylex", - "Head", - "Huge", - "IsoC", - "IsoCUndated", - "IsoFloatUndated", - "IsoPosix", - "IsoPosixUndated", - "LARGE", - "Large", - "N", - "NTS", - "NewDocumentCommand", - "NewEnviron", - "OldTextAsciiTilde", - "Pi", - "RSsmallest", - "Range", - "S", - "Sec", - "Sigma", - "StrClosingbrace", - "StrSubstitute", - "StrTextsc", - "StrTextsmaller", - "The", - "U", - "UAX", - "UNSP", - "UNSPnc", - "W", - "a", - "abovecaptionskip", - "addcontentsline", - "added", - "addednb", - "addtocontents", - "addtocounter", - "addtolength", - "addxref", - "advance", - "afterchapskip", - "afterchapternum", - "afterskip", - "aliaspagestyle", - "allowbreak", - "alpha", - "annexlabel", - "annexnumberlinebox", - "appendix", - "appendixname", - "arabic", - "atsign", - "b", - "backmatter", - "backslash", - "bar", - "baselineskip", - "beforechapskip", - "beforeskip", - "begin", - "begingroup", - "belowcaptionskip", - "beta", - "bfseries", - "bgroup", - "bibitem", - "bibname", - "big", - "bigl", - "bigoh", - "bigr", - "bigskip", - "binom", - "bitand", - "bitor", - "bm", - "bmod", - "bnfindent", - "bnfindentfirst", - "bnfindentinc", - "bnfindentrest", - "bot", - "bottomline", - "br", - "brange", - "break", - "brk", - "c", - "capsep", - "caption", - "captionsetup", - "caret", - "cdot", - "cdots", - "cedef", - "centering", - "cftsetindents", - "change", - "changed", - "changednb", - "changeglossnumformat", - "chapnamefont", - "chapnumfont", - "chapter", - "chaptermark", - "chaptername", - "chapternamenum", - "chapternumberlinebox", - "chapterstyle", - "chaptitlefont", - "char", - "chdr", - "chdrx", - "checkandfixthelayout", - "cite", - "clauselabel", - "clearpage", - "cline", - "clist", - "closeout", - "clubpenalty", - "cname", - "colhdr", - "color", - "columnbreak", - "commentellip", - "complexity", - "constantwhen", - "constraints", - "contentsname", - "continuedcaption", - "copyboxwidth", - "copypagestyle", - "copyright", - "cos", - "countedrange", - "counterwithin", - "counterwithout", - "cppver", - "cptn", - "crange", - "cs", - "ctype", - "customlabel", - "cv", - "cvqual", - "d", - "def", - "default", - "defexposconcept", - "defexposconceptnc", - "definecolor", - "definition", - "deflibconcept", - "defn", - "defnadj", - "defnadjx", - "defncontext", - "defnlibxname", - "defnnewoldconcept", - "defnoldconcept", - "defnx", - "defnxname", - "delta", - "deprxref", - "descr", - "diffdef", - "diffhead", - "difficulty", - "diffref", - "diffrefs", - "discretionary", - "displaystyle", - "do", - "doccite", - "docno", - "documentclass", - "doindexescape", - "doneindexescape", - "dotsb", - "dotsc", - "ecname", - "edef", - "ednote", - "effect", - "effectafteritemize", - "effects", - "egroup", - "ell", - "else", - "emergencystretch", - "emph", - "end", - "endfirsthead", - "endgroup", - "endhead", - "endmulticols", - "ensuremath", - "ensures", - "eqref", - "equiv", - "errors", - "everypar", - "exp", - "expandafter", - "expects", - "expos", - "exposconcept", - "exposconceptnc", - "exposconceptx", - "exposid", - "exposidnc", - "f", - "fakegrammarterm", - "fbox", - "fboxrule", - "fboxsep", - "feetatbottom", - "fi", - "figurerefname", - "fill", - "firstlibchapter", - "fmtgrammarterm", - "fmtnontermdef", - "foo", - "footmarkstyle", - "footnote", - "footnotemark", - "footnotesize", - "footnotetext", - "frac", - "fref", - "frenchspacing", - "frontmatter", - "g", - "gamma", - "ge", - "geq", - "global", - "glossary", - "glossaryname", - "glossitem", - "glue", - "gramSec", - "gramWrite", - "grammarterm", - "grammartermnc", - "gramout", - "gterm", - "hangpara", - "hardexpects", - "hbox", - "hd", - "hdstyle", - "hdwidth", - "hfill", - "hline", - "howwide", - "hrule", - "hsize", - "hspace", - "hss", - "hyperpage", - "hyperref", - "hypersetup", - "hypertarget", - "hyphenation", - "idxCpp", - "idxbfpage", - "idxcode", - "idxconcept", - "idxexposconcept", - "idxgram", - "idxhdr", - "idxmname", - "idxnewoldconcept", - "idxoldconcept", - "idxterm", - "idxxname", - "if", - "ifcase", - "iffalse", - "ifhmode", - "ifinner", - "ifmmode", - "ifnum", - "iftrue", - "ifvmode", - "ifx", - "immediate", - "impdef", - "impdefnc", - "impdefx", - "impldef", - "impldefplain", - "impldefrootname", - "implimits", - "importexample", - "include", - "includegraphics", - "index", - "indexconcept", - "indexcont", - "indexdefn", - "indexescape", - "indexgram", - "indexgrammar", - "indexhdr", - "indexheader", - "indeximpldef", - "indexlibrary", - "indexlibraryboth", - "indexlibraryctor", - "indexlibrarydtor", - "indexlibraryglobal", - "indexlibrarymember", - "indexlibrarymemberexpos", - "indexlibrarymemberx", - "indexlibrarymisc", - "indexlibraryzombie", - "indexname", - "indexoff", - "indexordmem", - "indextext", - "indexunordmem", - "infannex", - "infty", - "input", - "int", - "iref", - "isocopyright", - "itcorr", - "itcorrwidth", - "item", - "itletterwidth", - "itshape", - "kern", - "keyword", - "kill", - "l", - "label", - "labelitemi", - "labelitemii", - "labelitemiii", - "labelitemiv", - "labelsep", - "lambda", - "land", - "large", - "larger", - "lastcorechapter", - "lastlibchapter", - "lbl", - "lceil", - "ldots", - "le", - "leaders", - "leavevmode", - "left", - "leftmargin", - "leftmargini", - "leftmark", - "leftshift", - "leq", - "let", - "lfloor", - "lhdr", - "lhdrx", - "libconcept", - "libconceptx", - "libdeprheaderref", - "libglobal", - "libheader", - "libheaderdef", - "libheaderref", - "libheaderrefx", - "libheaderrefxx", - "libheaderx", - "libmacro", - "libmember", - "libnoheader", - "libreqtabenv", - "libspec", - "libxmacro", - "lim", - "linebreak", - "listing", - "listparindent", - "ln", - "locgrammarterm", - "locnontermdef", - "log", - "logop", - "lor", - "lst", - "lstdefinelanguage", - "lsthk", - "lstnewenvironment", - "lstsaved", - "lstset", - "m", - "mainmatter", - "makeatletter", - "makeatother", - "makebox", - "makechapterstyle", - "makeevenfoot", - "makeevenhead", - "makeglossary", - "makeheadrule", - "makeindex", - "makeoddfoot", - "makeoddhead", - "makepagestyle", - "makepsmarks", - "mandates", - "mapsto", - "markboth", - "mathbin", - "mathit", - "mathop", - "mathrel", - "mathrm", - "mathscr", - "mathsf", - "mathtt", - "max", - "maxsecnumdepth", - "maybeaddpnum", - "mbox", - "meaning", - "meaningbody", - "microtypesetup", - "min", - "mname", - "mod", - "movedxref", - "movedxrefii", - "movedxrefiii", - "movedxrefs", - "mu", - "mulhi", - "mullo", - "multicols", - "multicolumn", - "n", - "nb", - "nbc", - "nc", - "neq", - "newcolumntype", - "newcommand", - "newcounter", - "newenvironment", - "newlabel", - "newlength", - "newline", - "newlist", - "newnoteenvironment", - "newoldconcept", - "newpage", - "newsavebox", - "newsubclausecounter", - "newwrite", - "nobreak", - "nocode", - "nocontentsline", - "nocorr", - "nodiffref", - "noexpand", - "noindent", - "nolinebreak", - "nonfrenchspacing", - "nontermdef", - "normalbaselines", - "normalbaselineskip", - "normalcolor", - "normalfont", - "normalsize", - "normannex", - "notdef", - "noteintro", - "noteoutro", - "ntbs", - "ntcxvis", - "ntcxxxiis", - "ntmbs", - "ntwcs", - "nu", - "numberwithin", - "numconst", - "oBreakableUnderscore", - "oCpp", - "oexposid", - "ogrammarterm", - "ohdr", - "ohdrx", - "oindex", - "oldconcept", - "oldconceptname", - "oldcontentsline", - "oldxref", - "omname", - "onelineskip", - "openout", - "operatorname", - "opt", - "or", - "orange", - "otcode", - "otextup", - "otherwise", - "overline", - "pagebreak", - "pagestyle", - "par", - "parabullnum", - "paragraph", - "paragraphmark", - "parindent", - "parshape", - "parskip", - "partopsep", - "pdfbookmark", - "pdfcolorstack", - "pdfcompresslevel", - "pdfminorversion", - "pdfobjcompresslevel", - "pdfstringdefDisableCommands", - "penalty", - "phantom", - "phantomsection", - "phi", - "pi", - "placeholder", - "placeholdernc", - "pnum", - "preglossaryhook", - "preindexhook", - "prevdocno", - "prime", - "printchapternum", - "printglossary", - "printindex", - "protect", - "protected", - "providecommand", - "qquad", - "quad", - "r", - "rSec", - "rSecindex", - "raggedbottom", - "raggedright", - "raise", - "raisebox", - "range", - "rationale", - "rb", - "rceil", - "realglossitem", - "recommended", - "ref", - "reflexpr", - "refstepcounter", - "regrammarterm", - "relax", - "reldate", - "remarks", - "remitem", - "removed", - "removednb", - "removedxref", - "removelastskip", - "renewcommand", - "renontermdef", - "required", - "result", - "returns", - "returntype", - "rfloor", - "rhdr", - "rhdrx", - "rho", - "right", - "rightarrow", - "rightmargin", - "rightmark", - "rightshift", - "rlap", - "rmfamily", - "rowhdr", - "rowsep", - "s", - "savedallowbreak", - "scriptsize", - "secref", - "section", - "sectionmark", - "seeabove", - "seeabovenc", - "seebelow", - "seebelownc", - "setafterparaskip", - "setaftersecskip", - "setaftersubparaskip", - "setaftersubsecskip", - "setaftersubsubsecskip", - "setbeforeparaskip", - "setbeforesecskip", - "setbeforesubparaskip", - "setbeforesubsecskip", - "setbeforesubsubsecskip", - "setbox", - "setcounter", - "setglobalstyles", - "setheaderspaces", - "setheadfoot", - "setlength", - "setlist", - "setlrmarginsandblock", - "setmarginnotes", - "setparaheadstyle", - "setparaindent", - "setsecheadstyle", - "setsecindent", - "setsubparaheadstyle", - "setsubparaindent", - "setsubsecheadstyle", - "setsubsecindent", - "setsubsubsecheadstyle", - "setsubsubsecindent", - "settowidth", - "setulmarginsandblock", - "sffamily", - "sigma", - "sin", - "small", - "smaller", - "sout", - "space", - "sqrt", - "stage", - "state", - "stepcounter", - "stopindexescape", - "string", - "strip", - "subparagraph", - "subsection", - "subsectionmark", - "subsubsection", - "subsubsectionmark", - "sum", - "supercite", - "swallow", - "sync", - "t", - "tabcolsep", - "tableofcontents", - "tablerefname", - "tabularnewline", - "tcode", - "temp", - "templalias", - "term", - "terminal", - "termref", - "texorpdfstring", - "text", - "textasciitilde", - "textbackslash", - "textbf", - "textbullet", - "textcolor", - "textit", - "textlangle", - "textlarger", - "textmu", - "textnormal", - "textrangle", - "textregistered", - "textrm", - "textsc", - "textsection", - "textsf", - "textsl", - "textsmaller", - "textsuperscript", - "texttt", - "textunderscore", - "textup", - "textwidth", - "the", - "thechapter", - "thepage", - "theparagraph", - "thesection", - "thesubsection", - "thesubsubsection", - "theta", - "thispagestyle", - "throws", - "times", - "tllo", - "to", - "today", - "tokenize", - "topline", - "topskip", - "tref", - "ttfamily", - "twocolglossary", - "u", - "ucode", - "uline", - "uname", - "ungap", - "unhbox", - "unicode", - "unskip", - "unspec", - "unspecalloctype", - "unspecbool", - "unspecnc", - "unspecuniqtype", - "unun", - "upshape", - "url", - "urlstyle", - "usepackage", - "v", - "value", - "vbox", - "vcenter", - "vdots", - "verb", - "verbtocs", - "vfill", - "vrule", - "vskip", - "vspace", - "vtop", - "w", - "widowpenalties", - "write", - "x", - "xA", - "xBB", - "xBF", - "xEF", - "xF", - "xc", - "xdef", - "xname", - "xor", - "xref", - "xrefc", - "z", - "zeta", -} +from data import * # =============================================================================== # Terminal color support diff --git a/tools/data.py b/tools/data.py new file mode 100644 index 0000000000..d0631f8352 --- /dev/null +++ b/tools/data.py @@ -0,0 +1,834 @@ +# Remember to keep this in sync with [structure.specifications] and our set of macros. +# The order matters for ElementOrderCheck. +FUNCTION_DESCRIPTORS = [ + "constraints", # Constraints + "mandates", # Mandates + "constantwhen", # Constant When + "expects", # Preconditions + "hardexpects", # Hardened preconditions + "effects", # Effects + "sync", # Synchronization + "ensures", # Postconditions + "returns", # Returns + "throws", # Throws + "complexity", # Complexity + "remarks", # Remarks + "errors", # Error conditions +] + +# All library descriptors. +# The order doesn't matter; this list is used for checking whether e.g. `\recommended` +# is preceded by `\pnum` +PARAGRAPH_DESCRIPTORS = [ + *FUNCTION_DESCRIPTORS, + "recommended", # Recommended practice + "required", # Required behavior + "implimits", # Implementation limits + "returntype", # Return type + "ctype", # Type + "templalias", # Alias template +] + +DIFF_DESCRIPTORS = [ + "change", + "rationale", + "effect", + "difficulty", + "howwide", +] + +KNOWN_COMMANDS = { + "0", + "Annex", + "BODY", + "BnfInc", + "BnfIndent", + "BnfNontermshape", + "BnfReNontermshape", + "BnfRest", + "BnfTermshape", + "BreakableUnderscore", + "CodeBlockSetup", + "CodeStyle", + "CodeStylex", + "CodeStylexGuarded", + "Cpp", + "CppIII", + "CppXI", + "CppXIV", + "CppXVII", + "CppXX", + "CppXXIII", + "CppXXVI", + "D", + "DeclareCaptionLabelSeparator", + "DeclareMathOperator", + "DisableLigatures", + "EXPO", + "ExplSyntaxOff", + "ExplSyntaxOn", + "Fundesc", + "Fundescx", + "Gamma", + "GrammarStylex", + "Head", + "Huge", + "IsoC", + "IsoCUndated", + "IsoFloatUndated", + "IsoPosix", + "IsoPosixUndated", + "LARGE", + "Large", + "N", + "NTS", + "NewDocumentCommand", + "NewEnviron", + "OldTextAsciiTilde", + "Pi", + "RSsmallest", + "Range", + "S", + "Sec", + "Sigma", + "StrClosingbrace", + "StrSubstitute", + "StrTextsc", + "StrTextsmaller", + "The", + "U", + "UAX", + "UNSP", + "UNSPnc", + "W", + "a", + "abovecaptionskip", + "addcontentsline", + "added", + "addednb", + "addtocontents", + "addtocounter", + "addtolength", + "addxref", + "advance", + "afterchapskip", + "afterchapternum", + "afterskip", + "aliaspagestyle", + "allowbreak", + "alpha", + "annexlabel", + "annexnumberlinebox", + "appendix", + "appendixname", + "arabic", + "atsign", + "b", + "backmatter", + "backslash", + "bar", + "baselineskip", + "beforechapskip", + "beforeskip", + "begin", + "begingroup", + "belowcaptionskip", + "beta", + "bfseries", + "bgroup", + "bibitem", + "bibname", + "big", + "bigl", + "bigoh", + "bigr", + "bigskip", + "binom", + "bitand", + "bitor", + "bm", + "bmod", + "bnfindent", + "bnfindentfirst", + "bnfindentinc", + "bnfindentrest", + "bot", + "bottomline", + "br", + "brange", + "break", + "brk", + "c", + "capsep", + "caption", + "captionsetup", + "caret", + "cdot", + "cdots", + "cedef", + "centering", + "cftsetindents", + "change", + "changed", + "changednb", + "changeglossnumformat", + "chapnamefont", + "chapnumfont", + "chapter", + "chaptermark", + "chaptername", + "chapternamenum", + "chapternumberlinebox", + "chapterstyle", + "chaptitlefont", + "char", + "chdr", + "chdrx", + "checkandfixthelayout", + "cite", + "clauselabel", + "clearpage", + "cline", + "clist", + "closeout", + "clubpenalty", + "cname", + "colhdr", + "color", + "columnbreak", + "commentellip", + "complexity", + "constantwhen", + "constraints", + "contentsname", + "continuedcaption", + "copyboxwidth", + "copypagestyle", + "copyright", + "cos", + "countedrange", + "counterwithin", + "counterwithout", + "cppver", + "cptn", + "crange", + "cs", + "ctype", + "customlabel", + "cv", + "cvqual", + "d", + "def", + "default", + "defexposconcept", + "defexposconceptnc", + "definecolor", + "definition", + "deflibconcept", + "defn", + "defnadj", + "defnadjx", + "defncontext", + "defnlibxname", + "defnnewoldconcept", + "defnoldconcept", + "defnx", + "defnxname", + "delta", + "deprxref", + "descr", + "diffdef", + "diffhead", + "difficulty", + "diffref", + "diffrefs", + "discretionary", + "displaystyle", + "do", + "doccite", + "docno", + "documentclass", + "doindexescape", + "doneindexescape", + "dotsb", + "dotsc", + "ecname", + "edef", + "ednote", + "effect", + "effectafteritemize", + "effects", + "egroup", + "ell", + "else", + "emergencystretch", + "emph", + "end", + "endfirsthead", + "endgroup", + "endhead", + "endmulticols", + "ensuremath", + "ensures", + "eqref", + "equiv", + "errors", + "everypar", + "exp", + "expandafter", + "expects", + "expos", + "exposconcept", + "exposconceptnc", + "exposconceptx", + "exposid", + "exposidnc", + "f", + "fakegrammarterm", + "fbox", + "fboxrule", + "fboxsep", + "feetatbottom", + "fi", + "figurerefname", + "fill", + "firstlibchapter", + "fmtgrammarterm", + "fmtnontermdef", + "foo", + "footmarkstyle", + "footnote", + "footnotemark", + "footnotesize", + "footnotetext", + "frac", + "fref", + "frenchspacing", + "frontmatter", + "g", + "gamma", + "ge", + "geq", + "global", + "glossary", + "glossaryname", + "glossitem", + "glue", + "gramSec", + "gramWrite", + "grammarterm", + "grammartermnc", + "gramout", + "gterm", + "hangpara", + "hardexpects", + "hbox", + "hd", + "hdstyle", + "hdwidth", + "hfill", + "hline", + "howwide", + "hrule", + "hsize", + "hspace", + "hss", + "hyperpage", + "hyperref", + "hypersetup", + "hypertarget", + "hyphenation", + "idxCpp", + "idxbfpage", + "idxcode", + "idxconcept", + "idxexposconcept", + "idxgram", + "idxhdr", + "idxmname", + "idxnewoldconcept", + "idxoldconcept", + "idxterm", + "idxxname", + "if", + "ifcase", + "iffalse", + "ifhmode", + "ifinner", + "ifmmode", + "ifnum", + "iftrue", + "ifvmode", + "ifx", + "immediate", + "impdef", + "impdefnc", + "impdefx", + "impldef", + "impldefplain", + "impldefrootname", + "implimits", + "importexample", + "include", + "includegraphics", + "index", + "indexconcept", + "indexcont", + "indexdefn", + "indexescape", + "indexgram", + "indexgrammar", + "indexhdr", + "indexheader", + "indeximpldef", + "indexlibrary", + "indexlibraryboth", + "indexlibraryctor", + "indexlibrarydtor", + "indexlibraryglobal", + "indexlibrarymember", + "indexlibrarymemberexpos", + "indexlibrarymemberx", + "indexlibrarymisc", + "indexlibraryzombie", + "indexname", + "indexoff", + "indexordmem", + "indextext", + "indexunordmem", + "infannex", + "infty", + "input", + "int", + "iref", + "isocopyright", + "itcorr", + "itcorrwidth", + "item", + "itletterwidth", + "itshape", + "kern", + "keyword", + "kill", + "l", + "label", + "labelitemi", + "labelitemii", + "labelitemiii", + "labelitemiv", + "labelsep", + "lambda", + "land", + "large", + "larger", + "lastcorechapter", + "lastlibchapter", + "lbl", + "lceil", + "ldots", + "le", + "leaders", + "leavevmode", + "left", + "leftmargin", + "leftmargini", + "leftmark", + "leftshift", + "leq", + "let", + "lfloor", + "lhdr", + "lhdrx", + "libconcept", + "libconceptx", + "libdeprheaderref", + "libglobal", + "libheader", + "libheaderdef", + "libheaderref", + "libheaderrefx", + "libheaderrefxx", + "libheaderx", + "libmacro", + "libmember", + "libnoheader", + "libreqtabenv", + "libspec", + "libxmacro", + "lim", + "linebreak", + "listing", + "listparindent", + "ln", + "locgrammarterm", + "locnontermdef", + "log", + "logop", + "lor", + "lst", + "lstdefinelanguage", + "lsthk", + "lstnewenvironment", + "lstsaved", + "lstset", + "m", + "mainmatter", + "makeatletter", + "makeatother", + "makebox", + "makechapterstyle", + "makeevenfoot", + "makeevenhead", + "makeglossary", + "makeheadrule", + "makeindex", + "makeoddfoot", + "makeoddhead", + "makepagestyle", + "makepsmarks", + "mandates", + "mapsto", + "markboth", + "mathbin", + "mathit", + "mathop", + "mathrel", + "mathrm", + "mathscr", + "mathsf", + "mathtt", + "max", + "maxsecnumdepth", + "maybeaddpnum", + "mbox", + "meaning", + "meaningbody", + "microtypesetup", + "min", + "mname", + "mod", + "movedxref", + "movedxrefii", + "movedxrefiii", + "movedxrefs", + "mu", + "mulhi", + "mullo", + "multicols", + "multicolumn", + "n", + "nb", + "nbc", + "nc", + "neq", + "newcolumntype", + "newcommand", + "newcounter", + "newenvironment", + "newlabel", + "newlength", + "newline", + "newlist", + "newnoteenvironment", + "newoldconcept", + "newpage", + "newsavebox", + "newsubclausecounter", + "newwrite", + "nobreak", + "nocode", + "nocontentsline", + "nocorr", + "nodiffref", + "noexpand", + "noindent", + "nolinebreak", + "nonfrenchspacing", + "nontermdef", + "normalbaselines", + "normalbaselineskip", + "normalcolor", + "normalfont", + "normalsize", + "normannex", + "notdef", + "noteintro", + "noteoutro", + "ntbs", + "ntcxvis", + "ntcxxxiis", + "ntmbs", + "ntwcs", + "nu", + "numberwithin", + "numconst", + "oBreakableUnderscore", + "oCpp", + "oexposid", + "ogrammarterm", + "ohdr", + "ohdrx", + "oindex", + "oldconcept", + "oldconceptname", + "oldcontentsline", + "oldxref", + "omname", + "onelineskip", + "openout", + "operatorname", + "opt", + "or", + "orange", + "otcode", + "otextup", + "otherwise", + "overline", + "pagebreak", + "pagestyle", + "par", + "parabullnum", + "paragraph", + "paragraphmark", + "parindent", + "parshape", + "parskip", + "partopsep", + "pdfbookmark", + "pdfcolorstack", + "pdfcompresslevel", + "pdfminorversion", + "pdfobjcompresslevel", + "pdfstringdefDisableCommands", + "penalty", + "phantom", + "phantomsection", + "phi", + "pi", + "placeholder", + "placeholdernc", + "pnum", + "preglossaryhook", + "preindexhook", + "prevdocno", + "prime", + "printchapternum", + "printglossary", + "printindex", + "protect", + "protected", + "providecommand", + "qquad", + "quad", + "r", + "rSec", + "rSecindex", + "raggedbottom", + "raggedright", + "raise", + "raisebox", + "range", + "rationale", + "rb", + "rceil", + "realglossitem", + "recommended", + "ref", + "reflexpr", + "refstepcounter", + "regrammarterm", + "relax", + "reldate", + "remarks", + "remitem", + "removed", + "removednb", + "removedxref", + "removelastskip", + "renewcommand", + "renontermdef", + "required", + "result", + "returns", + "returntype", + "rfloor", + "rhdr", + "rhdrx", + "rho", + "right", + "rightarrow", + "rightmargin", + "rightmark", + "rightshift", + "rlap", + "rmfamily", + "rowhdr", + "rowsep", + "s", + "savedallowbreak", + "scriptsize", + "secref", + "section", + "sectionmark", + "seeabove", + "seeabovenc", + "seebelow", + "seebelownc", + "setafterparaskip", + "setaftersecskip", + "setaftersubparaskip", + "setaftersubsecskip", + "setaftersubsubsecskip", + "setbeforeparaskip", + "setbeforesecskip", + "setbeforesubparaskip", + "setbeforesubsecskip", + "setbeforesubsubsecskip", + "setbox", + "setcounter", + "setglobalstyles", + "setheaderspaces", + "setheadfoot", + "setlength", + "setlist", + "setlrmarginsandblock", + "setmarginnotes", + "setparaheadstyle", + "setparaindent", + "setsecheadstyle", + "setsecindent", + "setsubparaheadstyle", + "setsubparaindent", + "setsubsecheadstyle", + "setsubsecindent", + "setsubsubsecheadstyle", + "setsubsubsecindent", + "settowidth", + "setulmarginsandblock", + "sffamily", + "sigma", + "sin", + "small", + "smaller", + "sout", + "space", + "sqrt", + "stage", + "state", + "stepcounter", + "stopindexescape", + "string", + "strip", + "subparagraph", + "subsection", + "subsectionmark", + "subsubsection", + "subsubsectionmark", + "sum", + "supercite", + "swallow", + "sync", + "t", + "tabcolsep", + "tableofcontents", + "tablerefname", + "tabularnewline", + "tcode", + "temp", + "templalias", + "term", + "terminal", + "termref", + "texorpdfstring", + "text", + "textasciitilde", + "textbackslash", + "textbf", + "textbullet", + "textcolor", + "textit", + "textlangle", + "textlarger", + "textmu", + "textnormal", + "textrangle", + "textregistered", + "textrm", + "textsc", + "textsection", + "textsf", + "textsl", + "textsmaller", + "textsuperscript", + "texttt", + "textunderscore", + "textup", + "textwidth", + "the", + "thechapter", + "thepage", + "theparagraph", + "thesection", + "thesubsection", + "thesubsubsection", + "theta", + "thispagestyle", + "throws", + "times", + "tllo", + "to", + "today", + "tokenize", + "topline", + "topskip", + "tref", + "ttfamily", + "twocolglossary", + "u", + "ucode", + "uline", + "uname", + "ungap", + "unhbox", + "unicode", + "unskip", + "unspec", + "unspecalloctype", + "unspecbool", + "unspecnc", + "unspecuniqtype", + "unun", + "upshape", + "url", + "urlstyle", + "usepackage", + "v", + "value", + "vbox", + "vcenter", + "vdots", + "verb", + "verbtocs", + "vfill", + "vrule", + "vskip", + "vspace", + "vtop", + "w", + "widowpenalties", + "write", + "x", + "xA", + "xBB", + "xBF", + "xEF", + "xF", + "xc", + "xdef", + "xname", + "xor", + "xref", + "xrefc", + "z", + "zeta", +} From fe02c01039cbda58353081a27e4333d46cc5c724 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:38:08 +0200 Subject: [PATCH 05/24] Add new commands --- tools/data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/data.py b/tools/data.py index d0631f8352..f55de58c65 100644 --- a/tools/data.py +++ b/tools/data.py @@ -356,6 +356,8 @@ "ifhmode", "ifinner", "ifmmode", + "ifndrdef", + "ifndrdescription", "ifnum", "iftrue", "ifvmode", @@ -785,6 +787,8 @@ "ttfamily", "twocolglossary", "u", + "ubdef", + "ubdescription", "ucode", "uline", "uname", From c0c5e2391c01345dfb44ae2126d7c5816a8852dd Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:42:30 +0200 Subject: [PATCH 06/24] Disable library checks in UB/IFNDR annexes --- source/ifndr.tex | 1 + source/ub.tex | 1 + 2 files changed, 2 insertions(+) diff --git a/source/ifndr.tex b/source/ifndr.tex index 036de5cf6c..1747dc5316 100644 --- a/source/ifndr.tex +++ b/source/ifndr.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \infannex{ifndr}{Ill-formed, no diagnostic required} \rSec1[ifndr.general]{General} diff --git a/source/ub.tex b/source/ub.tex index ea49eae87b..2b15b84177 100644 --- a/source/ub.tex +++ b/source/ub.tex @@ -1,4 +1,5 @@ %!TEX root = std.tex +%NOCHECKBEGIN(lib-*) \infannex{ub}{Core undefined behavior} \rSec1[ub.general]{General} From 1e73ceabe278c10aaefba00ff91266b813a7e4c7 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:44:01 +0200 Subject: [PATCH 07/24] Remove unnecessary pass --- tools/check-source.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 8b98aee65c..c48669a160 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -249,15 +249,12 @@ def begin_file(self, file_path: Path, lines: list[str]) -> None: def check_line(self, line_num: int, line: str) -> None: """Called for each line while the check is active.""" - pass def end_file(self, file_path: Path) -> None: """Called after processing all lines of `file_path`.""" - pass def end_checks(self) -> None: """Called once after all files have been processed.""" - pass def fail( self, From 270d41642a5e88db297a8033239d08e73aa28eaf Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:44:28 +0200 Subject: [PATCH 08/24] Simplify if statement to max --- tools/check-source.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index c48669a160..888901e49d 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -1357,9 +1357,7 @@ def main() -> None: comment_line = entry.comment_line if comment_line < len(lines): line_text = lines[comment_line] - column = line_text.find(entry.check_id) - if column < 0: - column = 0 + column = max(line_text.find(entry.check_id), 0) fail = Failure( file=entry.file, line=comment_line, From 8daba1d5edffc50bb68f1fa5830a563961189971 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:51:41 +0200 Subject: [PATCH 09/24] Remove unnecessary trailing comments --- tools/data.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tools/data.py b/tools/data.py index f55de58c65..0730f6c75b 100644 --- a/tools/data.py +++ b/tools/data.py @@ -1,19 +1,19 @@ # Remember to keep this in sync with [structure.specifications] and our set of macros. # The order matters for ElementOrderCheck. FUNCTION_DESCRIPTORS = [ - "constraints", # Constraints - "mandates", # Mandates - "constantwhen", # Constant When - "expects", # Preconditions - "hardexpects", # Hardened preconditions - "effects", # Effects - "sync", # Synchronization - "ensures", # Postconditions - "returns", # Returns - "throws", # Throws - "complexity", # Complexity - "remarks", # Remarks - "errors", # Error conditions + "constraints", + "mandates", + "constantwhen", + "expects", + "hardexpects", + "effects", + "sync", + "ensures", + "returns", + "throws", + "complexity", + "remarks", + "errors", ] # All library descriptors. @@ -21,12 +21,12 @@ # is preceded by `\pnum` PARAGRAPH_DESCRIPTORS = [ *FUNCTION_DESCRIPTORS, - "recommended", # Recommended practice - "required", # Required behavior - "implimits", # Implementation limits - "returntype", # Return type - "ctype", # Type - "templalias", # Alias template + "recommended", + "required", + "implimits", + "returntype", + "ctype", + "templalias", ] DIFF_DESCRIPTORS = [ From 1a5fc29e33104e2eeb7a9fe5b4d03b71b9c9860f Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:55:44 +0200 Subject: [PATCH 10/24] Simplify exit code computation --- tools/check-source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/check-source.py b/tools/check-source.py index 888901e49d..f4dc0cde8c 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -1380,7 +1380,7 @@ def main() -> None: file=sys.stderr, ) - exit_code = 1 if num_failures > 0 else 0 + exit_code = num_failures > 1 sys.exit(exit_code) From 35ed7e7853baea4f7caed69614c519304c16582f Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 06:57:08 +0200 Subject: [PATCH 11/24] Move space into preceding string --- tools/check-source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index f4dc0cde8c..114e5b3e2c 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -157,8 +157,8 @@ def format_failure(fail: Failure, lines: list[str]) -> str: f"{style(fail.file, ANSI_BOLD)}:" f"{style(str(fail.line + 1), ANSI_BOLD)}:" f"{style(str(fail.column_start + 1), ANSI_BOLD)}: " - f"{style('error:', ANSI_RED)} {message}" - f" {style(f'[{fail.check_id}]', ANSI_GRAY)}" + f"{style('error:', ANSI_RED)} {message} " + f"{style(f'[{fail.check_id}]', ANSI_GRAY)}" ) if fail.line < len(lines): src_line = lines[fail.line] From e20070e4d7223476d14688e5ed8b2e3c79bda436 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 07:57:59 +0200 Subject: [PATCH 12/24] Fix typos for base-tcode-exposid expectations --- tools/test-checks.tex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/test-checks.tex b/tools/test-checks.tex index 7bb780ccab..dc3b5c32b0 100644 --- a/tools/test-checks.tex +++ b/tools/test-checks.tex @@ -70,9 +70,9 @@ \unicode{00FF} %EXPECTCHECKNEXTLINE(base-tcode-exposid) -\tcode{\exposid{snake-case}} +\tcode{\exposid{snake_case}} %EXPECTCHECKNEXTLINE(base-tcode-exposid) -\tcode{\exposid{kebak-case}} +\tcode{\exposid{kebab-case}} %EXPECTCHECKNEXTLINE(base-ref-in-parens) See the section.(\ref{expr.const}) From 4513a7576f009f1971080b4a7e1974519fe4472b Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 07:58:55 +0200 Subject: [PATCH 13/24] Ignore common Python files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 6db5d22375..e8cd76d983 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,8 @@ tools/sections *.synctex.gz *.synctex* .check.stamp + __pycache__ +*.pyc +*.egg +*.egg-info From 51114133846d4d6df93c0021a07d54d626f0fb8d Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 08:02:42 +0200 Subject: [PATCH 14/24] Clean code around col1 and col2 --- tools/check-source.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 114e5b3e2c..de62a1728d 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -166,14 +166,14 @@ def format_failure(fail: Failure, lines: list[str]) -> str: line_num_str = str(fail.line + 1) num_width = max(6, len(line_num_str)) pad = " " * (num_width - len(line_num_str)) - col1 = fail.column_start + 1 - col2 = fail.column_end + 1 parts.append(f"{pad}{line_num_str} | {display_line}") - prefix = display_line[: col1 - 1] + start = fail.column_start + 1 + end = fail.column_end + 1 + prefix = display_line[: start - 1] highlight_pad = len(prefix) - highlight_len = max(1, col2 - col1) - if col1 - 1 + highlight_len > len(display_line): - highlight_len = max(1, len(display_line) - (col1 - 1)) + highlight_len = max(1, end - start) + if start - 1 + highlight_len > len(display_line): + highlight_len = max(1, len(display_line) - (start - 1)) gutter = " " * (num_width + 1) + "| " hl = style("^" + "~" * (highlight_len - 1), ANSI_GREEN) parts.append(f"{gutter}{' ' * highlight_pad}{hl}") From 665dbe04cae6cb619fbc9a29cccf4d168df79b2e Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 08:04:04 +0200 Subject: [PATCH 15/24] Remove unnecessary begin_pattern and end_pattern variables --- tools/check-source.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index de62a1728d..d8c9c57f8e 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -184,14 +184,12 @@ def find_env_ranges( lines: list[str], env: Environment, ) -> list[tuple[int, int]]: - begin_pattern = env.begin_pattern - end_pattern = env.end_pattern ranges: list[tuple[int, int]] = [] stack: list[int] = [] for idx, line in enumerate(lines): - if begin_pattern.search(line): + if env.begin_pattern.search(line): stack.append(idx) - if end_pattern.search(line) and stack: + if env.end_pattern.search(line) and stack: ranges.append((stack.pop(), idx + 1)) return ranges From 7cbded15c9ebc4cf655f0ae6253b76085f0761ec Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 09:43:45 +0200 Subject: [PATCH 16/24] Fix Ruff default warnings 1. Nested if statements get flagged. These are replaced with "and". 2. Mutable default initializers for members are flagged. Initialization is moved to __init__. --- tools/check-source.py | 54 +++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index d8c9c57f8e..bb2852c507 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -683,19 +683,20 @@ def check_file(self, file_path: Path, lines: list[str]) -> None: in_cb = False cb_text = "\n".join(cb_lines) stripped = self.TEMPLATE_PATTERN.sub("", cb_text) - if self.CLASS_DEFINITION_PATTERN.search(stripped): - if not self.NAMESPACE_PATTERN.search(stripped): - for ci, cline in enumerate(cb_lines): - cs = self.TEMPLATE_PATTERN.sub("", cline) - if self.CLASS_DEFINITION_PATTERN.search(cs): - self.fail( - cb_start + ci, - 0, - 0, - "Class definition in a `Class` section " - "not wrapped in a `namespace` block.", - ) - break + if self.CLASS_DEFINITION_PATTERN.search( + stripped + ) and not self.NAMESPACE_PATTERN.search(stripped): + for ci, cline in enumerate(cb_lines): + cs = self.TEMPLATE_PATTERN.sub("", cline) + if self.CLASS_DEFINITION_PATTERN.search(cs): + self.fail( + cb_start + ci, + 0, + 0, + "Class definition in a `Class` section " + "not wrapped in a `namespace` block.", + ) + break continue if in_cb: cb_lines.append(line) @@ -725,11 +726,12 @@ def end_checks(self) -> None: class FunctionDescriptorOutOfOrderCheck(Check): - element_index: dict[str, int] = {e: i for i, e in enumerate(FUNCTION_DESCRIPTORS)} + element_index: dict[str, int] relevant_line_pattern = re.compile(r"^\\" + make_alt_pattern(FUNCTION_DESCRIPTORS)) def __init__(self, check_id: str): self.id = check_id + self.element_index = {e: i for i, e in enumerate(FUNCTION_DESCRIPTORS)} def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -745,14 +747,16 @@ def check_file(self, file_path: Path, lines: list[str]) -> None: if not m: continue name = m.group(0)[1:] - if prev_name is not None: - if self.element_index[name] < self.element_index[prev_name]: - self.fail( - idx, - m.start(), - m.end(), - f"`{name}` must not precede `{prev_name}`.", - ) + if ( + prev_name is not None + and self.element_index[name] < self.element_index[prev_name] + ): + self.fail( + idx, + m.start(), + m.end(), + f"`{name}` must not precede `{prev_name}`.", + ) prev_name = name @@ -897,7 +901,6 @@ class RefUndefinedCheck(Check): SECTION_PATTERN = re.compile(r"\\rSec[0-9]\[([^\]]+)\]") DEFINITION_PATTERN = re.compile(r"\\definition\{[^\}]*?\}\{(.+?)\}") ETC_PATTERN = re.compile(r"\\(?:infannex|normannex|label)\{(.+?)\}") - DEFINING_PATTERNS = [SECTION_PATTERN, DEFINITION_PATTERN, ETC_PATTERN] # TODO: Excluding `:` means that `tab:...` references are currently unchecked. # Maybe that could be added in the future, @@ -905,6 +908,11 @@ class RefUndefinedCheck(Check): REF_IREF_PATTERN = re.compile(r"\\i?ref\{([^}\\:]+)\}") def __init__(self, check_id: str): + self.DEFINING_PATTERNS = [ + self.SECTION_PATTERN, + self.DEFINITION_PATTERN, + self.ETC_PATTERN, + ] self.id = check_id self.defined: set[str] = set() self.used: dict[str, list[tuple[str, int, int, int]]] = defaultdict(list) From c48c2dd7d7d34bf546bb88056a4740629faa1d7d Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 09:52:41 +0200 Subject: [PATCH 17/24] Rename id->uid, manage properly in derived classes --- tools/check-source.py | 51 +++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index bb2852c507..e783c2f889 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -210,7 +210,7 @@ def emit_check_failure( Line numbers and columns follow the same convention as `Failure`. """ global unexpected_count - if consume_expected(file, line, check.id): + if consume_expected(file, line, check.uid): return unexpected_count += 1 fail = Failure( @@ -219,7 +219,7 @@ def emit_check_failure( column_start=column_start, column_end=column_end, message=message, - check_id=check.id, + check_id=check.uid, ) # Reading the file from scratch is very slow, # but we don't care because this is the unhappy path anyway, @@ -238,7 +238,10 @@ def emit_check_failure( class Check(ABC): """Base class for all checks.""" - id: str = "" + uid: str + + def __init__(self, uid: str): + self.uid = uid def begin_file(self, file_path: Path, lines: list[str]) -> None: """Called before processing any line of `file_path`.""" @@ -281,7 +284,7 @@ def __init__( pattern: Pattern[str], message: str, ): - self.id = check_id + super().__init__(check_id) self.pattern = pattern self.message = message @@ -325,7 +328,7 @@ def __init__( pattern: Pattern[str], message: str, ): - self.id = check_id + super().__init__(check_id) self.env = env self.pattern = pattern self.message = message @@ -354,7 +357,7 @@ class NonAsciiCheck(Check): NON_ASCII_PATTERN = re.compile(r"[^\x09\x0a\x0d\x20-\x7e]") def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def check_line(self, line_num: int, line: str) -> None: for m in self.NON_ASCII_PATTERN.finditer(line): @@ -371,7 +374,7 @@ def check_line(self, line_num: int, line: str) -> None: class TrailingEmptyLinesCheck(Check): def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def end_file(self, file_path: Path) -> None: try: @@ -391,7 +394,7 @@ def end_file(self, file_path: Path) -> None: class ConsecutivePnumCheck(Check): def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -413,7 +416,7 @@ def check_line(self, line_num: int, line: str) -> None: class TailnoteTailexampleCheck(Check): def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) END_PATTERN = re.compile(r"\\end\{(?:example|note)\}") TAIL_PATTERN = re.compile(r"- *(?:\\\\|&)") @@ -443,7 +446,7 @@ def check_line(self, line_num: int, line: str) -> None: class BlankLineExampleCodeblockCheck(Check): def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -468,7 +471,7 @@ def check_line(self, line_num: int, line: str) -> None: class CommentAlignmentCheck(Check): def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) # This pattern checks for //, with some notable exemptions: # - If '@' is present anywhere, we don't match the comment because we cannot compute alignment. @@ -509,7 +512,7 @@ class HangingParagraphsCheck(Check): SECTION_PATTERN = re.compile(r"^\\rSec([0-9])") def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -544,7 +547,7 @@ class SubclausesWithoutSiblingsCheck(Check): SECTION_PATTERN = re.compile(r"^\\rSec([0-9])") def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -580,7 +583,7 @@ class SectionSelfReferenceCheck(Check): IREF_PATTERN = re.compile(r"\\iref\{([^\}]*)\}") def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -608,7 +611,7 @@ class PnumMissingInItemdescrCheck(Check): ELEMENT_PATTERN = re.compile(r"^\\" + make_alt_pattern(PARAGRAPH_DESCRIPTORS)) def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -646,7 +649,7 @@ class ClassDefinitionOutsideNamespaceCheck(Check): NAMESPACE_PATTERN = re.compile(r"^\s*namespace\s", re.MULTILINE) def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -704,7 +707,7 @@ def check_file(self, file_path: Path, lines: list[str]) -> None: class OutdatedFiguresCheck(Check): def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def end_checks(self) -> None: for dot_file in sorted(source_dir.glob("*.dot")): @@ -730,7 +733,7 @@ class FunctionDescriptorOutOfOrderCheck(Check): relevant_line_pattern = re.compile(r"^\\" + make_alt_pattern(FUNCTION_DESCRIPTORS)) def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) self.element_index = {e: i for i, e in enumerate(FUNCTION_DESCRIPTORS)} def begin_file(self, file_path: Path, lines: list[str]) -> None: @@ -764,7 +767,7 @@ class UnbalancedBeginAndEndCheck(Check): BEGIN_OR_END_PATTERN = re.compile(r"\\(begin|end)\{([^}]+)\}") def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) def begin_file(self, file_path: Path, lines: list[str]) -> None: super().begin_file(file_path, lines) @@ -822,7 +825,7 @@ class UnknownCommandCheck(Check): """ def __init__(self, check_id: str): - self.id = check_id + super().__init__(check_id) COMMAND_PATTERN = re.compile(r"\\([a-zA-Z][a-zA-Z]*)") @@ -857,7 +860,7 @@ def __init__( definition_pattern: Pattern[str], usage_pattern: list[Pattern[str]], ): - self.id = check_id + super().__init__(check_id) self.definition_pattern = definition_pattern self.usage_pattern = usage_pattern self.defined: set[str] = set() @@ -913,7 +916,7 @@ def __init__(self, check_id: str): self.DEFINITION_PATTERN, self.ETC_PATTERN, ] - self.id = check_id + super().__init__(check_id) self.defined: set[str] = set() self.used: dict[str, list[tuple[str, int, int, int]]] = defaultdict(list) @@ -1221,7 +1224,7 @@ def run_checks(tex_files: list[Path]) -> int: for fp in tex_files: file_locations[os.path.relpath(fp)] = fp - all_ids = {c.id for c in CHECKS if c.id} + all_ids = {c.uid for c in CHECKS if c.uid} for file_path in tex_files: if not file_path.exists(): @@ -1267,7 +1270,7 @@ def run_checks(tex_files: list[Path]) -> int: # Call check_line only on active checks (not skipped). for c in CHECKS: - if c.id in active and line_num not in skip_next.get(c.id, set()): + if c.uid in active and line_num not in skip_next.get(c.uid, set()): c.check_line(line_num, line) # Always call end_file. From 388e42cb7345447528e2ffd6e4cb98f9060f4585 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 09:57:54 +0200 Subject: [PATCH 18/24] Convert find_env_ranges to coroutine --- tools/check-source.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index e783c2f889..8aa8092e99 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -183,15 +183,13 @@ def format_failure(fail: Failure, lines: list[str]) -> str: def find_env_ranges( lines: list[str], env: Environment, -) -> list[tuple[int, int]]: - ranges: list[tuple[int, int]] = [] +): stack: list[int] = [] for idx, line in enumerate(lines): if env.begin_pattern.search(line): stack.append(idx) if env.end_pattern.search(line) and stack: - ranges.append((stack.pop(), idx + 1)) - return ranges + yield stack.pop(), idx + 1 unexpected_count = 0 From 7af3d566b813c579f2f7bec092800c6dfb854922 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 10:22:39 +0200 Subject: [PATCH 19/24] Eliminate global state --- tools/check-source.py | 143 +++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 65 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 8aa8092e99..4090ca02b1 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -38,7 +38,7 @@ def style(text: str, code: str) -> str: # ================================================================================================== -# Data types +# Utilities # ================================================================================================== @@ -87,48 +87,38 @@ class ExpectedFailure: """`true` if a matching failure was reported.""" -# ================================================================================================== -# Global state -# ================================================================================================== - -source_dir: Path = Path() -file_locations: dict[str, Path] = {} -expected_registry: dict[tuple[str, int], ExpectedFailure] = {} - -# ================================================================================================== -# Expected-failure tracking -# ================================================================================================== - - -def register_expected(file: str, comment_line: int, check_id: str) -> None: - key = (file, comment_line) - if key not in expected_registry: - expected_registry[key] = ExpectedFailure(file, comment_line, check_id) - - -def consume_expected(file: str, failure_line: int, check_id: str) -> bool: - # Walk backwards through *consecutive* %EXPECTCHECKNEXTLINE directives - # only. A blank line (or any other content) breaks the chain — - # EXPECTCHECKNEXTLINE always refers to the immediately following line. - for offset in range(1, min(failure_line, 20) + 1): - entry = expected_registry.get((file, failure_line - offset)) - if entry is None: - # Not an EXPECTCHECKNEXTLINE line — chain is broken. - return False - if entry.check_id == check_id: - entry.hit = True - return True - # A different EXPECTCHECKNEXTLINE — keep looking (stacked directives). - return False - - -def collect_unexpectedly_not_failed() -> list[ExpectedFailure]: - return [e for e in expected_registry.values() if not e.hit] - - -# ================================================================================================== -# Utilities -# ================================================================================================== +class ExpectedTracker: + """Tracks ``%EXPECTCHECKNEXTLINE`` directives and whether they were hit.""" + + def __init__(self) -> None: + self._registry: dict[tuple[str, int], ExpectedFailure] = {} + + def register(self, file: str, comment_line: int, check_id: str) -> None: + key = (file, comment_line) + if key not in self._registry: + self._registry[key] = ExpectedFailure(file, comment_line, check_id) + + def consume(self, file: str, failure_line: int, check_id: str) -> bool: + """ + Consumes a possibly expected failure with the specified location and `check_id`. + Returns `true` if the failure was expected, + in which case the expected failure is considered "hit". + """ + # Walk backwards through *consecutive* %EXPECTCHECKNEXTLINE directive only. + # A blank line (or any other content) breaks the chain: + # EXPECTCHECKNEXTLINE always refers to the immediately following line. + for offset in range(1, min(failure_line, 20) + 1): + entry = self._registry.get((file, failure_line - offset)) + if entry is None: + return False + if entry.check_id == check_id: + entry.hit = True + return True + # A different EXPECTCHECKNEXTLINE -> keep looking (stacked directives). + return False + + def collect_unhit(self) -> list[ExpectedFailure]: + return [e for e in self._registry.values() if not e.hit] COMMENT_PATTERN = re.compile(r"^\s*%") @@ -192,9 +182,6 @@ def find_env_ranges( yield stack.pop(), idx + 1 -unexpected_count = 0 - - def emit_check_failure( check: Check, file: str, @@ -207,10 +194,9 @@ def emit_check_failure( Prints a failure immediately (unless consumed by an expected-failure marker). Line numbers and columns follow the same convention as `Failure`. """ - global unexpected_count - if consume_expected(file, line, check.uid): + if check.expected_tracker.consume(file, line, check.uid): return - unexpected_count += 1 + check.failure_count += 1 fail = Failure( file=file, line=line, @@ -222,7 +208,7 @@ def emit_check_failure( # Reading the file from scratch is very slow, # but we don't care because this is the unhappy path anyway, # and we usually don't expect failures anyway. - file_path = file_locations[file] + file_path = check.file_locations[file] lines = read_file(file_path) print(format_failure(fail, lines), file=sys.stderr) print(file=sys.stderr) @@ -234,9 +220,22 @@ def emit_check_failure( class Check(ABC): - """Base class for all checks.""" + """Base class for all checks. + + The following attributes are late-initialized by `run_checks` + before any file processing begins: + """ uid: str + """Unique identifier for this check.""" + file_locations: dict[str, Path] + """Mapping from relative file path to absolute `Path`.""" + expected_tracker: ExpectedTracker + """Shared registry of `%EXPECTCHECKNEXTLINE` directives.""" + failure_count: int + """Number of unexpected failures emitted by this check.""" + source_dir: Path + """Project TeX source directory.""" def __init__(self, uid: str): self.uid = uid @@ -708,7 +707,7 @@ def __init__(self, check_id: str): super().__init__(check_id) def end_checks(self) -> None: - for dot_file in sorted(source_dir.glob("*.dot")): + for dot_file in sorted(self.source_dir.glob("*.dot")): pdf_file = dot_file.with_suffix(".pdf") if ( pdf_file.exists() @@ -1213,15 +1212,24 @@ def end_checks(self) -> None: NO_CHECK_PATTERN = re.compile(r"^\s*%NOCHECK(BEGIN|END|NEXTLINE)(?:\((\S*)\))?\s*$") -def run_checks(tex_files: list[Path]) -> int: - """Run all registered checks. Returns the number of unexpected failures.""" - global unexpected_count - unexpected_count = 0 - file_locations.clear() - +def run_checks( + tex_files: list[Path], + expected_tracker: ExpectedTracker, + source_dir: Path, +) -> tuple[int, dict[str, Path]]: + """Run all registered checks. + Returns ``(unexpected_failures, file_locations)``. + """ + file_locations: dict[str, Path] = {} for fp in tex_files: file_locations[os.path.relpath(fp)] = fp + for c in CHECKS: + c.file_locations = file_locations + c.expected_tracker = expected_tracker + c.failure_count = 0 + c.source_dir = source_dir + all_ids = {c.uid for c in CHECKS if c.uid} for file_path in tex_files: @@ -1279,10 +1287,13 @@ def run_checks(tex_files: list[Path]) -> int: for c in CHECKS: c.end_checks() - return unexpected_count + return sum(c.failure_count for c in CHECKS), file_locations -def parse_expected_from_files(tex_files: list[Path]) -> None: +def parse_expected_from_files( + tex_files: list[Path], + expected_tracker: ExpectedTracker, +) -> None: """Pre-scan ``.tex`` files for ``%EXPECTCHECKNEXTLINE(id)`` directives. The directive must appear alone on its line; *id* is the check that is @@ -1295,7 +1306,9 @@ def parse_expected_from_files(tex_files: list[Path]) -> None: for idx in range(len(lines) - 1): m = EXPECT_CHECK_PATTERN.match(lines[idx]) if m: - register_expected(os.path.relpath(file_path), idx, m.group(1).strip()) + expected_tracker.register( + os.path.relpath(file_path), idx, m.group(1).strip() + ) def collect_tex_files_recursively(root_paths: list[Path]) -> list[Path]: @@ -1336,7 +1349,6 @@ def main() -> None: ) args = parser.parse_args() - global source_dir project_root = find_project_root() source_dir = ( project_root / "source" if (project_root / "source").is_dir() else project_root @@ -1351,10 +1363,11 @@ def main() -> None: print("error: no .tex files found", file=sys.stderr) sys.exit(1) - parse_expected_from_files(tex_files) - num_failures = run_checks(tex_files) + expected_tracker = ExpectedTracker() + parse_expected_from_files(tex_files, expected_tracker) + num_failures, file_locations = run_checks(tex_files, expected_tracker, source_dir) - unhit = collect_unexpectedly_not_failed() + unhit = expected_tracker.collect_unhit() num_failures += len(unhit) for entry in unhit: fp = file_locations.get(entry.file) From 33b9379c17fd09dcaf21857da1abc061570a7918 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 10:35:50 +0200 Subject: [PATCH 20/24] Simplify read_file --- tools/check-source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 4090ca02b1..5501e13b71 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -130,8 +130,7 @@ def make_alt_pattern(items: list[str]) -> str: def read_file(path: Path) -> list[str]: try: - with open(path, encoding="utf-8", errors="replace") as f: - return f.read().splitlines() + return path.read_text(encoding="utf-8", errors="replace").split(os.linesep) except OSError: return [] From 147575ecb748aef62d970c9d4bf010fc3f7c4718 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 10:39:32 +0200 Subject: [PATCH 21/24] Eliminate uses for os.path.relpath --- tools/check-source.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 5501e13b71..807f59af21 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -263,7 +263,7 @@ def fail( """Report a failure at `self.file_path`.""" emit_check_failure( self, - os.path.relpath(self.file_path), + str(self.file_path.relative_to(Path.cwd())), line, column_start, column_end, @@ -714,7 +714,7 @@ def end_checks(self) -> None: ): emit_check_failure( self, - os.path.relpath(dot_file), + str(dot_file.relative_to(Path.cwd())), 0, 0, 0, @@ -873,7 +873,7 @@ def check_line(self, line_num: int, line: str) -> None: continue self.used[name].append( ( - os.path.relpath(self.file_path), + str(self.file_path.relative_to(Path.cwd())), line_num, m.start(1), m.end(1), @@ -923,7 +923,12 @@ def check_line(self, line_num: int, line: str) -> None: for m in self.REF_IREF_PATTERN.finditer(line): for usage in m.group(1).split(","): self.used[usage.strip()].append( - (os.path.relpath(self.file_path), line_num, m.start(1), m.end(1)) + ( + str(self.file_path.relative_to(Path.cwd())), + line_num, + m.start(1), + m.end(1), + ) ) def end_checks(self) -> None: @@ -1221,7 +1226,7 @@ def run_checks( """ file_locations: dict[str, Path] = {} for fp in tex_files: - file_locations[os.path.relpath(fp)] = fp + file_locations[str(fp.relative_to(Path.cwd()))] = fp for c in CHECKS: c.file_locations = file_locations @@ -1306,7 +1311,7 @@ def parse_expected_from_files( m = EXPECT_CHECK_PATTERN.match(lines[idx]) if m: expected_tracker.register( - os.path.relpath(file_path), idx, m.group(1).strip() + str(file_path.relative_to(Path.cwd())), idx, m.group(1).strip() ) From 0cebc60a25fb843e20420f79bceb50bed123c0fb Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 10:45:59 +0200 Subject: [PATCH 22/24] Clean up NOCHECK directive handling in run_checks --- tools/check-source.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 807f59af21..15dc7af92e 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -1251,32 +1251,27 @@ def run_checks( # Per-check next-line skips: check_id → set of line numbers. skip_next: dict[str, set[int]] = defaultdict(set) + def _resolve_ids(cid: str) -> set[str]: + if cid == "*": + return set(all_ids) + return {a for a in all_ids if fnmatch.fnmatch(a, cid)} + for idx, line in enumerate(lines): line_num = idx - # Process %NOCHECK… directives. + # Process %NOCHECK... directives. m = NO_CHECK_PATTERN.match(line) if m: directive = m.group(1) # BEGIN, END, or NEXTLINE cid = m.group(2) or "*" + matched = _resolve_ids(cid) if directive == "BEGIN": - if cid == "*": - active.clear() - else: - active = {a for a in active if not fnmatch.fnmatch(a, cid)} + active -= matched elif directive == "END": - if cid == "*": - active = set(all_ids) - else: - active |= {a for a in all_ids if fnmatch.fnmatch(a, cid)} + active |= matched elif directive == "NEXTLINE": - if cid == "*": - for c in all_ids: - skip_next[c].add(line_num + 1) - else: - matched = {a for a in all_ids if fnmatch.fnmatch(a, cid)} - for c in matched: - skip_next[c].add(line_num + 1) + for c in matched: + skip_next[c].add(line_num + 1) # Call check_line only on active checks (not skipped). for c in CHECKS: From 7a038709d45d9af12f2d7944b72953108e6e3aed Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 10:50:01 +0200 Subject: [PATCH 23/24] Drop ABC --- tools/check-source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 15dc7af92e..2f65629644 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -8,7 +8,6 @@ import re import sys import unicodedata -from abc import ABC from collections import defaultdict from dataclasses import dataclass from enum import Enum @@ -218,7 +217,7 @@ def emit_check_failure( # ================================================================================================== -class Check(ABC): +class Check: """Base class for all checks. The following attributes are late-initialized by `run_checks` From 9460b52d26ea004e4ce6d867f4238ecd8a951402 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Sat, 25 Jul 2026 10:59:33 +0200 Subject: [PATCH 24/24] Add walkUp=True argument to allow running from source/ --- tools/check-source.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/check-source.py b/tools/check-source.py index 2f65629644..a988e47f6a 100644 --- a/tools/check-source.py +++ b/tools/check-source.py @@ -262,7 +262,7 @@ def fail( """Report a failure at `self.file_path`.""" emit_check_failure( self, - str(self.file_path.relative_to(Path.cwd())), + str(self.file_path.relative_to(Path.cwd(), walk_up=True)), line, column_start, column_end, @@ -713,7 +713,7 @@ def end_checks(self) -> None: ): emit_check_failure( self, - str(dot_file.relative_to(Path.cwd())), + str(dot_file.relative_to(Path.cwd(), walk_up=True)), 0, 0, 0, @@ -872,7 +872,7 @@ def check_line(self, line_num: int, line: str) -> None: continue self.used[name].append( ( - str(self.file_path.relative_to(Path.cwd())), + str(self.file_path.relative_to(Path.cwd(), walk_up=True)), line_num, m.start(1), m.end(1), @@ -923,7 +923,7 @@ def check_line(self, line_num: int, line: str) -> None: for usage in m.group(1).split(","): self.used[usage.strip()].append( ( - str(self.file_path.relative_to(Path.cwd())), + str(self.file_path.relative_to(Path.cwd(), walk_up=True)), line_num, m.start(1), m.end(1), @@ -1225,7 +1225,7 @@ def run_checks( """ file_locations: dict[str, Path] = {} for fp in tex_files: - file_locations[str(fp.relative_to(Path.cwd()))] = fp + file_locations[str(fp.relative_to(Path.cwd(), walk_up=True))] = fp for c in CHECKS: c.file_locations = file_locations @@ -1305,7 +1305,9 @@ def parse_expected_from_files( m = EXPECT_CHECK_PATTERN.match(lines[idx]) if m: expected_tracker.register( - str(file_path.relative_to(Path.cwd())), idx, m.group(1).strip() + str(file_path.relative_to(Path.cwd(), walk_up=True)), + idx, + m.group(1).strip(), )