texlive[74271] branches/branch2024.final/Master/texmf-dist: expltools

commits+karl at tug.org commits+karl at tug.org
Mon Feb 24 21:32:09 CET 2025


Revision: 74271
          https://tug.org/svn/texlive?view=revision&revision=74271
Author:   karl
Date:     2025-02-24 21:32:08 +0100 (Mon, 24 Feb 2025)
Log Message:
-----------
expltools (branch) (24feb25)

Modified Paths:
--------------
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/CHANGES.md
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/README.md
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/markdownthemewitiko_expltools_explcheck_warnings-and-errors.sty
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/project-proposal.pdf
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/s103.tex
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/w200.tex
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/warnings-and-errors-02-lexical-analysis.md
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/warnings-and-errors.pdf
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-cli.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-config.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-format.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-issues.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-lexical-analysis.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-obsolete.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-parsers.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-preprocessing.lua
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-utils.lua

Added Paths:
-----------
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/explcheck-config.toml
    branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-ranges.lua

Removed Paths:
-------------
    branches/branch2024.final/Master/texmf-dist/doc/support/expltools/e203.tex

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/CHANGES.md
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/CHANGES.md	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/CHANGES.md	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,5 +1,132 @@
 # Changes
 
+## expltools 2025-02-24
+
+### explcheck v0.7.0
+
+#### Development
+
+- Generate a static web site for the exploration of issues in all expl3 files
+  from TeX Live. (discussed with @norbusan and @koppor in #28 and #32,
+  implemented in <https://github.com/koppor/explcheck-issues> by @koppor)
+
+  The web side is available here: <https://koppor.github.io/explcheck-issues/>.
+
+- Add support for config file sections `[filename."…"]` for specifying
+  file-specific configuration. (#32, #57, #62)
+
+  For example, here is how you might configure a file `expl3-code.tex`:
+
+  ``` toml
+  [filename."expl3-code.tex"]
+  expl3_detection_strategy = "always"
+  ignored_issues = ["w200", "w202", "e209"]
+  max_line_length = 140
+  ```
+
+- Pre-configure well-known files from current TeX Live with more than 100 error
+  detections in <https://koppor.github.io/explcheck-issues/>. (#32, #57, #62)
+
+- Add command-line option `--error-format` and Lua option `error_format`.
+  (discussed with @koppor in koppor/errorformat-to-html#2, added in #40,
+  5034639, and #43)
+
+  This allows users to specify Vim's quickfix errorformat used for the
+  machine-readable output when the command-line option `--porcelain` or the Lua
+  option `porcelain` is enabled.
+
+- Add command-line option `--expl3-detection-strategy` and Lua option
+  `expl3_detection_strategy`. (drafted and discussed with @koppor in #38,
+  added in #49)
+
+- Add command-line option `--make-at-letter` and Lua option `make_at_letter`.
+  (discussed with @zepinglee in #30 and #36, added in #61)
+
+  These options determine how the at sign (`@`) should be tokenized. The
+  default value `"auto"` automatically determines the category code based on
+  context cues.
+
+#### Fixes
+
+- Prevent false positive E102 (Unknown argument specifiers) detections for
+  control sequences with multiple colons (`::`). (#62)
+
+- Ensure that whole files are considered to be in expl3 when the Lua option
+  `expl3_detection_strategy` is set to `"always"`, even when the files contain
+  standard delimiters `\ProvidesExpl*`. (#62)
+
+  This also prevents false positive E102 (expl3 material in non-expl3 parts)
+  detections.
+
+- Only report warning S103 (Line too long) in expl3 parts. (#38, #49)
+
+- In machine-readable output, report the line and column number 1 for file-wide
+  issues. (reported by @koppor in #39, fixed in #40)
+
+- Exclude comments from maximum line length checks. (reported by @muzimuzhi in
+  #27, fixed in #43, #58, and #59)
+
+  This includes spaces before the comments.
+
+- Always accept both lower- and upper-case issue identifiers. (reported by
+  @muzimuzhi in #26, fixed in #44)
+
+  This includes Lua options and configuration files, in addition to
+  command-line options and inline TeX comments.
+
+- Exclude "weird" argument specifiers (`:w`) from warning W200. (reported by
+  @muzimuzhi in #25, fixed in #45)
+
+- Remove error E203. (reported by @koppor in #53, fixed in #54)
+
+- Fix two instances of explcheck crashing while processing input files.
+  (reported by @koppor in #31, fixed in #52 and #59)
+
+- Do not recognize `@` as a part of an expl3 control sequence.
+  (reported by @zepinglee in #30 and #37, fixed in #60)
+
+  This prevents warnings S205 and S206 for LaTeX2e control sequence
+  (re)definitions.
+
+#### Deprecation
+
+- Deprecate the command-line option `--expect-expl3-everywhere` and remove the
+  Lua option `expect_expl3_everywhere`. (#49)
+
+  Use the command-line option `--expl3-detection-strategy=always` or the
+  corresponding Lua option `expl3_detection_stragegy = "always"` instead.
+
+- Deprecate the default config file section `[options]`. (#62)
+
+  Rename the section to `[defaults]` instead.
+
+#### Documentation
+
+- Add SPDX license identifier to `README.md`. (added by @koppor in #50)
+
+- Link a list of all currently supported issues from `README.md`.
+  (added by @koppor in #51)
+
+- Link <https://koppor.github.io/explcheck-issues/> from `README.md`.
+  (#28, #32, b774ba77)
+
+#### Continuous integration
+
+- Continuously run explcheck on all packages in historical TeX Live Docker
+  images. (suggested by @hansonchar in #28 and #31, added in #52 and #56)
+
+- Use ShellCheck to check code style of Bash scripts. (#61)
+
+#### Housekeeping
+
+- Make off-by-one errors less likely when working with byte ranges.
+  (#47, #48, 13ebfc6e, a0923d06)
+
+#### Artwork
+
+- Add artwork by https://www.quickcartoons.com/ to directory `artwork/`.
+  (566769b)
+
 ## expltools 2025-01-20
 
 ### explcheck v0.6.1
@@ -23,12 +150,12 @@
   For example, the following configuration file would increase the maximum line
   length before the warning S103 (Line too long) is produced from 80 to 120
   characters and also disable the warnings W100 (No standard delimiters) and
-  S206 (Missing stylistic whitespaces):
+  S204 (Missing stylistic whitespaces):
 
   ``` toml
   [options]
   max_line_length = 120
-  ignored_issues = ["w100", "s206"]
+  ignored_issues = ["w100", "s204"]
   ```
 
 #### Fixes
@@ -54,16 +181,16 @@
   TeX comments. (#23)
 
   For example, a comment `% noqa` will ignore any issues on the current line,
-  whereas a comment `% noqa: W100, S206` will ignore the file-wide warning W100
-  (No standard delimiters) and the warning S206 (Malformed variable or constant
-  name) on the current line.
+  whereas a comment `% noqa: W100, S204` will ignore the file-wide warning W100
+  (No standard delimiters) and the warning S204 (Missing stylistic whitespaces)
+  on the current line.
 
 - Add command-line option `--ignored-issues` and Lua option `ignored_issues`
   for ignoring issues. (#23)
 
-  For example, `--ignored-issues=w100,s206` will ignore the file-wide warning
-  W100 (No standard delimiters) and all warnings S206 (Malformed variable or
-  constant name).
+  For example, `--ignored-issues=w100,s204` will ignore the file-wide warning
+  W100 (No standard delimiters) and all warnings S204 (Missing stylistic
+  whitespaces).
 
 #### Fixes
 

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/README.md
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/README.md	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/README.md	2025-02-24 20:32:08 UTC (rev 74271)
@@ -14,6 +14,7 @@
 
 5. [Frank Mittelbach in Brno, first public release of explcheck, and expl3 usage statistics][8] from Dec 5, 2024
 6. [A flurry of releases, CSTUG talk, and what's next][9] from December 19, 2024
+7. [Lexical analysis and a public website listing issues in current TeX Live][12] from February 24, 2025
 
 In the future, this repository may also contain the code of other useful development tools for expl3 programmers, such as a command-line utility similar to `grep` that will ignore whitespaces and newlines as well as other tools.
 
@@ -26,16 +27,21 @@
  [7]: https://tug.org/tc/devfund/documents/2024-09-expltools.pdf
  [8]: https://witiko.github.io/Expl3-Linter-5/
  [9]: https://witiko.github.io/Expl3-Linter-6/
+ [10]: https://github.com/witiko/expltools/releases/download/latest/warnings-and-errors.pdf
+ [11]: https://koppor.github.io/explcheck-issues/
+ [12]: https://witiko.github.io/Expl3-Linter-7/
 
 ## Usage
 
-You may use the tool from the command line as follows:
+You may browse the results of the tool on all packages in current TeX Live [here][11].
 
+You may also use the tool from the command line as follows:
+
 ```
 $ explcheck [options] [.tex, .cls, and .sty files]
 ```
 
-You may also use the tool from your own Lua code by importing the corresponding files `explcheck-*.lua`.
+Furthermore, you may also use the tool from your own Lua code by importing the corresponding files `explcheck-*.lua`.
 For example, here is Lua code that applies the preprocessing step to the code from a file named `code.tex`:
 
 ``` lua
@@ -49,7 +55,7 @@
   kpse.set_program_name("texlua", "explcheck")
 end
 
--- Apply the preprocessing step to a file "code.tex".
+-- Process file "code.tex" and print warnings and errors.
 local filename = "code.tex"
 local issues = new_issues()
 
@@ -57,8 +63,8 @@
 local content = assert(file:read("*a"))
 assert(file:close())
 
-local _, expl_ranges = preprocessing(issues, content)
-lexical_analysis(issues, content, expl_ranges)
+local _, expl_ranges = preprocessing(issues, filename, content)
+lexical_analysis(issues, filename, content, expl_ranges)
 
 print(
   "There were " .. #issues.warnings .. " warnings, "
@@ -67,7 +73,7 @@
 )
 ```
 
-You may also use the tool from continuous integration workflows using the Docker image `ghcr.io/witiko/expltools/explcheck`.
+Next, you may also use the tool from continuous integration workflows using the Docker image `ghcr.io/witiko/expltools/explcheck`.
 For example, here is a GitHub Actions workflow file that applies the tool to all .tex files in a Git repository:
 
 ``` yaml
@@ -88,10 +94,10 @@
 
 You may configure the tool using command-line options.
 
-For example, the following command-line options would increase the maximum line length before the warning S103 (Line too long) is produced from 80 to 120 characters and also disable the warnings W100 (No standard delimiters) and S206 (Missing stylistic whitespaces).
+For example, the following command-line options would increase the maximum line length before the warning S103 (Line too long) is produced from 80 to 120 characters and also disable the warnings W100 (No standard delimiters) and S204 (Missing stylistic whitespaces).
 
 ``` sh
-$ explcheck --max-line-length=120 --ignored-issues=w100,s206 *.tex
+$ explcheck --max-line-length=120 --ignored-issues=w100,S204 *.tex
 ```
 
 Use the command `explcheck --help` to list the available options.
@@ -100,9 +106,9 @@
 For example, here is a configuration file that applies the same configuration as the above command-line options:
 
 ``` toml
-[options]
+[defaults]
 max_line_length = 120
-ignored_issues = ["w100", "s206"]
+ignored_issues = ["w100", "S204"]
 ```
 
 You may also configure the tool from within your Lua code.
@@ -112,7 +118,7 @@
 local options = { max_line_length = 120 }
 
 issues:ignore("w100")
-issues:ignore("s206")
+issues:ignore("S204")
 
 local _, expl_ranges = preprocessing(issues, content, options)
 lexical_analysis(issues, content, expl_ranges, options)
@@ -122,8 +128,10 @@
 To ignore them in just some of your expl3 code, you may use TeX comments.
 
 For example, a comment `% noqa` will ignore any issues on the current line.
-As another example, a comment `% noqa: w100, s206` will ignore the file-wide warning W100 and also the warning S206 on the current line.
+As another example, a comment `% noqa: w100, S204` will ignore the file-wide warning W100 and also the warning S204 on the current line.
 
+A list of all currently supported issues is available [here][10].
+
 ## Notes to distributors
 
 You can prepare the expltools bundle for distribution with the following two commands:
@@ -136,7 +144,12 @@
 ## Authors
 
 - Vít Starý Novotný (<witiko at mail.muni.cz>)
+- Oliver Kopp (<kopp.dev at gmail.com>)
 
 ## License
 
 This material is dual-licensed under GNU GPL 2.0 or later and LPPL 1.3c or later.
+
+``` yaml
+SPDX-License-Identifier: GPL-2.0-or-later OR LPPL-1.3c
+```

Deleted: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/e203.tex
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/e203.tex	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/e203.tex	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,5 +0,0 @@
-\msg_log:n  % error on this line
-  {
-    Foo~bar~
-    \c_one_thousand  % error on this line
-  }

Added: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/explcheck-config.toml
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/explcheck-config.toml	                        (rev 0)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/explcheck-config.toml	2025-02-24 20:32:08 UTC (rev 74271)
@@ -0,0 +1,116 @@
+[defaults]
+expl3_detection_strategy = "auto"
+error_format = "%f:%l:%c: %t%n %m"
+ignored_issues = []
+make_at_letter = "auto"
+max_line_length = 80
+porcelain = false
+warnings_are_errors = false
+
+[filename."expl3-code.tex"]
+expl3_detection_strategy = "always"
+ignored_issues = ["w200", "w202", "e209"]
+max_line_length = 140
+
+[filename."tex4ht.sty"]
+expl3_detection_strategy = "precision"
+
+[filename."nicematrix.sty"]
+expl3_detection_strategy = "always"
+
+[filename."xintexpr.sty"]
+expl3_detection_strategy = "precision"
+
+[filename."xinttools.sty"]
+expl3_detection_strategy = "precision"
+
+[filename."xCJK2uni.sty"]
+expl3_detection_strategy = "always"
+ignored_issues = ["e209"]
+
+[filename."stex.sty"]
+expl3_detection_strategy = "always"
+
+[filename."spath3.sty"]
+expl3_detection_strategy = "always"
+
+[filename."xparse-generic.tex"]
+expl3_detection_strategy = "always"
+ignored_issues = ["e209", "w200"]
+max_line_length = 90
+
+[filename."functional.sty"]
+expl3_detection_strategy = "always"
+max_line_length = 90
+
+[filename."hobby.code.tex"]
+expl3_detection_strategy = "always"
+max_line_length = 90
+
+[filename."gtl.sty"]
+expl3_detection_strategy = "always"
+
+[filename."piton.sty"]
+expl3_detection_strategy = "always"
+ignored_issues = ["e208"]
+max_line_length = 90
+
+[filename."statistics.sty"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."chemmacros.sty"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."problem.sty"]
+expl3_detection_strategy = "always"
+max_line_length = 90
+
+[filename."xunicode-addon.sty"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."interlinear.sty"]
+expl3_detection_strategy = "precision"
+
+[filename."skrapport.cls"]
+expl3_detection_strategy = "always"
+max_line_length = 150
+
+[filename."namedef.sty"]
+expl3_detection_strategy = "always"
+
+[filename."tikzlibrarytikzmark.code.tex"]
+expl3_detection_strategy = "always"
+
+[filename."ctexart.cls"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."ctexrep.cls"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."ctexbook.cls"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."ctexpatch.sty"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."ctexbeamer.cls"]
+expl3_detection_strategy = "always"
+max_line_length = 100
+
+[filename."grading-scheme.sty"]
+ignored_issues = ["e201"]
+
+[filename."scontents.tex"]
+expl3_detection_strategy = "always"
+max_line_length = 90
+
+[filename."hwexam.sty"]
+expl3_detection_strategy = "always"
+max_line_length = 110

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/markdownthemewitiko_expltools_explcheck_warnings-and-errors.sty
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/markdownthemewitiko_expltools_explcheck_warnings-and-errors.sty	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/markdownthemewitiko_expltools_explcheck_warnings-and-errors.sty	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,8 +1,8 @@
 \NeedsTeXFormat{LaTeX2e}
 \ProvidesExplPackage
   {markdownthemewitiko_expltools_explcheck_warnings-and-errors}%
-  {2024-11-29}%
-  {0.0.3}%
+  {2025-02-14}%
+  {0.0.4}%
   {Snippets for typesetting the documentation of the warnings and errors for %
    the expl3 analysis tool}
 \int_new:N
@@ -151,6 +151,29 @@
                           \l__expltools_explcheck_current_label_tl
                       }
                   }
+                \str_if_eq:nnT
+                  { ##1 }
+                  { removed }
+                  {
+                    % Remove the rest of this section from the output.
+                    \markdownSetup
+                      {
+                        renderers = {
+                          heading(Two|Tree) = {
+                            \__expltools_explcheck_increment_counters:
+                          },
+                          headerAttributeContextEnd += {
+                            \cs_set:Npn
+                              \next
+                              ########1 \markdownRendererSectionEnd
+                              {
+                                \markdownRendererSectionEnd
+                              }
+                            \next
+                          },
+                        },
+                      }
+                  }
               },
               attributeClassName = {
                 \tl_set:Nx

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/project-proposal.pdf
===================================================================
(Binary files differ)

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/s103.tex
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/s103.tex	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/s103.tex	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,2 +1,3 @@
 This line is not very long, because it is 80 characters long, not 81 characters.
-This line is very long, because it is 81 characters long.  % warning on this line
+This line is overly long, because it is 81 characters long excluding the comment.  % warning on this line
+This line is not very long, because it is 80 characters long, comments excluded.  % no warning on this line

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/w200.tex
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/w200.tex	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/w200.tex	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,8 +1,4 @@
-\cs_new:Npn
-  \show_until_if:w  % warning on this line
-  #1 \if^^zw  % warning on this line
-  { \tl_show:n {#1} }
-\show_until_if:^^7  % warning on this line
-  \tex_if:D  % warning on this line
-  \if_charcode:^^77  % warning on this line
-  \if^^3aw  % warning on this line
+\tex_space:D  % warning on this line
+\tex_italiccor^^3aD  % warning on this line
+\tex_hyphen^^zD  % warning on this line
+\tex_let:^^44  % warning on this line

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/warnings-and-errors-02-lexical-analysis.md
===================================================================
--- branches/branch2024.final/Master/texmf-dist/doc/support/expltools/warnings-and-errors-02-lexical-analysis.md	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/doc/support/expltools/warnings-and-errors-02-lexical-analysis.md	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,8 +1,8 @@
 # Lexical analysis
 In the lexical analysis step, the expl3 analysis tool converts the expl3 parts of the input files into a list of `\TeX`{=tex} tokens.
 
-## “Weird” and “Do not use” argument specifiers {.w label=w200}
-Some control sequence tokens correspond to functions with `w` (weird) or `D` (do not use) argument specifiers.
+## “Do not use” argument specifiers {.w label=w200}
+Some control sequence tokens correspond to functions with `D` (do not use) argument specifiers.
 
  /w200.tex
 
@@ -18,7 +18,7 @@
 
  /w202.tex
 
-## Removed control sequences {.e label=e203}
+## Removed control sequences {.e label=e203 removed=2025-02-14}
 Some control sequence tokens correspond to removed expl3 control sequences from `l3obsolete.txt` [@josephwright2024obsolete].
 
  /e203.tex

Modified: branches/branch2024.final/Master/texmf-dist/doc/support/expltools/warnings-and-errors.pdf
===================================================================
(Binary files differ)

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-cli.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-cli.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-cli.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,8 +1,8 @@
 -- A command-line interface for the static analyzer explcheck.
 
-local config = require("explcheck-config")
+local format = require("explcheck-format")
+local get_option = require("explcheck-config")
 local new_issues = require("explcheck-issues")
-local format = require("explcheck-format")
 local utils = require("explcheck-utils")
 
 local preprocessing = require("explcheck-preprocessing")
@@ -26,30 +26,11 @@
   return deduplicated_pathnames
 end
 
--- Convert a pathname of a file to the suffix of the file.
-local function get_suffix(pathname)
-  return pathname:gsub(".*%.", "."):lower()
-end
-
--- Convert a pathname of a file to the base name of the file.
-local function get_basename(pathname)
-  return pathname:gsub(".*[\\/]", "")
-end
-
--- Convert a pathname of a file to the pathname of its parent directory.
-local function get_parent(pathname)
-  if pathname:find("[\\/]") then
-    return pathname:gsub("(.*)[\\/].*", "%1")
-  else
-    return "."
-  end
-end
-
 -- Check that the pathname specifies a file that we can process.
 local function check_pathname(pathname)
-  local suffix = get_suffix(pathname)
+  local suffix = utils.get_suffix(pathname)
   if suffix == ".ins" then
-    local basename = get_basename(pathname)
+    local basename = utils.get_basename(pathname)
     if basename:find(" ") then
       basename = "'" .. basename .. "'"
     end
@@ -59,12 +40,12 @@
       .. 'Use a command such as "luatex ' .. basename .. '" '
       .. "to generate .tex, .cls, and .sty files and process these files instead."
   elseif suffix == ".dtx" then
-    local parent = get_parent(pathname)
+    local parent = utils.get_parent(pathname)
     local basename = "*.ins"
     local has_lfs, lfs = pcall(require, "lfs")
     if has_lfs then
       for candidate_basename in lfs.dir(parent) do
-        local candidate_suffix = get_suffix(candidate_basename)
+        local candidate_suffix = utils.get_suffix(candidate_basename)
         if candidate_suffix == ".ins" then
           basename = candidate_basename
           if basename:find(" ") then
@@ -93,38 +74,43 @@
   end
 
   for pathname_number, pathname in ipairs(pathnames) do
+    local is_ok, error_message = xpcall(function()
 
-    -- Set up the issue registry.
-    local issues = new_issues()
-    for _, issue_identifier in ipairs(utils.get_option(options, "ignored_issues")) do
-      issues:ignore(issue_identifier)
-    end
+      -- Set up the issue registry.
+      local issues = new_issues()
+      for _, issue_identifier in ipairs(get_option("ignored_issues", options, pathname)) do
+        issues:ignore(issue_identifier)
+      end
 
-    -- Load an input file.
-    local file = assert(io.open(pathname, "r"), "Could not open " .. pathname .. " for reading")
-    local content = assert(file:read("*a"))
-    assert(file:close())
+      -- Load an input file.
+      local file = assert(io.open(pathname, "r"), "Could not open " .. pathname .. " for reading")
+      local content = assert(file:read("*a"))
+      assert(file:close())
 
-    -- Run all processing steps.
-    local line_starting_byte_numbers, expl_ranges, tokens  -- luacheck: ignore tokens
+      -- Run all processing steps.
+      local line_starting_byte_numbers, expl_ranges, seems_like_latex_style_file, tokens  -- luacheck: ignore tokens
 
-    line_starting_byte_numbers, expl_ranges = preprocessing(issues, content, options)
+      line_starting_byte_numbers, expl_ranges, seems_like_latex_style_file = preprocessing(issues, pathname, content, options)
 
-    if #issues.errors > 0 then
-      goto continue
-    end
+      if #issues.errors > 0 then
+        goto continue
+      end
 
-    tokens = lexical_analysis(issues, content, expl_ranges, options)
+      tokens = lexical_analysis(issues, pathname, content, expl_ranges, seems_like_latex_style_file, options)
 
-    -- syntactic_analysis(issues)
-    -- semantic_analysis(issues)
-    -- pseudo_flow_analysis(issues)
+      -- syntactic_analysis(issues)
+      -- semantic_analysis(issues)
+      -- pseudo_flow_analysis(issues)
 
-    -- Print warnings and errors.
-    ::continue::
-    num_warnings = num_warnings + #issues.warnings
-    num_errors = num_errors + #issues.errors
-    format.print_results(pathname, issues, line_starting_byte_numbers, pathname_number == #pathnames, options.porcelain)
+      -- Print warnings and errors.
+      ::continue::
+      num_warnings = num_warnings + #issues.warnings
+      num_errors = num_errors + #issues.errors
+      format.print_results(pathname, issues, line_starting_byte_numbers, pathname_number == #pathnames, options)
+    end, debug.traceback)
+    if not is_ok then
+      error("Failed to process " .. pathname .. ": " .. tostring(error_message), 0)
+    end
   end
 
   -- Print a summary.
@@ -134,7 +120,7 @@
 
   if(num_errors > 0) then
     return 1
-  elseif(utils.get_option(options, "warnings_are_errors") and num_warnings > 0) then
+  elseif(get_option("warnings_are_errors", options) and num_warnings > 0) then
     return 2
   else
     return 0
@@ -144,15 +130,33 @@
 local function print_usage()
   print("Usage: " .. arg[0] .. " [OPTIONS] FILENAMES\n")
   print("Run static analysis on expl3 files.\n")
-  local max_line_length = tostring(config.max_line_length)
+  local expl3_detection_strategy = get_option("expl3_detection_strategy")
+  local make_at_letter = tostring(get_option("make_at_letter"))
+  local max_line_length = tostring(get_option("max_line_length"))
   print(
     "Options:\n\n"
-    .. "\t--expect-expl3-everywhere  Expect that the whole files are in expl3, ignoring \\ExplSyntaxOn and Off.\n"
-    .. "\t                           This prevents the error E102 (expl3 material in non-expl3 parts).\n\n"
+    .. "\t--error-format=FORMAT      The Vim's quickfix errorformat used for the output with --porcelain enabled.\n"
+    .. "\t                           The default format is FORMAT=\"" .. get_option("error_format") .. "\".\n\n"
+    .. "\t--expl3-detection-strategy={always|precision|recall|auto}\n\n"
+    .. "\t                           The strategy for detecting expl3 parts of the input files:\n\n"
+    .. '\t                           - "always": Assume that the whole input files are in expl3.\n'
+    .. '\t                           - "precision", "recall", and "auto": Analyze standard delimiters such as \n'
+    .. '\t                             \\ExplSyntaxOn and Off. If no standard delimiters exist, assume either that:\n'
+    .. '\t                               - "precision": No part of the input file is in expl3.\n'
+    .. '\t                               - "recall": The entire input file is in expl3.\n'
+    .. '\t                               - "auto": Use context cues to determine whether no part or the whole input file\n'
+    .. "\t                                 is in expl3.\n\n"
+    .. "\t                           The default setting is --expl3-detection-strategy=" .. expl3_detection_strategy .. ".\n\n"
     .. "\t--ignored-issues=ISSUES    A comma-list of warning and error identifiers that should not be reported.\n\n"
+    .. "\t--make-at-letter[={true|false|auto}]\n\n"
+    .. '\t                           How the at sign ("@") should be tokenized:\n\n'
+    .. '\t                           - empty or "true": Tokenize "@" as a letter (catcode 11), like in LaTeX style files.\n'
+    .. '\t                           - "false": Tokenize "@" as an other character (catcode 12), like in plain TeX.\n'
+    .. '\t                           - "auto": Use context cues to determine the catcode of "@".\n\n'
+    .. "\t                           The default setting is --make-at-letter=" .. make_at_letter .. ".\n\n"
     .. "\t--max-line-length=N        The maximum line length before the warning S103 (Line too long) is produced.\n"
     .. "\t                           The default maximum line length is N=" .. max_line_length .. " characters.\n\n"
-    .. "\t--porcelain, -p            Produce machine-readable output.\n\n"
+    .. "\t--porcelain, -p            Produce machine-readable output. See also --error-format.\n\n"
     .. "\t--warnings-are-errors      Produce a non-zero exit code if any warnings are produced by the analysis.\n"
   )
   print("The options are provisional and may be changed or removed before version 1.0.0.")
@@ -159,7 +163,7 @@
 end
 
 local function print_version()
-  print("explcheck (expltools 2025-01-20) v0.6.1")
+  print("explcheck (expltools 2025-02-24) v0.7.0")
   print("Copyright (c) 2024-2025 Vít Starý Novotný")
   print("Licenses: LPPL 1.3 or later, GNU GPL v2 or later")
 end
@@ -183,13 +187,29 @@
     elseif argument == "--version" or argument == "-v" then
       print_version()
       os.exit(0)
+    elseif argument:sub(1, 15) == "--error-format=" then
+      options.error_format = argument:sub(16)
+    elseif argument:sub(1, 27) == "--expl3-detection-strategy=" then
+      options.expl3_detection_strategy = argument:sub(28)
     elseif argument == "--expect-expl3-everywhere" then
-      options.expect_expl3_everywhere = true
+      -- TODO: Remove `--expect-expl3-everywhere` in v1.0.0.
+      options.expl3_detection_strategy = "always"
     elseif argument:sub(1, 17) == "--ignored-issues=" then
       options.ignored_issues = {}
       for issue_identifier in argument:sub(18):gmatch('[^,]+') do
-        table.insert(options.ignored_issues, issue_identifier:lower())
+        table.insert(options.ignored_issues, issue_identifier)
       end
+    elseif argument == "--make-at-letter" then
+      options.make_at_letter = true
+    elseif argument:sub(1, 17) == "--make-at-letter=" then
+      local make_at_letter = argument:sub(18)
+      if make_at_letter == "true" then
+        options.make_at_letter = true
+      elseif make_at_letter == "false" then
+        options.make_at_letter = false
+      else
+        options.make_at_letter = make_at_letter
+      end
     elseif argument:sub(1, 18) == "--max-line-length=" then
       options.max_line_length = tonumber(argument:sub(19))
     elseif argument == "--porcelain" or argument == "-p" then
@@ -205,6 +225,11 @@
     end
   end
 
+  if #pathnames == 0 then
+    print_usage()
+    os.exit(1)
+  end
+
   -- Deduplicate and check that pathnames specify files that we can process.
   pathnames = deduplicate_pathnames(pathnames)
   for _, pathname in ipairs(pathnames) do

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-config.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-config.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-config.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,16 +1,8 @@
 -- The configuration for the static analyzer explcheck.
 
 local toml = require("explcheck-toml")
+local utils = require("explcheck-utils")
 
--- The default options
-local default_options = {
-  expect_expl3_everywhere = false,
-  max_line_length = 80,
-  porcelain = false,
-  warnings_are_errors = false,
-  ignored_issues = {},
-}
-
 -- Read a TOML file with a user-defined configuration.
 local function read_config_file(pathname)
   local file = io.open(pathname, "r")
@@ -22,15 +14,36 @@
   return toml.parse(content)
 end
 
--- The user-defined configuration
-local config_file = read_config_file(".explcheckrc")
+-- Load the default configuration from the pre-installed config file `explcheck-config.toml`.
+local default_config_pathname = string.sub(debug.getinfo(1).source, 2, (#".lua" + 1) * -1) .. ".toml"
+local default_config = read_config_file(default_config_pathname)
 
--- The user-defined options, falling back on the default options
-local options = {}
-for _, template_options in ipairs({default_options, config_file.options or {}}) do
-  for key, value in pairs(template_options) do
-    options[key] = value
+-- Load the user-defined configuration from the config file .explcheckrc (if it exists).
+local user_config = read_config_file(".explcheckrc")
+
+-- Get the value of an option.
+local function get_option(key, options, pathname)
+  -- If a table of options is provided and the option is specified there, use it.
+  if options ~= nil and options[key] ~= nil then
+    return options[key]
   end
+  -- Otherwise, try the user-defined configuration first, if it exists, and then the default configuration.
+  for _, config in ipairs({user_config, default_config}) do
+    if pathname ~= nil then
+      -- If a a pathname is provided and the current configuration specifies the option for the provided filename, use it.
+      local filename = utils.get_basename(pathname)
+      if config["filename"] and config["filename"][filename] ~= nil and config["filename"][filename][key] ~= nil then
+        return config["filename"][filename][key]
+      end
+    end
+    -- If the current configuration specifies the option in the defaults, use it.
+    for _, section in ipairs({"defaults", "options"}) do  -- TODO: Remove `[options]` in v1.0.0.
+      if config[section] ~= nil and config[section][key] ~= nil then
+        return config[section][key]
+      end
+    end
+  end
+  error('Failed to get a value for option "' .. key .. '"')
 end
 
-return options
+return get_option

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-format.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-format.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-format.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,5 +1,6 @@
 -- Formatting for the command-line interface of the static analyzer explcheck.
 
+local get_option = require("explcheck-config")
 local utils = require("explcheck-utils")
 
 -- Transform a singular into plural if the count is zero or greater than two.
@@ -69,10 +70,11 @@
 end
 
 -- Print the results of analyzing a file.
-local function print_results(pathname, issues, line_starting_byte_numbers, is_last_file, porcelain)
+local function print_results(pathname, issues, line_starting_byte_numbers, is_last_file, options)
   -- Display an overview.
   local all_issues = {}
   local status
+  local porcelain = get_option('porcelain', options, pathname)
   if(#issues.errors > 0) then
     status = (
       colorize(
@@ -83,7 +85,7 @@
         ), 1, 31
       )
     )
-    table.insert(all_issues, {issues.errors, "error: "})
+    table.insert(all_issues, issues.errors)
     if(#issues.warnings > 0) then
       status = (
         status
@@ -96,7 +98,7 @@
           ), 1, 33
         )
       )
-      table.insert(all_issues, {issues.warnings, "warning: "})
+      table.insert(all_issues, issues.warnings)
     end
   elseif(#issues.warnings > 0) then
     status = colorize(
@@ -106,7 +108,7 @@
         .. pluralize("warning", #issues.warnings)
       ), 1, 33
     )
-    table.insert(all_issues, {issues.warnings, "warning: "})
+    table.insert(all_issues, issues.warnings)
   else
     status = colorize("OK", 1, 32)
   end
@@ -145,8 +147,7 @@
 
   -- Display the errors, followed by warnings.
   if #all_issues > 0 then
-    for _, warnings_or_errors_and_porcelain_prefix in ipairs(all_issues) do
-      local warnings_or_errors, porcelain_prefix = table.unpack(warnings_or_errors_and_porcelain_prefix)
+    for _, warnings_or_errors in ipairs(all_issues) do
       if not porcelain then
         print()
       end
@@ -155,11 +156,14 @@
         local code = issue[1]
         local message = issue[2]
         local range = issue[3]
-        local position = ":"
+        local start_line_number, start_column_number = 1, 1
+        local end_line_number, end_column_number = 1, 1
         if range ~= nil then
-          local line_number, column_number = utils.convert_byte_to_line_and_column(line_starting_byte_numbers, range[1])
-          position = position .. tostring(line_number) .. ":" .. tostring(column_number) .. ":"
+          start_line_number, start_column_number = utils.convert_byte_to_line_and_column(line_starting_byte_numbers, range:start())
+          end_line_number, end_column_number = utils.convert_byte_to_line_and_column(line_starting_byte_numbers, range:stop())
+          end_column_number = end_column_number
         end
+        local position = ":" .. tostring(start_line_number) .. ":" .. tostring(start_column_number) .. ":"
         local max_line_length = 88
         local reserved_position_length = 10
         local reserved_suffix_length = 30
@@ -198,7 +202,32 @@
           )
           io.write("\n" .. line)
         else
-          print(pathname .. position .. " " .. porcelain_prefix .. suffix)
+          local line = get_option('error_format', options, pathname)
+
+          local function replace_item(item)
+            if item == '%%' then
+              return '%'
+            elseif item == '%c' then
+              return tostring(start_column_number)
+            elseif item == '%e' then
+              return tostring(end_line_number)
+            elseif item == '%f' then
+              return pathname
+            elseif item == '%k' then
+              return tostring(end_column_number)
+            elseif item == '%l' then
+              return tostring(start_line_number)
+            elseif item == '%m' then
+              return message
+            elseif item == '%n' then
+              return code:sub(2)
+            elseif item == '%t' then
+              return code:sub(1, 1):lower()
+            end
+          end
+
+          line = line:gsub("%%[%%cefklmnt]", replace_item)
+          print(line)
         end
       end
     end

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-issues.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-issues.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-issues.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -3,9 +3,11 @@
 local Issues = {}
 
 function Issues.new(cls)
+  -- Instantiate the class.
   local new_object = {}
   setmetatable(new_object, cls)
   cls.__index = cls
+  -- Initialize the class.
   new_object.errors = {}
   new_object.warnings = {}
   new_object.ignored_issues = {}
@@ -12,8 +14,14 @@
   return new_object
 end
 
+-- Normalize an issue identifier.
+function Issues._normalize_identifier(identifier)
+  return identifier:lower()
+end
+
 -- Convert an issue identifier to either a table of warnings or a table of errors.
 function Issues:_get_issue_table(identifier)
+  identifier = self._normalize_identifier(identifier)
   local prefix = identifier:sub(1, 1)
   if prefix == "s" or prefix == "w" then
     return self.warnings
@@ -25,14 +33,10 @@
 end
 
 -- Add an issue to the table of issues.
-function Issues:add(identifier, message, range_start, range_end)
+function Issues:add(identifier, message, range)
+  identifier = self._normalize_identifier(identifier)
+
   -- Construct the issue.
-  local range
-  if range_start == nil then
-    range = nil
-  else
-    range = {range_start, range_end}
-  end
   local issue = {identifier, message, range}
 
   -- Determine if the issue should be ignored.
@@ -44,18 +48,19 @@
 
   -- Add the issue to the table of issues.
   local issue_table = self:_get_issue_table(identifier)
-  table.insert(issue_table, {identifier, message, range})
+  table.insert(issue_table, issue)
 end
 
 -- Prevent issues from being present in the table of issues.
-function Issues:ignore(identifier, range_start, range_end)
+function Issues:ignore(identifier, range)
+  identifier = self._normalize_identifier(identifier)
+
   -- Determine which issues should be ignored.
   local function match_issue_range(issue_range)
-    local issue_range_start, issue_range_end = table.unpack(issue_range)
     return (
-      issue_range_start >= range_start and issue_range_start <= range_end  -- issue starts within the range
-      or issue_range_start <= range_start and issue_range_end >= range_end  -- issue is in the middle of the range
-      or issue_range_end >= range_start and issue_range_end <= range_end  -- issue ends within the range
+      issue_range:start() >= range:start() and issue_range:start() <= range:stop()  -- issue starts within range
+      or issue_range:start() <= range:start() and issue_range:stop() >= range:stop()  -- issue is in middle of range
+      or issue_range:stop() >= range:start() and issue_range:stop() <= range:stop()  -- issue ends within range
     )
   end
   local function match_issue_identifier(issue_identifier)
@@ -65,7 +70,7 @@
   local ignore_issue, issue_tables
   if identifier == nil then
     -- Prevent any issues within the given range.
-    assert(range_start ~= nil and range_end ~= nil)
+    assert(range ~= nil)
     issue_tables = {self.warnings, self.errors}
     ignore_issue = function(issue)
       local issue_range = issue[3]
@@ -75,7 +80,7 @@
         return match_issue_range(issue_range)
       end
     end
-  elseif range_start == nil then
+  elseif range == nil then
     -- Prevent any issues with the given identifier.
     assert(identifier ~= nil)
     issue_tables = {self:_get_issue_table(identifier)}
@@ -85,7 +90,7 @@
     end
   else
     -- Prevent any issues with the given identifier that are also either within the given range or file-wide.
-    assert(range_start ~= nil and range_end ~= nil and identifier ~= nil)
+    assert(range ~= nil and identifier ~= nil)
     issue_tables = {self:_get_issue_table(identifier)}
     ignore_issue = function(issue)
       local issue_identifier = issue[1]
@@ -129,7 +134,7 @@
   end
   table.sort(sorted_warnings_and_errors, function(a, b)
     local a_identifier, b_identifier = a[1], b[1]
-    local a_range, b_range = (a[3] and a[3][1]) or 0, (b[3] and b[3][1]) or 0
+    local a_range, b_range = (a[3] and a[3]:start()) or 0, (b[3] and b[3]:start()) or 0
     return a_range < b_range or (a_range == b_range and a_identifier < b_identifier)
   end)
   return sorted_warnings_and_errors

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-lexical-analysis.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-lexical-analysis.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-lexical-analysis.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,12 +1,14 @@
 -- The lexical analysis step of static analysis converts expl3 parts of the input files into TeX tokens.
 
+local get_option = require("explcheck-config")
+local new_range = require("explcheck-ranges")
+local obsolete = require("explcheck-obsolete")
 local parsers = require("explcheck-parsers")
-local obsolete = require("explcheck-obsolete")
 
 local lpeg = require("lpeg")
 
 -- Tokenize the content and register any issues.
-local function lexical_analysis(issues, all_content, expl_ranges, options)  -- luacheck: ignore issues options
+local function lexical_analysis(issues, pathname, all_content, expl_ranges, seems_like_latex_style_file, options)
 
   -- Process bytes within a given range similarly to TeX's input processor (TeX's "eyes" [1]) and produce lines.
   --
@@ -21,25 +23,25 @@
   --       https://petr.olsak.net/ftp/olsak/tbn/tbn.pdf
   --
   local function get_lines(range)
-    local range_start, range_end = table.unpack(range)
-    local content = all_content:sub(range_start, range_end - 1)
+    local content = all_content:sub(range:start(), range:stop())
     for _, line in ipairs(lpeg.match(parsers.tex_lines, content)) do
       local line_start, line_text, line_end = table.unpack(line)
-      local map_back = (function(line_start, line_text, line_end)  -- luacheck: ignore line_start line_text line_end
+      local line_range = new_range(line_start, line_end, "exclusive", #all_content)
+      local map_back = (function(line_text, line_range)  -- luacheck: ignore line_text line_range
         return function (index)
           assert(index > 0)
           assert(index <= #line_text + #parsers.expl3_endlinechar)
-          if index > 0 and index <= #line_text then
-            local mapped_index = range_start + line_start + index - 2  -- a line character
+          if index <= #line_text then
+            local mapped_index = range:start() + line_range:start() + index - 2  -- a line character
             assert(line_text[index] == content[mapped_index])
             return mapped_index
           elseif index > #line_text and index <= #line_text + #parsers.expl3_endlinechar then
-            return range_start + line_end - 2  -- an \endlinechar
+            return math.max(1, range:start() + line_range:start() + #line_text - 2)  -- an \endlinechar
           else
             assert(false)
           end
         end
-      end)(line_start, line_text, line_end)
+      end)(line_text, line_range)
       coroutine.yield(line_text .. parsers.expl3_endlinechar, map_back)
     end
   end
@@ -60,18 +62,39 @@
     local tokens = {}
     local state
     local num_open_groups_upper_estimate = 0
+
+    -- Determine the category code of the at sign ("@").
+    local make_at_letter = get_option("make_at_letter", options, pathname)
+    if make_at_letter == "auto" then
+      make_at_letter = seems_like_latex_style_file
+    end
+
     for line_text, map_back in lines do
       state = "N"
       local character_index = 1
 
+      local function determine_expl3_catcode(character)
+        local catcode
+        if character == "@" then
+          if make_at_letter then
+            catcode = 11  -- letter
+          else
+            catcode = 12  -- other
+          end
+        else
+          catcode = lpeg.match(parsers.determine_expl3_catcode, character)
+        end
+        return catcode
+      end
+
       local function get_character_and_catcode(index)
         assert(index <= #line_text)
         local character = line_text:sub(index, index)
-        local catcode = lpeg.match(parsers.determine_expl3_catcode, character)
+        local catcode = determine_expl3_catcode(character)
         -- Process TeX' double circumflex convention (^^X and ^^XX).
         local actual_character, index_increment = lpeg.match(parsers.double_superscript_convention, line_text, index)
         if actual_character ~= nil then
-          local actual_catcode = lpeg.match(parsers.determine_expl3_catcode, actual_character)
+          local actual_catcode = determine_expl3_catcode(actual_character)
           return actual_character, actual_catcode, index_increment  -- double circumflex convention
         else
           return character, catcode, 1  -- single character
@@ -81,8 +104,7 @@
       local previous_catcode, previous_csname = 9, nil
       while character_index <= #line_text do
         local character, catcode, character_index_increment = get_character_and_catcode(character_index)
-        local range_start = map_back(character_index)
-        local range_end = range_start + 1
+        local range = new_range(character_index, character_index, "inclusive", #line_text, map_back, #all_content)
         if (
               catcode ~= 9 and catcode ~= 10  -- a potential missing stylistic whitespace
               and (
@@ -101,7 +123,7 @@
                 and (previous_catcode ~= 1 or catcode ~= 6)  -- allow a parameter after begin grouping without whitespace in between
                 and (previous_catcode ~= 2 or character ~= ",")  -- allow a comma after end grouping without whitespace in between
               ) then
-            issues:add('s204', 'missing stylistic whitespaces', range_start, range_end)
+            issues:add('s204', 'missing stylistic whitespaces', range)
           end
         end
         if catcode == 0 then  -- control sequence
@@ -132,22 +154,22 @@
             end
           end
           local csname = table.concat(csname_table)
-          range_end = map_back(previous_csname_index) + 1
-          table.insert(tokens, {"control sequence", csname, 0, range_start, range_end})
+          range = new_range(character_index, previous_csname_index, "inclusive", #line_text, map_back, #all_content)
+          table.insert(tokens, {"control sequence", csname, 0, range})
           if (
                 previous_catcode ~= 9 and previous_catcode ~= 10  -- a potential missing stylistic whitespace
                 -- do not require whitespace before non-expl3 control sequences or control sequences with empty or one-character names
                 and #csname > 1 and lpeg.match(parsers.non_expl3_csname, csname) == nil
               ) then
-            issues:add('s204', 'missing stylistic whitespaces', range_start, range_end)
+            issues:add('s204', 'missing stylistic whitespaces', range)
           end
           previous_catcode, previous_csname = 0, csname
           character_index = csname_index
         elseif catcode == 5 then  -- end of line
           if state == "N" then
-            table.insert(tokens, {"control sequence", "par", range_start, range_end})
+            table.insert(tokens, {"control sequence", "par", range})
           elseif state == "M" then
-            table.insert(tokens, {"character", " ", 10, range_start, range_end})
+            table.insert(tokens, {"character", " ", 10, range})
           end
           character_index = character_index + character_index_increment
         elseif catcode == 9 then  -- ignored character
@@ -155,7 +177,7 @@
           character_index = character_index + character_index_increment
         elseif catcode == 10 then  -- space
           if state == "M" then
-            table.insert(tokens, {"character", " ", 10, range_start, range_end})
+            table.insert(tokens, {"character", " ", 10, range})
           end
           previous_catcode = catcode
           character_index = character_index + character_index_increment
@@ -162,7 +184,7 @@
         elseif catcode == 14 then  -- comment character
           character_index = #line_text + 1
         elseif catcode == 15 then  -- invalid character
-          issues:add('e209', 'invalid characters', range_start, range_end)
+          issues:add('e209', 'invalid characters', range)
           character_index = character_index + character_index_increment
         else
           if catcode == 1 or catcode == 2 then  -- begin/end grouping
@@ -172,7 +194,7 @@
               if num_open_groups_upper_estimate > 0 then
                 num_open_groups_upper_estimate = num_open_groups_upper_estimate - 1
               else
-                issues:add('e208', 'too many closing braces', range_start, range_end)
+                issues:add('e208', 'too many closing braces', range)
               end
             end
             if (
@@ -182,7 +204,7 @@
                     and (previous_catcode ~= 1 or catcode ~= 2)  -- allow an end grouping immediately after begin grouping
                     and (previous_catcode ~= 6 or catcode ~= 1 and catcode ~= 2)  -- allow a parameter immediately before grouping
                 ) then
-              issues:add('s204', 'missing stylistic whitespaces', range_start, range_end)
+              issues:add('s204', 'missing stylistic whitespaces', range)
             end
             previous_catcode = catcode
           elseif (  -- maybe a parameter?
@@ -193,7 +215,7 @@
           else  -- some other character
             previous_catcode = catcode
           end
-          table.insert(tokens, {"character", character, catcode, range_start, range_end})
+          table.insert(tokens, {"character", character, catcode, range})
           state = "M"
           character_index = character_index + character_index_increment
         end
@@ -204,10 +226,10 @@
 
   -- Tokenize the content.
   local all_tokens = {}
-  for _, expl_range in ipairs(expl_ranges) do
+  for _, range in ipairs(expl_ranges) do
     local lines = (function()
       local co = coroutine.create(function()
-        get_lines(expl_range)
+        get_lines(range)
       end)
       return function()
         local _, line_text, map_back = coroutine.resume(co)
@@ -220,27 +242,24 @@
 
   for _, tokens in ipairs(all_tokens) do
     for token_index, token in ipairs(tokens) do
-      local token_type, payload, catcode, range_start, range_end = table.unpack(token)  -- luacheck: ignore catcode
+      local token_type, payload, catcode, range = table.unpack(token)  -- luacheck: ignore catcode
       if token_type == "control sequence" then
         local csname = payload
-        local _, _, argument_specifiers = csname:find(":(.*)")
+        local _, _, argument_specifiers = csname:find(":([^:]*)")
         if argument_specifiers ~= nil then
-          if lpeg.match(parsers.weird_argument_specifiers, argument_specifiers) then
-            issues:add('w200', '"weird" and "do not use" argument specifiers', range_start, range_end)
+          if lpeg.match(parsers.do_not_use_argument_specifiers, argument_specifiers) then
+            issues:add('w200', '"do not use" argument specifiers', range)
           end
           if lpeg.match(parsers.argument_specifiers, argument_specifiers) == nil then
-            issues:add('e201', 'unknown argument specifiers', range_start, range_end)
+            issues:add('e201', 'unknown argument specifiers', range)
           end
         end
         if lpeg.match(obsolete.deprecated_csname, csname) ~= nil then
-          issues:add('w202', 'deprecated control sequences', range_start, range_end)
+          issues:add('w202', 'deprecated control sequences', range)
         end
-        if lpeg.match(obsolete.removed_csname, csname) ~= nil then
-          issues:add('e203', 'removed control sequences', range_start, range_end)
-        end
         if token_index + 1 <= #tokens then
           local next_token = tokens[token_index + 1]
-          local next_token_type, next_csname, _, next_range_start, next_range_end = table.unpack(next_token)
+          local next_token_type, next_csname, _, next_range = table.unpack(next_token)
           if next_token_type == "control sequence" then
             if (
                   lpeg.match(parsers.expl3_function_assignment_csname, csname) ~= nil
@@ -247,7 +266,7 @@
                   and lpeg.match(parsers.non_expl3_csname, next_csname) == nil
                   and lpeg.match(parsers.expl3_function_csname, next_csname) == nil
                 ) then
-              issues:add('s205', 'malformed function name', next_range_start, next_range_end)
+              issues:add('s205', 'malformed function name', next_range)
             end
             if (
                   lpeg.match(parsers.expl3_variable_or_constant_use_csname, csname) ~= nil
@@ -255,13 +274,13 @@
                   and lpeg.match(parsers.expl3_scratch_variable_csname, next_csname) == nil
                   and lpeg.match(parsers.expl3_variable_or_constant_csname, next_csname) == nil
                 ) then
-              issues:add('s206', 'malformed variable or constant name', next_range_start, next_range_end)
+              issues:add('s206', 'malformed variable or constant name', next_range)
             end
             if (
                   lpeg.match(parsers.expl3_quark_or_scan_mark_definition_csname, csname) ~= nil
                   and lpeg.match(parsers.expl3_quark_or_scan_mark_csname, next_csname) == nil
                 ) then
-              issues:add('s207', 'malformed quark or scan mark name', next_range_start, next_range_end)
+              issues:add('s207', 'malformed quark or scan mark name', next_range)
             end
           end
         end

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-obsolete.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-obsolete.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-obsolete.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -5,12 +5,11 @@
 
 local eof = P(-1)
 local regular_character = P(1)
-local wildcard = regular_character^0
+local wildcard = regular_character^0  -- luacheck: ignore wildcard
 
 -- luacheck: push no max line length
 local obsolete = {}
-obsolete.deprecated_csname = (P("t") * (P("l") * (P("_") * (P("b") * (P("u") * (P("i") * (P("l") * (P("d") * (P("_") * (P("g") * (P("et:NN") + P("clear:N")) + P("clear:N"))))))) + P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("N") * (P("n") * (P("TF") + P("F") + P("T")) + P("n")) + P("c") * (P("n") * (P("TF") + P("F") + P("T")) + P("n"))))))) + P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n")))))))))))) + P("m") * (P("i") * (P("x") * (P("e") * (P("d") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n")))))))))))) + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n")))))))))))))) + P("e") * (P("x") * (P("t") * (P("_") * (P("t") * (P("i") * (P("t") * (P("l") * (P("e") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n")))))))))))))))) + P("c") * (P("h") * (P("a") * (P("r") * (P("_") * (P("s") * (P("t") * (P("r") * (P("_") * (P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("case:N") + P("_case:N")))))) + P("mixed_case:N") + P("f") * (P("o") * (P("l") * (P("d") * (P("case:N") + P("_case:N"))))) + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("case:N") + P("_case:N")))))) + P("titlecase:N"))))) + P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("case:N") + P("_case:N")))))) + P("mixed_case:N") + P("f") * (P("o") * (P("l") * (P("d") * (P("case:N") + P("_case:N"))))) + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("case:N") + P("_case:N")))))) + P("t") * (P("o") * (P("_") * (P("nfd:N") + P("utfviii_bytes:n"))) + P("itlecase:N")))))) + P("s_argument_spec:N")) + P("l") * (P("_") * (P("t") * (P("e") * (P("x") * (P("t") * (P("_") * (P("letterlike_tl") + P("accents_tl")))))) + P("k") * (P("e") * (P("y") * (P("s") * (P("_") * (P("path_tl") + P("key_tl")))))))) + P("m") * (P("s") * (P("g") * (P("_") * (P("g") * (P("s") * (P("e") * (P("t") * (P(":") * (P("n") * (P("n") * (P("nn") + P("n"))))))))))))!
  + P("s") * (P("ys_load_deprecation:") + P("t") * (P("r") * (P("_") * (P("f") * (P("o") * (P("l") * (P("d") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("n") + P("V")))))) + P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("n") + P("V"))))))))))) + P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("f") + P("n")))))))))))) + P("declare_eight_bit_encoding:nnn") + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("f") + P("n"))))))))))))))) + P("e") * (P("q") * (P("_") * (P("gset_map_x:NNn") + P("set_map_x:NNn") + P("i") * (P("n") * (P("d") * (P("e") * (P("x") * (P("e") * (P("d") * (P("_") * (P("m") * (P("a") * (P("p") * (P("_") * (P("function:NN") + P("inline:Nn"))))))))))))))))) + P("k") * (P("e") * (P("y") * (P("s") * (P("_") * (P("s") * (P("e") * (P("t") * (P("_") * (P("f") * (P("i") * (P("l") * (P("t") * (P("e") * (P("r") * (P(":") * (P("n") * (P("n") * (P("V") * (P("nN") + P("N")) + P("o") * (P("nN") + P("N")) + P("v") * (P("nN") + P("N")) + P("n") * (P("nN") + P("N")) + P("n") + P("V") + P("v") + P("o"))))))))))))))))))) + P("p") * (P("r") * (P("o") * (P("p") * (P("_") * (P("p") * (P("u") * (P("t") * (P("_") * (P("i") * (P("f") * (P("_") * (P("n") * (P("e") * (P("w") * (P(":") * (P("N") * (P("n") * (P("n") + P("V")) + P("Vn")) + P("c") * (P("n") * (P("n") + P("V")) + P("Vn"))))))))))))) + P("g") * (P("p") * (P("u") * (P("t") * (P("_") * (P("i") * (P("f") * (P("_") * (P("n") * (P("e") * (P("w") * (P(":") * (P("N") * (P("n") * (P("n") + P("V")) + P("Vn")) + P("c") * (P("n") * (P("n") + P("V")) + P("Vn")))))))))))))))))) + P("d") * (P("f") * (P("_") * (P("o") * (P("b") * (P("j") * (P("e") * (P("c") * (P("t") * (P("_") * (P("new:nn") + P("w") * (P("r") * (P("i") * (P("t") * (P("e") * (P(":") * (P("n") * (P("n") + P("x")))))))))))))))))) + P("e") * (P("e") * (P("k") * (P("_") * (P("m") * (P("e") * (P("a") * (P("n") * (P("i") * (P("n") * (P("g") * (P("_") * (P("remove_i!
 gnore_spaces:N") + P("ignore_spaces:N"))))))))) + P("c") * (P("h") * (P("a") * (P("r") * (P("c") * (P("o") * (P("d") * (P("e") * (P("_") * (P("remove_ignore_spaces:N") + P("ignore_spaces:N"))))))))) + P("a") * (P("t") * (P("c") * (P("o") * (P("d") * (P("e") * (P("_") * (P("remove_ignore_spaces:N") + P("ignore_spaces:N")))))))))))))) + P("i") * (P("o") * (P("w") * (P("_") * (P("s") * (P("h") * (P("i") * (P("p") * (P("o") * (P("u") * (P("t") * (P("_") * (P("x") * (P(":") * (P("N") * (P("n") + P("x")) + P("c") * (P("n") + P("x"))))))))))))))))) * eof
-obsolete.removed_csname = (P("b") * (P("o") * (P("x") * (P("_") * (P("r") * (P("e") * (P("s") * (P("i") * (P("z") * (P("e") * (P(":") * (P("Nnn") + P("cnn")))))))) + P("g") * (P("s") * (P("e") * (P("t") * (P("_") * (P("e") * (P("q") * (P("_") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("N") * (P("c") + P("N")) + P("c") * (P("c") + P("N")))))))))))))))) + P("s") * (P("e") * (P("t") * (P("_") * (P("e") * (P("q") * (P("_") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("N") * (P("c") + P("N")) + P("c") * (P("c") + P("N"))))))))))))))) + P("u") * (P("s") * (P("e") * (P("_") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("c") + P("N"))))))))))))))) + P("c") * (P("s") * (P("_") * (P("set_eq:NwN") + P("g") * (P("n") * (P("e") * (P("w") * (P(":") * (P("N") * (P("p") * (P("n") + P("x"))) + P("c") * (P("p") * (P("n") + P("x")))) + P("_") * (P("n") * (P("o") * (P("p") * (P("a") * (P("r") * (P(":") * (P("N") * (P("p") * (P("n") + P("x"))) + P("c") * (P("p") * (P("n") + P("x"))))))))) + P("p") * (P("r") * (P("o") * (P("t") * (P("e") * (P("c") * (P("t") * (P("e") * (P("d") * (P(":") * (P("N") * (P("p") * (P("n") + P("x"))) + P("c") * (P("p") * (P("n") + P("x")))) + P("_") * (P("n") * (P("o") * (P("p") * (P("a") * (P("r") * (P(":") * (P("N") * (P("p") * (P("n") + P("x"))) + P("c") * (P("p") * (P("n") + P("x"))))))))))))))))))) + P("e") * (P("q") * (P(":") * (P("Nc") + P("c") * (P("c") + P("N"))))))))) + P("u") * (P("n") * (P("d") * (P("e") * (P("f") * (P("i") * (P("n") * (P("e") * (P(":") * (P("c") + P("N"))))))))))))) + P("_") * (P("zero") + P("catcode_active_tl") + P("t") * (P("w") * (P("o") * (P("_") * (P("h") * (P("u") * (P("n") * (P("d") * (P("r") * (P("e") * (P("d") * (P("_") * (P("f") * (P("i") * (P("f") * (P("t") * (P("y") * (P("_") * (P("five") + P("six"))))))))))))))))) + P("elve") + P("o")) + P("h") * (P("ree") + P("i") * (P("r") * (P("t") * (P("y_two") + P("een"))))) + P("e") * (P("rm_ior") + P("n_thousand") + P("n"))) + P("e") * (P("mpty_toks") + P(!
 "leven") + P("ight")) + P("f") * (P("o") * (P("u") * (P("rteen") + P("r"))) + P("i") * (P("fteen") + P("ve"))) + P("undefined_fp") + P("xetex_is_engine_bool") + P("s") * (P("i") * (P("xteen") + P("x")) + P("tring_cctab") + P("even")) + P("job_name_tl") + P("keys_code_root_tl") + P("l") * (P("uatex_is_engine_bool") + P("etter_token")) + P("m") * (P("inus_one") + P("ath_shift_token")) + P("nine") + P("o") * (P("n") * (P("e") * (P("_") * (P("hundred") + P("thousand"))) + P("e")) + P("ther_char_token")) + P("pdftex_is_engine_bool") + P("a") * (P("lignment_tab_token") + P("ctive_char_token"))) + P("h") * (P("k_if_free_cs:N") + P("a") * (P("r") * (P("_") * (P("v") * (P("a") * (P("l") * (P("u") * (P("e") * (P("_") * (P("catcode:w") + P("lccode:w") + P("mathcode:w") + P("uccode:w") + P("sfcode:w"))))))) + P("s") * (P("h") * (P("o") * (P("w") * (P("_") * (P("v") * (P("a") * (P("l") * (P("u") * (P("e") * (P("_") * (P("catcode:w") + P("lccode:w") + P("mathcode:w") + P("uccode:w") + P("sfcode:w"))))))))))) + P("e") * (P("t") * (P("_") * (P("catcode:w") + P("lccode:w") + P("mathcode:w") + P("uccode:w") + P("sfcode:w"))))) + P("m") * (P("a") * (P("k") * (P("e") * (P("_") * (P("b") * (P("e") * (P("g") * (P("i") * (P("n") * (P("_") * (P("g") * (P("r") * (P("o") * (P("u") * (P("p") * (P(":") * (P("N") + P("n"))))))))))))) + P("c") * (P("o") * (P("m") * (P("m") * (P("e") * (P("n") * (P("t") * (P(":") * (P("N") + P("n"))))))))) + P("e") * (P("n") * (P("d") * (P("_") * (P("l") * (P("i") * (P("n") * (P("e") * (P(":") * (P("N") + P("n")))))) + P("g") * (P("r") * (P("o") * (P("u") * (P("p") * (P(":") * (P("N") + P("n")))))))))) + P("s") * (P("c") * (P("a") * (P("p") * (P("e") * (P(":") * (P("N") + P("n")))))))) + P("i") * (P("n") * (P("v") * (P("a") * (P("l") * (P("i") * (P("d") * (P(":") * (P("N") + P("n")))))))) + P("g") * (P("n") * (P("o") * (P("r") * (P("e") * (P(":") * (P("N") + P("n")))))))) + P("l") * (P("e") * (P("t") * (P("t") * (P("e") * (P("r") * (P(":") * (P("N") + P("n")))))))) + P("m") * (P("a") * (P("t") * (P("h") * (P(!
 "_") * (P("s") * (P("h") * (P("i") * (P("f") * (P("t") * (P(":") * (P("N") + P("n")))))) + P("u") * (P("b") * (P("s") * (P("c") * (P("r") * (P("i") * (P("p") * (P("t") * (P(":") * (P("N") + P("n"))))))))) + P("p") * (P("e") * (P("r") * (P("s") * (P("c") * (P("r") * (P("i") * (P("p") * (P("t") * (P(":") * (P("N") + P("n")))))))))))))))))) + P("s") * (P("p") * (P("a") * (P("c") * (P("e") * (P(":") * (P("N") + P("n"))))))) + P("o") * (P("t") * (P("h") * (P("e") * (P("r") * (P(":") * (P("N") + P("n"))))))) + P("p") * (P("a") * (P("r") * (P("a") * (P("m") * (P("e") * (P("t") * (P("e") * (P("r") * (P(":") * (P("N") + P("n"))))))))))) + P("a") * (P("l") * (P("i") * (P("g") * (P("n") * (P("m") * (P("e") * (P("n") * (P("t") * (P("_") * (P("t") * (P("a") * (P("b") * (P(":") * (P("N") + P("n")))))))))))))) + P("c") * (P("t") * (P("i") * (P("v") * (P("e") * (P(":") * (P("N") + P("n"))))))))))))))))) + P("l") * (P("i") * (P("s") * (P("t") * (P("_") * (P("remove_element:Nn") + P("d") * (P("i") * (P("s") * (P("p") * (P("l") * (P("a") * (P("y") * (P(":") * (P("c") + P("N"))))))))) + P("u") * (P("s") * (P("e") * (P(":") * (P("c") + P("N"))))) + P("t") * (P("r") * (P("i") * (P("m") * (P("_") * (P("s") * (P("p") * (P("a") * (P("c") * (P("e") * (P("s") * (P(":") * (P("c") + P("N")))))))))))) + P("op:cN")) + P("g") * (P("remove_element:Nn") + P("t") * (P("r") * (P("i") * (P("m") * (P("_") * (P("s") * (P("p") * (P("a") * (P("c") * (P("e") * (P("s") * (P(":") * (P("c") + P("N")))))))))))))) + P("l") * (P("e") * (P("n") * (P("g") * (P("t") * (P("h") * (P(":") * (P("c") + P("N") + P("n")))))))) + P("i") * (P("f") * (P("_") * (P("e") * (P("q") * (P(":") * (P("N") * (P("c") * (P("TF") + P("F") + P("T"))) + P("c") * (P("N") * (P("TF") + P("F") + P("T")) + P("c") * (P("TF") + P("F") + P("T")))) + P("_") * (P("p") * (P(":") * (P("Nc") + P("c") * (P("c") + P("N")))))))))))))))) + P("d") * (P("i") * (P("m") * (P("_") * (P("s") * (P("e") * (P("t") * (P("_") * (P("m") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn")))) + P("a") * (P("x") * (P(!
 ":") * (P("Nn") + P("cn"))))))))) + P("case:nnn") + P("g") * (P("s") * (P("e") * (P("t") * (P("_") * (P("m") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn")))) + P("a") * (P("x") * (P(":") * (P("Nn") + P("cn")))))))))) + P("e") * (P("v") * (P("a") * (P("l") * (P(":w") + P("_end:"))))))))) + P("e") * (P("t") * (P("e") * (P("x") * (P("_") * (wildcard * P(".:D")))))) + P("f") * (P("p") * (P("_") * (P("r") * (P("o") * (P("u") * (P("n") * (P("d") * (P("_") * (P("f") * (P("i") * (P("g") * (P("u") * (P("r") * (P("e") * (P("s") * (P(":") * (P("Nn") + P("cn"))))))))) + P("p") * (P("l") * (P("a") * (P("c") * (P("e") * (P("s") * (P(":") * (P("Nn") + P("cn")))))))))))))) + P("c") * (P("o") * (P("s") * (P(":") * (P("Nn") + P("cn"))) + P("m") * (P("p") * (P("a") * (P("r") * (P("e") * (P(":") * (P("N") * (P("N") * (P("N") * (P("TF") + P("F") + P("T")))))))))))) + P("d") * (P("i") * (P("v") * (P(":") * (P("Nn") + P("cn"))))) + P("e") * (P("x") * (P("p") * (P(":") * (P("Nn") + P("cn"))))) + P("g") * (P("r") * (P("o") * (P("u") * (P("n") * (P("d") * (P("_") * (P("f") * (P("i") * (P("g") * (P("u") * (P("r") * (P("e") * (P("s") * (P(":") * (P("Nn") + P("cn"))))))))) + P("p") * (P("l") * (P("a") * (P("c") * (P("e") * (P("s") * (P(":") * (P("Nn") + P("cn")))))))))))))) + P("c") * (P("o") * (P("s") * (P(":") * (P("Nn") + P("cn"))))) + P("d") * (P("i") * (P("v") * (P(":") * (P("Nn") + P("cn"))))) + P("e") * (P("x") * (P("p") * (P(":") * (P("Nn") + P("cn"))))) + P("t") * (P("a") * (P("n") * (P(":") * (P("Nn") + P("cn"))))) + P("l") * (P("n") * (P(":") * (P("Nn") + P("cn")))) + P("m") * (P("u") * (P("l") * (P(":") * (P("Nn") + P("cn"))))) + P("n") * (P("e") * (P("g") * (P(":") * (P("c") + P("N"))))) + P("s") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn"))))) + P("p") * (P("o") * (P("w") * (P(":") * (P("Nn") + P("cn"))))) + P("a") * (P("b") * (P("s") * (P(":") * (P("c") + P("N")))))) + P("i") * (P("f") * (P("_") * (P("z") * (P("e") * (P("r") * (P("o") * (P(":") * (P("N") * (P("TF") + P("F") + P("T"))) + P("_p:N"))))) + P("u") * (P(!
 "n") * (P("d") * (P("e") * (P("f") * (P("i") * (P("n") * (P("e") * (P("d") * (P(":") * (P("N") * (P("TF") + P("F") + P("T"))) + P("_p:N"))))))))))))) + P("t") * (P("a") * (P("n") * (P(":") * (P("Nn") + P("cn"))))) + P("l") * (P("n") * (P(":") * (P("Nn") + P("cn")))) + P("m") * (P("u") * (P("l") * (P(":") * (P("Nn") + P("cn"))))) + P("n") * (P("e") * (P("g") * (P(":") * (P("c") + P("N"))))) + P("s") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn"))))) + P("p") * (P("o") * (P("w") * (P(":") * (P("Nn") + P("cn"))))) + P("a") * (P("b") * (P("s") * (P(":") * (P("c") + P("N"))))))) + P("i") * (P("l") * (P("e") * (P("_") * (P("p") * (P("a") * (P("t") * (P("h") * (P("_") * (P("remove:n") + P("include:n")))))) + P("i") * (P("f") * (P("_") * (P("e") * (P("x") * (P("i") * (P("s") * (P("t") * (P("_") * (P("i") * (P("n") * (P("p") * (P("u") * (P("t") * (P(":") * (P("n") * (P("TF") + P("T"))))))))))))))))) + P("list:") + P("add_path:nN")))))) + P("g") * (P("roup_execute_after:N") + P("_") * (P("file_current_name_tl") + P("t") * (P("m") * (P("p") * (P("b_toks") + P("c_toks") + P("a_toks")))))) + P("h") * (P("b") * (P("o") * (P("x") * (P("_") * (P("g") * (P("s") * (P("e") * (P("t") * (P("_") * (P("i") * (P("n") * (P("l") * (P("i") * (P("n") * (P("e") * (P("_") * (P("b") * (P("e") * (P("g") * (P("i") * (P("n") * (P(":") * (P("c") + P("N"))))))) + P("end:"))))))))))))) + P("s") * (P("e") * (P("t") * (P("_") * (P("i") * (P("n") * (P("l") * (P("i") * (P("n") * (P("e") * (P("_") * (P("b") * (P("e") * (P("g") * (P("i") * (P("n") * (P(":") * (P("c") + P("N"))))))) + P("end:")))))))))))) + P("u") * (P("n") * (P("p") * (P("a") * (P("c") * (P("k") * (P("_") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("c") + P("N"))))))))))))))))))) + P("i") * (P("n") * (P("t") * (P("_") * (P("c") * (P("o") * (P("n") * (P("v") * (P("e") * (P("r") * (P("t") * (P("_") * (P("f") * (P("r") * (P("o") * (P("m") * (P("_") * (P("base_ten:nn") + P("symbols:nn")))))) + P("to_base_ten:nn")))))))) + P("ase:nnn")) + P("t") * (P("o") * (P("_") * !
 (P("binary:n") + P("octal:n") + P("hexadecimal:n") + P("symbol:n")))) + P("e") * (P("v") * (P("a") * (P("l") * (P(":w") + P("_end:"))))) + P("f") * (P("r") * (P("o") * (P("m") * (P("_") * (P("binary:n") + P("octal:n") + P("hexadecimal:n")))))) + P("value:w")))) + P("o") * (P("r") * (P("_") * (P("s") * (P("t") * (P("r") * (P("_") * (P("to:NN") + P("gto:NN"))))) + P("l") * (P("og_streams:") + P("ist_streams:")) + P("open_streams:") + P("to:NN") + P("g") * (P("to:NN") + P("et_str:NN")))) + P("w") * (P("_") * (P("n") * (P("o") * (P("w") * (P("_") * (P("b") * (P("u") * (P("f") * (P("f") * (P("e") * (P("r") * (P("_") * (P("s") * (P("a") * (P("f") * (P("e") * (P(":") * (P("N") * (P("n") + P("x")))))))))))))) + P("w") * (P("h") * (P("e") * (P("n") * (P("_") * (P("a") * (P("v") * (P("a") * (P("i") * (P("l") * (P(":") * (P("N") * (P("n") + P("x"))))))))))))))))) + P("open_streams:") + P("l") * (P("og_streams:") + P("ist_streams:")) + P("wrap:xnnnN")))) + P("f_num:w")) + P("s") * (P("k") * (P("i") * (P("p") * (P("_") * (P("i") * (P("f") * (P("_") * (P("i") * (P("n") * (P("f") * (P("i") * (P("n") * (P("i") * (P("t") * (P("e") * (P("_") * (P("g") * (P("l") * (P("u") * (P("e") * (P(":") * (P("n") * (P("TF") + P("F") + P("T"))) + P("_p:n"))))))))))))))))))))) + P("t") * (P("r") * (P("_") * (P("i") * (P("f") * (P("_") * (P("e") * (P("q") * (P(":") * (P("x") * (P("x") * (P("TF") + P("F") + P("T")))) + P("_") * (P("p:xx") + P("x") * (P(":") * (P("n") * (P("n") * (P("TF") + P("F") + P("T")))) + P("_p:nn")))))))) + P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nnn") + P("onn")) + P("_") * (P("x") * (P(":") * (P("n") * (P("n") * (P("TF") + P("F") + P("n") + P("T")) + P("n")))))))))))) + P("e") * (P("q") * (P("_") * (P("d") * (P("i") * (P("s") * (P("p") * (P("l") * (P("a") * (P("y") * (P(":") * (P("c") + P("N"))))))))) + P("t") * (P("o") * (P("p") * (P(":") * (P("NN") + P("cN"))))) + P("l") * (P("e") * (P("n") * (P("g") * (P("t") * (P("h") * (P(":") * (P("c") + P("N")))))))) + P("u") * (P("s") * (P("e") * (P(":") * (P("c") + P!
 ("N")))))))) + P("o") * (P("r") * (P("t") * (P("_") * (P("reversed:") + P("ordered:"))))) + P("can_align_safe_stop:")) + P("t") * (P("l") * (P("_") * (P("r") * (P("e") * (P("p") * (P("l") * (P("a") * (P("c") * (P("e") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nnn") + P("cnn")))) + P("a") * (P("l") * (P("l") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nnn") + P("cnn")))))))))))))) + P("m") * (P("o") * (P("v") * (P("e") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn")))) + P("a") * (P("l") * (P("l") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn"))))))))))))))) + P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("Nnn") + P("cnn")))))) + P("t") * (P("o") * (P("_") * (P("lowercase:n") + P("uppercase:n"))) + P("ail:w")) + P("e") * (P("l") * (P("t") * (P("_") * (P("c") * (P("o") * (P("u") * (P("n") * (P("t") * (P(":") * (P("c") + P("n") + P("N") + P("o") + P("V"))))))))))) + P("g") * (P("r") * (P("e") * (P("p") * (P("l") * (P("a") * (P("c") * (P("e") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nnn") + P("cnn")))) + P("a") * (P("l") * (P("l") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nnn") + P("cnn")))))))))))))) + P("m") * (P("o") * (P("v") * (P("e") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn")))) + P("a") * (P("l") * (P("l") * (P("_") * (P("i") * (P("n") * (P(":") * (P("Nn") + P("cn"))))))))))))))) + P("s") * (P("e") * (P("t") * (P(":Nc") + P("_") * (P("f") * (P("r") * (P("o") * (P("m") * (P("_") * (P("f") * (P("i") * (P("l") * (P("e") * (P(":") * (P("Nnn") + P("cnn")) + P("_") * (P("x") * (P(":") * (P("Nnn") + P("cnn")))))))))))))))))) + P("h") * (P("e") * (P("a") * (P("d") * (P("_") * (P("i") * (P(":") * (P("n") + P("w")) + P("i") * (P("i") * (P(":") * (P("f") + P("n") + P("w")))))))))) + P("i") * (P("f") * (P("_") * (P("h") * (P("e") * (P("a") * (P("d") * (P("_") * (P("N") * (P("_") * (P("t") * (P("y") * (P("p") * (P("e") * (P(":") * (P("n") * (P("TF") + P("F") + P("T"))) + P("_p:n"))))))) + P("g") * (P("r") * (P("o") * (P("u") * (P("p") * (P(":") * (P("n") * (!
 P("TF") + P("F") + P("T"))) + P("_p:n")))))) + P("s") * (P("p") * (P("a") * (P("c") * (P("e") * (P(":") * (P("n") * (P("TF") + P("F") + P("T"))) + P("_p:n"))))))))))) + P("e") * (P("m") * (P("p") * (P("t") * (P("y") * (P(":") * (P("x") * (P("TF") + P("F") + P("T"))))))))))) + P("l") * (P("e") * (P("n") * (P("g") * (P("t") * (P("h") * (P(":") * (P("c") + P("n") + P("N") + P("o") + P("V")))))))) + P("n") * (P("e") * (P("w") * (P(":") * (P("N") * (P("n") + P("x")) + P("cn"))))) + P("s") * (P("h") * (P("o") * (P("w") * (P("_") * (P("a") * (P("n") * (P("a") * (P("l") * (P("y") * (P("s") * (P("i") * (P("s") * (P(":") * (P("N") + P("n")))))))))))))) + P("e") * (P("t") * (P(":Nc") + P("_") * (P("f") * (P("r") * (P("o") * (P("m") * (P("_") * (P("f") * (P("i") * (P("l") * (P("e") * (P(":") * (P("Nnn") + P("cnn")) + P("_") * (P("x") * (P(":") * (P("Nnn") + P("cnn"))))))))))))))))))) + P("o") * (P("k") * (P("s") * (P("_") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("c") + P("N"))))))) + P("s") * (P("h") * (P("o") * (P("w") * (P(":") * (P("c") + P("N"))))) + P("e") * (P("t") * (P(":") * (P("N") * (P("f") + P("n") + P("o") + P("V") + P("v") + P("x")) + P("c") * (P("f") + P("n") + P("o") + P("V") + P("v") + P("x"))) + P("_") * (P("e") * (P("q") * (P(":") * (P("N") * (P("c") + P("N")) + P("c") * (P("c") + P("N"))))))))) + P("u") * (P("s") * (P("e") * (P(":") * (P("c") + P("N")) + P("_") * (P("g") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("c") + P("N")))))))) + P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("c") + P("N"))))))))))) + P("n") * (P("e") * (P("w") * (P(":") * (P("c") + P("N"))))) + P("g") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("c") + P("N"))))))) + P("p") * (P("u") * (P("t") * (P("_") * (P("l") * (P("e") * (P("f") * (P("t") * (P(":") * (P("N") * (P("n") + P("o") + P("V") + P("x")) + P("c") * (P("n") + P("o") + P("V"))))))) + P("r") * (P("i") * (P("g") * (P("h") * (P("t") * (P(":") * (P("N") * (P("n") + P("o") + P("V") + P("x")) + P("!
 c") * (P("n") + P("o") + P("V")))))))))))) + P("s") * (P("e") * (P("t") * (P(":") * (P("N") * (P("n") + P("o") + P("V") + P("x")) + P("c") * (P("n") + P("o") + P("V") + P("x"))) + P("_") * (P("e") * (P("q") * (P(":") * (P("N") * (P("c") + P("N")) + P("c") * (P("c") + P("N")))))))))) + P("p") * (P("u") * (P("t") * (P("_") * (P("l") * (P("e") * (P("f") * (P("t") * (P(":") * (P("N") * (P("n") + P("o") + P("V") + P("x")) + P("c") * (P("n") + P("o") + P("V"))))))) + P("r") * (P("i") * (P("g") * (P("h") * (P("t") * (P(":") * (P("N") * (P("f") + P("n") + P("o") + P("V") + P("x")) + P("c") * (P("n") + P("o") + P("V")))))))))))) + P("i") * (P("f") * (P("_") * (P("e") * (P("q") * (P(":") * (P("N") * (P("N") * (P("TF") + P("F") + P("T")) + P("c") * (P("TF") + P("F") + P("T"))) + P("c") * (P("N") * (P("TF") + P("F") + P("T")) + P("c") * (P("TF") + P("F") + P("T")))) + P("_") * (P("p") * (P(":") * (P("N") * (P("c") + P("N")) + P("c") * (P("c") + P("N")))))) + P("m") * (P("p") * (P("t") * (P("y") * (P(":") * (P("N") * (P("TF") + P("F") + P("T")) + P("c") * (P("TF") + P("F") + P("T"))) + P("_") * (P("p") * (P(":") * (P("c") + P("N")))))))))))))) + P("e") * (P("n") * (P("_") * (P("new:Nn") + P("g") * (P("e") * (P("t") * (P("_") * (P("replacement_spec:N") + P("prefix_spec:N") + P("arg_spec:N"))))) + P("i") * (P("f") * (P("_") * (P("o") * (P("t") * (P("h") * (P("e") * (P("r") * (P("_") * (P("c") * (P("h") * (P("a") * (P("r") * (P(":") * (P("N") * (P("TF") + P("F") + P("T"))) + P("_p:N"))))))))))) + P("m") * (P("a") * (P("t") * (P("h") * (P("_") * (P("s") * (P("h") * (P("i") * (P("f") * (P("t") * (P(":") * (P("N") * (P("TF") + P("F") + P("T"))) + P("_p:N"))))))))))) + P("a") * (P("l") * (P("i") * (P("g") * (P("n") * (P("m") * (P("e") * (P("n") * (P("t") * (P("_") * (P("t") * (P("a") * (P("b") * (P(":") * (P("N") * (P("TF") + P("F") + P("T"))) + P("_p:N"))))))))))))) + P("c") * (P("t") * (P("i") * (P("v") * (P("e") * (P("_") * (P("c") * (P("h") * (P("a") * (P("r") * (P(":") * (P("N") * (P("TF") + P("F") + P("T"))) + P("_p:N")))))))!
 )))))))))))))) + P("u") * (P("s") * (P("e") * (P("_") * (P("i") * (P("_") * (P("a") * (P("f") * (P("t") * (P("e") * (P("r") * (P("_") * (P("fi:nw") + P("o") * (P("r") * (P(":nw") + P("else:nw"))) + P("else:nw")))))))))))) + P("p") * (P("t") * (P("e") * (P("x") * (P("_") * (wildcard * P(":D")))))) + P("t") * (P("e") * (P("x") * (P("_") * (wildcard * P(":D")))))) + P("v") * (P("b") * (P("o") * (P("x") * (P("_") * (P("g") * (P("s") * (P("e") * (P("t") * (P("_") * (P("i") * (P("n") * (P("l") * (P("i") * (P("n") * (P("e") * (P("_") * (P("b") * (P("e") * (P("g") * (P("i") * (P("n") * (P(":") * (P("c") + P("N"))))))) + P("end:"))))))))))))) + P("s") * (P("e") * (P("t") * (P("_") * (P("i") * (P("n") * (P("l") * (P("i") * (P("n") * (P("e") * (P("_") * (P("b") * (P("e") * (P("g") * (P("i") * (P("n") * (P(":") * (P("c") + P("N"))))))) + P("end:")))))))))))) + P("u") * (P("n") * (P("p") * (P("a") * (P("c") * (P("k") * (P("_") * (P("c") * (P("l") * (P("e") * (P("a") * (P("r") * (P(":") * (P("c") + P("N"))))))))))))))))))) + P("x") * (P("e") * (P("t") * (P("e") * (P("x") * (P("_") * (wildcard * P(":D") + P("i") * (P("f") * (P("_") * (P("e") * (P("n") * (P("g") * (P("i") * (P("n") * (P("e") * (P(":") * (P("TF") + P("F") + P("T")) + P("_p:")))))))))))))))) + P("K") * (P("V") * (P("_") * (P("p") * (P("r") * (P("o") * (P("c") * (P("e") * (P("s") * (P("s") * (P("_") * (P("no_space_removal_no_sanitize:NNn") + P("s") * (P("p") * (P("a") * (P("c") * (P("e") * (P("_") * (P("r") * (P("e") * (P("m") * (P("o") * (P("v") * (P("a") * (P("l") * (P("_") * (P("no_sanitize:NNn") + P("sanitize:NNn")))))))))))))))))))))))))) + P("l") * (P("u") * (P("a") * (P("t") * (P("e") * (P("x") * (P("_") * (wildcard * P(":D") + P("i") * (P("f") * (P("_") * (P("e") * (P("n") * (P("g") * (P("i") * (P("n") * (P("e") * (P(":") * (P("TF") + P("F") + P("T")) + P("_p:")))))))))))))) + P("_") * (P("now_x:n") + P("shipout_x:n") + P("escape_x:n")))) + P("_") * (P("t") * (P("l_replace_toks") + P("m") * (P("p") * (P("b_toks") + P("c") * (P("_") * (P("toks") + P("int"))!
 ) + P("a_toks")))) + P("last_box") + P("iow_line_length_int"))) + P("m") * (P("s") * (P("g") * (P("_") * (P("c") * (P("l") * (P("a") * (P("s") * (P("s") * (P("_") * (P("new:nn") + P("set:nn"))))))) + P("d") * (P("i") * (P("r") * (P("e") * (P("c") * (P("t") * (P("_") * (P("term:xx") + P("log:xx") + P("interrupt:xxxxx")))))))) + P("t") * (P("r") * (P("a") * (P("c") * (P("e") * (P(":") * (P("n") * (P("n") * (P("x") * (P("x") * (P("xx") + P("x")) + P("x")) + P("x")) + P("n"))))))) + P("wo_newlines:") + P("e") * (P("r") * (P("m") * (P(":") * (P("n") + P("x")))))) + P("newline:") + P("g") * (P("e") * (P("n") * (P("e") * (P("r") * (P("i") * (P("c") * (P("_") * (P("n") * (P("e") * (P("w") * (P(":") * (P("n") * (P("nn") + P("n")))))) + P("s") * (P("e") * (P("t") * (P(":") * (P("n") * (P("nn") + P("n")))))))))))))) + P("l") * (P("o") * (P("g") * (P(":") * (P("n") + P("x"))))) + P("i") * (P("n") * (P("t") * (P("e") * (P("r") * (P("r") * (P("u") * (P("p") * (P("t") * (P(":") * (P("nn") + P("xxx"))))))))))))))) + P("E") * (P("x") * (P("p") * (P("l") * (P("S") * (P("y") * (P("n") * (P("t") * (P("a") * (P("x") * (P("N") * (P("a") * (P("m") * (P("e") * (P("s") * (P("O") * (P("ff") + P("n"))))))))))))))))) + P("p") * (P("r") * (P("o") * (P("p") * (P("_") * (P("g") * (P("put:ccx") + P("g") * (P("e") * (P("t") * (P(":") * (P("N") * (P("nN") + P("VN")) + P("c") * (P("nN") + P("VN")))))) + P("d") * (P("e") * (P("l") * (P(":") * (P("N") * (P("n") + P("V")) + P("c") * (P("n") + P("V")))))) + P("e") * (P("t") * (P(":") * (P("Nn") + P("cn")) + P("_gdel:NnN")))) + P("d") * (P("i") * (P("s") * (P("p") * (P("l") * (P("a") * (P("y") * (P(":") * (P("c") + P("N")))))))) + P("e") * (P("l") * (P(":") * (P("N") * (P("n") + P("V")) + P("c") * (P("n") + P("V")))))) + P("i") * (P("f") * (P("_") * (P("i") * (P("n") * (P(":") * (P("c") * (P("c") * (P("TF") + P("F") + P("T")))))) + P("e") * (P("q") * (P(":") * (P("N") * (P("N") * (P("TF") + P("F") + P("T")) + P("c") * (P("TF") + P("F") + P("T"))) + P("c") * (P("N") * (P("TF") + P("F") + P("T")) + P("c!
 ") * (P("TF") + P("F") + P("T")))) + P("_") * (P("p") * (P(":") * (P("N") * (P("c") + P("N")) + P("c") * (P("c") + P("N"))))))))))))) + P("g") * (P("_") * (P("new_map_functions:Nn") + P("c") * (P("a") * (P("s") * (P("e") * (P("_") * (P("t") * (P("l") * (P(":") * (P("Nnn") + P("cnn")))) + P("s") * (P("t") * (P("r") * (P(":") * (P("nnn") + P("onn") + P("xxn"))))) + P("dim:nnn") + P("int:nnn")))))) + P("s") * (P("t") * (P("e") * (P("p") * (P("w") * (P("i") * (P("s") * (P("e") * (P("_") * (P("function:nnnN") + P("variable:nnnNn") + P("inline:nnnn"))))))))) + P("et_map_functions:Nn"))))) + P("t") * (P("e") * (P("x") * (P("_") * (wildcard * P(":D"))))) + P("d") * (P("f") * (P("t") * (P("e") * (P("x") * (P("_") * (wildcard * P(":D") + P("i") * (P("f") * (P("_") * (P("e") * (P("n") * (P("g") * (P("i") * (P("n") * (P("e") * (P(":") * (P("TF") + P("F") + P("T")) + P("_p:")))))))))))))))) + P("e") * (P("e") * (P("k") * (P("_") * (P("gafter:NN") + P("after:NN")))))) + P("q") * (P("u") * (P("a") * (P("r") * (P("k") * (P("_") * (P("i") * (P("f") * (P("_") * (P("r") * (P("e") * (P("c") * (P("u") * (P("r") * (P("s") * (P("i") * (P("o") * (P("n") * (P("_") * (P("t") * (P("a") * (P("i") * (P("l") * (P("_") * (P("b") * (P("r") * (P("e") * (P("a") * (P("k") * (P(":") * (P("N") + P("n")))))))))))))))))))))))))))))))) * eof
+obsolete.deprecated_csname = (P("k") * (P("e") * (P("y") * (P("s") * (P("_") * (P("s") * (P("e") * (P("t") * (P("_") * (P("f") * (P("i") * (P("l") * (P("t") * (P("e") * (P("r") * (P(":") * (P("n") * (P("n") * (P("o") * (P("nN") + P("N")) + P("v") * (P("nN") + P("N")) + P("V") * (P("nN") + P("N")) + P("n") * (P("nN") + P("N")) + P("n") + P("V") + P("v") + P("o"))))))))))))))))))) + P("l") * (P("_") * (P("k") * (P("e") * (P("y") * (P("s") * (P("_") * (P("key_tl") + P("path_tl")))))) + P("t") * (P("e") * (P("x") * (P("t") * (P("_") * (P("accents_tl") + P("letterlike_tl")))))))) + P("m") * (P("s") * (P("g") * (P("_") * (P("g") * (P("s") * (P("e") * (P("t") * (P(":") * (P("n") * (P("n") * (P("nn") + P("n")))))))))))) + P("t") * (P("e") * (P("x") * (P("t") * (P("_") * (P("t") * (P("i") * (P("t") * (P("l") * (P("e") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n"))))))))))))))) + P("l") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("c") * (P("n") * (P("TF") + P("F") + P("T")) + P("n")) + P("N") * (P("n") * (P("TF") + P("F") + P("T")) + P("n"))))))) + P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n")))))))))))) + P("m") * (P("i") * (P("x") * (P("e") * (P("d") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n")))))))))))) + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("nn") + P("n")))))))))))) + P("b") * (P("u") * (P("i") * (P("l") * (P("d") * (P("_") * (P("clear:N") + P("g") * (P("clear:N") + P("et:NN"))))))))))) + P("s") * (P("t") * (P("r") * (P("_") * (P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("f") + P("n")))))))))))) + P("declare_eight_bit_encoding:nnn") + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("f") + P("n")))))))))))) + P("f") * (P("o") * (P("l") * (P("d") * !
 (P("_") * (P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("n") + P("V"))))))) + P("c") * (P("a") * (P("s") * (P("e") * (P(":") * (P("n") + P("V"))))))))))))) + P("e") * (P("q") * (P("_") * (P("gset_map_x:NNn") + P("i") * (P("n") * (P("d") * (P("e") * (P("x") * (P("e") * (P("d") * (P("_") * (P("m") * (P("a") * (P("p") * (P("_") * (P("inline:Nn") + P("function:NN"))))))))))))) + P("set_map_x:NNn")))) + P("ys_load_deprecation:")) + P("p") * (P("d") * (P("f") * (P("_") * (P("o") * (P("b") * (P("j") * (P("e") * (P("c") * (P("t") * (P("_") * (P("w") * (P("r") * (P("i") * (P("t") * (P("e") * (P(":") * (P("n") * (P("n") + P("x")))))))) + P("new:nn"))))))))))) + P("e") * (P("e") * (P("k") * (P("_") * (P("c") * (P("a") * (P("t") * (P("c") * (P("o") * (P("d") * (P("e") * (P("_") * (P("ignore_spaces:N") + P("remove_ignore_spaces:N")))))))) + P("h") * (P("a") * (P("r") * (P("c") * (P("o") * (P("d") * (P("e") * (P("_") * (P("ignore_spaces:N") + P("remove_ignore_spaces:N")))))))))) + P("m") * (P("e") * (P("a") * (P("n") * (P("i") * (P("n") * (P("g") * (P("_") * (P("ignore_spaces:N") + P("remove_ignore_spaces:N"))))))))))))) + P("r") * (P("o") * (P("p") * (P("_") * (P("g") * (P("p") * (P("u") * (P("t") * (P("_") * (P("i") * (P("f") * (P("_") * (P("n") * (P("e") * (P("w") * (P(":") * (P("c") * (P("Vn") + P("n") * (P("n") + P("V"))) + P("N") * (P("Vn") + P("n") * (P("n") + P("V"))))))))))))))) + P("p") * (P("u") * (P("t") * (P("_") * (P("i") * (P("f") * (P("_") * (P("n") * (P("e") * (P("w") * (P(":") * (P("c") * (P("Vn") + P("n") * (P("n") + P("V"))) + P("N") * (P("Vn") + P("n") * (P("n") + P("V"))))))))))))))))))) + P("i") * (P("o") * (P("w") * (P("_") * (P("s") * (P("h") * (P("i") * (P("p") * (P("o") * (P("u") * (P("t") * (P("_") * (P("x") * (P(":") * (P("c") * (P("n") + P("x")) + P("N") * (P("n") + P("x")))))))))))))))) + P("c") * (P("s_argument_spec:N") + P("h") * (P("a") * (P("r") * (P("_") * (P("s") * (P("t") * (P("r") * (P("_") * (P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("_case:N") + P("case:N")))))) + P("m!
 ixed_case:N") + P("f") * (P("o") * (P("l") * (P("d") * (P("_case:N") + P("case:N"))))) + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("_case:N") + P("case:N")))))) + P("titlecase:N"))))) + P("l") * (P("o") * (P("w") * (P("e") * (P("r") * (P("_case:N") + P("case:N")))))) + P("mixed_case:N") + P("f") * (P("o") * (P("l") * (P("d") * (P("_case:N") + P("case:N"))))) + P("u") * (P("p") * (P("p") * (P("e") * (P("r") * (P("_case:N") + P("case:N")))))) + P("t") * (P("itlecase:N") + P("o") * (P("_") * (P("utfviii_bytes:n") + P("nfd:N")))))))))) * eof
 -- luacheck: pop
 
 return obsolete

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-parsers.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-parsers.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-parsers.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -11,6 +11,7 @@
 
 ---- Tokens
 local ampersand = P("&")
+local at_sign = P("@")
 local backslash = P([[\]])
 local circumflex = P("^")
 local colon = P(":")
@@ -76,7 +77,7 @@
 end
 
 ---- Syntax recognized by TeX's input and token processors
-local optional_spaces = space^0
+local optional_spaces = expl3_catcodes[9]^0
 local optional_spaces_and_newline = (
   optional_spaces
   * (
@@ -99,7 +100,7 @@
   (
     (
       linechar
-      - (space * #blank_or_empty_last_line)
+      - (expl3_catcodes[9] * #blank_or_empty_last_line)
     )^1
     * (
       blank_or_empty_last_line / ""
@@ -108,7 +109,7 @@
   + (
     (
       linechar
-      - (space * #blank_line)
+      - (expl3_catcodes[9] * #blank_line)
     )^0
     * (
       blank_line / ""
@@ -157,15 +158,20 @@
   * expl3_catcodes[2]
 )
 
-local weird_argument_specifier = S("wD")
-local argument_specifier = S("NncVvoxefTFp") + weird_argument_specifier
+local weird_argument_specifier = S("w")
+local do_not_use_argument_specifier = S("D")
+local argument_specifier = (
+  S("NncVvoxefTFp")
+  + weird_argument_specifier
+  + do_not_use_argument_specifier
+)
 local argument_specifiers = argument_specifier^0 * eof
-local weird_argument_specifiers = (
+local do_not_use_argument_specifiers = (
   (
     argument_specifier
-    - weird_argument_specifier
+    - do_not_use_argument_specifier
   )^0
-  * weird_argument_specifier
+  * do_not_use_argument_specifier
 )
 
 ---- Function, variable, and constant names
@@ -246,7 +252,7 @@
 )
 
 local non_expl3_csname = (
-  letter^1
+  (letter + at_sign)^1
   * eof
 )
 
@@ -255,56 +261,71 @@
   linechar
   + newline
   - expl3_catcodes[0]
+  - expl3_catcodes[9]
   - expl3_catcodes[14]
 )
-local commented_line = (
-  (
+local function commented_line(closer)
+  return (
     (
       commented_line_letter
-      - newline
+      - closer
     )^1  -- initial state
     + (
       expl3_catcodes[0]  -- even backslash
       * (
         expl3_catcodes[0]
-        + #newline
+        + #closer
       )
     )^1
     + (
-      expl3_catcodes[0]
+      expl3_catcodes[0]  -- odd backslash
       * (
-        expl3_catcodes[14]
+        expl3_catcodes[9]
+        + expl3_catcodes[14]
         + commented_line_letter
       )
     )
+    + (
+      #(
+        expl3_catcodes[9]^1
+        * -expl3_catcodes[14]
+      )
+      * expl3_catcodes[9]^1  -- spaces
+    )
   )^0
   * (
-    #expl3_catcodes[14]
+    #(
+      expl3_catcodes[9]^0
+      * expl3_catcodes[14]
+    )
     * Cp()
     * (
       (
-        expl3_catcodes[14]  -- comment
+        expl3_catcodes[9]^0
+        * expl3_catcodes[14]  -- comment
         * linechar^0
         * Cp()
-        * newline
-        * #blank_line  -- blank line
+        * closer
+        * (
+          #blank_line  -- blank line
+          + expl3_catcodes[9]^0  -- leading spaces
+        )
       )
-      + expl3_catcodes[14]  -- comment
-      * linechar^0
-      * Cp()
-      * newline
-      * optional_spaces  -- leading spaces
     )
-    + newline
+    + closer
   )
+end
+
+local commented_lines = Ct(
+  commented_line(newline)^0
+  * commented_line(eof)
+  * eof
+  + eof
 )
 
 -- Explcheck issues
 local issue_code = (
   S("EeSsTtWw")
-  / function(prefix)
-    return prefix:lower()
-  end
   * decimal_digit
   * decimal_digit
   * decimal_digit
@@ -311,6 +332,8 @@
 )
 local ignored_issues = Ct(
   optional_spaces
+  * expl3_catcodes[14]
+  * optional_spaces
   * P("noqa")
   * (
     P(":")
@@ -349,6 +372,94 @@
 local expl_syntax_on = expl3_catcodes[0] * P([[ExplSyntaxOn]])
 local expl_syntax_off = expl3_catcodes[0] * P([[ExplSyntaxOff]])
 
+---- Commands from LaTeX style files
+local latex_style_file_csname =
+(
+  -- LaTeX2e package writer commands
+  -- See <https://www.latex-project.org/help/documentation/clsguide.pdf>.
+  P("AddToHook")
+  + P("AtBeginDocument")
+  + P("AtEndDocument")
+  + P("AtEndOfClass")
+  + P("AtEndOfPackage")
+  + P("BCPdata")
+  + P("CheckCommand")
+  + P("ClassError")
+  + P("ClassInfo")
+  + P("ClassWarning")
+  + P("ClassWarningNoLine")
+  + P("CurrentOption")
+  + P("DeclareInstance")
+  + P("DeclareKeys")
+  + P("DeclareOption")
+  + P("DeclareRobustCommand")
+  + P("DeclareTemplateCode")
+  + P("DeclareTemplateInterface")
+  + P("DeclareUnknownKeyHandler")
+  + P("ExecuteOptions")
+  + P("IfClassAtLeastTF")
+  + P("IfClassLoadedTF")
+  + P("IfClassLoadedWithOptionsTF")
+  + P("IfFileAtLeastTF")
+  + P("IfFileExists")
+  + P("IfFileLoadedTF")
+  + P("IfFormatAtLeastTF")
+  + P("IfPackageAtLeastTF")
+  + P("IfPackageLoadedTF")
+  + P("IfPackageLoadedWithOptionsTF")
+  + P("InputIfFileExists")
+  + P("LinkTargetOff")
+  + P("LinkTargetOn")
+  + P("LoadClass")
+  + P("LoadClassWithOptions")
+  + P("MakeLinkTarget")
+  + P("MakeLowercase")
+  + P("MakeTitlecase")
+  + P("MakeUppercase")
+  + P("MessageBreak")
+  + P("NeedsTeXFormat")
+  + P("NewDocumentCommand")
+  + P("NewDocumentEnvironment")
+  + P("NewProperty")
+  + P("NewTemplateType")
+  + P("NextLinkTarget")
+  + P("OptionNotUsed")
+  + P("PackageError")
+  + P("PackageInfo")
+  + P("PackageWarning")
+  + P("PackageWarningNoLine")
+  + P("PassOptionsToClass")
+  + P("PassOptionsToPackage")
+  + P("ProcessKeyOptions")
+  + P("ProcessOptions")
+  + P("ProvidesClass")
+  + P("ProvidesFile")
+  + P("ProvidesPackage")
+  + P("RecordProperties")
+  + P("RefProperty")
+  + P("RefUndefinedWarn")
+  + P("RequirePackage")
+  + P("RequirePackageWithOptions")
+  + P("SetKeys")
+  + P("SetProperty")
+  + P("UseInstance")
+  -- LaTeX3 package writer commands
+  + P("ProvidesExplClass")
+  + P("ProvidesExplPackage")
+)
+
+local latex_style_file_content = (
+  (
+    any
+    - #(
+      expl3_catcodes[0]
+      * latex_style_file_csname
+    )
+  )^0
+  * expl3_catcodes[0]
+  * latex_style_file_csname
+)
+
 ---- Assigning functions
 local expl3_function_assignment_csname = (
   P("cs_")
@@ -397,9 +508,10 @@
 return {
   any = any,
   argument_specifiers = argument_specifiers,
-  commented_line = commented_line,
+  commented_lines = commented_lines,
   decimal_digit = decimal_digit,
   determine_expl3_catcode = determine_expl3_catcode,
+  do_not_use_argument_specifiers = do_not_use_argument_specifiers,
   double_superscript_convention = double_superscript_convention,
   eof = eof,
   fail = fail,
@@ -415,10 +527,10 @@
   expl_syntax_off = expl_syntax_off,
   expl_syntax_on = expl_syntax_on,
   ignored_issues = ignored_issues,
+  latex_style_file_content = latex_style_file_content,
   linechar = linechar,
   newline = newline,
   non_expl3_csname = non_expl3_csname,
   provides = provides,
   tex_lines = tex_lines,
-  weird_argument_specifiers = weird_argument_specifiers,
 }

Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-preprocessing.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-preprocessing.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-preprocessing.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,13 +1,15 @@
 -- The preprocessing step of static analysis determines which parts of the input files contain expl3 code.
 
+local get_option = require("explcheck-config")
+local new_range = require("explcheck-ranges")
 local parsers = require("explcheck-parsers")
 local utils = require("explcheck-utils")
 
 local lpeg = require("lpeg")
-local Cp, Ct, P, V = lpeg.Cp, lpeg.Ct, lpeg.P, lpeg.V
+local Cp, P, V = lpeg.Cp, lpeg.P, lpeg.V
 
 -- Preprocess the content and register any issues.
-local function preprocessing(issues, content, options)
+local function preprocessing(issues, pathname, content, options)
 
   -- Determine the bytes where lines begin.
   local line_starting_byte_numbers = {}
@@ -16,18 +18,11 @@
     table.insert(line_starting_byte_numbers, line_start)
   end
 
-  local function line_too_long(range_start, range_end)
-    issues:add('s103', 'line too long', range_start, range_end + 1)
-  end
-
   local line_numbers_grammar = (
     Cp() / record_line
     * (
       (
-        (
-          Cp() * parsers.linechar^(utils.get_option(options, 'max_line_length') + 1) * Cp() / line_too_long
-          + parsers.linechar^0
-        )
+        parsers.linechar^0
         * parsers.newline
         * Cp()
       ) / record_line
@@ -40,7 +35,7 @@
     local transformed_index = 0
     local numbers_of_bytes_removed = {}
     local transformed_text_table = {}
-    for index, text_position in ipairs(lpeg.match(Ct(parsers.commented_line^1), content)) do
+    for index, text_position in ipairs(lpeg.match(parsers.commented_lines, content)) do
       local span_size = text_position - transformed_index - 1
       if span_size > 0 then
         if index % 2 == 1 then  -- chunk of text
@@ -53,17 +48,19 @@
             local comment_line_number = utils.convert_byte_to_line_and_column(line_starting_byte_numbers, transformed_index + 1)
             assert(comment_line_number <= #line_starting_byte_numbers)
             local comment_range_start = line_starting_byte_numbers[comment_line_number]
-            local comment_range_end
+            local comment_range_end, comment_range
             if(comment_line_number + 1 <= #line_starting_byte_numbers) then
-              comment_range_end = line_starting_byte_numbers[comment_line_number + 1] - 1
+              comment_range_end = line_starting_byte_numbers[comment_line_number + 1]
+              comment_range = new_range(comment_range_start, comment_range_end, "exclusive", #content)
             else
               comment_range_end = #content
+              comment_range = new_range(comment_range_start, comment_range_end, "inclusive", #content)
             end
             if #ignored_issues == 0 then  -- ignore all issues on this line
-              issues:ignore(nil, comment_range_start, comment_range_end)
+              issues:ignore(nil, comment_range)
             else  -- ignore specific issues on this line or everywhere (for file-wide issues)
               for _, identifier in ipairs(ignored_issues) do
-                issues:ignore(identifier, comment_range_start, comment_range_end)
+                issues:ignore(identifier, comment_range)
               end
             end
           end
@@ -100,41 +97,39 @@
   local expl_ranges = {}
 
   local function capture_range(range_start, range_end)
-    range_start, range_end = map_back(range_start), map_back(range_end)
-    table.insert(expl_ranges, {range_start, range_end})
+    local range = new_range(range_start, range_end, "exclusive", #transformed_content, map_back, #content)
+    table.insert(expl_ranges, range)
   end
 
   local function unexpected_pattern(pattern, code, message, test)
     return Cp() * pattern * Cp() / function(range_start, range_end)
-      range_start, range_end = map_back(range_start), map_back(range_end)
+      local range = new_range(range_start, range_end, "exclusive", #transformed_content, map_back, #content)
       if test == nil or test() then
-        issues:add(code, message, range_start, range_end + 1)
+        issues:add(code, message, range)
       end
     end
   end
 
   local num_provides = 0
-  local Opener = unexpected_pattern(
-    parsers.provides,
-    "e104",
-    [[multiple delimiters `\ProvidesExpl*` in a single file]],
-    function()
-      num_provides = num_provides + 1
-      return num_provides > 1
-    end
-  )
-  local Closer = parsers.fail
-  if not utils.get_option(options, 'expect_expl3_everywhere') then
+  local Opener, Closer = parsers.fail, parsers.fail
+  local expl3_detection_strategy = get_option('expl3_detection_strategy', options, pathname)
+  if expl3_detection_strategy ~= 'always' then
     Opener = (
       parsers.expl_syntax_on
-      + Opener
+      + unexpected_pattern(
+        parsers.provides,
+        "e104",
+        [[multiple delimiters `\ProvidesExpl*` in a single file]],
+        function()
+          num_provides = num_provides + 1
+          return num_provides > 1
+        end
+      )
     )
-    Closer = (
-      parsers.expl_syntax_off
-      + Closer
-    )
+    Closer = parsers.expl_syntax_off
   end
 
+  local has_expl3like_material = false
   local analysis_grammar = P{
     "Root";
     Root = (
@@ -154,7 +149,11 @@
         + unexpected_pattern(
             parsers.expl3like_material,
             "e102",
-            "expl3 material in non-expl3 parts"
+            "expl3 material in non-expl3 parts",
+            function()
+              has_expl3like_material = true
+              return true
+            end
           )
         + (parsers.any - V"Opener")
       )^0
@@ -178,15 +177,61 @@
   }
   lpeg.match(analysis_grammar, transformed_content)
 
-  -- If no parts were detected, assume that the whole input file is in expl3.
+  -- Determine whether the pathname/content looks like it originates from a LaTeX style file.
+  local seems_like_latex_style_file
+  local suffix = utils.get_suffix(pathname)
+  if suffix == ".cls" or suffix == ".opt" or suffix == ".sty" then
+    seems_like_latex_style_file = true
+  else
+    seems_like_latex_style_file = lpeg.match(parsers.latex_style_file_content, transformed_content)
+  end
+
+  -- If no expl3 parts were detected, decide whether no part or the whole input file is in expl3.
   if(#expl_ranges == 0 and #content > 0) then
-    table.insert(expl_ranges, {1, #content + 1})
     issues:ignore('e102')
-    if not utils.get_option(options, 'expect_expl3_everywhere') then
-      issues:add('w100', 'no standard delimiters')
+    if expl3_detection_strategy == "precision" then
+      -- Assume that no part of the input file is in expl3.
+    elseif expl3_detection_strategy == "recall" or expl3_detection_strategy == "always" then
+      -- Assume that the whole input file is in expl3.
+      if expl3_detection_strategy == "recall" then
+        issues:add('w100', 'no standard delimiters')
+      end
+      local range = new_range(1, #content, "inclusive", #content)
+      table.insert(expl_ranges, range)
+    elseif expl3_detection_strategy == "auto" then
+      -- Use context clues to determine whether no part or the whole
+      -- input file is in expl3.
+      if has_expl3like_material then
+        issues:add('w100', 'no standard delimiters')
+        local range = new_range(1, #content, "inclusive", #content)
+        table.insert(expl_ranges, range)
+      end
+    else
+      assert(false, 'Unknown strategy "' .. expl3_detection_strategy .. '"')
     end
   end
-  return line_starting_byte_numbers, expl_ranges
+
+  -- Check for overlong lines within the expl3 parts.
+  for _, expl_range in ipairs(expl_ranges) do
+    local offset = expl_range:start() - 1
+
+    local function line_too_long(range_start, range_end)
+      local range = new_range(offset + range_start, offset + range_end, "exclusive", #transformed_content, map_back, #content)
+      issues:add('s103', 'line too long', range)
+    end
+
+    local overline_lines_grammar = (
+      (
+        Cp() * parsers.linechar^(get_option('max_line_length', options, pathname) + 1) * Cp() / line_too_long
+        + parsers.linechar^0
+      )
+      * parsers.newline
+    )^0
+
+    lpeg.match(overline_lines_grammar, transformed_content:sub(expl_range:start(), expl_range:stop()))
+  end
+
+  return line_starting_byte_numbers, expl_ranges, seems_like_latex_style_file
 end
 
 return preprocessing

Added: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-ranges.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-ranges.lua	                        (rev 0)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-ranges.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -0,0 +1,77 @@
+-- A class for working with index ranges in arrays.
+
+local Range = {}
+
+-- Create a new range based on the start/end indices, the type of the end index
+-- (inclusive/exclusive), the size of the array that contains the range, and an
+-- optional nondecreasing map-back function from indices in the array to
+-- indices in an original array and the size of the original array.
+--
+-- Since empty ranges are usually a mistake, they are not allowed.
+-- For example, `Range:new(index, index, "exclusive", #array)` will cause an
+-- assertion error. The exception to this rule are empty arrays, which permit
+-- the empty range `Range:new(0, 0, "inclusive", 0)`.
+function Range.new(cls, range_start, range_end, end_type, transformed_array_size, map_back, original_array_size)
+  -- Instantiate the class.
+  local new_object = {}
+  setmetatable(new_object, cls)
+  cls.__index = cls
+  -- Check pre-conditions.
+  if transformed_array_size == 0 then
+    assert(range_start == 0)
+  else
+    assert(range_start >= 1)
+    assert(range_start <= transformed_array_size)
+  end
+  assert(end_type == "inclusive" or end_type == "exclusive")
+  if end_type == "exclusive" then
+    -- Convert exclusive range end to inclusive.
+    range_end = range_end - 1
+  end
+  if transformed_array_size == 0 then
+    assert(range_end == 0)
+  else
+    assert(range_end >= range_start)
+    assert(range_end <= transformed_array_size)
+  end
+  local mapped_range_start, mapped_range_end
+  if map_back ~= nil then
+    -- Apply the map-back function to the endpoints of the range.
+    assert(original_array_size ~= nil)
+    mapped_range_start = map_back(range_start)
+    if original_array_size == 0 then
+      assert(mapped_range_start == 0)
+    else
+      assert(mapped_range_start >= 1)
+      assert(mapped_range_start <= original_array_size)
+    end
+    mapped_range_end = map_back(range_end)
+    if original_array_size == 0 then
+      assert(mapped_range_end == 0)
+    else
+      assert(mapped_range_end >= mapped_range_start)
+      assert(mapped_range_end <= original_array_size)
+    end
+  else
+    mapped_range_start = range_start
+    mapped_range_end = range_end
+  end
+  -- Initialize the class.
+  new_object.range_start = mapped_range_start
+  new_object.range_end = mapped_range_end
+  return new_object
+end
+
+-- Get the inclusive start of the range, optionally mapped back to the original array.
+function Range:start()
+  return self.range_start
+end
+
+-- Get the inclusive end of the range, optionally mapped back to the original array.
+function Range:stop()
+  return self.range_end
+end
+
+return function(...)
+  return Range:new(...)
+end


Property changes on: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-ranges.lua
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Modified: branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-utils.lua
===================================================================
--- branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-utils.lua	2025-02-24 20:31:39 UTC (rev 74270)
+++ branches/branch2024.final/Master/texmf-dist/scripts/expltools/explcheck-utils.lua	2025-02-24 20:32:08 UTC (rev 74271)
@@ -1,7 +1,5 @@
 -- Common functions used by different modules of the static analyzer explcheck.
 
-local config = require("explcheck-config")
-
 -- Convert a byte number in a file to a line and column number in a file.
 local function convert_byte_to_line_and_column(line_starting_byte_numbers, byte_number)
   local line_number = 0
@@ -18,15 +16,28 @@
   return line_number, column_number
 end
 
--- Get the value of an option or the default value if unspecified.
-local function get_option(options, key)
-  if options == nil or options[key] == nil then
-    return config[key]
+-- Convert a pathname of a file to the suffix of the file.
+local function get_suffix(pathname)
+  return pathname:gsub(".*%.", "."):lower()
+end
+
+-- Convert a pathname of a file to the base name of the file.
+local function get_basename(pathname)
+  return pathname:gsub(".*[\\/]", "")
+end
+
+-- Convert a pathname of a file to the pathname of its parent directory.
+local function get_parent(pathname)
+  if pathname:find("[\\/]") then
+    return pathname:gsub("(.*)[\\/].*", "%1")
+  else
+    return "."
   end
-  return options[key]
 end
 
 return {
   convert_byte_to_line_and_column = convert_byte_to_line_and_column,
-  get_option = get_option,
+  get_basename = get_basename,
+  get_parent = get_parent,
+  get_suffix = get_suffix,
 }



More information about the tex-live-commits mailing list.