Static analysis of expl3 programs (11): Inter-file dependencies, segments, and code coverage

Today, I’m excited to release the next major update to expltools, a bundle that includes the static analysis tool explcheck for the expl3 programming language. This update improves upon the semantic analysis processing step introduced in the previous post and lays groundwork for the final processing step of flow analysis.

Inter-file dependencies

The original project proposal assumed that files would be checked individually. In practice, this limited the semantic analysis of multi-file projects, where one file defines a symbol and another uses it. To address this, the new release introduces the concept of file groups: sets of files that are assumed to be used together.

For example, suppose we have a file foo.tex:

\cs_new:Nn
  \__example_foo:n
  { foo }
\cs_generate_variant:Nn
  \__example_foo:n
  { V }

and another file bar.tex:

\__example_foo:V
  \l_tmpa_tl

Previously, running explcheck foo.tex bar.tex produced:

Checking 2 files

Checking foo.tex                                         1 warning

    foo.tex:4:1:              W402 unused private function variant

Checking bar.tex                                           1 error

    bar.tex:1:1:                E408 calling an undefined function

Total: 1 error, 1 warning in 2 files

With the new release, files from the same directory are automatically grouped, and the result changes to:

Checking 2 files

Checking foo.tex                                                OK
Checking bar.tex                                                OK

Total: 0 errors, 0 warnings in 2 files

This greatly reduces false positives.

You can control this behavior with the --group-files option or by using + and , between filenames to group or separate them.

In addition, explcheck will report missing symbols from external packages. To suppress these, use the imported_prefixes option in your configuration.

Segments

Earlier versions of explcheck had special-case support for only one kind of nested code: function replacement texts. For instance, consider the example from a previous post:

\cs_new:Nn
  \example_foo:n
  {
    \cs_new:Nn
      \example_bar:n
      {
        #1,~##1!
      }
  }
\example_foo:n { Hello }
\example_bar:n { world }

Here we have three top-level calls (\cs_new:Nn, \example_foo:n, \example_bar:n) and two replacement texts.

But expl3 contains many more forms of nested code: T- and F-branches of conditional functions, loop bodies (e.g., \ior_map_inline:Nn), key–value definitions, and more. Ad-hoc support for each would be brittle, and the upcoming flow analysis requires a uniform way to connect code across different nesting types and levels.

To address this, the new release introduces segments, a unified representation for both top-level and nested code. This simplifies implementation and makes future extensions easier.

Entity relationship diagram of the analysis data model, highlighting our representation of input files.
Image: Entity relationship diagram of the analysis data model, highlighting our representation of input files.

The first new segment type added is support for T- and F-branches of conditional functions, and future updates will continue expanding segment coverage.

Code coverage

To show how well explcheck understands a piece of code, the --verbose output now reports code coverage: the ratio of well-understood expl3 tokens to the total number of tokens. A token is well-understood if it is either simple text or part of a recognized statement in the most deeply nested segment containing it.

For example:

\cs_new:Nn
  \example_baz:n
  { \unexpected }
some~text
  • The nine tokens in some~text are well-understood (simple text).
  • The four tokens \cs_new:Nn, \example_baz:n, {, and } are well-understood (recognized statement).
  • The token \unexpected is not well-understood, because there are no recognized statements in the replacement text.

Thus, coverage = 13/14 tokens ≈ 93%.

For context, explcheck currently achieves ~14% code coverage across all expl3 code in TeX Live. Future releases aim to raise this figure by recognizing more statements, segment types, and plain TeX / LaTeX2e constructs.

Working with the community

Since March, I’ve been reaching out to package maintainers with reports of issues found by explcheck. Of 11 reported issues, 8 have already been fixed:

  1. BITNP/BIThesis#604: fixed
  2. jspitz/jslectureplanner#7: fixed
  3. dbitouze/nwejm#5
  4. dbitouze/gzt#56
  5. michal-h21/responsive-latex#1: fixed
  6. fpantigny/nicematrix#12: fixed
  7. josephwright/siunitx#796: wontfix
  8. BITNP/BIThesis#640: fixed
  9. se2p/se2thesis#23: fixed
  10. John02139/asmeconf#11: fixed
  11. dickimaw-books.com#303: fixed

Following today’s update, at least 15 new potential issues were detected in public TeX Live repositories. Reports have been submitted, including:

  1. An e-mail to Martin Hensel about the issues detected in package mhchem
  2. An e-mail to Herbert Voß about the issues detected in package hvarabic
  3. An e-mail to Andrew Parsloe about the issues detected in their many packages
  4. nwafu_nan/nwafuthesis-l3#ID0M68
  5. nwafu_nan/chinesechess#ID0MAM
  6. samcarter/beamertheme-tcolorbox#5
  7. hust-latex/hustthesis#45
  8. Zeta611/simplebnf#10
  9. irenier/sysuthesis#1
  10. slatex/sTeX#453
  11. dcpurton/scripture#117
  12. luatexja/luatexja#35
  13. leo-colisson/robust-externalize#59
  14. Vidabe/cooking-units#32
  15. fpantigny/nicematrix#117

Let’s see how many more we can fix before the next update! 😉

Get involved!

Your feedback is invaluable. Feel free to contribute or share your thoughts by visiting the project repository. More improvements are on the way as explcheck continues to grow.

Written on September 30, 2025
Last updated on September 30, 2025