Transforming Deprecated Code with RewriteTool

Deprecating code is quite common practice. You want to move forward, fix bad code and improve your API. However this can easily become daunting task, because there may be a lot of code to change, or you simply don’t want to break code of other people. Ideally you would like to have some automatic way to change code that can be used both by you, and users of your library. So let’s explore how this can be done in Pharo Smalltalk! I will demonstrate it on deprecating couple of methods in Roassal visualization library.

Deprecating Code

Imagine that you want to deprecate a method. In Roassal we would like to deprecate all methods RTEdge class>>buildEdges*, and later replace them with RTEdgeBuilder.

That’s easy, you just add #deprecated: to your method and give it some description.

1
2
3
RTEdge class>>buildEdgesFromObjects: objects from: fromBlock to: toBlock inView: view
self deprecated: 'Please use RTEdgeBuilder instead'.
^ self buildEdgesFromObjects: objects from: fromBlock to: toBlock using: RTLine inView: view

Now every time someone calls this method a warning will appear:

deprecated-warning.png

If you want to disable the warning popup, you can execute:

1
Deprecation raiseWarning: false

This will disable the warning, but the information is still available in Transcript (Tools > Transcript).

Identifying Senders

Now we have quite a lot of deprecated methods:

RTEdge-class.png

That’s a lot of methods. Just finding all the code that uses it one method at a time is a lot of work, let alone fixing it. So how do we tackle this Pharo way?

In Smalltalk, everything is an Object

String? Object. Number? Object. Class? Object. Method? Object.

What does this mean practically? It means that we can simply ask a class for all its methods and filter out the deprecated ones.

So let’s open up Playground and inspect (Do it all and go) all the methods of the class (both normal and deprecated).

RTEdge-methods.png

You can then click in the inspector on one of the methods to see its details.

RTEdge-method.png

It shows that methods are actually instances of CompiledMethod. If you browse the CompiledMethods class, you can see many interesting methods such as #senders, #linesOfCode, #sourceCode, but most importantly #isDeprecated. (I recommend also looking as how the method is implemented; it surprised me, and it might surprise you. Hint: source code is also object.)

So, now we can simply filter the methods we want.

1
RTEdge class methods select: #isDeprecated

(#isDeprecated is same as sending [ :each | each isDeprecated ]).

But we are not really interested in those methods, we want the code that sends them. So how we find them? Method is an object, so we ask it for its senders.

1
compiledMethod senders

Combine it all together and we have senders of all our deprecated methods.

senders-1.png

(I’ve removed all empty results and ignored RTEdge senders (because they are themselves already deprecated)). Clicking through the inspector you can again quickly navigate to the sender method. In fact you can change the code directly in the inspector!

senders-2.png

Unfortunately there’s quite a lot of methods that needs to be changed. Doing that by hand is laborious, repetitive and error-prone task. So ideally you would like some tool that does it automatically for you. In the saved time you can write a blog post. :) It’s time for Rewrite Tool!

Rewrite Tool

Rewrite Tool is a brand new tool that allows you to create rules for rewriting your code. It has won third place in ESUG 2015 Innovation Technology Awards, so I’m definitely not the only person impressed by it. You can watch a demo here http://myfuncoding.blogspot.cz/2015/02/video-presentation-of-rewrite-tool.html

(I’ll skip installation instruction, since you can find it in the link above. (Or just use Pharo’s Catalog Browser.))

So how can we use it for our needs?

Let’s start by opening it via Tools > RewriteRuleBuilder.

rewriterulebuilder.png

In bottom half you write templates for both original and target code. An input (in top left corner) is then transformed according to the templates, and the output ends up in top right corner.

So let’s put in some code of what we want to change and how we want to change it (don’t forget to press ‘’ctrl+s’’ (or ‘’+s’’) to save the text area):

rewriterulebuilder-2.png

