...buch! Prawie rok od ostatniego wpisu. Czasem tak bywa, że nie ma na nic czasu. Czasem jakiś projekt nas wysysa, a po zakończeniu zostaje niesmak i trzeba odchorować... Np. jako odtrutkę można zaaplikować rozpoczęty kiedyś projekt obsługi Toml dla Netbeans i dopisać jakąś kolejną funkcję :)

Skoro mamy już kolorowanie składni, breadcrumbs i zwijanie kodu — można się przymierzyć do znajdowania i oznaczania błędów składni.

Popraweczki

Zanim przejdę do właściwego tematu:

  • zauważyłem, że z gramatyką jest coś nie tak — czasami podkreślanie dziwnie działało/nie działało etc. Okazało się, że oryginalne repozytorium miało już poprawki, więc po prostu uaktualniłem gramatykę do najnowszej wersji
  • odbył się mały refactoring - moduł dostał własny pakiet io.gitlab.ihsahn.netbeans.modules.toml, dodałem stałe na MIME_TYPE oraz ICON_PATH... etc

Podkreślanie błędów

Antlr error listener

Do parsowania używany biblioteki Antlr - jedną z jej cech jest fajna obsługa błędów podczas tokenizacji oraz parsowania - odpowiednio lexer oraz parser wykrywają błędy i raportują je do przekazanego listenera, jednocześnie same starają się jak najlepiej odnaleźć się w strukturze po wykryciu błędu i kontynuować swoją pracę.

Każdy taki listener musi implementować interfejs ANTLRErrorListener. Oczywiście interfejs posiada metody odpowiadające za różne problemy, w naszym przypadku tak naprawdę interesuje nas tylko syntaxError. W związku z tym nasz własny listener będzie dziedziczył z BaseErrorListener,który implementuje wszystkie metody interfejsu (jako puste).

public class TomlAntlrErrorListener extends BaseErrorListener {

    private final List<TomlSyntaxError> errors;

    public TomlAntlrErrorListener(List<TomlSyntaxError> errors) {
        this.errors = errors;
    }

    @Override
    public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int column,
                            String errorMessage, RecognitionException re) {
        // special handling for lexer `no viable` exceptions becayse we do rewrite some of them
        if (re instanceof LexerNoViableAltException) {
            lexerNoViableAltException((TomlGrammarLexer) recognizer, errorMessage, (LexerNoViableAltException) re);
        } else if (offendingSymbol instanceof CommonToken) {
            CommonToken ct = (CommonToken) offendingSymbol;
            if ((ct.getType() == -1) && (recognizer instanceof TomlGrammarParser)) {
                errors.add(new TomlSyntaxError(errorMessage, ct.getStartIndex() - 1, ct.getStartIndex()));
            } else {
                errors.add(new TomlSyntaxError(errorMessage, ct.getStartIndex(), ct.getStartIndex() + ct.getText().length()));
            }
        }
    }

    private void lexerNoViableAltException(TomlGrammarLexer lexer, String errorMessage, LexerNoViableAltException noViableAltException) {
        CharStream inputStream = lexer.getInputStream();
        String errorDisplay = lexer.getErrorDisplay(inputStream.getText(
                new Interval(noViableAltException.getStartIndex(), inputStream.index())));
        //original message is "token recognition error at ", so let's rewrite it
        if (errorDisplay != null && errorDisplay.startsWith("\"")) {
            errors.add(new TomlSyntaxError("Unfinished double quoted string literal",
                    noViableAltException.getStartIndex(),
                    noViableAltException.getStartIndex() + errorDisplay.length()));

        } else {
            errors.add(new TomlSyntaxError(errorMessage,
                    noViableAltException.getStartIndex(),
                    noViableAltException.getStartIndex() + 1));
        }
    }
}

syntaxError podczas wywołania przez antlr dostaje następujące parametry:

  • recognizer parser lub lexer, który wykrył błąd
  • offendingSymbol - jeśli recognizer jest parserem to symbol, który powoduje błąd
  • line - linia, w której występuje błąd
  • column - kolumna, w której występuje błąd
  • errorMessage - opis błędu wygenerowany przez antlr
  • re jedna z klas dziedziczących po RecognitionException

W przypadku błędów pochodzących z lexera niektóre komunikaty mogą być... mało informujące. Stad dodatkowa obsługa LexerNoViableAltException i nadpisanie komunikatu błędu w przypadku problemu z brakiem zamknięcia " (pomysł zaczerpnięty z kodu parsera dla mysql)

Z pozostałych dziwolągów w kodzie:

