Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I've been looking high-and-low for a way to rename Java classes en masse without breaking the build.

https://stackoverflow.com/questions/75154524/bulk-rename-of-...

A number of classes have been renamed to have the Kt prefix. There are other changes that don't yet have the new prefix applied because changing those classes would ripple into, essentially, the entire code base. Hence the search for a mass rename procedure that works. Suggestions welcome.

Donald Knuth set that rule up so that "plain.tex" would be the same, world-wide. I don't know why the authors applied the same license to the entire Java application, rather than limit it to Knuth's work.

I've also added a comment "// Originally" that indicates the original name of the class, rather than the name of the file. This minor license violation keeps its spirit if not its letter. Otherwise there'd be no way to extract an inner class.



Ignoring the case of the TFA license that actually requires you to rename the files, renaming packages is normally enough, and is rather straightforward, which is probably why there isn’t much existing tooling for batch-renaming classes.


Wouldn't something like the following shell one-liner rename get you quite close?

   export javas=$(find . -regex '.*\.java$'); for old in $(grep -hrPo 'class (?!Kt)\K[A-Z]\w+'); do echo $old; sed -i "s/\<$old\>/Kt$old/g" $javas;  rename "s/^$old\$/Kt$old/" $javas; done
Untested, unoptimized, needs gnu grep and perl's rename utility (variously packaged as prename or rename-files, as opposed to the crappy linux-utils rename).


Here you are, "gradle build" works with this cleaned up and faster version (and the diff looks plausible on first sight):

   javas=$(find . -regex '.*\.java$')
   sed -i -E "$(printf 's/\\<(%s)\\>/Kt\\1/g;' $(grep -hrPo '\b(class|interface|record|enum) (?!Kt)(?!List\b)(?!Entry\b)\K[A-Z]\w+'))" $(echo $javas); 
   rename 's;\b(?!Kt)(\w+[.]java)$;Kt$1;' $(echo $javas)
(The $(echo $javas) is so it works in both bash and zsh).


If you're fine using intellij then you can probably use its scripting to do this. It has access to most of the same API as plugins and can access intellij features such as its refactor rename.

The (meager) docs on scripting: https://www.jetbrains.com/help/idea/ide-scripting-console.ht.... The plugin docs & intellij source code (especially the unit tests) are a better source of information.

And here's a script that will use intellij's refactoring to rename all classes recursively in a directory to add the prefix Renamed, correctly dealing with references & renaming files if necessary:

  @file:Suppress("NAME_SHADOWING")

  import com.intellij.notification.Notification
  import com.intellij.notification.NotificationType
  import com.intellij.notification.Notifications
  import com.intellij.openapi.actionSystem.*
  import com.intellij.openapi.keymap.KeymapManager
  import com.intellij.openapi.command.WriteCommandAction
  import com.intellij.psi.*
  import com.intellij.psi.search.*
  import com.intellij.refactoring.rename.RenameProcessor
  import com.intellij.util.ThrowableConsumer
  import java.io.PrintWriter
  import java.io.StringWriter
  import javax.swing.KeyStroke

  // Usage: In IDEA: Tools -> IDE Scripting Console -> Kotlin
  // Ctrl+A, Ctrl+Enter to run the script
  // Select folder containing target classes, Ctrl+Shift+A to open action menu, search for Bulk refactor

  //<editor-fold desc="Boilerplate">
  val b = bindings as Map<*, *>
  val IDE = b["IDE"] as com.intellij.ide.script.IDE

  fun registerAction(
    name: String,
    keyBind: String? = null,
    consumer: ThrowableConsumer<AnActionEvent, Throwable>
  ) {
    registerAction(name, keyBind, object : AnAction() {
      override fun actionPerformed(event: AnActionEvent) {
        try {
          consumer.consume(event);
        } catch (t: Throwable) {
          val sw = StringWriter()
          t.printStackTrace(PrintWriter(sw))
          log("Exception in action $name: $t\n\n\n$sw", NotificationType.ERROR)
          throw t
        }
      }
    });
  }

  fun registerAction(name: String, keyBind: String? = null, action: AnAction) {
    action.templatePresentation.text = name;
    action.templatePresentation.description = name;

    KeymapManager.getInstance().activeKeymap.removeAllActionShortcuts(name);
    ActionManager.getInstance().unregisterAction(name);
    ActionManager.getInstance().registerAction(name, action);

    if (keyBind != null) {
      KeymapManager.getInstance().activeKeymap.addShortcut(
        name,
        KeyboardShortcut(KeyStroke.getKeyStroke(keyBind), null)
      );
    }
  }

  fun log(msg: String, notificationType: NotificationType = NotificationType.INFORMATION) {
    log("Scripted Action", msg, notificationType)
  }

  fun log(
    title: String,
    msg: String,
    notificationType: NotificationType = NotificationType.INFORMATION
  ) {
    Notifications.Bus.notify(
      Notification(
        "scriptedAction",
        title,
        msg,
        notificationType
      )
    )
  }
  //</editor-fold>

  registerAction("Bulk refactor") lambda@{ event ->
    val project = event.project ?: return@lambda;
    val psiElement = event.getData(LangDataKeys.PSI_ELEMENT) ?: return@lambda

    log("Bulk refactor for: $psiElement")

    WriteCommandAction.writeCommandAction(event.project).withGlobalUndo().run<Throwable> {
      psiElement.accept(object : PsiRecursiveElementWalkingVisitor() {
        override fun visitElement(element: PsiElement) {
          super.visitElement(element);
          if (element !is PsiClass) {
            return
          }

          if(element.name?.startsWith("Renamed") == false) {
            log("Renaming $element")

            // arg4 = isSearchInComments
            // arg5 = isSearchTextOccurrences
            val processor = object : RenameProcessor(project, element, "Renamed" + element.name, false, false) {
              override fun isPreviewUsages(usages: Array<out UsageInfo>): Boolean {
                return false
              }
            }
  
            processor.run()
          }
        }
      })
    }
  }
