Skip to content

Add pmd recipes - #1034

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

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

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.

2 participants