Static analysis or validation is one of the most interesting aspects when developing a programming language. The users of your languages will be grateful if they get informative feedback as they type. In Xtext there are basically three different kinds of validation.
The syntactical correctness of any textual input is validated automatically by the parser. The error messages are generated by the underlying parser technology and cannot be customized using a general hook. Any syntax errors can be retrieved from the Resource using the common EMF API:
org.eclipse.emf.ecore.resource.Resource.getErrors()
org.eclipse.emf.ecore.resource.Resource.getWarnings()
Any broken crosslinks can be checked generically. As crosslink resolution is done lazily (see linking), any broken links are resolved lazily as well. If you want to validate whether all links are valid, you will have to navigate through the model so that all installed EMF proxies get resolved. This is done automatically in the editor.
Any unresolvable crosslinks will be reported and can be obtained through:
org.eclipse.emf.ecore.resource.Resource.getErrors()
org.eclipse.emf.ecore.resource.Resource.getWarnings()
In addition to the afore mentioned kinds of validation, which are more or less done automatically, you can specify additional constraints specific for your Ecore model.
We leverage existing EMF API (mainly
EValidator
) and have put some convenience stuff on top.
Basically all you need to do is to make sure that an
EValidator
is registered for your
EPackage
. The registry for
EValidators
(
EValidator.Registry.INSTANCE
) can only be filled programmatically.
That means contrary to the EPackage and
Resource.Factory
registries there is no Equinox extension point to populate the validator registry.
For Xtext we provide a
generator fragment for the convenient Java-based
EValidator
API. Just add the following fragment to your generator configuration and you are good to go:
<fragment class=
"org.eclipse.xtext.generator.validation.JavaValidatorFragment"/>
The generator will provide you with two Java classes. An abstract class generated to
src-gen/
which extends the library class
AbstractDeclarativeValidator
. This one just registers the EPackages for which this validator introduces constraints.
The other class is a subclass of that abstract class and is generated to the
src/
folder in order to be edited by you. That’s where you put the constraints in.
The purpose of the
AbstractDeclarativeValidator
is to allow you to write constraints in a declarative way – as the class name already suggests. That is instead of writing exhaustive if-else constructs or extending the generated EMF switch you just have to add the @Check
annotation to any method and it will be invoked automatically when validation takes place.
Moreover you can state for what type the respective constraint method is, just by declaring a typed parameter. This also lets you avoid any type casts.
In addition to the reflective invocation of validation methods the
AbstractDeclarativeValidator
provides a couple of convenient assertions.
All in all this is very similar to how JUnit works. Here is an example:
public class DomainmodelJavaValidator
extends AbstractDomainmodelJavaValidator {
@Check
public void checkTypeNameStartsWithCapital(Type type) {
if (!Character.isUpperCase(type.getName().charAt(0)))
warning("Name should start with a capital",
DomainmodelPackage.TYPE__NAME);
}
}
In addition to the Java-based validation code you can use the language Check (from M2T/Xpand) to implement constraint checks against your model. To do so, you have to configure the generator with the CheckFragment. Please note, that you can combine both types of validation in your project.
<fragment class=
"org.eclipse.xtext.generator.validation.CheckFragment"/>
After regenerating your language artifacts you will find three new files “YourLanguageChecks.chk”, “YourLanguageFastChecks.chk” and “YourLanguageExpensiveChecks.chk” in the
src/
folder in the sub-package
validation
. The checks in these files will be executed when saving a file, while typing (FastChecks) or when triggering the validation explicitly (ExpensiveChecks). When using Check the example of the previous chapter could be written like this.
context Type#name WARNING "Name should start with a capital":
name.toFirstUpper() == name;
Each check works in a specific context (here:
Type
) and can further denote a feature to which a warning or error should be attached to (here:
name
). Each check could either be a
WARNING
or an
ERROR
with a given string to explain the situation. The essential part of each check is an invariant that must hold true for the given context. If it fails the check will produce an issue with the provided explanation.