If your project is large this will temporarily freeze intellij since it doesn't properly run on the background thread but just give it a minute.

By the way, this should work on all languages that IntelliJ has a language plugin for, not just Java.

Edit: Modified script to override isPreviewUsages to prevent intellij from opening confirmation dialogs


This looks rather promising. Doesn't work in my version of IDEA (Build #IC-223.8214.52, built on December 20, 2022, Community Edition), due to the following errors when running the script:

> Argument for @NotNull parameter 'module' of org/jetbrains/.../...GroovyRunner must not be null.

The project doesn't have any references to the NotNull annotation, and there are a number of what appear to be compile errors:

https://i.ibb.co/dfxbV4p/rename-script.png

Running with Ctrl+Enter produces:

    > @file:Suppress("NAME_SHADOWING")
    MultipleCompilationErrorsException: startup failed:
    Script1.groovy: 1: Unexpected input: '@file:' @ line 1, column 6.
       @file:Suppress("NAME_SHADOWING")
            ^

    1 error
I removed the line and re-ran, which resulted in:

    > import com.intellij.notification.Notification
    [22 ms]=> null
Then I selected the entire text and pressed Ctrl+Enter again, which returned:

    MultipleCompilationErrorsException: startup failed:
    Script4.groovy: 20: Unexpected input: '<*' @ line 20, column 25.
       val b = bindings as Map<*, *>
                               ^

    1 error
After changing the asterisks to question marks, the script failed to run, indicating:

    MultipleCompilationErrorsException: startup failed:
    Script5.groovy: 25: Unexpected input: 'registerAction(\n    name: String,\n    keyBind: String? =' @ line 25, column 22.
           keyBind: String? = null,
                            ^

    1 error
I appreciate the effort, though!


Make sure you pick Kotlin for the scripting language, not Groovy.

Then run the script like this:

1. In IDEA: Tools -> IDE Scripting Console -> Kotlin

2. Paste in the script

3. Ctrl+A, Ctrl+Enter to load the script. This should show a green "Loaded!" notification

4. Select a folder containing the classes in the Project window

5. Press Ctrl+Shift+A to open action menu

6. Search for Bulk refactor and select it

Also please note the first version of the script had a bug that would open confirmation dialogues for some refactorings, so see the edited script.


> 1. In IDEA: Tools -> IDE Scripting Console -> Kotlin

No such sub-menu of Tools exists, but that's fine because I've mapped Ctrl+Shift+A to bring up the menu. I installed the Kotlin plugin and tried again. There was a missing import. After fixing the missing import:

    [732 ms]=> null
I switched "Rename" to "Kt", of course, and re-ran the script.

> 4. Select a folder containing the classes in the Project window

> 5. Press Ctrl+Shift+A to open action menu

> 6. Search for Bulk refactor and select it

Opening the menu with Ctrl+Shift+A doesn't show the bulk refactor script, but that could be because I remapped Ctrl+Shift+A and am using the NetBeans keyboard mappings, rather than the IntelliJ map.

https://i.ibb.co/5LTsffj/bulk-refactor.png


> There was a missing import

Oops :)

When pressing Ctrl+Enter, did you see the green "Loaded!" notification in the bottom right? If so, then I guess installing the Kotlin plugin worked.

> Opening the menu with Ctrl+Shift+A doesn't show the bulk refactor script

The Ctrl+Shift+A menu from the default keymap that I'm talking about is the "Actions" dialog. You can also get there via "Navigate -> Search Everywhere" and then selecting the "Actions" tab (or just searching in All).

Also you only have to select a single folder/package and it will be processed recursively. I did not test what happens when you select multiple folders. Might be fine, might explode.

edit: Just realized the script I pasted here does not emit the "Loaded!" log message on load. So disregard those comments. If you want to verify it loads correctly, add this as the last line:

    log("Loaded!")
edit: HN rate limited my account again, so I'm not allowed to post any new comments for the next few hours. Good luck!

If you do wind up using this script and want to modify it, it is helpful to read https://plugins.jetbrains.com/docs/intellij/psi.html & to install the PsiViewer plugin from the marketplace.


There are some minor clean-up issues, but overall, it did the trick.

https://i.ibb.co/MP2JNRM/classes-renamed.png

Thank you!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: