From 239377269031b641e8ab6f2fc741ea47e0c7fc28 Mon Sep 17 00:00:00 2001 From: Jente Sondervorst Date: Fri, 29 May 2026 14:21:20 +0200 Subject: [PATCH 1/2] xml: extend `RemoveEmptyXmlTags` with XPath whitelist, file matcher, and file deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three optional, JSON-deserializable options to `RemoveEmptyXmlTags`: - `xPaths` — whitelist of XPath expressions; attribute predicates act as an attribute allowlist (`@*` wildcard, `@a`/`@a or @b` enumerated names), so a tag matches only when its attribute set is a subset of the allowed names. Empty/null preserves the existing "remove empty no-attribute tag" behavior. - `fileMatcher` — restrict the recipe to matching files. - `deleteFileIfEmpty` — when the root tag would itself be removed, delete the source file. Defaults to true. The visitor runs inside `Repeat.repeatUntilStable` so nested empties cascade and trigger file deletion in a single recipe run. The original no-arg constructor is preserved via Lombok `@NoArgsConstructor(force = true)` so existing callers and tests are unaffected. --- .../openrewrite/xml/RemoveEmptyXmlTags.java | 185 +++++++++++-- .../resources/META-INF/rewrite/recipes.csv | 32 +-- .../xml/RemoveEmptyXmlTagsTest.java | 251 ++++++++++++++++++ 3 files changed, 436 insertions(+), 32 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java b/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java index 72d59341d74..dcc1ec9178a 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java @@ -15,31 +15,184 @@ */ package org.openrewrite.xml; -import lombok.Getter; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; import org.openrewrite.xml.tree.Xml; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Value +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor(force = true) public class RemoveEmptyXmlTags extends Recipe { - @Getter - final String displayName = "Remove empty XML Tag"; - @Getter - final String description = "Removes XML tags that do not have attributes or children, including self closing tags."; + private static final Pattern PREDICATE_WITH_ATTR = Pattern.compile("\\[([^\\[\\]]*@[^\\[\\]]*)]"); + private static final Pattern ATTR_REFERENCE = Pattern.compile("@(\\*|[A-Za-z_][\\w.\\-]*)"); + + @Option(displayName = "XPaths", + description = "Whitelist of XPath expressions identifying empty tags eligible for removal. Attribute predicates " + + "enumerate the attributes a candidate tag is *allowed* to carry: a tag is a candidate when its " + + "attribute set is a subset of the union of attribute names appearing in matched predicates, with " + + "`@*` acting as a wildcard. " + + "Examples: `/server` matches only attribute-free `` tags; `/server[@*]` also matches tags " + + "carrying any attributes; `/server[@description]` matches a `` with no attributes or only " + + "a `description` attribute; `/server[@description or @other]` matches when attributes are a subset " + + "of `{description, other}`. When this list is omitted, every empty no-attribute tag is removed.", + required = false, + example = "/server/featureManager") + @Nullable + List xPaths; + + @Option(displayName = "File matcher", + description = "If provided only matching files will be modified. This is a glob expression.", + required = false, + example = "**/server.xml") + @Nullable + String fileMatcher; + + @Option(displayName = "Delete file if empty", + description = "Delete the source file when the root tag has no remaining content after collapsing empty tags. Defaults to true.", + required = false, + example = "false") + @Nullable + Boolean deleteFileIfEmpty; + + @JsonCreator + public RemoveEmptyXmlTags(@Nullable List xPaths, @Nullable String fileMatcher, @Nullable Boolean deleteFileIfEmpty) { + this.xPaths = xPaths; + this.fileMatcher = fileMatcher; + this.deleteFileIfEmpty = deleteFileIfEmpty; + } + + @Override + public String getDisplayName() { + return "Remove empty XML tags"; + } + + @Override + public String getDescription() { + return "Repeatedly removes empty XML tags (optionally scoped by an XPath whitelist) until the tree is stable, " + + "and optionally deletes the file when its root tag becomes empty. " + + "Useful as a follow-up to recipes that strip individual tags and leave empty containers behind."; + } @Override public TreeVisitor getVisitor() { - return new XmlIsoVisitor() { + TreeVisitor repeat = Repeat.repeatUntilStable(() -> new TreeVisitor() { + final List rules; + { + if (xPaths == null || xPaths.isEmpty()) { + rules = Collections.emptyList(); + } else { + rules = new ArrayList<>(xPaths.size()); + for (String xPath : xPaths) { + rules.add(new XPathRule(xPath)); + } + } + } + + @Override + public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { + return sourceFile instanceof Xml.Document; + } + @Override - public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { - Xml.Tag t = super.visitTag(tag, ctx); - //noinspection ConstantValue - if (t != null && (t.getContent() == null || t.getContent().isEmpty()) && t.getAttributes().isEmpty()) { - doAfterVisit(new RemoveContentVisitor<>(t, true, true)); + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (!(tree instanceof Xml.Document)) { + return tree; + } + Xml.Document doc = (Xml.Document) new XmlIsoVisitor() { + @Override + public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext innerCtx) { + Xml.Tag t = super.visitTag(tag, innerCtx); + if (isRemovalCandidate(t, getCursor())) { + doAfterVisit(new RemoveContentVisitor<>(t, false, true)); + } + return t; + } + }.visitNonNull(tree, ctx); + if (deleteFileIfEmpty == null || deleteFileIfEmpty) { + Xml.Tag root = doc.getRoot(); + Cursor rootCursor = new Cursor(new Cursor(null, doc), root); + if (isRemovalCandidate(root, rootCursor)) { + return null; + } + } + return doc; + } + + private boolean isRemovalCandidate(Xml.Tag t, Cursor cursor) { + boolean empty = t.getContent() == null || t.getContent().isEmpty(); + if (!empty) { + return false; + } + if (rules.isEmpty()) { + return t.getAttributes().isEmpty(); + } + for (XPathRule rule : rules) { + if (rule.matches(cursor, t)) { + return true; + } + } + return false; + } + }, 50); + return fileMatcher == null ? repeat : Preconditions.check(new FindSourceFiles(fileMatcher), repeat); + } + + private static final class XPathRule { + final XPathMatcher pathMatcher; + final Set allowedAttributes; + final boolean wildcardAttributes; + + XPathRule(String xPath) { + Set allowed = new HashSet<>(); + boolean wildcard = false; + StringBuilder strippedPath = new StringBuilder(); + Matcher predicate = PREDICATE_WITH_ATTR.matcher(xPath); + int lastEnd = 0; + while (predicate.find()) { + strippedPath.append(xPath, lastEnd, predicate.start()); + lastEnd = predicate.end(); + Matcher attrRef = ATTR_REFERENCE.matcher(predicate.group(1)); + while (attrRef.find()) { + String name = attrRef.group(1); + if ("*".equals(name)) { + wildcard = true; + } else { + allowed.add(name); + } + } + } + strippedPath.append(xPath, lastEnd, xPath.length()); + this.pathMatcher = new XPathMatcher(strippedPath.toString()); + this.allowedAttributes = allowed; + this.wildcardAttributes = wildcard; + } + + boolean matches(Cursor cursor, Xml.Tag tag) { + if (!pathMatcher.matches(cursor)) { + return false; + } + if (wildcardAttributes) { + return true; + } + for (Xml.Attribute attr : tag.getAttributes()) { + if (!allowedAttributes.contains(attr.getKeyAsString())) { + return false; } - return t; } - }; + return true; + } } } diff --git a/rewrite-xml/src/main/resources/META-INF/rewrite/recipes.csv b/rewrite-xml/src/main/resources/META-INF/rewrite/recipes.csv index e09911217ff..4c5e2226348 100644 --- a/rewrite-xml/src/main/resources/META-INF/rewrite/recipes.csv +++ b/rewrite-xml/src/main/resources/META-INF/rewrite/recipes.csv @@ -1,29 +1,29 @@ ecosystem,packageName,name,displayName,description,recipeCount,category1,category2,category1Description,category2Description,options,dataTables +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeTagValue,Change XML tag value,Alters the value of XML tags matching the provided expression. When regex is enabled the replacement happens only for text nodes provided the pattern matches.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose value is to be changed. Interpreted as an XPath Expression."",""example"":""/settings/servers/server/username"",""required"":true},{""name"":""oldValue"",""type"":""String"",""displayName"":""Old value"",""description"":""The old value of the tag. Interpreted as pattern if regex is enabled."",""example"":""user""},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value for the tag. Supports capture groups when regex is enabled. If literal $,\\ characters are needed in newValue, with regex true, then it should be escaped."",""example"":""user"",""required"":true},{""name"":""regex"",""type"":""Boolean"",""displayName"":""Regex"",""description"":""Default false. If true, `oldValue` will be interpreted as a [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression), and capture group contents will be available in `newValue`.""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeTagName,Change XML tag name,Alters the name of XML tags matching the provided expression.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose attribute's value is to be changed. Interpreted as an XPath expression."",""example"":""/settings/servers/server/username"",""required"":true},{""name"":""newName"",""type"":""String"",""displayName"":""New name"",""description"":""The new name for the tag."",""example"":""user"",""required"":true}]", maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.AddCommentToXmlTag,Add a comment to an XML tag,Adds a comment as the first element in an XML tag.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""xPath"",""type"":""String"",""displayName"":""XPath"",""description"":""An XPath expression used to find matching tags."",""example"":""/project/dependencies/dependency"",""required"":true},{""name"":""commentText"",""type"":""String"",""displayName"":""Comment text"",""description"":""The text to add as a comment.."",""example"":""This is excluded due to CVE and will be removed when we upgrade the next version is available."",""required"":true}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.AddOrUpdateChildTag,Add or update child tag,"Adds or updates a child element below the parent(s) matching the provided `parentXPath` expression. If a child with the same name already exists, it will be replaced by default. Otherwise, a new child will be added. This ensures idempotent behaviour.",1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""parentXPath"",""type"":""String"",""displayName"":""Parent XPath"",""description"":""XPath identifying the parent to which a child tag must be added"",""example"":""/project//plugin//configuration"",""required"":true},{""name"":""newChildTag"",""type"":""String"",""displayName"":""New child tag"",""description"":""The XML of the new child to add or update on the parent tag."",""example"":""true"",""required"":true},{""name"":""replaceExisting"",""type"":""Boolean"",""displayName"":""Replace existing child"",""description"":""Set to `false` to not replace the child tag if it already exists. Defaults to true.""}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.AddTagAttribute,Add new XML attribute for an Element,Add new XML attribute with value on a specified element.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose attribute's value is to be added. Interpreted as an XPath expression."",""example"":""//beans/bean"",""required"":true},{""name"":""attributeName"",""type"":""String"",""displayName"":""Attribute name"",""description"":""The name of the new attribute."",""example"":""attribute-name"",""required"":true},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value to be used for key specified by `attributeName`."",""example"":""value-to-add"",""required"":true}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeNamespaceValue,Change XML attribute of a specific resource version,Alters XML Attribute value within specified element of a specific resource versions.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose attribute's value is to be changed. Interpreted as an XPath Expression."",""example"":""property""},{""name"":""oldValue"",""type"":""String"",""displayName"":""Old value"",""description"":""Only change the property value if it matches the configured `oldValue`."",""example"":""newfoo.bar.attribute.value.string""},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value to be used for the namespace."",""example"":""newfoo.bar.attribute.value.string"",""required"":true},{""name"":""versionMatcher"",""type"":""String"",""displayName"":""Resource version"",""description"":""The version of resource to change"",""example"":""1.1""},{""name"":""searchAllNamespaces"",""type"":""Boolean"",""displayName"":""Search all namespaces"",""description"":""Specify whether evaluate all namespaces. Defaults to true"",""example"":""true""},{""name"":""newVersion"",""type"":""String"",""displayName"":""New Resource version"",""description"":""The new version of the resource"",""example"":""2.0"",""required"":true},{""name"":""newSchemaLocation"",""type"":""String"",""displayName"":""Schema location"",""description"":""The new value to be used for the namespace schema location."",""example"":""newfoo.bar.attribute.value.string""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.RemoveXmlTag,Remove XML tag,Removes XML tags matching the provided expression.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""xPath"",""type"":""String"",""displayName"":""XPath"",""description"":""An XPath expression used to find matching tags."",""example"":""/project/dependencies/dependency"",""required"":true},{""name"":""fileMatcher"",""type"":""String"",""displayName"":""File matcher"",""description"":""If provided only matching files will be modified. This is a glob expression."",""example"":""'**/application-*.xml'""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.RemoveEmptyXmlTags,Remove empty XML tags,"Repeatedly removes empty XML tags (optionally scoped by an XPath whitelist) until the tree is stable, and optionally deletes the file when its root tag becomes empty. Useful as a follow-up to recipes that strip individual tags and leave empty containers behind.",1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""xPaths"",""type"":""List"",""displayName"":""XPaths"",""description"":""Whitelist of XPath expressions identifying empty tags eligible for removal. Attribute predicates enumerate the attributes a candidate tag is *allowed* to carry: a tag is a candidate when its attribute set is a subset of the union of attribute names appearing in matched predicates, with `@*` acting as a wildcard. Examples: `/server` matches only attribute-free `` tags; `/server[@*]` also matches tags carrying any attributes; `/server[@description]` matches a `` with no attributes or only a `description` attribute; `/server[@description or @other]` matches when attributes are a subset of `{description, other}`. When this list is omitted, every empty no-attribute tag is removed."",""example"":""/server/featureManager""},{""name"":""fileMatcher"",""type"":""String"",""displayName"":""File matcher"",""description"":""If provided only matching files will be modified. This is a glob expression."",""example"":""**/server.xml""},{""name"":""deleteFileIfEmpty"",""type"":""Boolean"",""displayName"":""Delete file if empty"",""description"":""Delete the source file when the root tag has no remaining content after collapsing empty tags. Defaults to true."",""example"":""false""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.CreateXmlFile,Create XML file,Create a new XML file.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""relativeFileName"",""type"":""String"",""displayName"":""Relative file path"",""description"":""File path of new file."",""example"":""foo/bar/baz.xml"",""required"":true},{""name"":""fileContents"",""type"":""String"",""displayName"":""File contents"",""description"":""Multiline text content for the file."",""example"":""\n\n 1""},{""name"":""overwriteExisting"",""type"":""Boolean"",""displayName"":""Overwrite existing file"",""description"":""If there is an existing file, should it be overwritten.""}]", maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeTagAttribute,Change XML attribute,Alters XML attribute value on a specified element.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose attribute's value is to be changed. Interpreted as an XPath expression."",""example"":""property"",""required"":true},{""name"":""attributeName"",""type"":""String"",""displayName"":""Attribute name"",""description"":""The name of the attribute whose value is to be changed."",""example"":""name"",""required"":true},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value to be used for key specified by `attributeName`, Set to null if you want to remove the attribute."",""example"":""newfoo.bar.attribute.value.string"",""required"":true},{""name"":""oldValue"",""type"":""String"",""displayName"":""Old value"",""description"":""Only change the property value if it matches the configured `oldValue`."",""example"":""foo.bar.attribute.value.string""},{""name"":""regex"",""type"":""Boolean"",""displayName"":""Regex"",""description"":""Default false. If true, `oldValue` will be interpreted as a Regular Expression, and capture group contents will be available in `newValue`.""}]", maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeTagAttributeKey,Change XML attribute key,Change an attributes key on XML elements using an XPath expression.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""xPath"",""type"":""String"",""displayName"":""Attribute XPath"",""description"":""XPath expression to match the attribute."",""example"":""//a4j:ajax/@reRender"",""required"":true},{""name"":""newAttributeName"",""type"":""String"",""displayName"":""New attribute name"",""description"":""The new name for the attribute."",""example"":""render"",""required"":true}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeTagName,Change XML tag name,Alters the name of XML tags matching the provided expression.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose attribute's value is to be changed. Interpreted as an XPath expression."",""example"":""/settings/servers/server/username"",""required"":true},{""name"":""newName"",""type"":""String"",""displayName"":""New name"",""description"":""The new name for the tag."",""example"":""user"",""required"":true}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeTagValue,Change XML tag value,Alters the value of XML tags matching the provided expression. When regex is enabled the replacement happens only for text nodes provided the pattern matches.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose value is to be changed. Interpreted as an XPath Expression."",""example"":""/settings/servers/server/username"",""required"":true},{""name"":""oldValue"",""type"":""String"",""displayName"":""Old value"",""description"":""The old value of the tag. Interpreted as pattern if regex is enabled."",""example"":""user""},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value for the tag. Supports capture groups when regex is enabled. If literal $,\\ characters are needed in newValue, with regex true, then it should be escaped."",""example"":""user"",""required"":true},{""name"":""regex"",""type"":""Boolean"",""displayName"":""Regex"",""description"":""Default false. If true, `oldValue` will be interpreted as a [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression), and capture group contents will be available in `newValue`.""}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.CreateXmlFile,Create XML file,Create a new XML file.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""relativeFileName"",""type"":""String"",""displayName"":""Relative file path"",""description"":""File path of new file."",""example"":""foo/bar/baz.xml"",""required"":true},{""name"":""fileContents"",""type"":""String"",""displayName"":""File contents"",""description"":""Multiline text content for the file."",""example"":""\n\n 1""},{""name"":""overwriteExisting"",""type"":""Boolean"",""displayName"":""Overwrite existing file"",""description"":""If there is an existing file, should it be overwritten.""}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.RemoveEmptyXmlTags,Remove empty XML Tag,"Removes XML tags that do not have attributes or children, including self closing tags.",1,,XML,,Basic building blocks for transforming XML.,, -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.RemoveTrailingWhitespace,Remove trailing whitespace,Remove any extra trailing whitespace from the end of each line.,1,,XML,,Basic building blocks for transforming XML.,, -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.RemoveXmlTag,Remove XML tag,Removes XML tags matching the provided expression.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""xPath"",""type"":""String"",""displayName"":""XPath"",""description"":""An XPath expression used to find matching tags."",""example"":""/project/dependencies/dependency"",""required"":true},{""name"":""fileMatcher"",""type"":""String"",""displayName"":""File matcher"",""description"":""If provided only matching files will be modified. This is a glob expression."",""example"":""'**/application-*.xml'""}]", maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.XsltTransformation,XSLT transformation,Apply the specified XSLT transformation on matching files. Note that there are no format matching guarantees when running this recipe.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""xslt"",""type"":""String"",""displayName"":""XSLT Configuration transformation"",""description"":""The transformation to be applied."",""example"":""...""},{""name"":""xsltResource"",""type"":""String"",""displayName"":""XSLT Configuration transformation classpath resource"",""description"":""Recipe transformation provided as a classpath resource."",""example"":""/changePlugin.xslt""},{""name"":""filePattern"",""type"":""String"",""displayName"":""File pattern"",""description"":""A glob expression that can be used to constrain which directories or source files should be searched. Multiple patterns may be specified, separated by a semicolon `;`. If multiple patterns are supplied any of the patterns matching will be interpreted as a match."",""example"":""**/*.xml"",""required"":true}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.AutoFormat,Format XML,Indents XML using the most common indentation size and tabs or space choice in use in the file.,1,Format,XML,,Basic building blocks for transforming XML.,, -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.LineBreaks,Blank lines,Add line breaks at appropriate places between XML syntax elements.,1,Format,XML,,Basic building blocks for transforming XML.,, -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.NormalizeFormat,Normalize format,Move whitespace to the outermost LST element possible.,1,Format,XML,,Basic building blocks for transforming XML.,, -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.NormalizeLineBreaks,Normalize line breaks,Consistently use either Windows style (CRLF) or Linux style (LF) line breaks. If no `GeneralFormatStyle` is specified this will use whichever style of line endings are more common.,1,Format,XML,,Basic building blocks for transforming XML.,, +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.ChangeNamespaceValue,Change XML attribute of a specific resource version,Alters XML Attribute value within specified element of a specific resource versions.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose attribute's value is to be changed. Interpreted as an XPath Expression."",""example"":""property""},{""name"":""oldValue"",""type"":""String"",""displayName"":""Old value"",""description"":""Only change the property value if it matches the configured `oldValue`."",""example"":""newfoo.bar.attribute.value.string""},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value to be used for the namespace."",""example"":""newfoo.bar.attribute.value.string"",""required"":true},{""name"":""versionMatcher"",""type"":""String"",""displayName"":""Resource version"",""description"":""The version of resource to change"",""example"":""1.1""},{""name"":""searchAllNamespaces"",""type"":""Boolean"",""displayName"":""Search all namespaces"",""description"":""Specify whether evaluate all namespaces. Defaults to true"",""example"":""true""},{""name"":""newVersion"",""type"":""String"",""displayName"":""New Resource version"",""description"":""The new version of the resource"",""example"":""2.0"",""required"":true},{""name"":""newSchemaLocation"",""type"":""String"",""displayName"":""Schema location"",""description"":""The new value to be used for the namespace schema location."",""example"":""newfoo.bar.attribute.value.string""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.AddTagAttribute,Add new XML attribute for an Element,Add new XML attribute with value on a specified element.,1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""elementName"",""type"":""String"",""displayName"":""Element name"",""description"":""The name of the element whose attribute's value is to be added. Interpreted as an XPath expression."",""example"":""//beans/bean"",""required"":true},{""name"":""attributeName"",""type"":""String"",""displayName"":""Attribute name"",""description"":""The name of the new attribute."",""example"":""attribute-name"",""required"":true},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value to be used for key specified by `attributeName`."",""example"":""value-to-add"",""required"":true}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.AddOrUpdateChildTag,Add or update child tag,"Adds or updates a child element below the parent(s) matching the provided `parentXPath` expression. If a child with the same name already exists, it will be replaced by default. Otherwise, a new child will be added. This ensures idempotent behaviour.",1,,XML,,Basic building blocks for transforming XML.,"[{""name"":""parentXPath"",""type"":""String"",""displayName"":""Parent XPath"",""description"":""XPath identifying the parent to which a child tag must be added"",""example"":""/project//plugin//configuration"",""required"":true},{""name"":""newChildTag"",""type"":""String"",""displayName"":""New child tag"",""description"":""The XML of the new child to add or update on the parent tag."",""example"":""true"",""required"":true},{""name"":""replaceExisting"",""type"":""Boolean"",""displayName"":""Replace existing child"",""description"":""Set to `false` to not replace the child tag if it already exists. Defaults to true.""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.RemoveTrailingWhitespace,Remove trailing whitespace,Remove any extra trailing whitespace from the end of each line.,1,,XML,,Basic building blocks for transforming XML.,, maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.NormalizeTabsOrSpaces,Normalize to tabs or spaces,Consistently use either tabs or spaces in indentation.,1,Format,XML,,Basic building blocks for transforming XML.,, +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.NormalizeLineBreaks,Normalize line breaks,Consistently use either Windows style (CRLF) or Linux style (LF) line breaks. If no `GeneralFormatStyle` is specified this will use whichever style of line endings are more common.,1,Format,XML,,Basic building blocks for transforming XML.,, maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.TabsAndIndents,Tabs and indents,Format tabs and indents in XML code.,1,Format,XML,,Basic building blocks for transforming XML.,, -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.search.DoesNotUseNamespaceUri,Find files without Namespace URI,"Find XML root elements that do not have a specific Namespace URI, optionally restricting the search by an XPath expression.",1,Search,XML,,Basic building blocks for transforming XML.,"[{""name"":""namespaceUri"",""type"":""String"",""displayName"":""Namespace URI"",""description"":""The Namespace URI to check."",""example"":""http://www.w3.org/2001/XMLSchema-instance"",""required"":true}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.search.FindNamespacePrefix,Find XML namespace prefixes,"Find XML namespace prefixes, optionally restricting the search by a XPath expression.",1,Search,XML,,Basic building blocks for transforming XML.,"[{""name"":""namespacePrefix"",""type"":""String"",""displayName"":""Namespace prefix"",""description"":""The Namespace Prefix to find."",""example"":""http://www.w3.org/2001/XMLSchema-instance"",""required"":true},{""name"":""xPath"",""type"":""String"",""displayName"":""XPath"",""description"":""An XPath expression used to find namespace URIs."",""example"":""/dependencies/dependency""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.AutoFormat,Format XML,Indents XML using the most common indentation size and tabs or space choice in use in the file.,1,Format,XML,,Basic building blocks for transforming XML.,, +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.NormalizeFormat,Normalize format,Move whitespace to the outermost LST element possible.,1,Format,XML,,Basic building blocks for transforming XML.,, +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.format.LineBreaks,Blank lines,Add line breaks at appropriate places between XML syntax elements.,1,Format,XML,,Basic building blocks for transforming XML.,, maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.search.FindTags,Find XML tags,Find XML tags by XPath expression.,1,Search,XML,,Basic building blocks for transforming XML.,"[{""name"":""xPath"",""type"":""String"",""displayName"":""XPath"",""description"":""An XPath expression used to find matching tags."",""example"":""//dependencies/dependency"",""required"":true}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.search.FindNamespacePrefix,Find XML namespace prefixes,"Find XML namespace prefixes, optionally restricting the search by a XPath expression.",1,Search,XML,,Basic building blocks for transforming XML.,"[{""name"":""namespacePrefix"",""type"":""String"",""displayName"":""Namespace prefix"",""description"":""The Namespace Prefix to find."",""example"":""http://www.w3.org/2001/XMLSchema-instance"",""required"":true},{""name"":""xPath"",""type"":""String"",""displayName"":""XPath"",""description"":""An XPath expression used to find namespace URIs."",""example"":""/dependencies/dependency""}]", maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.search.HasNamespaceUri,Find XML namespace URIs,"Find XML namespace URIs, optionally restricting the search by a XPath expression.",1,Search,XML,,Basic building blocks for transforming XML.,"[{""name"":""namespaceUri"",""type"":""String"",""displayName"":""Namespace URI"",""description"":""The Namespace URI to find."",""example"":""http://www.w3.org/2001/XMLSchema-instance"",""required"":true},{""name"":""xPath"",""type"":""String"",""displayName"":""XPath"",""description"":""An XPath expression used to find namespace URIs."",""example"":""/dependencies/dependency""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.search.DoesNotUseNamespaceUri,Find files without Namespace URI,"Find XML root elements that do not have a specific Namespace URI, optionally restricting the search by an XPath expression.",1,Search,XML,,Basic building blocks for transforming XML.,"[{""name"":""namespaceUri"",""type"":""String"",""displayName"":""Namespace URI"",""description"":""The Namespace URI to check."",""example"":""http://www.w3.org/2001/XMLSchema-instance"",""required"":true}]", maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.security.AddOwaspDateBoundSuppressions,Add date bounds to OWASP suppressions,Adds an expiration date to all OWASP suppressions in order to ensure that they are periodically reviewed. For use with the OWASP `dependency-check` tool. More details: https://jeremylong.github.io/DependencyCheck/general/suppression.html.,1,Security,XML,,Basic building blocks for transforming XML.,"[{""name"":""untilDate"",""type"":""String"",""displayName"":""Until date"",""description"":""Optional. The date to add to the suppression. Default will be 30 days from today."",""example"":""2023-01-01""}]", -maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.security.IsOwaspSuppressionsFile,Find OWASP vulnerability suppression XML files,These files are used to suppress false positives in OWASP [Dependency Check](https://jeremylong.github.io/DependencyCheck).,1,Security,XML,,Basic building blocks for transforming XML.,, maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.security.RemoveOwaspSuppressions,Remove out-of-date OWASP suppressions,"Remove all OWASP suppressions with a suppression end date in the past, as these are no longer valid. For use with the OWASP `dependency-check` tool. More details on OWASP suppression files can be found [here](https://jeremylong.github.io/DependencyCheck/general/suppression.html).",1,Security,XML,,Basic building blocks for transforming XML.,"[{""name"":""cutOffDate"",""type"":""String"",""displayName"":""Until date"",""description"":""Suppressions will be removed if they expired before the provided date. Default will be yesterday."",""example"":""2023-01-01""}]", maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.security.UpdateOwaspSuppressionDate,Update OWASP suppression date bounds,Updates the expiration date for OWASP suppressions having a matching cve tag. For use with the OWASP `dependency-check` tool. More details: https://jeremylong.github.io/DependencyCheck/general/suppression.html.,1,Security,XML,,Basic building blocks for transforming XML.,"[{""name"":""cveList"",""type"":""List"",""displayName"":""CVE List"",""description"":""Update suppressions having any of the specified CVE tags."",""example"":""CVE-2022-1234"",""required"":true},{""name"":""untilDate"",""type"":""String"",""displayName"":""Until date"",""description"":""Optional. The date to add to the suppression. Default will be 30 days from today."",""example"":""2023-01-01""}]", +maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.security.IsOwaspSuppressionsFile,Find OWASP vulnerability suppression XML files,These files are used to suppress false positives in OWASP [Dependency Check](https://jeremylong.github.io/DependencyCheck).,1,Security,XML,,Basic building blocks for transforming XML.,, maven,org.openrewrite:rewrite-xml,org.openrewrite.xml.style.AutodetectDebug,XML style Auto-detection debug,Runs XML Autodetect and records the results in data tables and search markers. A debugging tool for figuring out why XML documents get styled the way they do.,1,Style,XML,,Basic building blocks for transforming XML.,,"[{""name"":""org.openrewrite.xml.table.XmlStyleReport"",""displayName"":""XML style report"",""instanceName"":""XML style report"",""description"":""Records style information about XML documents. Used for debugging style auto-detection issues."",""columns"":[{""name"":""name"",""type"":""String"",""displayName"":""File name"",""description"":""The name of the file that was analyzed.""},{""name"":""useTabCharacter"",""type"":""boolean"",""displayName"":""Use tabs"",""description"":""When 'true', tabs are used for indentation. When 'false', spaces are used.""},{""name"":""indentSize"",""type"":""int"",""displayName"":""Indent size"",""description"":""The number of spaces that are used for each level of indentation.""},{""name"":""tabSize"",""type"":""int"",""displayName"":""Tab size"",""description"":""The number of spaces that a tab character represents.""},{""name"":""continuationIndentSize"",""type"":""int"",""displayName"":""Continuation indent size"",""description"":""The number of spaces that are used to indent an attribute that is on its own line.""},{""name"":""indentCount"",""type"":""int"",""displayName"":""Indent count"",""description"":""Count of tags in the file whose prefixes were evaluated.""},{""name"":""indentsMatchingOwnStyle"",""type"":""int"",""displayName"":""Indents matching own style"",""description"":""Count of tags in the file whose prefix match the style of the file itself.""},{""name"":""indentsMatchingProjectStyle"",""type"":""int"",""displayName"":""Indents matching project style"",""description"":""Count of tags in the file whose prefix match the overall style of the project.""},{""name"":""continuationIndentCount"",""type"":""int"",""displayName"":""Continuation indent count"",""description"":""Count of attributes in the file whose prefixes were evaluated.""},{""name"":""continuationIndentsMatchingOwnStyle"",""type"":""int"",""displayName"":""Continuation indents matching own style"",""description"":""CCount of attributes in the file whose prefix matches the style of the file itself.""},{""name"":""continuationIndentsMatchingProjectStyle"",""type"":""int"",""displayName"":""Continuation indents matching project style"",""description"":""Count of attributes in the file whose prefix matches the overall style of the project.""}]}]" diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/RemoveEmptyXmlTagsTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/RemoveEmptyXmlTagsTest.java index 935d4f0abc4..91bdf7b3430 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/RemoveEmptyXmlTagsTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/RemoveEmptyXmlTagsTest.java @@ -15,6 +15,7 @@ */ package org.openrewrite.xml; +import java.util.List; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.test.RecipeSpec; @@ -105,4 +106,254 @@ void retainWhenThereAttributes() { ) ); } + + @Test + void deletesFileWhenRootCollapses() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("/server/featureManager", "/server[@*]"), + "**/mp-telemetry.xml", + true)), + xml( + """ + + + + + + """, + (String) null, + spec -> spec.path("src/main/liberty/config/configDropins/defaults/mp-telemetry.xml") + ) + ); + } + + @Test + void deleteFileIfEmptyDefaultsToTrue() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags(null, null, null)), + xml( + """ + + + + """, + (String) null + ) + ); + } + + @Test + void preservesFileWhenRootHasAttributes() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("/server/featureManager"), + null, + true)), + xml( + """ + + + + + """, + """ + + + """ + ) + ); + } + + @Test + void xPathWhitelistScopesRemoval() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("//featureManager"), + null, + false)), + xml( + """ + + + + + + + """, + """ + + + + + """ + ) + ); + } + + @Test + void tagsWithAttributesArePreserved() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("//feature"), + null, + false)), + xml( + """ + + + + + """, + """ + + + + """ + ) + ); + } + + @Test + void attributeWildcardAllowsAnyAttributes() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("//feature[@*]"), + null, + false)), + xml( + """ + + + + + + """, + """ + + + """ + ) + ); + } + + @Test + void singleAttributeAllowlist() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("//feature[@description]"), + null, + false)), + xml( + """ + + + + + + """, + """ + + + + """ + ) + ); + } + + @Test + void multipleAttributeAllowlist() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("//feature[@description or @other]"), + null, + false)), + xml( + """ + + + + + + + + """, + """ + + + + """ + ) + ); + } + + @Test + void fileMatcherSkipsNonMatchingFile() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("//featureManager"), + "**/server.xml", + false)), + xml( + """ + + + + + """, + spec -> spec.path("src/main/other.xml") + ) + ); + } + + @Test + void deleteFileIfEmptyFalseLeavesEmptyRoot() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("/server/featureManager"), + null, + false)), + xml( + """ + + + + + """, + """ + + + """ + ) + ); + } + + @Test + void nestedEmptyTagsCollapseInOneRun() { + rewriteRun( + spec -> spec.recipe(new RemoveEmptyXmlTags( + List.of("//plugin", "//plugins", "//pluginManagement", "//build"), + null, + false)), + xml( + """ + + 4.0.0 + + + + + + + + + """, + """ + + 4.0.0 + + """ + ) + ); + } } From 82536d28ddb0d8fbb95c2393feafd65ee3e66eca Mon Sep 17 00:00:00 2001 From: Jente Sondervorst Date: Fri, 29 May 2026 14:54:44 +0200 Subject: [PATCH 2/2] Revert displayName/description to Lombok field initializers --- .../openrewrite/xml/RemoveEmptyXmlTags.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java b/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java index dcc1ec9178a..2a8a8537f50 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/RemoveEmptyXmlTags.java @@ -67,6 +67,12 @@ public class RemoveEmptyXmlTags extends Recipe { @Nullable Boolean deleteFileIfEmpty; + String displayName = "Remove empty XML tags"; + + String description = "Repeatedly removes empty XML tags (optionally scoped by an XPath whitelist) until the tree is stable, " + + "and optionally deletes the file when its root tag becomes empty. " + + "Useful as a follow-up to recipes that strip individual tags and leave empty containers behind."; + @JsonCreator public RemoveEmptyXmlTags(@Nullable List xPaths, @Nullable String fileMatcher, @Nullable Boolean deleteFileIfEmpty) { this.xPaths = xPaths; @@ -74,18 +80,6 @@ public RemoveEmptyXmlTags(@Nullable List xPaths, @Nullable String fileMa this.deleteFileIfEmpty = deleteFileIfEmpty; } - @Override - public String getDisplayName() { - return "Remove empty XML tags"; - } - - @Override - public String getDescription() { - return "Repeatedly removes empty XML tags (optionally scoped by an XPath whitelist) until the tree is stable, " + - "and optionally deletes the file when its root tag becomes empty. " + - "Useful as a follow-up to recipes that strip individual tags and leave empty containers behind."; - } - @Override public TreeVisitor getVisitor() { TreeVisitor repeat = Repeat.repeatUntilStable(() -> new TreeVisitor() {