- Substitute Architectural Decision
- Refactor Architectural Structure
- Widespread Architectural Change
- Other Architectural Changes
- Migrating or upgrading (similar) APIs, frameworks or libraries
- Changing or unifying coding conventions
- Fixing compiler warnings and removing technical debt
- Consistently applying or changing aspects like logging or security
- Applying internationalization
- Migrate between languages, e.g. SQL vs. HQL
Options
I have been using some techniques since 2004 because as Code Cop I value code consistency. Two years ago I had the opportunity to discuss the topic with Alexandru Bolboaca and other experienced developers during a small unconference and we came up with even more options, as shown in the picture on the right. The goal of this and the next articles is to introduce you to these options, the power of each one and the cost of using it. A word of warning: This is a raw list. I have used some but not all of them and I have yet to explore many options in more detail.
Supporting Manual Changes With Fast Navigation
The main challenge of widespread changes is the high number of occurrences. If the change itself is small, finding all occurrences and navigating to them is a major effort. Support for fast navigation would make things easier. The most basic form of this is to modify or delete something and see all resulting compile errors. Of course this only works with static languages. In Eclipse this works very well because Eclipse is compiling the code all the time and you get red markers just in time. Often it is possible to create a list of all lines that need to be changed. In the past have created custom rules of static analysis tools like PMD to find all places I needed to change. Such a list can be used to open the file and jump to the proper line, one source file after another.
As example, here is a Ruby script that converts
pmd.xml
which is contains the PMD violations from the Apache Maven PMD Plugin to Java stack traces suitable for Eclipse.#! ruby require 'rexml/document' xml_doc = REXML::Document.new(File.new('./target/site/pmd.xml')) xml_doc.elements.each('pmd/file/violation') do |violation| if violation.attributes['class'] class_name = violation.attributes['package'] + '.' + violation.attributes['class'] short_name = violation.attributes['class'].sub(/\$.*$/, '') line_number = violation.attributes['beginline'] rule = violation.attributes['rule'] puts "#{class_name}.m(#{short_name}.java:#{line_number}) #{rule}" end endThe script uses an XML parser to extract class names and line numbers from the violation report. After pasting the output into Eclipse's Stacktrace Console, you can click each line one by one and Eclipse opens each file in the editor with the cursor in the proper line. This is pretty neat. Another way is to script Vim to open each file and navigate to the proper line using
vi +<line number> <file name>
. Many editors support similar navigation with the pattern <file name>:<line number>
.Enabling fast navigation is a big help. The power this option is high (that is 4 out of 5 on my personal, totally subjective scale) and the effort to find the needed lines and script them might be medium.
Search and Replace (Across File System)
The most straight forward way to change similar code is by Search and Replace. Many tools offer to search and replace across the whole project, allowing to change many files at once. Even converting basic scenarios, which sometimes cover up to 80%, helps a lot. Unfortunately basic search is very limited. I rate its power low and the effort to use it is also low.
Scripted Search and Replace using Regular Expressions
Now Regular Expressions are much more powerful than basic search. Many editors allow Regular Expressions in Search and Replace. I recommend creating a little script. The traditional approach would be Bash with
sed
and awk
but I have used Ruby and Python (or even Perl) to automate lots of different changes. While the script adds extra work to traverse all the source directories and files, the extra flexibility is needed for conditional logic, e.g. adding an import to a new class if it was not imported before. Also in a script Regular Expressions can be nested, i.e. analysing the match of an expression further in a second step. This helps to keep the expressions simple.For example I used a script to migrate Java's
clone()
methods from version 1.4 to 5. Java 5 offers covariant return types which can be used for clone()
, removing the cast from client code. The following Ruby snippet is called with the name of the class and the full Java source as string:shortName = shortClassNameFromClassName(className) if source =~ / Object clone\(\)/ # use covariant return type for method signature source = $` + " #{shortName} clone()" + $' end if source =~ /return super\.clone\(\);/ # add cast to make code compile again source = $` + "return (#{shortName}) super.clone();" + $' endIn the code base where I applied this widespread change, it fixed 90% of all occurrences as
clone
methods did not do anything else. It also created some broken code which I reverted. I always review automated changes, even large numbers, as jumping from diff to diff is pretty fast with modern tooling. Scripts using Regular Expressions helped me a lot in the past and I rate their power to high. Creating them is some effort, e.g. a medium amount of work.Macros and Scripts
Many tools like Vim, Emacs, Visual Studio, Notepad++ and IntelliJ IDEA allow creation or recording Keyboard and or mouse macros or Application scripts. With them, frequently used or repetitive sequences of keystrokes and mouse movements can be automated, and that is exactly what we want to do. When using Macros the approach is the opposite as for navigation markers: We find the place of the needed change manually and let the Macro do its magic.
Most people I have talked to know Macros and used them earlier (e.g. 20 years ago) but not recently in modern IDEs. Alex has used Vim Scripts to automate repetitive coding tasks. I have not used it and relating to his experience. Here is a Vim script function which extracts a variable, taken from Gary Bernhardt's dotfiles:
function! ExtractVariable() let name = input("Variable name: ") if name == '' return endif " Enter visual mode normal! gv " Replace selected text with the variable name exec "normal c" . name " Define the variable on the line above exec "normal! O" . name . " = " " Paste the original selected text to be the variable value normal! $p endfunctionI guess large macros will not be very readable and hard to change but they will be able to do everything what Vim can do, which is everything. ;-) They are powerful and easy to create - as soon as you know Vim script of course.
Structural Search and Replace
IntelliJ IDEA offers Structural Search and Replace which performs search and replace across the whole project, taking advantage of IntelliJ IDEA's awareness of the syntax and code structure of the supported languages.. In other words it is Search and Replace on the Abstract Syntax Tree (AST). Additionally it is possible to apply semantic conditions to the search, for example locate the symbols that are read or written to. It is available for Java and C# (Resharper) and probably in other JetBrains products as well. I have not used it. People who use it tell me that it is useful and easy to use - or maybe not that easy to use. Some people say it is too complicated and they can not make it work.
Online help says that you can apply constraints described as Groovy scripts and make use of IntelliJ IDEA PSI (Program Structure Interface) API for the used programming language. As PSI is the IntelliJ version of the AST, this approach is very powerful but you need to work the PSI/AST which is (by its nature) complicated. This requires a higher effort.
To be continued
And there are many more options to be explored, e.g. scripting code changes inside IDEs or using Refactoring APIs as well as advanced tools outside of IDEs. I will continue my list in the next part. Stay tuned.
5 comments:
There is also a wonderful tool similar to Structural Search and Replace: Refaster, which allows to express the patterns in Java code. There is an analogue to SSR with PSI too: writing an ErrorProne check operating on the AST.
https://errorprone.info/docs/refaster
Thanks for the very useful article. On the macro part "...IntelliJ IDEA (by use of a plugin)..." - don't suppose you recall the plugin? Is it IdeaVim? TIA
kimptoc, sorry I do not know which plugin. I never used it, it just came up in the discussion. There seems to be native support for macros anyway.
Дмитрий, thank you. Refaster looks awesome. I will add it to my list in part two of the article where I will discuss tools working on the AST directly.
Thanks Peter. I’ve found the native macros a bit limited.
Post a Comment