Refactorings in Haskell-tools are monadic functions that take a syntax tree (AST) and return a modified version of that tree. The framework guarantees that only the source code of the changed syntax tree elements will be modified, so formatting and comments will not be lost.
One important decision when writing a refactoring is whether we want to refactor elements in a selection or we want to refactor a whole module, package or project.
If we want to create a refactoring that changes only one module, we can create a local refactoring otherwise we must create an ordinary refactoring. Of course if we don't need the monadic effects, we can define a pure function that works on the AST and call return
to create a local or regular refactoring.
You should also check the general development tips.
When you try to create a refactoring, most likely you only want to change small parts of the AST. To access them you have two options.
If you want to change AST fragments that are located at a particular place in the AST and not on a very deep level, you can use references to access that part.
For example, to replace data type definitions with newtype definitions wherever possible, you can use the modDecl & annList
reference, that accesses each declaration in the module. See the whole source code of the transformation here.
The API documentation for the complete list of references
Check out the reference tutorial
On the other hand, if the AST elements you want to change can be found at multiple levels of the AST, it would be inconvenient to select each possible location with references. In these cases you can use generics to select all possible location where an AST element of a given type can appear.
For example, replace an if statement on the right-hand side of a binding with a function guard. The problem is that bindings can be local, so they can appear on different levels of the AST. But we can use the generic reference nodesContaining sp
that selects the AST elements that contain the current selection. If we apply a function to that reference that has ValueBind dom -> ValueBind dom
type, it will select all bindings contain the current selection. By checking the right-hand sides of these bindings, we can check to which one can be the subject of the refactoring. See the whole source code of the transformation here.
biplateRef
is the most general way to use generics, it is a reference for all elements contained in an AST node.nodesContaining
gets all the nodes that contain a given selectionnodesContained
gets all the nodes that are contained in a given selection
The Haskell syntax tree is quite complex with all the language features and extensions. See the latest specification and the GHC documentation for the use of specific language elements. We grouped the AST elements into groups based on which part of the language they are related to. You can find these categories in different parts of the framework, at both representation, transformation, generation and pattern matching of elements. The groups are:
- Modules (module heads, exports, imports, file- and module-level pragmas),
- Declarations (top-level declarations, like types and classes)
- Bindings (function and value bindings, pattern matching)
- Types (user types, contexts)
- Kinds (including promoted kinds)
- Expressions (simple expressions, pattern matching, arrow-commands)
- Statements (do-notation, list comprehensions)
- Patterns (simple patterns, record field matching)
- Names (normal names and operators, qualified names and their parts)
The complete list of the different types of AST elements
The AST elements are annotated with semantic and source-related information. The API we present here hides this complexity behind the definitions for generation and pattern matching or the references for accessing parts of the AST.
You can use the pattern synonyms we provide to pattern match on different AST elements. For example if you want to transform infix expressions, you can pattern match on InfixApp rhs operator lhs
.
Check out pattern synonyms for different elements
Each function that generates an AST element has the defined above to generate parts of the AST. So if you have lhs
and rhs
expressions, and operator
, you can construct an infix expression with mkInfixApp rhs operator lhs
.
Check out generator functions for different elements
We annotate our AST representation with both syntactic and semantic information to make refactoring possible. The collection of the semantic informations added to a given AST is the domain of the AST. The domain decides which AST elements have semantic annotation and what will be the type of these annotations.
The domain of an AST depends on how far GHC can progress with the compilation of the module. If the module is type-correct, its domain will be IdDom
, if it can be renamed but it is not type correct, its domain will be Dom Name
, if the module can be parsed but cannot be renamed, the domain will be Dom RdrName
.
It is likely that you need to constraint the domain for your transformation to provide some kinds of semantic information for the refactoring. The constraints you can use and the functions to access the semantic information are listed in the SemaInfoClasses
module.
Semantic information is stored in the AST nodes. The semantic information we currently have:
- The unique name for
QualifiedName
elements. - An
isDefined
flag that tells if the current occurrence of a name defines the meaning of that name forQualifiedName
elements. - The list of definitions that are in scope for
QualifiedName
elements andExpr
elements. - The name of the imported module for
ImportDefinition
elements. - The list of actually imported definition for
ImportDefinition
elements. - The list of possibly imported definition for
ImportDefinition
elements (not considering hiding or explicit import). - The module name is available for
Module
elements. - A
defIsBootModule
flag that tells if a module is a hs-boot module is available forModule
elements. - A list of implicitely imported names (Prelude) is available for
Module
elements. - A list of fields and associated values is available for
FieldWildcard
elements.
The tryRefactor
function provides an easy way to test the defined refactoring. The daemon is parameterized with a set of refactorings, so if you add your refactoring here, you can use it in your editor.
The transformation can be easily extended by monadic effects. For example, see the dollar application example to see how a mutable state can be added to a transformation.
Not every element has single children in an AST, for example, there can be multiple definitions in a module and it may or may not contain a module head (optional element). In the Utils.Lists module there are some useful functions for changing AST elements with multiplicity, for more information, see the documentation.
If you work in the LocalRefactor
monad, you can ask the framework to automatically import the definitions that you use in the changed AST. To do this, use the referenceName
and referenceOperator
functions.
If stuck, check out the built-in refactorings, where you can find many useful examples on how to define refactorings.