Use ``@someLabel syntax to identify variables in the transformation rule. The top right corner is automatically generated, so we can see how the input has transformed.

Now let’s press Generate rule and name it RTEdgeDeprecation. This will create a new class RTEdgeDeprecation in RewriteRulesCustom package.

Finally let’s apply this newly created rule to an actual class.

Open Tools > RewriteRuleBrowser

rewriterulebrowser.png

Then select package and class(es) you want to change. (You can select multiple classes with shift, or right mouse click)

In my case I’ve selected Roassal2 package and RTGeneralExample class, because we’ve identified it earlier as one of the senders.

In the bottom toolbar check “Only custom rules” to see only your rules and select rule you want to apply.

And at last click Apply rule. This will open Changes Browser so you can review all the changes to the code that are about to happen. Note that this will also apply Pharo’s automatic formatting (which is usually a good thing).

With the described approach we can easily define new rewrite rules and change code in couple of clicks.

If you are maintainer of a library, you can even publish the rewrite rules alongside your code, so other people can migrate with ease.

Scripted Rewriting

But we are in Smalltalk, so why apply the rules manually when we can script it?

Rewrite Tool is a new tool and currently doesn’t support any direct way for applying multiple rules programmatically. However in Pharo that should not stop you; the source code is always available and ready to be explored, so you should always at least try to find a way. Even if you don’t succeed, you will learn a lot about the tool, system and Pharo.

You know that you can apply a rule through UI of RewriteRuleBrowser (Apply rule button mentioned earlier), so let’s find a class of similar (or in fact same) name. From there we can see couple of interesting methods: #apply, #applyRule:on: and #getResultOfRule:appliedTo:

They are just couple lines of code long and seem to provide all we need, so copy what you need into Playground… a good trick is to replace methods with blocks of similar name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|getRuleResult applyRule rewriteRules classes changes|
"Extracted from RewriteRuleBrowser>>getResultOfRule:appliedTo:"
getRuleResult := [ :aRuleClass :aCollection |
RBSmalllintChecker
runRule: aRuleClass new
onEnvironment: (RBClassEnvironment classes: aCollection)
].
"Extracted from RewriteRuleBrowser>>applyRule:on:"
applyRule := [ :aRuleClass :aCollection | |result|
result := getRuleResult value: aRuleClass value: aCollection.
result builder changes.
].
"All the rules we want to apply"
rewriteRules := { Rule1 . Rule2 }.
"All sender classes"
classes := { Class1 . Class2 . Class3 }.
changes := (rewriteRules collect: [ :rule | applyRule value: rule value: classes ]) flattened.
(ChangesBrowser changes: changes) open

It’s missing just two more things: the rules and the classes.

All rules I’ve created begins with RTEdgeRewrite, and it appears that they are all subclasses of RBTransformationRule. Since class is also an object, we can ask the parent class for it’s children and get what we want.

1
RBTransformationRule allSubclasses select: [ :each | each name beginsWith: #RTEdgeRewrite ]

To get the classes you want to transform, extend the code for retrieving senders (cf. beginning):

1
2
3
4
5
6
7
8
9
10
11
deps := RTEdge class methods select: #isDeprecated.
allSenders := deps collect: [ :method |
method senders select: [ :sender |
sender className ~= RTEdge className
]
].
nonEmpty := allSenders reject: #isEmpty.
“collect classes of senders and remove duplicates with asSet”
classes := (nonEmpty flattened collect: #realClass) asSet.
“select only Roassal classes”
rtClasses := classes select: [ :each | each name beginsWith: #RT ].

Now put it all together and execute your grandiose script. Review your changes and you are done!

finale.png

Final Notes

As with everything, learning things and doing things for the first time takes more time and effort. By the end it was faster for me to create rewrite rule than change code manually even if there were just two senders. Also I’ve asked Mark Rizun, the creator of Rewrite Tool for couple of things, so doing mass rewrites and rule editing should be easier in the future.