9

How to refactor this class programmatically via Java code/Groovy plugin? Let's say, I need to:

  • Rename foo.method2 to foo.method3
  • Rename myMethod to yourMethod
  • Change the imported package org.you.core.util.AnotherClassFromExternalPackage
import com.me.core.util.AnotherClassFromExternalPackage;
import com.me.core.util.Foo;

public class MyClass implements AnotherClassFromExternalPackage {

    private final Foo foo;

    public MyClass() {
        this.foo = new Foo();
    }

    @Override
    public long myMethod() {
        return foo.method2();
    }
}

How to create a script that will parse the code, apply syntactical transformations and save it using given instructions?

The code above is just an example. The bigger problem is that I have a lot of projects that are using the same external library. And sometimes they release a new version with breaking changes that is breaking the current code after the dependency bump. And I must update the dependency to the latest version every 2 weeks for 10+ projects. I need to write a script that will fix these breaking changes automatically. Ideally, apply code transformations one by one.

12
  • A long time ago I looked for something very similar for Eclipse and didn't find a good answer. It's quite possible that this thing now exists. Commented Feb 21, 2023 at 11:51
  • 1
    Well, the company I'm working for develops a rule-based refactoring engine (Eclipse plugin and Maven plugin), but it doesn't allow custom rules. It does have some for frequently used migration paths, such as between jUnit versions and various styles of Asserts: jsparrow.github.io Commented Feb 21, 2023 at 12:34
  • 2
    Get IntelliJ. It's terrific at refactoring. Not something I'd script. Commented Feb 24, 2023 at 13:20
  • 2
    @duffymo: indeed it is, but as a developer of libraries that others use, I want to hand them a simple way to do all the "mechanical" transformations that might be necessary to migrate to another major version of my library. Commented Feb 24, 2023 at 13:36
  • 2
    @duffymo backwards compatibility is hard, and there are occasionly valid reasons for a library to break it. If it does, helping users with the migration will ease the pain. But this comment thread is not the right place for this discussion. Commented Feb 27, 2023 at 4:27

3 Answers 3

5
+200

Sounds like a job for OpenRewrite. Standard recipes, e.g., for renaming a package are available. You can also write your own recipes, which make use of a visitor pattern over a tree-based representation of your source code (which will preserve, e.g., indentation).

You can use Maven or Gradle to execute the rewrite.

For instance the change package configuration from the getting started page looks like this:

type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.VetToVeterinary
recipeList:
  - org.openrewrite.java.ChangePackage:
    oldPackageName: org.springframework.samples.petclinic.vet
    newPackageName: org.springframework.samples.petclinic.veterinary
Sign up to request clarification or add additional context in comments.

1 Comment

Oh, i've never heard of OpenRewrite, but at quick glance this looks pretty much what I would be looking for (I'm not the original asker, so maybe they are looking for something different)!
2

Not Groovy based (but then other answers suggesting "OpenRewrite" or "IntelliJ" aren't Groovy based either).

What OP is asking for is a program transformation system (PTS) that will let him write custom transformation rules. Most PTS system provide you with access to the target language AST... and you write custom rules by coding procedural visitors that clamber around the tree, matching and splicing individual tree nodes. Eclipse works like this; apparantly also OpenRewrite.

That's a clumsy way to do it, mostly because the details of the AST are voluminous and sort of arbitrary, and your tree-crawling code has to know all of this. See program_transforms on AST vs surface syntax for more details. Ideally you'd write transformations using the surface syntax of the programming language and let the PTS take care of doing the matching and replacement against the actual AST.

Our DMS Software Reengineering Toolkit will let you do this. The transformations op wants do are coded like this:

Domain Java~v11; -- specifies which specific Java dialect grammar is relevant

rule rename_foo(p:access_path_prefix): access_path -> access_path
  = "\p foo.method2" -> "\p foo.method3";

rule rename_myMethod(p:access_path_prefix): access_path -> access_path
  = "\p myMethod" -> "\p your_method";

rule change_import_AnotherClassFromExternalPackage: import_statement -> import_statement
  = "import org.you.core.util.AnotherClassFromExternalPackage"
    -> "import com.me.core.util.AnotherClassFromExternalPackage";

ruleset my_patches = 
   { rename_foo, 
     rename_myMethod, 
     change_import_AnotherClassFromExternalPackage
   }

The Domain keyword tells DMS which precise grammar to use to parse the source text, including the specific dialect. (Java 6 is not the same as Java 11).

Individual rules are named (e.g., rename_foo). The parmeter list (e.g. p:access_path_prefix) allows one to specify a named piece of syntax (e.g., p) that can match an arbitrary tree of the syntax category (e.g., access_path_prefix). Each rule specifies that it is a mapping -> from one piece of syntax to another; for these rules, source and target syntax categories are the same (e.g. access_path).

Each rule also specifies the details of the mapping ( = "*match*" -> "*replacement*" ) where match and replacement are valid surface syntax patterns that match the source and target syntax categories respectively. The reason for the quote marks is to separate the rule syntax from the source/target language syntax (e..g Java) inside the quotes. The match pattern "\p foo.method2" matches an arbitrary path prefix (a sequence of identifiers trailed by dots or a special case of empty, as defined by the Java grammar) followed by the end of the access_path foo.method2 as op desired. The replacement pattern is that same access_path_prefix p followed by the changed end of access path foo.method3.

The ruleset mypatches collects the individual rules into a group that DMS can apply everywhere.

One can run a DMS RuleRewrite engine configured for Java~v11, point it at a legal Java file, and apply the mypatches ruleset to achieve what OP desires. DMS parses the file to build the AST, parses the rules to construct the ASTs necessary for matching, matches the rule ASTs and replaces ASTs where matches occur (so you don't have to do all this stuff with procedural tree hacking). When complete, DMS regenerates the transformed source text, preserving indentation, comments, etc.

So you get a mechanical process for applying the rules of interest, with the rules being fairly easy to write because they use (Java) surface syntax rather than tree-climbing primitives.

These are rather simple examples. We've used DMS to carry out massive rewrites across Java and C++, and even to translate COBOL to Java.

2 Comments

FYI: the semdesigns.com shows a certificate for semanticdesigns.com and even that is no longer valid, so I doubt many people are going to successfully follow that link.
@JoachimSauer Thanks for the note. We'll check it out.
0

I would have an excel with columns like,

enter image description here

  1. Use a library like Apache POI to read the Excel file and extract the required information (project name, package name, class name, method name, and field name).
  2. Use a library like Eclipse JDT to parse the source code and build the AST.
  3. Traverse the AST and identify the nodes that correspond to the specified classes, methods, and fields.
  4. Apply the required modifications to the identified AST nodes.
  5. Generate the modified source code from the updated AST.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.