Skip to content

[#1147] Fail the doc build when a page leaves an AsciiDoc attribute unresolved - #1148

Open
vharseko wants to merge 2 commits into
OpenIdentityPlatform:masterfrom
vharseko:issue-1147
Open

vharseko wants to merge 2 commits into
OpenIdentityPlatform:masterfrom
vharseko:issue-1147

Conversation

@vharseko

@vharseko vharseko commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Fixes #1147

Problem

A page that uses an attribute it does not define, or a verbatim block that uses an attribute without attribute subs, publishes {name} literally, and the build stays green. The only render in the build is the PDF book, where an attribute defined in an earlier chapter also resolves in the later ones; the site renders every chapter as a page of its own, so a page can be broken on the site while the PDF is fine (chap-uninstall.adoc before #1140). The antora goal of doc-maven-plugin only rewrites the .adoc files, it renders nothing.

Change

  • New check-attribute-references execution of asciidoctor-maven-plugin in the man-pages profile (phase verify, so it runs on the ubuntu legs of the PR build). It renders every .adoc of target/asciidoc/source on its own to throwaway HTML, with each file's own directory as the base, and fails on any WARN that mentions an attribute:
    1. an attribute that is not defined: attribute-missing=warn;
    2. an attribute in a listing, literal or passthrough block without attribute subs, or in a literal table cell (l|, cols="1l"): the new literal-attribute-references.rb tree processor. A reference counts when its name is an attribute at that point of the page, an intrinsic one such as {nbsp}, or one that a :name: entry anywhere in the doc sources sets - the last catches a page that neither defines nor substitutes the attribute. Braces around any other name ({SSHA} values, {cn} in MakeLDIF templates) are left alone. Without attribute subs a backslash does not escape, so \{name} is reported too, as published with its backslash.
      The advice in the warning is subs="+attributes", which keeps the block's default subs; a literal cell takes no subs, so it is told to become an a| cell with such a listing.
  • LiteralAttributeReferencesTest reads the execution's configuration from the pom - the required extensions, the attributes, the failIf - renders pages with it, and checks which of them would fail the build: 11 that must fail, 7 that must pass. The pom module binds testCompile and test for it, and the plugin and the test share asciidoctorj ${asciidoctorj.version} (2.5.11, the plugin's own).
  • The 16 blocks of the guides with subs="attributes" take subs="+attributes". The replacing form had dropped specialcharacters, so chap-writing-plugins published (build <unknown>, revision <unknown>) as raw tags, which a browser hides; the >, >>>> and && of the other blocks were raw as well. The rendered HTML of the six pages differs only in that escaping.
  • The two literal placeholders the check found on master are escaped like the generated reference does since [#1131] Escape the option placeholders of the generated reference, so AsciiDoc no longer reads them as missing attributes #1142: {options} in the windows-service synopsis and {path} in the password policy chapter (both were shown with braces, but by accident).

Asciidoctor resets the document attributes to the header before tree processors run, so the extension replays the attribute entries of the body in document order, as the converter does, and resets them again afterwards.

Limits

  • asciidoctor-maven-plugin 2.2.6 evaluates failIf after each file, so the build stops at the first page that fails; fix it and run mvn -Pdistribution-unix,man-pages -pl opendj-doc-generated-ref asciidoctor:process-asciidoc@check-attribute-references again for the next one (both profiles activate by themselves only on Linux). This is noted in the pom.
  • Asciidoctor names no file for a missing attribute; it is the page of the Converted line that follows the warning.
  • The plugin makes every Maven project property an attribute (product.name is {product-name}), and the site defines none of them, so a page that uses one passes the check and still shows the braces on the site. The plugin has no switch for it; no page uses one today. This is noted in the pom.

Verification

  • Current master (with [#1128] Replace the DocBook xinclude and olink leftovers of the generated reference with AsciiDoc #1132), with the extension of this round: 512 pages, no warning at all, BUILD SUCCESS; the check itself takes about 35 s. The hand-written pages were pre-processed again from this branch; the generated reference pages come from my last module build.
  • Before [#1129] Point the guides' Javadoc links at the published apidocs, and resolve {opendj-version} in the uninstall and monitoring pages #1140 it reported exactly what [#1129] Point the guides' Javadoc links at the published apidocs, and resolve {opendj-version} in the uninstall and monitoring pages #1140 fixed: the undefined {opendj-version} in chap-uninstall.adoc and the listing without subs in chap-monitoring.adoc.
  • Three mutants on the pre-processed copies each fail the build: :opendj-version: removed from chap-uninstall; subs="attributes" removed from a listing in chap-monitoring; both the subs and the definition removed from chap-monitoring (caught only through the :name: entries of the other pages).
  • LiteralAttributeReferencesTest: 18 green. Each of 13 mutants turns it red - in the extension: no report at all, the subs guard inverted, no literal-cell branch, no intrinsic names, no replay of body entries, the sources path back in the glob pattern, \{name} skipped, the old subs="attributes" advice; in the pom: a typo in containsText, failIf on ERROR, no attribute-missing, no literal-attribute-sources, the extension not required.
  • The HTML rendered with and without the extension is byte-identical (29 pages of two guides, books included), so replaying and resetting the attributes changes nothing in the output.
  • Edge cases checked by hand: header and body attributes, unset attributes, literal paragraphs, passthrough blocks, listings inside AsciiDoc table cells, literal cells, {SSHA}/{givenName} braces, a sources path with { and [.
  • Not run locally: the full module build with the PDF (killed for memory on my machine); the check ran on the sources the build had already pre-processed. CI runs the whole path.

@maximthomas maximthomas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: The check renders each page the way the site does and stays quiet on braces that were never attributes.

  • process replays the body's attribute entries with playback_attributes and restores every document afterwards (literal-attribute-references.rb:46-60), and the description shows the HTML is byte-identical with and without the extension.
  • names_in counts a {name} only when some :name: entry in the sources sets it (literal-attribute-references.rb:39-43), so {SSHA} values and MakeLDIF {cn} tokens stay quiet, while a page that neither defines nor substitutes {opendj-version} is still caught.
  • The three mutants in the description, and the fact that the check finds both defects #1140 fixed again, show that both halves of the check fire on real pages.

issue (non-blocking): A literal-style table cell (l|, or a cols spec with l) is never checked.

opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb:55-56

Asciidoctor::Block === block is false for Asciidoctor::Table::Cell, whose ancestors are AbstractBlock and AbstractNode. In gem 2.0.20 a literal cell gets content_model :verbatim with BASIC_SUBS (table.rb:312-314). A page that defines :opendj-version: and has l|literal cell {opendj-version} therefore publishes the braces. attribute-missing cannot fire there, and the extension never visits the cell. A probe with the jar's gem warned on the listing and the indented literal of the same page, but not on the cell. No such cell exists at HEAD. A cell also cannot take a subs attribute, so it needs its own advice.

      if Asciidoctor::Table::Cell === block
        check block, block.text, names if block.content_model == :verbatim
      elsif Asciidoctor::Block === block &&
          (block.content_model == :verbatim || block.content_model == :raw) && !(block.subs.include? :attributes)
        check block, (block.lines.join Asciidoctor::LF), names
      end

check then scans its text argument instead of block.lines. A literal cell's text only applies specialcharacters, so its braces stay intact.


issue (non-blocking): Dir.glob treats the literal-attribute-sources path as a pattern, so a checkout path that contains [ or { silently empties the cross-page name set.

opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb:40

File.join dir, '**', '*.adoc' with dir = ${project.build.directory}/asciidoc/source is not escaped. A probe on MRI 3.2.3 matched 0 files under ws[1]/src and ws{a}/src, and 1 under plain/src. The empty set is memoized with no message. After that, a listing that uses {opendj-version}, on a page that neither defines nor substitutes it, is no longer reported. CI paths contain no metacharacter, so only local checkouts are hit.

  def self.names_in dir
    @names_by_dir[dir] ||= Dir.glob('**/*.adoc', base: dir).each_with_object(Set.new) do |path, names|
      File.foreach((File.join dir, path), encoding: 'UTF-8') {|line| names << $1.downcase if EntryRx =~ line }
    end
  end

suggestion (non-blocking): Nothing pins the check itself. A mutant that disables the extension or drops attribute-missing=warn keeps the build green.

opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb:66, opendj-doc-generated-ref/pom.xml:639-648

The execution reads only the real pre-processed pages, and at HEAD they are clean, so its only observable is a green verify. Both placeholders the PR escapes are paragraph text, which attribute-missing caught, so the tree processor has never failed a build in the repo. These mutants all leave HEAD green, measured on scratch copies: next if true at line 66, the subs.include? :attributes guard inverted, a typo in <containsText>, <attribute-missing>warn removed. A scratch pom with this execution's configuration exits 1 on a planted listing and on {undefinedthing}, so the code works today. The module has no src/test, so nothing keeps it working.

public class LiteralAttributeReferencesTest {
    private static List<String> warnings(String page) {
        Asciidoctor asciidoctor = Asciidoctor.Factory.create();
        asciidoctor.requireLibrary(
                new File("src/main/resources/asciidoc/extensions/literal-attribute-references.rb").getAbsolutePath());
        List<String> warnings = new ArrayList<>();
        asciidoctor.registerLogHandler(r -> {
            if (r.getSeverity() == Severity.WARN) {
                warnings.add(r.getMessage());
            }
        });
        asciidoctor.convert(page, Options.builder().safe(SafeMode.UNSAFE)
                .attributes(Attributes.builder().attribute("attribute-missing", "warn").build()).build());
        return warnings;
    }

    @Test
    public void listingWithoutSubsIsReported() {
        assertTrue(warnings(":v: 1\n\n----\nunzip x-{v}.zip\n----\n").stream().anyMatch(m -> m.contains("attribute {v}")));
    }

    @Test
    public void undefinedAttributeIsReported() {
        assertTrue(warnings("tool {undefinedthing}\n").stream().anyMatch(m -> m.contains("missing attribute: undefinedthing")));
    }
}

Pin: with test-scoped org.asciidoctor:asciidoctorj:2.5.11 and org.testng:testng, the first case fails under the line-66 and line-56 mutants. The pom-side mutants (containsText, attribute-missing) need a maven-invoker IT over the same two pages with invoker.buildResult = failure.


suggestion (non-blocking): Every Maven project property is a document attribute in this render. A page that uses one passes the check and still shows the braces on the site.

opendj-doc-generated-ref/pom.xml:640

asciidoctor-maven-plugin 2.2.6 always calls AsciidoctorHelper.addMavenProperties in AsciidoctorMojo.createAttributesBuilder. It adds each project property with dots turned into dashes. attribute-missing and the extension's attributes.key? therefore treat {product-name}, {commons-version} or {docTargetVersion} as defined. The Antora site defines none of them: its playbook sets only page-toclevels/page-pagination, and opendj/antora.yml sets only pdfs. Nothing at HEAD references such a name, and {opendj-version} is not masked because no pom defines opendj.version. The plugin has no switch for this, so the comment can at least name the blind spot.

                              Maven project properties are attributes here (product.name is
                              {product-name}) but not on the site, so a page that uses one passes
                              this check and still shows the braces there.

suggestion (non-blocking): The warning's advice, add subs="attributes", replaces a verbatim block's default subs instead of adding to them.

opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb:67, :18

On a listing or literal block, subs="attributes" drops specialcharacters and callouts. A probe of [subs="attributes"] over <b>1</b> &amp; emits the markup raw. The 16 existing uses in the guides are all subs="attributes", so the advice matches the repo today. It only breaks on a block that holds <, >, & or callouts. +attributes is the additive form.

      logger.warn message_with_context %(attribute {#{name}} is published as literal text: the #{block.context} block does not substitute attributes, add subs="+attributes"), source_location: block.source_location

suggestion (non-blocking): Inside a verbatim block the extension does not report \{name}, nor the intrinsic attributes ({nbsp}, {empty}, {lt}).

opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb:66

A listing without attribute subs does not consume the backslash, so \{v} is published as \{v}. The intrinsic names live in Asciidoctor::INTRINSIC_ATTRIBUTES, not in document.attributes, so {nbsp} is published as {nbsp}. A probe showed neither is reported. Both gaps are latent. The pom comment's \{name} advice is correctly scoped to text.

      key = name.downcase
      next unless (attributes.key? key) || (names.include? key) || (Asciidoctor::INTRINSIC_ATTRIBUTES.key? key)
      logger.warn message_with_context(escaped ?
          %(\\{#{name}} is published with its backslash: the #{block.context} block does not substitute attributes) :
          %(attribute {#{name}} is published as literal text: the #{block.context} block does not substitute attributes, add subs="attributes")),
          source_location: block.source_location

Or: grep HEAD's verbatim blocks for \{ first. A hit there turns the build red.


nitpick (non-blocking): The rerun command in the pom comment fails anywhere but Linux.

opendj-doc-generated-ref/pom.xml:615-618

The module is in the reactor only through the Linux-activated distribution-unix profile of opendj-packages/pom.xml, and the execution sits in the Linux-activated man-pages profile. On macOS, Maven 3.9.16 runs the command against HEAD's poms and stops with Could not find the selected project in the reactor: opendj-doc-generated-ref. With -Pdistribution-unix,man-pages, Maven selects the module and the execution resolves.

                              The plugin stops at the first page that fails, so fix it and run again to see
                              the next one: mvn -Pdistribution-unix,man-pages -pl opendj-doc-generated-ref
                              asciidoctor:process-asciidoc@check-attribute-references, after a build of this
                              module (both profiles activate by themselves only on Linux).

…AsciiDoc attribute unresolved

Render every page of the pre-processed doc sources on its own, the way Antora
publishes each chapter, with attribute-missing=warn and an extension that reports
an attribute reference in a verbatim block without subs="attributes". Either
warning fails the build in the new check-attribute-references execution.

Escape the two literal placeholders the check found on master: {options} in the
windows-service synopsis and {path} in the password policy chapter.
…intrinsic references, test the check with its pom configuration, and keep the special characters of the attribute listings

- literal-attribute-references.rb also checks a literal table cell (l|, cols="1l"),
  which takes no subs and gets its own advice; reports \{name} and intrinsic names
  such as {nbsp} in a block without attribute subs; advises subs="+attributes",
  which keeps the block's default subs; and globs the sources with the directory as
  base, so braces in its path no longer empty the set of names.
- LiteralAttributeReferencesTest renders pages with the configuration of the
  check-attribute-references execution, read from the pom, and checks which fail
  the build. The pom module binds testCompile and test for it; the plugin and the
  test share asciidoctorj ${asciidoctorj.version}.
- The 16 blocks of the guides with subs="attributes" take subs="+attributes": the
  replacing form had dropped specialcharacters, so the plugin guide published
  "(build <unknown>, revision <unknown>)" as raw tags, which a browser hides.
- The pom comment gives the rerun command with the profiles it needs off Linux and
  names the Maven project properties as a blind spot of the check.
@vharseko

vharseko commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review. All seven points are taken in the new commit; the branch is rebased on master with #1132 first.

Literal table cells: taken. A cell with content_model == :verbatim is checked on its text. Your probe holds on asciidoctorj 2.5.11: l| and cols="1l" cells published {v} and nothing warned. Since a cell takes no subs, its warning says use an a| cell with a listing that has subs="+attributes" and names a "literal table cell" instead of the table_cell context.

Dir.glob on the sources path: taken, with Dir.glob('**/*.adoc', base: dir). On the JRuby the plugin runs, only the brace half reproduced: under ws{a}/src the cross-page name was missed, under ws[1]/src it was still found. The test now keeps its pre-processed sources under a temporary directory whose name has both {1} and [1].

Nothing pins the check: taken, with one change of approach. Instead of a maven-invoker IT, LiteralAttributeReferencesTest reads the check-attribute-references execution from the pom - requires, attributes, backend, sourcemap and failIf - and renders its pages with exactly that. So it pins the pom side too. It has 11 pages that must fail and 7 that must pass. Each of 13 mutants turns it red: your four (next if true, the subs guard inverted, a containsText typo, no attribute-missing), plus failIf on ERROR, no literal-attribute-sources, the extension not required, and one per fix of this round. The pom module binds testCompile and test for it, and the plugin and the test now share asciidoctorj ${asciidoctorj.version} (2.5.11, the plugin's default), so they cannot drift apart.

Maven project properties: taken as a comment in the pom and a line under Limits in the description, close to your wording. I confirmed addMavenProperties in the 2.2.6 mojo. The module's 27 properties are referenced by no .adoc today.

subs="attributes" advice: taken. The warning now says add subs="+attributes", and the comments of the extension and the pom use the same form. The 16 existing blocks are not as safe as both of us assumed, though. Seven of them hold >, >>>> or &&, and chap-writing-plugins.adoc:173 has (build <unknown>, revision <unknown>), which went to the HTML as raw <unknown> tags, so a browser shows (build , revision ). All 16 now use subs="+attributes". The rendered HTML of the six pages differs only in &lt;, &gt; and &amp;, and the attributes still resolve.

\{name} and intrinsic names in verbatim blocks: taken. Asciidoctor::INTRINSIC_ATTRIBUTES counts as a name, and \{name} is reported as "published with its backslash" with the same advice, since attribute subs make the backslash escape. The grep you suggested: the only \{ in a verbatim block on master is man-makeldif-template.adoc:125. That one is MakeLDIF's own escape (\{"emails") and does not match the reference pattern. A run of all 512 pages with this round's extension stays clean.

Rerun command off Linux: taken, with your text: mvn -Pdistribution-unix,man-pages -pl opendj-doc-generated-ref asciidoctor:process-asciidoc@check-attribute-references.

The description is updated to match: the Change, Limits and Verification sections.

@vharseko vharseko added the tests Test suites: fixing, enabling, un-disabling label Oct 1, 2026
@vharseko
vharseko requested a review from maximthomas October 1, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build CI docs enhancement tests Test suites: fixing, enabling, un-disabling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The doc build does not fail when a page leaves an AsciiDoc attribute unresolved

2 participants