if ((ct.getType() == -1) && (recognizer instanceof TomlGrammarParser)) {

to specjalna obsługa dla brakujących elementów na końcu pliku (inna logika wyliczania początku i końca występowania błędu)

Podpięcie pod parser i lexer

Tu nie ma żadnej filozofii:

  • trzeba odpiąć domyślne error listenery (wypisujące blędy na konsole)
  • zapiąć do lexera i parsera nasz nowy listener
  • zebrane błędy gdzieś zapisać (w naszym przypadku w polu w klasie TomlParserResult)

Odpowiednio zmieniona metoda z NetbeansTomlParser:

    @Override
    public void parse(Snapshot snapshot, Task task, SourceModificationEvent sourceModificationEvent) throws ParseException {
        CharSequence text = snapshot.getText();

        CharStream inp = new CharSequenceCharStream(text, text.length(), snapshot.getSource().getFileObject().getNameExt());
        TomlGrammarLexer lexer = new TomlGrammarLexer(inp);
        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
        TomlGrammarParser parser = new TomlGrammarParser(tokenStream);

        // remove default listeners
        parser.removeErrorListeners();
        lexer.removeErrorListeners();

        List<TomlSyntaxError> syntaxErrors = new LinkedList<>();
        TomlAntlrErrorListener errorListener = new TomlAntlrErrorListener(syntaxErrors);

        lexer.addErrorListener(errorListener);
        parser.addErrorListener(errorListener);

        TomlGrammarParser.DocumentContext document = parser.document();
        lastResult = new TomlParserResult(snapshot, document, syntaxErrors);
    }

Task i TaskFactory

Operacje działające na rezultatach parsingu (u nas TomlParserResult) powinny wykonywać się w tle. W zależności od tego co dana operacja ma robić powinna powstać klasa dziedzicząca z ParserResultTask oraz odpowiednia fabryka tworząca jej instancje (dziedzicząca z TaskFactory).

W naszym przypadku będzie to TomlSyntaxErrorHighlightingTaskFactory:

@MimeRegistration(mimeType=Constants.MIME_TYPE,service=TaskFactory.class)
public class TomlSyntaxErrorHighlightingTaskFactory extends TaskFactory {

 @Override
 public Collection<? extends SchedulerTask> create(Snapshot snpshot) {
  return Collections.singleton(new TomlSyntaxErrorHighlightingTask());
 }
}

Z @MimeRegistration korzystaliśmy już wcześniej, poza tym nie ma tu prawie wcale logiki. Po zmianach w pliku Netbeans zawoła metodę create , która stworzy nasz nowy task podświetlający błędy. Skoro o Task mowa:

public class TomlSyntaxErrorHighlightingTask extends ParserResultTask {

    private boolean cancelled = false;

    @Override
    public void run(Parser.Result result, SchedulerEvent se) {
        FileObject fileObject = result.getSnapshot().getSource().getFileObject();
        TomlParserResult parserResult = (TomlParserResult) result;
        List<ErrorDescription> errors = new ArrayList<>();
        for (TomlSyntaxError syntaxError : parserResult.getSyntaxErrors()) {
            if (cancelled) {
                return;
            }
            errors.add(ErrorDescriptionFactory.createErrorDescription(Severity.ERROR,
                    syntaxError.getErrorMessage(), fileObject, syntaxError.getStartPosition(), syntaxError.getEndPosition()));
        }

        HintsController.setErrors(fileObject, "base-toml-parser", errors);
    }

    @Override
    public int getPriority() {
        return 100;
    }

    @Override
    public Class<? extends Scheduler> getSchedulerClass() {
        return Scheduler.EDITOR_SENSITIVE_TASK_SCHEDULER;
    }

    @Override
    public void cancel() {
        cancelled = true;
    }

}

Oprócz bardzo podstawowych implementacji getPriority(), getSchedulerClass() i cancel() mamy mięsko czyli run:

  • wyciągamy FileObject , do którego będziemy podpinać potem informacje o błędach
  • z rezultatu wyciągamy listę błędów i przepakowujemy ją w ErrorDescription
  • gotową listę błędów "ustawiamy" na pliku poprzez HintsController.setErrors. Tu ważny jest parametr layer (u nas ma wartość base-toml-parser) - na danym pliku może operować wiele zadań rozpoznających różne problemy - każdy będzie miał swoją warstwę z błędami.

Klasy towarzyszące i inne zmiany

Do kompletu potrzebna nam klasa TomlSyntaxError - nie wklejam jej, bo jest wybitnie prosta (trzy pola przechowywujące komunikat i pozycje w pliku), oraz jawna deklaracja zależności na org-netbeans-spi-editor-hints w pom.xml.

Podsumowanie

Efekt końcowy:

Wykryte błędy

Repozytorium z aktualnym kodem: https://gitlab.com/ihsahn/netbeans-toml

Literatura

  1. The Definitive ANTLR 4 Reference książka o antlr (naprawdę dobra rzecz!)
  2. Api Antlr
  3. NetBeans api javadoc