Xtext editors for domain specific languages (DSLs) provide many error messages out of the box, such as syntactical errors, duplicate name errors or unresolvable references. For an improved user experience, some technical error messages from the editor (or, more
specifically, from the Antlr parser that is used by the editor) may be customized. In many DSLs, identifiers (for DSL concepts like packages, entities and so on) are expected to conform to the regular expression of the terminal rule ID:
terminal ID : '^'?('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'_'|'0'..'9')*;
In addition, any keyword that is defined in other rules of the DSL grammar may not be used as an identifier
. Keywords may be escaped with the caret (^) symbol, which is certainly arguable. This would be similar to using "class" as a
name for a class in Java (if that would be possible). Here are some snippets of a DSL with a package concept:
package package // the second word is a reserved keyword and therefore not be valid as identifier
package ^package // okay, the keyword was escaped
package myPackage // okay unless 'myPackage' is a grammar keyword
The default
error message when using a reserved keyword where an identifier is
expected looks like this.
mismatched input 'package' expecting RULE_ID
This message can be customized using Xtext's
SyntaxErrorMessageProvider (written in
Xtend):
class SyntaxErrorMessageProviderCustom extends SyntaxErrorMessageProvider {
public static val String USED_RESERVED_KEYWORD = "USED_RESERVED_KEYWORD"
@Inject IGrammarAccess grammarAccess /** * Customized error message for reserved keywords */ override getSyntaxErrorMessage(IParserErrorContext context) { val unexpectedText = context?.recognitionException?.token?.text if (GrammarUtil::getAllKeywords(grammarAccess.getGrammar()).contains(unexpectedText)) { println(context.defaultMessage) return new SyntaxErrorMessage(''' "«unexpectedText»" is a reserved keyword which is not allowed as Identifier. Please choose another word or alternatively confuse your co-workers by escaping it with the caret (^) character like this: "^«unexpectedText»".''', USED_RESERVED_KEYWORD) } super.getSyntaxErrorMessage(context) } }
|
The customized error message provider has to be bound in
MyDslRuntimeModule.java like this
:
/** * custom error messages for syntax errors */ public Class<X extends ISyntaxErrorMessageProvider> bindISyntaxErrorMessageProvider() { return SyntaxErrorMessageProviderCustom.class; }
|
A simple quickfix in
MyDslQuickfixProvider could look like this:
/** * Provide a fix when reserved keywords are used as identifiers */ @Fix(SyntaxErrorMessageProviderCustom::USED_RESERVED_KEYWORD) def public void reservedKeywordUsed(Issue issue, IssueResolutionAcceptor acceptor) { val unexpectedText = issue.data?.get(0) acceptor.accept(issue, '''Change '«unexpectedText»' to '«unexpectedText.generateUniqueIdentifier».' ''', ''' Change '«unexpectedText»' to '«unexpectedText.generateUniqueIdentifier»', which is not a reserved keyword.''', "correction_linked_rename.gif", [ IModificationContext context | val xtextDocument = context.getXtextDocument xtextDocument.replace(issue.offset, issue.length, unexpectedText.generateUniqueIdentifier) ]) }
def String generateUniqueIdentifier(String it) { val candidate = 'my' + it?.toFirstUpper?:'Name' var count = 1 val reserved = GrammarUtil::getAllKeywords(grammarAccess.getGrammar()) if (reserved.contains(candidate)) { while (reserved.contains(candidate + count)) { count = count + 1 } return candidate + count } return candidate }
|
This kind of customization has been available for a long time now. For more information, see
Customizing error messages from Sebastian Zarnekow.