I will assume you're somewhat familiar to the AST concept and JDT's AST in particular. If you are not, I recommend you the following articles
- Eclipse JDT - Abstract Syntax Tree (AST) and the Java Model - Tutorial
- Eclipse Corner: Abstract Syntax Tree
Here's a link to the source code of the project used for this post.
From Text to AST
If you read my previous installment you'll recall that I got to a point where we could get the refactoring wizard running without knowing much about what happens next. Let's build from there by taking a look at the whole ToFinalCmdHandler class. And I'll comment on the methods on a top-down fashion from the execute method. Then we'll do the same for the remaining classes.
ToFinalCmdHandler
execute
When the handler is executed it tries to update the fields fCompilationUnit and fField using the current editor and the current selection. The method responsible for that operation is updateDataFromSelection. Once the data is ready, the refactoring wizard is started by the method startWizard.
updateDataFromSelection
The first thing updated is the field fCompilationUnit using the method CompilationUnitForCurrentEditor. The next step is updating the selected field using the current selection. The current selection can be an instance of the following two interfaces:
ITextSelection: The current selection is an instance of an ITextSelection when there's text selected in the current Editor.
IStructuredSelection: The current selection is an IStructuredSelection when almost anything else is selected, like a node in the package explorer or an error in the problems view. I'm interested in the case when there's a field selected in the outline view.
Two different approaches are used to obtain the parsed field. When the selection is structured. The code is already parsed and the corresponding node from the AST can be obtained directly from the selection. When the selection is text we use the method codeResolve from the class SelectionConverter. The method codeResolve obtains the AST node that is located in the coordinates of the text selection. I have to warn you here. SelectionConverter is part of the JDT internal API. So there's no warranty that it will be there in future versions of JDT or if it will ever be public. But is handy nonetheless. The only thing left here is to validate if the obtained node (if any) is in fact a field. Then it is stored in fField. Let's take a closer look to CompilationUnitForCurrentEditor
CompilationUnitForCurrentEditor
Sorry about the violation to the coding standard. I found out my mistake after I published the code. The first thing you notice (after noticing the c#'ish name) when you see this method is the comment. I really don't like the idea of parsing the current editor. When I explored the code of the existing refactorings I found that the compilation unit was being passed as a parameter to them by a third party. But I have to admit that I didn't try to dig deeper to find out how to integrate with that mechanism. However I needed a compilation unit and this mechanism works. What this method does is simple but needs several steps to get the content of the current Editor as an IResource instance. By the way the getAdapter method is a perfect sample of an implementation of the Adapter pattern.
startWizard
There isn't much about this method. It receives the wizard and launches it using a RefactoringWizardOpenOperation. So we had already covered the RefactoringWizard in the previous post. Let's move to the Refactoring itself.
ToFinalRefactoring
Ok so we get to the interesting part. In order to implement the refactoring my class must inherit from the Refactoring abstract class. The official javadoc explains very well the Refactoring class:
Abstract super class for all refactorings. Refactorings are used to perform behavior-preserving workspace transformations. A refactoring offers two different kind of methods:
1. methods to check conditions to determine if the refactoring can be carried out in general and if transformation will be behavior-preserving.
2. a method to create a Change object that represents the actual work space modifications.
The life cycle of a refactoring is as follows:
1. the refactoring gets created
2. the refactoring is initialized with the elements to be refactored. It is up to a concrete refactoring implementation to provide corresponding API.
3. checkInitialConditions(IProgressMonitor) is called. The method can be called more than once.
4. additional arguments are provided to perform the refactoring (for example the new name of a element in the case of a rename refactoring). It is up to a concrete implementation to provide corresponding API.
5. checkFinalConditions(IProgressMonitor) is called. The method can be called more than once. The method must not be called if checkInitialConditions(IProgressMonitor) returns a refactoring status of severity RefactoringStatus.FATAL.
6. createChange(IProgressMonitor) is called. The method must only be called once after each call to checkFinalConditions(IProgressMonitor) and should not be called if one of the condition checking methods returns a refactoring status of severity RefactoringStatus.FATAL.
7. steps 4 to 6 can be executed repeatedly (for example when the user goes back from the preview page).
A refactoring can not assume that all resources are saved before any methods are called on it. Therefore a refactoring must be able to deal with unsaved resources.
The class should be subclassed by clients wishing to implement new refactorings.
1. methods to check conditions to determine if the refactoring can be carried out in general and if transformation will be behavior-preserving.
2. a method to create a Change object that represents the actual work space modifications.
The life cycle of a refactoring is as follows:
1. the refactoring gets created
2. the refactoring is initialized with the elements to be refactored. It is up to a concrete refactoring implementation to provide corresponding API.
3. checkInitialConditions(IProgressMonitor) is called. The method can be called more than once.
4. additional arguments are provided to perform the refactoring (for example the new name of a element in the case of a rename refactoring). It is up to a concrete implementation to provide corresponding API.
5. checkFinalConditions(IProgressMonitor) is called. The method can be called more than once. The method must not be called if checkInitialConditions(IProgressMonitor) returns a refactoring status of severity RefactoringStatus.FATAL.
6. createChange(IProgressMonitor) is called. The method must only be called once after each call to checkFinalConditions(IProgressMonitor) and should not be called if one of the condition checking methods returns a refactoring status of severity RefactoringStatus.FATAL.
7. steps 4 to 6 can be executed repeatedly (for example when the user goes back from the preview page).
A refactoring can not assume that all resources are saved before any methods are called on it. Therefore a refactoring must be able to deal with unsaved resources.
The class should be subclassed by clients wishing to implement new refactorings.
If someone asks me how the refactorings for the JDT were designed I'd say they used a Behavior Driven Approach because the Refactoring class is clearly modeled in function of the User Interface workflow. And looks like the RefactoringDescriptor is a tradeoff of this design decision. When a refactoring needs to be run without user interaction the steps needed to make it work don't appear as clean as when it needs user interaction.
Now back to business. Let's see how I implemented the abstract methods to get my refactoring going. Let's start with checkInitialConditions.
checkInitialConditions
Half of the job is done by this method. What this method does basically is to determine if the refactoring can be applied on the selected field. These are the things that are validated:
- The selection is actually a field
- The class that defines the field is not an annotation
- The field is private
- The field is not already final
- The field is initialized at declaration time and is not assigned anywhere else
checkFinalConditions
After the method checkInitialConditions is invoked an input screen is shown to the user. Like the window that shows when moving code into a new method and the method name must be introduced. Once the user presses OK on that screen the next method invoked is checkFinalConditions. Now that there's more information available further validations can be performed. In this case there's no user input, rendering the method useless.
doesFieldMatchesInitialConditions
It's easy to find out all the things this method is looking for just by querying the field's properties and using the Flags class.
AssignmentsFinder
The responsibility of AssignmentsFinder is to traverse the code of the current class, and after doing so it must be able to respond to the question "Is the field initialized at declaration time and only is assigned there?"
If you take a look at the code you'll find that the code is rather simple. It overrides the visit method for the nodes in the AST that correspond with assignments:
Class | Example |
---|---|
VariableDeclarationFragment | int variable=1; |
PostfixExpression | variable++ |
PrefixExpression | ++variable |
Assignment | variable = value; variable += value |
When visiting the VariableDeclarationFragment instances the intention is to find the declaration of the selected field. Once found the next step is to verify that it has an initialization expression. As a side note on the code I'm checking if the initialization value is a literal. And at this point I honestly don't remember why I set that constraint. The rest of the visited nodes are forms of assignments that need to be inspected in order to validate that the selected field is not being assigned.
The first thing that comes to mind when implementing this code is how to know that the being visited is actually the node that corresponds to the field. That can be accomplished by the use of bindings. Every time the parser finds a reference to an entity the same binding is assigned to that reference. Making it easy to know exactly what a symbol is 'bound' to.
After the visitor is finished traversing it is ready to answer the question I mentioned above. The method used for that purpose is canVariableBeFinal. If the method returns true the refactoring can proceed otherwise an error must be reported and the refactoring should not continue. As you can see in the checkInitialConditions method.
No comments:
Post a Comment