Skip to content

Add pmd recipes - #1034

Closed
sullis wants to merge 3 commits into
openrewrite:mainfrom
sullis:ss-pmd
Closed

Add pmd recipes#1034
sullis wants to merge 3 commits into
openrewrite:mainfrom
sullis:ss-pmd

Conversation

@sullis

@sullis sullis commented Aug 30, 2026

Copy link
Copy Markdown

What's changed

Adds recipes that operate on PMD ruleset XML files, so a project's PMD configuration can be migrated automatically alongside its code.

  • ReplacePmdRule — updates <rule ref="..."/> references and <exclude name="..."/> elements to name a rule's replacement. oldRule and newRule may each be a fully qualified reference (category/java/errorprone.xml/MissingBreakInSwitch) or a bare rule name: a bare oldRule matches regardless of which ruleset file it is referenced from, and a bare newRule keeps the existing ruleset file. An <exclude> is only renamed when the replacement lives in the same ruleset file, since an exclusion can only name a rule from the ruleset its enclosing <rule> refers to; when the replacement moved to a different ruleset file the exclusion no longer names a rule that ruleset knows, so it is removed instead.
  • RemovePmdRule — removes both <rule ref="..."/> references to a rule and <exclude name="..."/> elements naming it, for rules PMD deleted without offering a replacement.
  • AddPmdRule — adds a <rule ref="..."/> reference to rulesets that do not have one yet. rule must be fully qualified, since PMD resolves a rule through the ruleset file it lives in; a bare name fails validate(). When the ruleset already pulls in the whole ruleset file, the rule is enabled by dropping the <exclude> that was keeping it out rather than by adding a second reference to it.
  • Pmd6to7Migration (declarative, in pmd.yml) — composes 49 applications of ReplacePmdRule and RemovePmdRule: 45 replacements and 4 removals. PMD 7 deleted the rules that had been deprecated throughout the PMD 6 line, and PMD refuses to load a ruleset that references a rule it does not know, so an un-migrated ruleset is a hard failure rather than a warning.
  • Pmd7RuleRenames (declarative, in pmd.yml) — composes 13 applications of ReplacePmdRule for the renames PMD made within the PMD 7 line (7.7.0, 7.18.0, 7.25.0, 7.27.0). Unlike the 6→7 deletions these are not hard failures yet — PMD keeps the old name as a deprecated alias — so this is a separate recipe rather than part of Pmd6to7Migration: applying it silences the deprecation warnings and keeps the ruleset loading once PMD 8 drops the aliases, but it also raises the required PMD version to 7.27.0, which is a choice the 6→7 migration should not make on a project's behalf.
  • ModernizePmd (declarative, in pmd.yml) — the entry point for a project that just wants its ruleset to name each rule as PMD knows it today: runs Pmd6to7Migration followed by Pmd7RuleRenames. Because it includes the renames, its result requires PMD 7.27.0 or later; a project pinned to an earlier PMD 7 should run Pmd6to7Migration on its own.
 <ruleset name="example">
-  <rule ref="category/java/errorprone.xml/MissingBreakInSwitch"/>
+  <rule ref="category/java/errorprone.xml/ImplicitSwitchFallThrough"/>
   <rule ref="category/java/codestyle.xml">
-    <exclude name="DontImportJavaLang"/>
+    <exclude name="UnnecessaryImport"/>
   </rule>
-  <rule ref="category/java/performance.xml/SimplifyStartsWith"/>
 </ruleset>

All three imperative recipes are scoped by returning the document unchanged from visitDocument unless its root tag is <ruleset>, rather than by a filename glob — PMD rulesets are not named consistently (ruleset.xml, pmd-rules.xml, …), and tags as generic as <rule> and <exclude> show up in unrelated XML. Two helpers are shared between them: PmdRuleRef parses a rule reference into ruleset file plus rule name, and RemoveRulesetContentVisitor extends RemoveContentVisitor to collapse a <rule> whose last <exclude> was just removed back to a self-closing tag, while leaving an emptied <ruleset> with its body intact.

Rules whose behaviour PMD split or redirected across a different successor are deliberately left out of the declarative recipes, because picking a single replacement for them requires a judgement call the recipe should not make silently:

  • from 6→7: VariableNamingConventions and the primitive wrapper *Instantiation rules;
  • within 7: AvoidCatchingNPEAvoidCatchingGenericException, GenericsNamingTypeParameterNamingConventions, UnnecessaryLocalBeforeReturnVariableCanBeInlined, UseObjectForClearerAPIExcessiveParameterList, and CheckSkipResult / AvoidLosingExceptionInformation / UselessOperationOnImmutableUnusedReturnValue. Each successor reports something different from the rule it replaces, so adopting it is an opt-in, not a rename.

