Structural Replace script to rename class renames class and deletes file content

I'd like to rename all classes that extend Spring's JUnit helper classes from `xxxTest` to `xxxIT`.

For instance, if I have

@SpringJUnitConfig(locations="...")
@Transactional
public abstract class AbstractBaseTest extends AbstractTransactionalJUnit4SpringContextTests {

@Autowired
private SomeInjection someInjection;
// more code elided
}

after running the replace I would have

        @SpringJUnitConfig(locations="...")
@Transactional
public abstract class AbstractBaseIT extends AbstractTransactionalJUnit4SpringContextTests {

@Autowired
private SomeInjection someInjection;
// more code elided
}

And the same would happen for all the classes that extend this base class.

To do this I've come up with the following template:

<replaceConfiguration name="Misnamed Integration Test" text="class $Class$ {}" recursive="false" caseInsensitive="true" type="JAVA" pattern_context="default" reformatAccordingToStyle="false" shortenFQN="false" replacement="class $NewName$ {}">
<constraint name="__context__" within="" contains="" />
<constraint name="Class" script="&quot;if (__context__.interface || __context__.enum) {&#10; return false&#10;}&#10;def p = __context__.getSuperClass()&#10;while (p != null) {&#10; println(&quot;parent: &quot; + p)&#10; if (p.getName().endsWith(&quot;JUnit4SpringContextTests&quot;))&#10; return true;&#10; p = p.getSuperClass()&#10;}&#10;return false&#10;&quot;" regexp="\b(.*)((?:Integration)?Test)(.*)\b" target="true" within="" contains="" />
<variableDefinition name="NewName" script="&quot;__context__.name.replaceAll(&quot;((?:Integration)?Test)&quot;, &quot;IT&quot;)&quot;" />
</replaceConfiguration>

The problem is, the replacement produces the following:

class AbstractBaseIT {}

That's it. It strips all the annotations, the modifiers, the extends and implements, as well as the entire contents of the class.

The find part works fine. It's correctly identifying all the classes that should be integration tests.

How do I get the replace to just change the name and not the rest?

UPDATE:

I got closer with the following:

<replaceConfiguration name="Misnamed Integration Test" text="class $Class$ {}" recursive="false" caseInsensitive="true" type="JAVA" pattern_context="default" reformatAccordingToStyle="false" shortenFQN="false" replacement="class $NewName$ {}">
<constraint name="__context__" within="" contains="" />
<constraint name="Class" script="&quot;if (__context__.interface || __context__.enum) {&#10; return false&#10;}&#10;println(&quot;inital context: &quot; + __context__)&#10;def p = __context__.getSuperClass()&#10;while (p != null) {&#10; if (p.getName().endsWith(&quot;JUnit4SpringContextTests&quot;))&#10; return true;&#10; p = p.getSuperClass()&#10;}&#10;return false&#10;&quot;" regexp="\b(.*)((?:Integration)?Test)(.*)\b" target="true" within="" contains="" />
<variableDefinition name="NewName" script="&quot;__context__.setName( __context__.name.replaceAll(&quot;((?:Integration)Test)&quot;, &quot;IT&quot;))&#10;__context__.text&quot;" />
</replaceConfiguration>

which, while admittedly still wrong, is closer, but throws the following exception:

INFO - tcher.predicates.ScriptSupport - Exception thrown by Structural Search Groovy Script 
com.intellij.util.IncorrectOperationException: Must not change PSI outside command or undo-transparent action. See com.intellij.openapi.command.WriteCommandAction or com.intellij.openapi.command.CommandProcessor
Is something like this just not even possible?
 
There are almost 1,500 classes to rename, so I'm happy to consider multi-phase approaches, too.
7 comments
Comment actions Permalink
Official comment

Something like the following should get you a little closer to your desired result:

<replaceConfiguration name="Misnamed Integration Test" text="class $Class$ extends $TestCase$ {}" recursive="false" caseInsensitive="true" type="JAVA" pattern_context="default" reformatAccordingToStyle="false" shortenFQN="false" replacement="$X$">
<constraint name="__context__" within="" contains="" />
<constraint name="Class" regexp="(.*)((?:Integration)?Test)(.*)" target="true" within="" contains="" />
<constraint name="TestCase" regexp=".*?JUnit4SpringContextTests" withinHierarchy="true" within="" contains="" />
<variableDefinition name="X" script="&quot;def newName = __context__.name.replaceAll(&quot;(?:Integration)?Test&quot;, &quot;IT&quot;)&#10;__context__.getText().replaceAll(&quot;\\b&quot; + __context__.name + &quot;\\b&quot;, newName)&quot;" />
</replaceConfiguration>

This should rename the class and keep all its contents. But it does not rename the files containing the classes.

I am working on improving structural replacement for the next versions of IntelliJ IDEA, so the situation should improve. Please also vote for https://youtrack.jetbrains.com/issue/IDEA-12246

Bas

Comment actions Permalink

UPDATE 2:

I know this is overkill, but I've now got the replace script to 

import com.intellij.psi.*;
import com.intellij.ide.*;
import com.intellij.openapi.actionSystem.impl.*;
import com.intellij.openapi.actionSystem.*;
import com.intellij.refactoring.*;
def dc = DataManager.getInstance().getDataContextFromFocus().getResult()
def n = __context__.clone()
n.setName(n.name.replaceAll("((?:Integration)Test)", "IT"))
def h = RefactoringActionHandlerFactory.getInstance().createRenameHandler()
PsiElement[] a = [(PsiElement)__context__].toArray() as PsiElement[]
h.invoke(__context__.project, a, (DataContext)dc)
__context__.text

and this fails with:

ERROR - api.command.WriteCommandAction - Must not start write action from within read action in the other thread - deadlock is coming 
java.lang.Throwable: Must not start write action from within read action in the other thread - deadlock is coming

So it's looking like I've got the script (possibly) doing close to what I want it to do (I noticed in editing this post I'm not actually changing the name), it's just impossible to do in just Structural Replace.

0
Comment actions Permalink

Is there a way to create an Intention based on a Structural Find that has a "Rename Refactor" as the suggested fix instead of the replace template?

0
Comment actions Permalink

>Is there a way to create an Intention based on a Structural Find that has a "Rename Refactor" as the suggested fix instead of the replace template?

You can create an inspection, based on the created SSR, that you may run and apply the fixes from the results.

I've created the following SSR template:

<replaceConfiguration name="Direct subclasses" text="@$Annotation$&#10;class $Class$ extends $Parent$ {&#10;    $CONTENT$&#10;}" recursive="false" caseInsensitive="true" type="JAVA" pattern_context="default" reformatAccordingToStyle="false" shortenFQN="false" replacement="@$Annotation$&#10;class $Replaced$ extends $Parent$ {&#10;    $CONTENT$&#10;}">
  <constraint name="__context__" within="" contains="" />
  <constraint name="Class" target="true" within="" contains="" />
  <constraint name="Parent" regexp="AbstractTransactionalJUnit4SpringContextTests" within="" contains="" />
  <constraint name="CONTENT" within="" contains="" />
  <constraint name="Annotation" minCount="0" maxCount="2147483647" within="" contains="" />
  <variableDefinition name="Replaced" script="&quot;Replaced.name.replaceAll(&quot;Test&quot;,&quot;IT&quot;)&quot;" />
</replaceConfiguration>

but it works only when there is single class annotation. I have asked the responsible developer to comment about this case.

0
Comment actions Permalink

Andrey Dernov Thanks! That's very similar to something I came up with along the way, but it unfortunately still misses the boat. There are a few things missing (like JavaDoc and class modifiers). It also suffers the same issue that while the class is renamed, the file is not, and there is no bulk-way to run the "rename file" inspection for mismatched file and class names.

0
Comment actions Permalink

Bas Leijdekkers Thanks! I got to very much the same place via slightly different means. I had upvoted that issue previously, and hope this gets resolved before I need it again ;)

I think what I'll have to do is export the "Find" results and write a Perl script to do the actual renaming.

0
Comment actions Permalink

That's very similar to something I came up with along the way, but it unfortunately still misses the boat. There are a few things missing (like JavaDoc and class modifiers). 

0

Please sign in to leave a comment.