Last year Jake Wharton published an article titled Just Say mNo to Hungarian Notation and I published a blog post showing how to flag Hungarian Notation as an error in Android Studio. It caused a little bit of controversy.

Just to be clear, it was never my intention for it to be a "you must get rid of Hungarian Notation" post. It just seemed like a good topic to use to show the power of structural search and inspections in Android Studio/IntelliJ.

Bearing that in mind, here is a way to use Android Studio's structural search and replace to replace all of your Hungarian Notations. :-)

(It's probably worth a quick read of how to flag Hungarian Notation as an error in Android Studio before continuing with this one.)

Before we start there's a plugin that will really help with structural search and replace. Install the plugin called PsiViewer. PSI stands for Program Structure Interface and you can learn more about it at http://jetbrains.org/intellij/sdk/docs/basics/architectural_overview/psi_files.html In a short it's the way that Android Studio/IntelliJ internally represents the code that you are writing. For what we are about to attempt we need to peek into the PSI. This plugin allows you to inspect the structure of the PSI tree for the code you are writing. So go ahead and install it. It's ok, I'll wait while you do that and restart Android Studio.

Time passes...

All done? Cool. We'll come back to it in a little while when we need it. For now let's start off with a very simple, contrived app with two fields that use Hungarian Notation.

Basic app

In this example code there are two fields that have Hungarian Notation (one 'm' and one 's'). There is also a method that looks similar - mDummy(). For this increasingly contrived example we want to leave mDummy() alone and just change the fields.

The first thing we are going to do is use the structural search to narrow down what we want to replace later. Use the Find Action shortcut (Shift-Command-A for Mac, Ctrl-Shift-A for Linux/Windows) and type in "Search Structurally"

Search Structurally

This will bring up the Strutural Search dialog. In the Search Template enter $Symbol$ This is basically a variable that will hold the search matches.

Symbols

Hit the button Edit variables and select the Symbol variable from the list. In the Text/regexp fields enter (m|s)[A-Z].* This regular expression will match any string that starts with a lower case 'm' or 's' followed by a capital letter (seem familiar?).

Edit Variables

Hit OK. Check that the Scope of the search is set to Module 'app' and hit the Find button and examine the search results at the bottom. Ah. There's a problem. As well as finding our two fields and the occurrences of them in the code, it has also found the method name mDummy(). How do we avoid that?

This is where PSI comes in. Show the PsiViewer tool window by clicking on the PsiViewer button on the side of the window, using the menu View -> Tool Windows -> PsiViewer or by using the Find Action command and entering PsiViewer. The PsiViewer tool window will show the PSI tree for the code. As you move your edit cursor around your code the element where the cursor is gets highlighted in the PsiViewer.

PSI Viewer

Move the cursor up to the mFmt field definition at the top of the class. The PsiViewer shows this as PsiIdentifier:mFmt and in the properties below it shows the class as com.intellij.psi.impl.source.tree.java.PsiIdentifierImpl. It just trips off the tongue doesn't it? It's the same class for the sAnotherFormat field. Can we make use of this?

Of course we can. Go back to Search Structurally, click on Edit Variables and select Symbol. The regular expression is still set. At the bottom of the dialog there is a section called Script constraints. Click on the three dots next to Script text and enter

Symbol instanceof com.intellij.psi.impl.source.tree.java.PsiIdentifierImpl

It should look a little like this:

Groovy Identifier

What happens is when Android Studio is doing a search it runs this piece of groovy against the possible matches it finds, and if it evaluates/returns true then it is a match. So the above means that it will only match things that match the regular expression and if they are of the correct Psi class. Let's give it a try. Click on OK, OK and Find. Drum roll....

Oh. Nothing is found.

The reason is the Psi symbol that is matched isn't actually the PsiIdentifier, it's the PsiField that it is part of a little way up the tree.

PSI Field

So go back to the structural search, edit variables and edit the script constraint to read

Symbol instanceof com.intellij.psi.impl.source.PsiFieldImpl

So now it looks like:

Groovy Field

This time when we perform the search we get something. But there's still a problem. Although it isn't matching mDummy() any more it has only found the field definitions, not the usages in the code. We've gone from finding too much to not finding enough. Sigh

Try selecting the usages of the mFmt and sAnotherFormat in the code and they will show in the PsiViewer as PsiIdentifier:mFmt. Hmm...we've been here before. Let's walk up the tree a branch and we see PsiReferenceExpression:mFmt which has a class of com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl. Let's use it.

Update the script constraint to

Symbol instanceof com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl ||
    Symbol instanceof com.intellij.psi.impl.source.PsiFieldImpl

So it now looks like:

Groovy Both

So now this will return true if the Psi element is either of these types. Doing the find now finds all of the instances we want to change and none of the ones we don't. Yippee!

How do we replace these variables with non-Hungarian versions? We use Replace Structurally of course. Use the Find Actions... erm...action to go to Replace Structurally. The search template should be the same as it was from the Search Structurally so we can leave that alone, but feel free to check it to be sure. In the Replacement template part enter $UpdatedSymbol$. Again like $Symbol$ this is just a variable. We are telling Android Studio that we want to replace whatever it assigns to $Symbol$ with $UpdatedSymbol$. So we need to tell it what we want that to be. Hit Edit variables and in the Edit Variables dialog click on UpdatedSymbol.

Updated Symbol

Another script. This time what we return from the script is what we want the replacement text to be. Here's one I prepared earlier for you to copy and paste into the script text. (Click on the three dots to get a text edit dialog.)

def vname
if (Symbol instanceof com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl) {
    vname  = Symbol.referenceName
} else {
    vname = Symbol.name
}
return vname.substring(1,2).toLowerCase() + vname.substring(2)

Groovy Constraint

What is this doing? Well the problem is that we can match on two different classes. For the PsiReferenceExpressionImpl class the field that contains the variable name is...well there are a few so I chose to use referenceName. However for the PsiFieldImp class the field we need is called name. So the first part gets the appropriate string based on the class that we have found and sticks it in vname. The last line removes the first letter, lowercases the second letter, and then appends the rest of the name.

(Note that in the script for $Symbol$ we don't use return but we do in this one. In actual fact we don't need the return here either. Groovy automatically returns the value of the last expression evaluated, and if you press an ALT-Return with the cursor on the return in the script dialogs the quick fix suggestion that comes up is Remove 'return' keyword. But it's useful to know as you could write some pretty complicated scripts in there and they might be clearer with return statements in, so I thought I'd show this one with it. But I digress...)

Hit OK a couple of times and then do the Find and in the results at the bottom you'll see, well, not much change except the addition of three buttons. If you're feeling brave you can do Replace All. If you're not so brave you can do Replace Selected for each result. You can also select one of the results and click on Preview Replacement. If you do that for one of the lines with mFmt the preview will show fmt and for sAnotherFormat it will show anotherFormat.

Live dangerously and hit Replace All and examine the new code. It's a thing of beauty isn't it?

You might need to experiment with the PsiViewer. There may be other types that you need to match against. For example if you were to import a static reference to a field that gets updated by the replace the import statement won't get updated with the matches listed above.; you would need to expand the scripts to include elements of class com.intellij.psi.impl.source.PsiImportStaticReferenceImpl. You will have to experiment to find any other PSI classes that need to be included.

Something that is noteworthy is that structural searching and replacing is not limited to Java files. Try opening up a layout XML file and looking at the PsiViewer. There are PSI classes for XML, so you could create a search/replace for specific elements in your layouts, strings, dimens, etc.

You have the tools and you have the talent, and now the world is your mLobster.

Darren @ Æ


Comments

comments powered by Disqus