Skip to content

Parse processing instructions that carry no data - #8361

Open
renechoi wants to merge 1 commit into
openrewrite:mainfrom
renechoi:fix/xml-processing-instruction-without-data
Open

Parse processing instructions that carry no data#8361
renechoi wants to merge 1 commit into
openrewrite:mainfrom
renechoi:fix/xml-processing-instruction-without-data

Conversation

@renechoi

@renechoi renechoi commented Aug 2, 2026

Copy link
Copy Markdown

What's changed?

XMLParser.g4 spelled the processing instruction rule as SPECIAL_OPEN PI_TEXT+ SPECIAL_CLOSE, requiring at least one data token. The XML specification makes a processing instruction's data optional:

[16] PI ::= '<?' PITarget (S (Char* - (Char* '?>' Char*)))? '?>'

The rule is now PI_TEXT*, and Xml.ProcessingInstruction#processingInstructions is nullable to model the absent data.

What's your motivation?

<?target?> and <?target ?> are well formed, but both failed to parse:

XmlParsingException: Syntax error in file.xml at line 4:32 mismatched input '?>' expecting PI_TEXT.

This is a parse failure rather than a recipe failure, so the blast radius is the whole file: the document never becomes an Xml.Document, and no recipe can run on it. A single data-less processing instruction anywhere in a POM or config file takes the entire file out of scope.

Existing coverage did not catch it because every processing instruction fixture in the suite carries data (<?xml-stylesheet href="mystyle.css" type="text/css"?>, <?m2e execute onConfiguration,onIncremental?>).

Anything in particular you'd like reviewers to focus on?

The nullable LST field. I checked every consumer of that child before changing it; all four already route through null-tolerant calls, so nothing needed adapting:

Consumer Call Null handling
XmlPrinter visit(pi.getProcessingInstructions(), p) TreeVisitor.visit(@Nullable Tree, P)
XmlVisitor visitAndCast(pi.getProcessingInstructions(), p) visitAndCast(@Nullable Tree, P)
XmlSender q.getAndSend(pi, ...) Function<T, @Nullable U>; skips onChange when after == null
XmlReceiver q.receive(pi.getProcessingInstructions(), ...) receive(@Nullable T before, ...); onChange only on ADD/CHANGE

No other code reads the field. TabsAndIndentsVisitor, CountLinesVisitor, XPathMatcher, XmlWhitespaceValidationService, and MavenIsoVisitor reference Xml.ProcessingInstruction but only its prefix or its type. XmlSendReceiveTest.sendReceiveProcessingInstructionWithoutData covers the RPC path specifically because of this change.

Guard strength. Reverting only the grammar hunk while keeping the Java changes turns exactly the three new tests red and leaves the other 298 in rewrite-xml green, so the tests are guarding the grammar and not something incidental.

Have you considered any alternatives or workarounds?

Synthesizing an empty Xml.CharData instead of null would have avoided touching the model, but it puts a node in the LST that has no counterpart in the source. Xml.Tag#closing and Xml.Prolog#xmlDecl are already nullable for optional children, so null follows the existing convention.

Any additional context

Two adjacent things I found while verifying, both left out of this PR because they are independent of it:

  • A processing instruction inside a DOCTYPE internal subset does not lex at all, failing with token recognition error at: '<?'. This is a lexer mode gap rather than the parser rule changed here: I confirmed it by control, and <?m2e execute onConfiguration?> (data present) fails identically on unpatched main. markupdecl permits processinginstruction, but no INSIDE_DTD rule ever emits SPECIAL_OPEN.
  • LineBreaksVisitor.visitProcessingInstruction unconditionally prepends a newline, so <exclude>a</exclude> <?why-a?> is reflowed onto its own line. This is the same shape as the comment case fixed in Keep XML comments trailing an element on the same line #8353, which added an isTrailingComment guard to visitComment but left visitProcessingInstruction untouched. It affects processing instructions that already parsed today, so it is not a consequence of this change.

I did not regenerate rewrite-csharp/csharp/OpenRewrite/Xml/Grammar, which is generated from this same .g4 by generateAntlrCSharpSources. Those checked-in files are already stale relative to main: regenerating them from an unmodified grammar produces 602 insertions and 545 deletions across 10 files, so including them would bury this change. Happy to do it in a separate PR if you want them resynced.

Verification

Local, JDK 21:

Gate Result
:rewrite-xml:test 301 tests, 0 failures, 0 skipped
:rewrite-maven:test 1415 tests, 0 failures, 15 skipped (all pre-existing @Disabled)
:rewrite-xml:license pass
generateAntlrSources regenerated; re-running is byte-identical

:rewrite-xml:recipeCsvValidateCompleteness fails on this branch, but it fails identically on unmodified main (org.openrewrite.xml.CreateXmlFile and org.openrewrite.xml.style.AutodetectDebug listed in recipes.csv but absent from the environment). Unrelated to this change.

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've read and applied the recipe conventions and best practices
  • I've used the IntelliJ IDEA auto-formatter on affected files

`processinginstruction` required `PI_TEXT+`, but the XML specification makes
a processing instruction's data optional:

    PI ::= '<?' PITarget (S (Char* - (Char* '?>' Char*)))? '?>'

So `<?target?>` and `<?target ?>` are well formed, yet both failed with
`mismatched input '?>' expecting PI_TEXT`. Because that is a parse failure
rather than a recipe failure, the whole document became unparseable and no
recipe could run on the file at all.

Relax the rule to `PI_TEXT*` and make `Xml.ProcessingInstruction#processingInstructions`
nullable to model the absent data. The printer, `XmlVisitor`, and the RPC
sender/receiver already route that child through null-tolerant calls, so the
round trip stays byte for byte.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant