Skip to content

Romain Degrave

3 posts by Romain Degrave

Transformation journey (2/2) : Copying FASTs and creating nodes

Welcome back to the little series of blog posts surrounding code transformation! In the previous post, we started to build a transformation tool using a basic fictional transformation use case on the software ArgoUML. In this post, we will add behavior to the class we created previously, to achieve the actual transformation while making sure that we do not modify the original model in the process. As a reminder, this is the scenario: in the ArgoUML system, three classes define and use a method named logError. A class ArgoLogger has been defined that contains a static method logError. The transformation task is to add a receiver node to each logError method invocation so that the method called is now ArgoLogger.logError.

As this blog post follows what was discussed and built in the first one of this series, a lot of information (how to build the image and model used for our use case, but also the use case itself) is provided in the previous post. If you haven’t read it, or if you forgot the content of the post, it is recommended to go back to it, as it will not be repeated in this post.

For this blog post, we will have to import a tool library, FAST-Java-Tools, which contains several tools to use on FAST for Java entities. It also contains practical tools to build transformation tools! To load the project, execute this command in a Playground :

Metacello new
baseline: 'FASTJavaTools';
repository: 'github://moosetechnology/FAST-Java-Tools:main/src';
load

Loading it will automatically load the FAST-Java repository. Carrefour and MoTion (which were used in the previous post) are still necessary, so if you are starting from a clean image, you will need them as well.

With our import done, we are ready to get to work! 😄 To create our transformation, we will modify the FAST of each candidate method for our transformation, and this modified FAST will then be generated into source code that we will be able to inject into the actual source files. Therefore, our next step is modifying the FAST of our methods. However, we will begin by making a copy of said FAST, to ensure that our transformation will not modify the actual original model of our method. If that happens, we will still be able to run Carrefour on our method again (using the message generateFastAndBind) but it is better to avoid that scenario. If we have a bug on our tool and the FAST of many methods ends up being modified, then re-calculating and binding every FAST will be a big waste of time. Making a copy allows us to make the same actions, but ensures that our model remains untouched.

To create a copy, we have a visitor that fortunately does all the job for us! Using this simple command will give you a FAST copy :

FASTJavaCopyVisitor new copy: aFASTMethod

However, to make things even easier, we will use an already made wrapper that will contain our candidate method for transformation in every useful “format” for transformation, which are :

  • The Famix entity of that method
  • The original FAST
  • The copied (transformed) FAST

{::comment} This wrapper also has collections to store specific nodes, but patience, we will see how to use these in the next blog post! 😉 {:/comment}

For now, let’s add a method to create that wrapper on a collection of candidate methods, in the class created in the first post :

createWrappersForCandidateMethods: aCollectionOfFamixJavaMethods
^ aCollectionOfFamixJavaMethods collect: [ :fmxMeth |
| fastMethod |
fmxMeth generateFastIfNotDoneAndBind.
fastMethod := fmxMeth fast.
JavaMethodToTransformWrapper
forFamixMethod: fmxMeth
andFAST: fastMethod ]

With this, we created wrappers for each method to transform, and each of these wrappers contains a copy of the FAST ready to be transformed. Before getting to the next step, we will define one more method, which will contain the “main behavior” of our tool, so that calling this method is enough to follow each step in order to transform the code.

findAndTransformAllLogErrorInvocations
| wrappers |
wrappers := self createWrappersForCandidateMethods:
self fetchLogErrorMethodInvocationsSenders.
wrappers do: [ :w | self transformMethod: w ].

If you followed everything so far, you can notice that there are two new methods called here. The first one, fetchLogErrorMethodInvocationsSenders, is just here to make the code easier to read :

fetchLogErrorMethodInvocationsSenders
^ self fetchLogErrorMethodInvocations
collect: [ :mi | mi sender ]

The other one, transformMethod:, is the next step in this transformation journey, creating a new receiver node for our logError invocations and applying it on said invocations.

Therefore, the first thing to do is pretty obvious. We need to build a receiver node, for the ArgoLogger class. This transformation use case is pretty easy, so the node to create is always the same and is a pretty basic node.

We can in this case simply build this node “manually” in a method :

createNewReceiverNode
^ FASTJavaIdentifier new
name: 'ArgoLogger';
yourself

However, this option is not possible for every use case. Sometimes the node to create will be dependent on the context (the name of an identifier, method or variable might change), or we might have to create more complex nodes, composed of several nodes. In order to create those more complex nodes, one option is to parse the code using the parsing tool from the FAST-Java repository, the JavaSmaCCProgramNodeImporterVisitor, before inspecting the parsed result using the FASTDump view.

"Inspecting with FASTDump"

Using this view of the inspector then enable us to get the node or tree that we seek for our transformation and copy-paste the code to create it in a method for our tool to use it when needed.

Now that we have created our node and saw different means to do so, all that remains to do is setting it as the receiver of every copied FAST method to complete our transformation! Let us do so right away, by finally implementing our transformMethod: from before:

transformMethod: aJavaMethodWrapper
| methodInvocationNode |
methodInvocationNode := self motionQueryForFastMethod:
aJavaMethodWrapper transformedFastMethod.
methodInvocationNode receiver: self createNewReceiverNode

There are two things worth saying after making this method. First, and hopefully without boring you by repeating this over and over… This is a very simple transformation use case! As you can see, the only thing we do is using the receiver: setter on the appropriate node. Depending on the kind of edit that you want to apply on a method, you might need to experiment a bit and read the comments and method list of the classes of the nodes you are trying to edit. To experiment, one way is inspecting one of the FAST method that you wish to transform, to find the nodes you need to locate and transform, then go and read the class documentation and method list of the types of those nodes to find the appropriate setters you need to use. Another fun way to do so involves the Playground and using once again the parsing tool used in the previous section to use the FASTDump view of the Moose Inspector.

"Parsing a method"

Using this little code snippet, you can parse a Java method and inspect its FAST. So, copy the method you need to transform, inspect it, then transform the code of this method manually, then parse it and inspect it again! It’s an easy way to start the work towards building a transformation tool. 😄

The second thing to notice, is the use of MoTion and not Carrefour. As we now modify a copy of a FAST, it is easier to run the MoTion query on it to get the node we are looking for right away, rather than go through the original Famix invocation. However, it is still doable, as the original and copied FAST nodes both store their counterpart in an attribute slot. We can see this by inspecting one of the fast methods in a wrapper resulting from our transformation (look at the inspected slots and mooseID of each object).

"Inspecting an original and copy"

We are now done with the second part of our three blog post journey into code transformation! Our class is now able to create wrappers for each candidate method containing a copy of the FAST to transform, creating the new nodes to insert and finally make the edit on the copied FAST.

Just like with the first post, feel free to now try the tool and methods that we created here in a Playground and experiment with the results, or the methods themselves!

t := LoggerTransformationTool onModel: MooseModel root first.
t findAndTransformAllLogErrorInvocations

"Testing class"

The whole source code that was written on this blog post (and the previous one) is also available on that repository.

With the help of some tools and wrappers for FAST-Java, we managed to copy the FASTs of our candidate methods for our small transformation use case, before creating the necessary node for the transformation. In the next and final blog post on code transformation, we will see how to view and edit our transformation before confirming its content, and applying it to the source files. Until next time! 😄

Transformation journey (1/2) : Locating entities and nodes

Sometimes we have to perform several similar edits on our source code. This can happen in order to fix a recurring bug, to introduce a new design pattern or to change the architecture of a portion of a software. When many entities are concerned or when the edits to perform are complicated and take too much time, it can be interesting to consider building a transformation tool to help us and make that task easier. Fortunately, Moose is here to help with several modeling levels and powerful tools enabling us to build what we need!

This little transformation journey will be divided into two blog posts. We will see how to build a simple transformation tool, with each post focusing on a different aspect:

  • First post : Locating entities and nodes to transform
  • Second post : Creating AST copies and AST nodes to make a transformation {::comment}
  • Final post : Viewing and editing our transformation, and applying it to the source files {:/comment}

Throughout those two posts, we will follow a simple transformation scenario, based on the software ArgoUML, an open-source Java project used in this wiki. The first step is to create the model for the software, using the sources and libraries available on that wiki post, but creating the model on the latest stable version of VerveineJ.

Using the available Docker image, this command (on Windows) will create the model and store it in the sources repository :

Terminal window
docker run -v .\src:/src -v .\libs:/dependency badetitou/verveinej -format json -o argouml.json -anchor assoc .

All that remains to do is to create a fresh image, and import this model (with the sources repository used to build the model as root folder) to start making our tool.

Note : As the creation of that tool is divided in three blog posts, keeping that image for all three blog posts is recommended.

The transformation case we will be dealing with in those blog posts is rather simple. In the ArgoUML system, three classes define and use a method named logError. In our scenario, a class ArgoLogger has been defined and contains a static method logError. The transformation task is to add a receiver node to each logError method invocation so that the method is called using the right class.

For this blog post, we will have to import two tools :

The first one is Carrefour, allowing us to bind and access the (F)AST model of an entity to its Famix counterpart. Loading it will also load the FAST Java metamodel. To load the project, execute this command in a Playground :

Metacello new
githubUser: 'moosetechnology' project: 'Carrefour' commitish: 'v5' path: 'src';
baseline: 'Carrefour';
load

Second, we will use MoTion, an object pattern matcher that will allow us to easily explore the FAST of our methods and find the specific nodes we are looking for. To load the project, execute this command in a Playground :

Metacello new
baseline: 'MoTion';
repository: 'github://AlessHosry/MoTion:main';
load: 'MoTion-Moose'

Finally done with explanations and setup! 😄 Let us start by creating a class with a model instance variable, accessors, and add a class side initializer method for ease of use:

"Creating our class"

onModel: aMooseModel
^ self new
model: aMooseModel;
yourself

This class will contain our entire code manipulation logic. It will be pretty short, but of course when working on more important transformations dividing the logic of our tool will help to make it more understandable and maintainable.

In any case, we will start with a basic Famix query, in order to find all the implementation of logError methods, which will later allow us to easily find their invocations :

fetchLogErrorMethods
^ model allModelMethods select: [ :m | m name = 'logError' ]

And then another query using this result, to get all invocations (the exact entities we seek to transform) :

fetchLogErrorMethodInvocations
^ self fetchLogErrorMethods flatCollect: [ :m |
m incomingInvocations ]

Using MooseQuery, you should be able to find any Famix entities you are seeking. From those Famix entities, we want to get the FAST nodes that we need to transform. We will look at two different methods to do so.

In this context, Carrefour is the perfect tool to use to find the nodes we want to transform in the FAST of our methods. Now that we found the entities that we have to transform in the Famix model, all that remains is building and binding the FAST node of every entity within our method, and then fetch the ones from our method invocations.

To do so, we will add two methods to our class. First, a method to fetch the FAST node matching a given method invocation :

fetchFastNodeForFamixInvocation: anInvocation
"building and binding the FAST of the invocating method"
anInvocation sender generateFastIfNotDoneAndBind.
"returning the actual node of the method invocation, our target"
^ anInvocation fast

And finally, a method that returns a list with every node we have to transform :

fetchAllFastNodesUsingCarrefour
^ self fetchLogErrorMethodInvocations collect: [ :mi |
self fetchFastNodeForFamixInvocation: mi ]

And just like that, we now have the complete list of FAST nodes to transform!

But before celebrating, we should keep in mind that this transformation is a very simple use case. Here, it is easy to find the entities to transform using Famix, but in some other cases it might be much more complex to find the methods that are candidates to a transformation, not to mention every node that must be transformed.

In those cases, a good way to make things easier is to divide the logic of this process, and use separate means to find the methods that are candidates to a transformation and to find the nodes that must be transformed.

Making queries on the Famix model remains a very reliable way to find the candidates methods, but then what about the nodes inside the AST of these methods? Methods can be quite complex (50 lines of code, 100, more…) and the resulting AST is huge. Finding the right node(s) in such AST is difficult. That’s where MoTion comes in. In order to find the nodes we are looking for, we can define patterns that describe those nodes, and the path to follow through the FAST to be able to reach those nodes.

MoTion is a powerful tool, allowing us to find specific items within a graph through concise patterns describing the objects we are looking for and the path in the graph used to reach them. However, it does have a very specific syntax that must be looked through before starting making our own patterns. Thankfully, everything is well documented and with examples (one of those being another example for FAST Java) on the repository of MoTion (look at the README!).

But enough description. Time to code! 😄

motionQueryForFastMethod: aFASTJavaMethodEntity
| query |
query := FASTJavaMethodEntity "type of the root node"
% { (#'children*' <=> FASTJavaMethodInvocation "looking through all childrens (with *)"
"until we find method invocation nodes"
% { (#name <=> 'logError') } as: #logErrorInvocation) }
"if their name is logError,"
"we save them to the given key"
collectBindings: { 'logErrorInvocation' } "at the end, we want all found invocations"
for: aFASTJavaMethodEntity. "and this the root entity for our search"
"the result of the query is a list of dictionaries, with each result in a dictionary"
"we only have one call to logError per method, so we can do a simple access"
^ query first at: 'logErrorInvocation'

And without commentaries, to have a clearer view on how the pattern looks :

motionQueryForFastMethod: aFASTJavaMethodEntity
| query |
query := FASTJavaMethodEntity
% { (#'children*' <=> FASTJavaMethodInvocation
% { (#name <=> 'logError') } as: #logErrorInvocation) }
collectBindings: { 'logErrorInvocation' }
for: aFASTJavaMethodEntity.
^ query first at: 'logErrorInvocation'

Now, to complete the use of our pattern, let’s make a final method that will fetch every node we need to transform :

fetchAllFastNodesUsingMotion
^ self fetchLogErrorMethodInvocations collect: [ :mi |
mi sender generateFastIfNotDoneAndBind.
self motionQueryForFastMethod: mi sender fast ]

As you can see, we still use Carrefour even in this context, as it remains the easiest way to get the FAST of our method before looking through it using MoTion. Those two tools can therefore be used together when dealing with complex transformation cases.

Now that our class is done, we are able to locate candidates methods for transformation and the specific nodes to transform using Famix, FAST, Carrefour and MoTion. You can use a Playground to test out our class and model and see for yourself the results of each method :

t := LoggerTransformationTool onModel: (MooseModel root at: 1).
t fetchAllFastNodesUsingMotion

"Testing our class"

The whole source code that was written on this blog post is also available on that repository.

Using Famix, FAST, Carrefour and MoTion, we are able to search and locate methods and nodes candidates for a given transformation test case. This first step is primordial to build a fully completed transformation tool. In the next blog posts, we will see how to create AST copies and AST nodes to use in a transformation, and finally how to view and edit our transformation before applying it to the source files.

Manage rules using MooseCritics

Software projects often leave specific architectural or programming rules that are not checked by the off-the-shelf static analysis tools and linters.
But MooseCritics is now here to make such things easy!

The first step to use this tool is of course to open its browser, findable in the Moose menu under the name Moose Critic Browser. As with every other tool of MooseIDE, we also need to propagate a model to give our tool entities to analyze. For this analysis, we will use a model of ArgoUML, an open-source Java project used in this wiki.

"MooseCritics browser"

Rules in MooseCritics are divided into two components: Context, and Condition.
A context is a collection of entities to specify the scope of our analysis. Using this, we are only executing our rules on the relevant entities for them.
Once we have a context, we add conditions to it, to verify the validity of every entity belonging to this context.

Let’s start building a few of those, to appreciate how easy and versatile this system can be!

To begin, we will right-click on the root context, the root of our rules, doing nothing but passing the whole set of entities propagated into our browser. Then, clicking on “Add Context” will open a new window, in which we can write our first context.

"Context maker user interface"

As you can see, a context has three properties :

  • Name: the name of our context
  • Context Block: a code block, using as a parameter the collection given by the parent context, and that must return a collection of entities
  • Summary: a quick explanation of the selection performed

In this case, the selection is very basic (keeping only the classes defined within our model), but any way of manipulating a collection (so long as it remains a collection) can be used to make a very specific choice of entities.
But for now, let’s keep things simple, and add a few more contexts to our root.

First, we select methods…

"Title:"
'Methods'
"Context Block:"
[ :collection | collection allMethods ]
"Summary:"
'Every method in our model or called by a model entity.'

… and secondly attributes.

"Title:"
'Attributes'
"Context Block:"
[ :collection | collection allAttributes ]
"Summary:"
'Every attributes in our model or accessed by a model entity.'

Once this is all done, we are met with this screen :

"Three contexts"

Now that our contexts are set, we can write a few conditions for those.
To do so, right-clicking on our Model Classes context and choosing “Add condition” which will open a new interface to write our conditions.

"Condition in Pharo Code : Dead Classes"

The properties are almost identical to a context, but we now use a query to know whether or not an entity violates a rule.
This query will have as a parameter every entity of our context, one by one, and will add a violation to it if the query returns true.

Now, the most perceptive readers (all of my readers, no doubts 😄) will have noticed the two radio buttons; Pharo Code and Queries Browser.
We can indeed use a query built in the Queries Browser, and we will do so for the next one, to find God Classes.

"Condition with Queries Browser : God Classes"

This may not be an option for every kind of rule, especially the more complex ones, but conditions verifying several simple things can be easily designed, thanks to the Queries Browser.

Now that we saw all possibilities, time to write one more condition, this time for the methods :

"Title:"
'Deprecated'
"Query Block:"
[ :entity |
entity annotationInstances notEmpty and: [
entity annotationTypes
anySatisfy: [ :a | a name = 'Deprecated' ] ] ]
"Summary:"
'Deprecated methods, that should be removed or not used anymore.'

We are now all set, and all that remains to do is pressing the “Run” button in the bottom right corner, and look at the result of our analysis in the right pane, showing every violation found, on the format violatingEntity -> violatedCondition.

"Analysis results"

Now that we executed our rules, you can also have fun clicking on contexts and conditions to see that the left and right panels will change to match your selection, the left one showing the context, and the right one showing the violations of the selected condition, or the violations of every condition of the selected context.

We may also be a bit more specific, both on the condition side of things, but also when it comes to context.
For the conditions, our perceptive minds did not forget about the attributes, so we will write a condition for them too :

"Title:"
'Directly accessed'
"Query Block:"
[ :entity |
entity accessors anySatisfy: [ :m |
m isGetter not and: [ m isSetter not ] ] ]
"Summary:"
'Every attribute accessed without the use of a getter or setter method.'

For a final rule, let’s work a bit more on our context. Let’s say we want to build a rule around getter methods, to verify that their cyclomatic complexity is equal to 1.
For that, we can start by making a new context, using the “Methods” context as its parent :

"Title:"
'Getters'
"Context Block:"
[ :collection |
collection select: [ :m |
(m name beginsWith: 'get') and: [ m isGetter ] ] ]
"Summary:"
'Every getter method of our model, meaning :
- Their name starts with 'get'
- They have the property 'isGetter' set to true'

Once that sub-context has been created, we can give it a condition to verify ! Let’s do so right away, with our cyclomatic complexity example :

"Title:"
'Cyclomatic Complexity > 1'
"Query Block:"
[ :entity | entity cyclomaticComplexity > 1 ]
"Summary:"
'A getter must have a cyclomatic complexity of one.'

We are now done with all of our rules. To get the result of our new conditions, you can use again the “Run” button, or, execute only the new ones by right-clicking on them and selecting the “Run condition” option.

"Analysis results"

Our work is now done, but we would like to be able to monitor the state of our project in the long run, and to simplify this, we can export and import sets of rules built with MooseCritics.

For that, by pressing the “Export rules” button, we can choose where we wish to save our rules. The loading works similarly and will restore the tree as it was when it was saved (if rules were already present, the imported rules are added after those).

"Export window"

MooseCritics can also propagate those violations, in order to access them in the entities exporter, to be able to save those violations in a CSV file.
The exported selection will be the violations found in the right pane when using the propagate button.

"Entities exporter"

MooseCritics enables us to verify the validity of our defined rules and puts us in the right direction to correct our mistakes by finding violations. Dividing our model into contexts allows us to make specific analyses while working on a large scale.
Even if most of the examples shown here are fairly simple, MooseCritics can represent complex structural rules using Famix properties and will surely make your life easier when it comes to software analysis using Moose. 😄