The new release of Xtext 2.35.0 comes with many new cool features. I personally worked on the introduction of the JvmGenericTypeValidator.
Quoting from the release notes:
Automatic validation for Xbase languages
The new
JvmGenericTypeValidator
was introduced to automatically perform several Java-related checks in the hierarchy of the inferredJvmGenericType
s of an Xbase language, with the corresponding error reporting.
For example, cycles in a hierarchy, extension of a final class, proper extension of an abstract class (do you implement all the abstract methods or declare the inferred class as abstract?), proper method overriding, etc. It also performs duplicate elements checks, like duplicate parameter names, duplicate fields and duplicate methods (keeping the type-erasure into consideration when using types with arguments).This mechanism assumes that you implement the
JvmModelInferrer
“correctly”.
It only checks the first inferredJvmGenericType
for the same DSL element (i.e., if for an elementEntity
you infer twoJvmGenericType
s,t1
andt2
, only the first one will be checked).
Moreover, it only checks Jvm model elements with an associated source element.
Concerning intended classes to extend and interfaces to extend/implement, it assumes the model inferrer uses the newJvmTypesBuilder#setSuperClass(JvmDeclaredType, JvmTypeReference)
andJvmTypesBuilder#addSuperInterface(JvmDeclaredType, JvmTypeReference)
, respectively.Currently, this validator must be enabled explicitly through the
composedCheck
in the MWE2 file or the@ComposedChecks
annotation in the validator, e.g.,@ComposedChecks(validators = JvmGenericTypeValidator.class)
.The Domainmodel example now uses this validator.
The Xtend validator has been refactored to also use this validator.
This means that if you implement your model inferrer “correctly” and enable this validator, you get lots of valuable checks, which you would usually have to implement yourself (if you ever did that).
Let’s see that in action in the Domainmodel example (you can materialize that in your workspace through the Xtext wizard).
The example used in this tutorial can be found online at https://github.com/LorenzoBettini/jvm-generic-type-validator-example. Remember that if you clone the example from GitHub, you’ll have to run the MWE2 workflow after importing the projects.
As reported in the release notes, the Domainmodel validator already uses the JvmGenericTypeValidator:
1 2 3 4 5 6 |
... import org.eclipse.xtext.xbase.validation.JvmGenericTypeValidator; ... @ComposedChecks(validators = JvmGenericTypeValidator.class) public class DomainmodelValidator extends AbstractDomainmodelValidator { ... |
Moreover, its model inferrer uses the above-mentioned API in JvmTypesBuilder for specifying the expected superclass, if present (note that setSuperClass is used here as an Xtend extension method and uses the syntactic sugar for setter):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
... class DomainmodelJvmModelInferrer extends AbstractModelInferrer { @Inject extension JvmTypesBuilder @Inject extension IQualifiedNameProvider @Inject extension DomainmodelJvmModelHelper @Inject extension IJvmModelAssociations @Inject extension IJvmModelAssociator def dispatch infer(Entity entity, extension IJvmDeclaredTypeAcceptor acceptor, boolean prelinkingPhase) { accept(entity.toClass( entity.fullyQualifiedName )) [ documentation = entity.documentation superClass = entity.superType ... |
Let’s start another Eclipse instance (in the GitHub project, you can use the launch “/org.eclipse.xtext.example.domainmodel.ui/Domainmodel Eclipse.launch”).
Let’s create an Eclipse plug-in project; in the MANIFEST, let’s add the Xbase lib as a dependency, and in the “src” folder, we create a file “Example.dmodel”, accepting to convert the project to an Xtext project.
Let’s start with a method with two parameters with the same name and see the reported error:
Let’s add two methods with the same name and a parameter but with the same type-erasure in the generic type:
Remember using setSuperClass to express that the extended class must be a class, not an interface? Let’s see that in action:
And don’t try to extend a final class 😉
Let’s see what happens if you try to extend an abstract class without defining all the abstract methods (remember that in the Domainmodel, all entities are mapped to concrete Java classes):
Of course, the error goes away if you implement the abstract methods:
Before going on, remember that implementing these validation checks manually is not trivial: you have to consider type-erasure and inherited methods. You get them for free thanks to the JvmGenericTypeValidator 🙂
Note that the error about missing implemented abstract methods, as the one above, contains information (i.e., in Xtext, “issue data”) about the missing methods, their types, and parameters. Thus, you could implement a quickfix to add the implementation methods automatically. This is not trivial, but it’s doable with the provided information. You might want to take inspiration from what Xtend does (remember that now Xtend uses the JvmGenericTypeValidator).
Let’s extend the Domainmodel a bit:
- we add the optional “abstract” keyword for entities
- we add the “implements” interfaces feature
These are the relevant parts in the modified grammar:
1 2 3 4 5 6 7 |
Entity: (abstract ?= 'abstract')? 'entity' name=ValidID ('extends' superType=JvmParameterizedTypeReference)? ('implements' implements+=JvmParameterizedTypeReference (',' implements+=JvmParameterizedTypeReference)*)? '{' features+=Feature* '}'; |
Let’s run MWE2 to regenerate the language, and let’s adapt the model inferrer accordingly (note the use of the new method JvmTypesBuilder.addSuperInterface; this method should also be used in the case your Xbase DSL has elements that are mapped to interfaces extending other interfaces):
1 2 3 4 5 6 7 8 |
def dispatch infer(Entity entity, extension IJvmDeclaredTypeAcceptor acceptor, boolean prelinkingPhase) { accept(entity.toClass( entity.fullyQualifiedName )) [ ... abstract = entity.abstract for (i : entity.implements) { addSuperInterface(i) } ... |
Let’s restart the running Eclipse and see the new features in action, according to checks automatically performed by the JvmGenericTypeValidator.
For example, similarly to extended classes, we have the check for implemented interfaces:
Moreover, declaring a mapped Java class as abstract is taken into consideration: now we don’t have errors anymore with an abstract entity:
And, of course, mapped implemented interfaces are considered when checking whether a concrete class implements all the abstract methods:
Speaking of quickfixes, we can create a quickfix that intercepts the problem of a concrete entity missing implemented methods to turn the entity into an abstract one:
1 2 3 4 5 6 7 8 |
public class DomainmodelQuickfixProvider extends XbaseQuickfixProvider { @Fix(org.eclipse.xtext.xbase.validation.IssueCodes.CLASS_MUST_BE_ABSTRACT) public void makeAbstract(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Make entity abstract", "Make entity abstract", "Entity.gif", (EObject element, IModificationContext context) -> ((Entity) element).setAbstract(true) ); } |
Let’s restart Eclipse and see the quickfix in action:
Unfortunately, as implemented, the semantic model quickfix leaves the resulting program text poorly formatted.
This version works fine, though it requires manual modification of the textual file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Fix(org.eclipse.xtext.xbase.validation.IssueCodes.CLASS_MUST_BE_ABSTRACT) public void makeAbstract(final Issue issue, IssueResolutionAcceptor acceptor) { acceptor.accept(issue, "Make entity abstract", "Make entity abstract", "Entity.gif", (EObject element, IModificationContext context) -> { IXtextDocument document = context.getXtextDocument(); ICompositeNode clazzNode = NodeModelUtils.findActualNodeFor(element); int offset = -1; for (ILeafNode leafNode : clazzNode.getLeafNodes()) { if (leafNode.getText().equals("entity")) { offset = leafNode.getOffset(); break; } } document.replace(offset, 0, "abstract "); } ); } |
Here’s the result after applying the quickfix:
Other features not shown in this blog post and not implemented by the Domainmodel example are related to declared thrown exceptions in operations. The JvmGenericTypeValidator will check that the thrown exceptions are correct in the case of overridden methods, according to the Java semantics.
Note that the JvmGenericTypeValidator can be customized if needed. For example, Xtend customizes it.
On a side note, while I took care of implementing and testing the JvmGenericTypeValidator and refactoring Xtend validation accordingly, most of the code extracted in JvmGenericTypeValidator comes from the original implementation in the XtendValidator made by Sebastian Zarnekow!
Happy validation-made-easy! 🙂