These best practices reflect the expertise of Obeo engineers who have developped Acceleo.
They can help you develop or tune your own Acceleo-based code generators.
The following best practices apply to Acceleo 3.0.x and Acceleo 3.1.x.
An Acceleo project is an Eclipse plugin project and as such it should be named following this structure:
<domain><project name (optional)><kind of input (optional)><input metamodel name>gen<output language name>
Example: com.mydomain.myproject.pim.uml.gen.java
The kind of input being "pim" (Platform Independent Model), "psm" (Platform Specific Model) or "dsl" (Domain Specific Language).
Acceleo modules, templates queries and variables must use camel case name starting with a lower case character.
Example: myModule, myAmazingTemplate, etc.
Variables should also be named using a camel case name starting with a lower case "a" or "an" and followed by the name of the type or the name of the feature.
Example: aVariable, anElement, anOwnedComment.
Do not use any OCL or Acceleo keyword for the name of your modules, templates, queries or variables.
The root package of an Acceleo project should share the same name as the project.
Example: com.mydomain.myproject.pim.uml.gen.java
Every Acceleo project should contain several key packages.
Example: the module generating a Java class could be named "classFile" and it should be in the package named "files". All the other modules related to the generation of the Java class can be contained in the package "common.classfile" for example "imports.mtl", "attributes.mtl", methods.mtl" etc.
The main package, the properties package and the service package should be exported. Java services will not work when the generator is deployed if the package containing those package is not exported in the MANIFEST.MF (runtime tab). It is recommended to export all the other packages but to keep them "hidden from all the plugins".
When writing an Acceleo module, there are some rules that should be respected:
In Acceleo and in OCL, "null" is "OclUndefined", when you are testing for a "null" variable, you should use "isOclUndefined()".
In Acceleo, the "let" block is strictly equals to an 'if instanceof". For example, those two expressions are equals: "[let x: Type = myExpression]|x.doSomething()/][/let]" and "[if (myExpression.oclIsKindOf(Type))][myExpression.oclAsType(Type).doSomething()/][/if]". Keep in mind that if your expression initializing the "let variable" returns null, then Acceleo will not enter the "let block".
Do not use Acceleo or OCL keywords in your expressions, for example, if you have to manipulate a feature named body, instead of [myElement.body/] you should use [myElement._body/] since body is an OCL keyword.
Do not use the implicit context to call an operation [myOperation()], always use an explicit variable [myVariable.myOperation()/]. In this example, you can see what Acceleo will call with implicit variable. The result may not be what you had in mind.
you should always use Acceleo operation with an explicit source for the operation [myExplicitSource.myOperation(myFirstParameter, mySecondParameter)/] and not an implicit source [myOperation(myFirstParameter, mySecondParameter)/]. If the operation does not have an implicit source, you may not easily know which template or query will be called as you can see it in this example.
Acceleo 3.1 has introduced a documentation block, it should be use to write the documentation of the templates, queries, modules and variables.
An Acceleo module should follow the same conventions regarding its metric as a Java class. It is thus not recommended to have an Acceleo module with more than 30 templates and queries or with several thousands lines.
All the "import" or "extend" declaration should always use the qualified name of the imported or extended module [import com::mydomain::myproject:: (...) ::myModule/] and not the simple name [myModule/]. If you use the simple name, the first module found with the same simple name will be used.
Polymorphism should be preferred to "[for ()] [if (a.oclIsKindOf(A))][elseif (a.oclIsKindOf(B))][elseif (a.oclIsKindOf(C))][/if] [/for]". If the common parent of all the type concerned by the polymorphism on a template does not generate anything, it should at least generate a comment indicating that the given element is not taken into account.
When generating a file, the path of the file should be calculated by a query. It is recommended to use queries to calculate repetitive piece of code because a query will keep its result in cache (if you call the query again with the same parameter, Acceleo will not re-evaluate the query, it will return the result of the query during the previous evaluation with the same parameter).
While writing a module, you should think about the re-use of your generator. As such, it is recommended to have a very small granularity for your templates even if it seems trivial. In the following example, you can see a template to generate the name of any NamedElement. One would imagine that a template like this would be useless but if you want to change the naming convention of all the element of your generator, you just have one template to change or to override.
Templates and queries have a visibility, it is recommended to use as much as possible the private and protected visibility in order to minimize the amount of template and queries in the API of the generator.
Parenthesis in the "if" and "for" block should always be used.
It is strongly recommended to start a generation with only one entry point. In order to improve performances, there should be at best only one main module with only one main template and this main template should take the whole model to prevent several call to this main template. Each call to a main template by Acceleo will launch a new initialization of the context of the generation and it can have an impact on the generation (the cache of the queries is cleared, the cross referencer used for "eInverse()" is cleared too, etc).
If you are not generating on the Ecore metamodel (https://eclipse.dev/modeling/emf/), you should not try to cast anything to one of Ecore classes. If you want to define a very generic template, you should use OclAny as the type of your variables.
In order to compare an enumeration literal, you have to do: MyObject.myEnumValue = MyEumeration::myEnumerationLiteral.
It is highly recommanded not to manipulate directly the result of an "invoke" but instead, to return the raw result as the result of the query and then manipulate the result of the query. In the following screenshot, the result of the query seems to be a Sequence of String while it is in reality a Sequence of Sequence of String.
The operation will return a Sequence of String (List<String> in the Java operation) but the Acceleo parser uses the signature of the operation invoke, as such the result is "OclAny" (the engine will make sure that this OclAny is a Sequence of String in reality). By using the type OclAny, the parser sees OclAny->asSequence() and thus the operation is compiled as Set(OclAny)->asSequence(). When the engine executes the call to "invoke", its result "replaces" the OclAny and as a result we obtain a Set(Sequence(String)) which is then pass to "->asSequence()" and the final result is "Sequence(Sequence(String))".
In order to define constants for an Acceleo generator, it is recommended to use properties files. If you want to know more about properties files, have a look at the properties files guide for Acceleo 3.1.1+
Some operations in Acceleo can be time consuming and as such it is recommended to embed them inside of a query to take advantage of the cache of the query. Among those operations, you can find "eAllContents()", "eAllContents(OclType)" which are both iterating on the whole subtree of the current model element and "eInverse()" which needs to initialize a cross referencer on the whole model the first time it is used.
Java services should be used to query the model and to filter the element of the model. It is not recommended to use Java services to return large amount of text. The text should be generated by Acceleo templates.
Java services should be wrapped in a query in order to minimize their impact in the generation by having a cache of their result and so as to only manipulate Acceleo concepts (templates and queries) in most of the generator.
It is recommended to minimize the use of Acceleo primitive as input parameter or output of Java services.
Java services should not be called directly from a template without being wrapped in a query.
In order to prevent problems with the traceability information calculated by Acceleo some conventions need to be followed.
The generated text should not be heavily modified. Any modification of the generated code will create problems with the traceability information calculated by Acceleo. Obeo Traceability can handle some modifications of the generated code but a massive change of the generated code like the use of a formatter on the generated code would create massive gaps between the traceability information and the code. The formatting of the generated code should be handle by formatting correctly the Acceleo templates.
It is recommended not to use Java services to generated code. Java services should be use to query the model and return one or several model elements. The code should be generated by Acceleo templates. Java services that are taking primitive types as parameter (int, string, float, char, etc.) and that are returning a primitive type too can create problems to the traceability information. In order to link the values returned by a Java service it is recommended to use at least one element from the model.
The use of templates or queries without any parameters is also not recommended. In order to link the generated text with a model element, Acceleo needs a model element as a parameter of the template or the query.
Like any project, a code generator evolves and to make sure that regression are detected it is recommended to maintain along with the generator a test project with a non regression model. This non regression model should contain all the type of model elements that are being handled by the generator. The test project should also contain the code generated form this non regression model with the generator. When a modification of the generator is done, the new generator should be launched once again with the non regression model as its input and then the new generated code should be compared with the old generated code to ensure that no features of the code generator has broke.
One of the key best practices in code generation is to generate code as if you would write it manually.
Nevertheless, mixing generated code and hand-written code in the same file is one of the main causes of desynchronization between model and generated code. The traceability functionnality reduces this risk, but it can't solve all the cases of desynchronization.
To minimize this risk, when it does not alter the quality or the performance of the code, you can break the rule mentionned previously, by separating generated code and hand-written code in different files. There exists several patterns depending on the implementation language (subclassing generated classes by hand-written classes in java, partial classes in C#).