Wednesday, July 18, 2012

Custom Syntax Error Messages with Quick Fix

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.