Showing posts with label Quick Fix. Show all posts
Showing posts with label Quick Fix. Show all posts

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.

Friday, July 16, 2010

Xtext Quick Fix Variants

One of my favorite Eclipse features is the quick fix functionality. When there is an error in your source code, Eclipse may have a quick fix available - it shows up with a bulb symbol on the left editor margin and may offer you several actions to fix it. You can also use the shortcut [Ctrl-1] to activate it. Xtext ships with a quick fix API which makes it easy to provide quick fixes for your own DSL, so you can provide quick fixes for your customized validation errors and warnings, which is really nice.
Quick fix actions in Xtext may either manipulate the Xtext document directly, or modify the underlying semantic model, and Xtext takes care of changing the document. Isn't that neat? For pure text manipulation, the IModification interface can be used, while for working on the model you may implement ISemanticModification. The example below shows you how to use those two variants. In the example, we use a simple domain specific language for a tourist guide. The model may contain an arbitrary number of cities, and each city may contain zero or more sights, where a sight has a name and a description. We implemented two validation warnings: One checks whether the city's name starts with a capital letter, the other warns the user when a city doesn't have any sights. Quick fixes could be to capitalize the first letter (by changing the Xtext document) and, for the sake of the example, just add a generic sight, but this time by modifying the semantic model. To associate a quick fix with a certain validation, an identification code is used, so you have to define one for your validation like this:
public static final String INVALID_NAME = "xtext.workshop.advanced.quickfix.InvalidTypeName";
The check could look like this:
public class TouristguideDslJavaValidator extends
AbstractTouristguideDslJavaValidator {
[...]

@Check
public void checkTypeNameStartsWithCapital(City city) {
if (city.getName() == null || city.getName().length() == 0)
return;
if (!Character.isUpperCase(city.getName().charAt(0))) {
warning("Name should start with a capital letter.",
TouristguideDslPackage.CITY__NAME, INVALID_NAME,
city.getName());
}
}
}
Note that the warning takes additional parameters (in the example only one is supplied) with "user data". Here you can supply an arbitrary number of Strings with user data that may be useful for the quick fix. Here is a step-by-step guide:
  1. Import the plug-ins with the Touristguide Language (download examples). If you only want to try the quick fix but not implement it yourself, just download the finished projects where quick fixes are already implemented. Have a quick look at the Touristguide.xtext file in to understand how a valid .guide-file looks like.
  2. Launch an Eclipse runtime application, create an new project, a new .guide-file and test if you get two validation warnings in the Problems view (Shift-Alt-Q X) for the following text:
    city "bonn" { }
  3. Switch back to the Eclipse development environment workbench and review the validator TouristguideDslJavaValidator.java (you may cf. section 7.3 Quick Fixes of the Xtext 1.0.0 documentation)
  4. Open the file TouristguideDslQuickfixProvider.java in the ui-Project. To implement the quick fix for the capital letters, you may implement methods like the ones below.
public class TouristguideDslQuickfixProvider extends DefaultQuickfixProvider {

@Fix(TouristguideDslJavaValidator.INVALID_NAME)
public void capitalizeName(final Issue issue,
IssueResolutionAcceptor acceptor) {
// retrieve the 'user data' from the validation warning
// upcase.png ... icon to display (in the icons folder)
acceptor.accept(issue, "Capitalize name", "Capitalize the name \""
+ issue.getData()[0] + "\".",
"upcase.png", new IModification() {
public void apply(IModificationContext context)
throws BadLocationException {
IXtextDocument xtextDocument = context
.getXtextDocument();
String firstLetter = xtextDocument.get(
issue.getOffset() + 1, 1);
xtextDocument.replace(issue.getOffset() + 1, 1,
firstLetter.toUpperCase());
}
});
}

@Fix(TouristguideDslJavaValidator.CITY_NOT_INTERESTING)
public void addSightToCity(final Issue issue,
IssueResolutionAcceptor acceptor) {
acceptor.accept(issue, "Add sight to make city more interesting",
"Add a random sight, to make the city look more interesting.",

// providing null for the icon name makes Eclipse use the
// standard quick fix icon
null, new ISemanticModification() {
public void apply(EObject element,
IModificationContext context) throws Exception {
// we know that the warning applies to cities
City c = (City) element;
// programmatic modification of the model
Sight sight = TouristguideDslFactory.eINSTANCE
.createSight();
sight.setName("Central Station");
sight.setDescription("The famous central station of "
+ Strings.toFirstUpper(c.getName()) + ".");
c.getSights().add(sight);
// Xtext automatically inserts text for the above
}
});
}
}
  1. Restart the runtime workbench and have fun testing your new quick fixes :-)