Other changes

  • build.gradle.kts: adds provided("org.openrewrite:rewrite-xml") alongside the other provided language modules, keeping it off downstream consumers' compile classpaths while making it available here.
  • recipes.csv: regenerated via recipeCsvGenerate to add the six new recipes (recipeCsvValidateCompleteness fails the build otherwise).
  • recipe-writing-lessons.md: notes from writing these.

Testing

43 tests across AddPmdRuleTest, RemovePmdRuleTest, ReplacePmdRuleTest, Pmd6to7MigrationTest, Pmd7RuleRenamesTest, and ModernizePmdTest cover the fully qualified and bare-name forms, <exclude> handling in both the same-ruleset and cross-ruleset cases, enabling a rule by removing its exclusion, rejection of a bare AddPmdRule rule name, a ruleset that needs both a 6→7 replacement and a 7-line rename in one pass, non-<ruleset> XML that must be left alone, and no-change cases. ./gradlew build passes locally.

PMD project

https://docs.pmd-code.org/latest/index.html

https://github.com/pmd/pmd

PMD release notes

https://docs.pmd-code.org/latest/pmd_release_notes.html

https://docs.pmd-code.org/pmd-doc-7.0.0/pmd_release_notes_pmd7.html

🤖 Generated with Claude Code

@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Aug 30, 2026
@sullis
sullis force-pushed the ss-pmd branch 3 times, most recently from e8df97a to 2c3e6ca Compare August 30, 2026 17:05
@sullis sullis changed the title Add PMD ruleset recipes: RemovePmdRule, ReplacePmdRule, and a PMD 6 to 7 migration Add pmd recipes Aug 30, 2026
@sullis
sullis force-pushed the ss-pmd branch 2 times, most recently from 5d4e4d3 to 0ce45f1 Compare August 30, 2026 17:18
@sullis
sullis marked this pull request as ready for review August 30, 2026 17:20
@sullis

sullis commented Aug 30, 2026

Copy link
Copy Markdown
Author

ready for review

@greg-at-moderne

Copy link
Copy Markdown
Contributor

Great job here!

I am wondering, what if we consider this test case:

@Test
void removeStaleExclusionWhenReplacementMovesToAnotherRuleset() {
    rewriteRun(
      spec -> spec.recipe(new ReplacePmdRule(
        "category/java/errorprone.xml/EmptyIfStmt", "category/java/codestyle.xml/EmptyControlStatement")),
      //language=xml
      xml(
        """
          <?xml version="1.0"?>
          <ruleset name="custom">
              <rule ref="category/java/errorprone.xml">
                  <exclude name="EmptyIfStmt"/>
                  <exclude name="EmptyCatchBlock"/>
              </rule>
          </ruleset>
          """,
        """
          <?xml version="1.0"?>
          <ruleset name="custom">
              <rule ref="category/java/errorprone.xml">
                  <exclude name="EmptyCatchBlock"/>
              </rule>
          </ruleset>
          """
      )
    );
}

It currently fails. Shouldn't the migration recipe take care of it?

@sullis
sullis force-pushed the ss-pmd branch 2 times, most recently from 27beecf to 0f71baf Compare August 31, 2026 15:36
@sullis

sullis commented Aug 31, 2026

Copy link
Copy Markdown
Author

Great job here!

I am wondering, what if we consider this test case:

@Test
void removeStaleExclusionWhenReplacementMovesToAnotherRuleset() {

Thanks for the feedback! I added this test case. I pushed a new implementation on this branch

@sullis

sullis commented Sep 3, 2026

Copy link
Copy Markdown
Author

ready for review / feedback @greg-at-moderne

@sullis

sullis commented Sep 3, 2026

Copy link
Copy Markdown
Author

Maybe we should move these recipes into a dedicated repo (rewrite-pmd)
@greg-at-moderne

@greg-at-moderne

Copy link
Copy Markdown
Contributor

Thanks for this valuable contribution!
As discussed, I've gone ahead and created a separate repository with the recipes you have contributed: https://github.com/openrewrite/rewrite-pmd.

And I have been able to get some real-life meaningful changes against one of the OSS repos:
image

@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Sep 4, 2026
@sullis

sullis commented Sep 4, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants