package com.dmarcotte.handlebars.parsing; import com.dmarcotte.handlebars.HbBundle; import com.intellij.lang.PsiBuilder; import com.intellij.psi.tree.IElementType; import java.util.HashSet; import java.util.Set; import static com.dmarcotte.handlebars.parsing.HbTokenTypes.*; /** * The parser is based directly on Handlebars.yy * (taken from the following revision: https://github.com/wycats/handlebars.js/blob/408192ba9f262bb82be88091ab3ec3c16dc02c6d/src/handlebars.yy) * <p/> * Methods mapping to expression in the grammar are commented with the part of the grammar they map to. * <p/> * Places where we've gone off book to make the live syntax detection a more pleasant experience are * marked HB_CUSTOMIZATION. If we find bugs, or the grammar is ever updated, these are the first candidates to check. */ @SuppressWarnings("Duplicates") // suppress duplicate detection since we want to maintain the structural parity between // the code and the jison grammar rules, which can appear to duplicate code public class HbParsing { private final PsiBuilder builder; // the set of tokens which, if we encounter them while in a bad state, we'll try to // resume parsing from them private static final Set<IElementType> RECOVERY_SET; static { RECOVERY_SET = new HashSet<>(); RECOVERY_SET.add(OPEN); RECOVERY_SET.add(OPEN_BLOCK); RECOVERY_SET.add(OPEN_ENDBLOCK); RECOVERY_SET.add(OPEN_INVERSE); RECOVERY_SET.add(OPEN_PARTIAL); RECOVERY_SET.add(OPEN_UNESCAPED); RECOVERY_SET.add(CONTENT); } public HbParsing(final PsiBuilder builder) { this.builder = builder; } public void parse() { while (!builder.eof()) { parseRoot(builder); if (builder.eof()) { break; } // jumped out of the parser prematurely... try and figure out what's tripping it up, // then jump back in // deal with some unexpected tokens IElementType tokenType = builder.getTokenType(); int problemOffset = builder.getCurrentOffset(); if (tokenType == OPEN_ENDBLOCK) { parseCloseBlock(builder); } if (builder.getCurrentOffset() == problemOffset) { // none of our error checks advanced the lexer, do it manually before we // try and resume parsing to avoid an infinite loop PsiBuilder.Marker problemMark = builder.mark(); builder.advanceLexer(); problemMark.error(HbBundle.message("hb.parsing.invalid")); } } } /** * root * : program EOF */ private void parseRoot(PsiBuilder builder) { parseProgram(builder); } /** * program * : statement* * | "" * ; */ private void parseProgram(PsiBuilder builder) { parseStatements(builder); } /** * statement* * : statement * | statements statement * ; */ private void parseStatements(PsiBuilder builder) { PsiBuilder.Marker statementsMarker = builder.mark(); // parse zero or more statements (empty statements are acceptable) while (true) { PsiBuilder.Marker optionalStatementMarker = builder.mark(); if (parseStatement(builder)) { optionalStatementMarker.drop(); } else { optionalStatementMarker.rollbackTo(); break; } } statementsMarker.done(STATEMENTS); } /** * statement * : block * | mustache (HB_CUSTOMIZATION we check `block` before `mustache` because our custom "{{else" gets incorrectly parsed as a broken * mustache if we parse this first) * | rawBlock * | partial * | partialBlock * | ESCAPE_CHAR (HB_CUSTOMIZATION the official Handlebars lexer just throws out the escape char; * it's convenient for us to keep it so that we can highlight it) * | CONTENT * | COMMENT * ; */ private boolean parseStatement(PsiBuilder builder) { IElementType tokenType = builder.getTokenType(); /** * block * : openBlock program inverseChain? closeBlock * | openInverse program inverseAndProgram? closeBlock */ { if (builder.getTokenType() == OPEN_INVERSE) { if (builder.lookAhead(1) == CLOSE) { /* HB_CUSTOMIZATION */ // this is actually a `{{^}}` simple inverse. Bail out. It gets parsed outside of `statement` return false; } PsiBuilder.Marker blockMarker = builder.mark(); if (parseOpenInverse(builder)) { parseProgram(builder); parseInverseAndProgram(builder); parseCloseBlock(builder); blockMarker.done(BLOCK_WRAPPER); } else { return false; } return true; } if (tokenType == OPEN_BLOCK) { PsiBuilder.Marker blockMarker = builder.mark(); // this is a fairly lo-fi way to detect this, but it's how it's done in handlebars.js (https://github.com/wycats/handlebars.js/commit/408192ba9f262bb82be88091ab3ec3c16dc02c6d#diff-e85944a1a496f573d1227511819c9e23R128) // so we avoid unneeded complexity by directly porting it boolean hasDecorator = (builder.getTokenText() != null && builder.getTokenText().equals("{{#*")); if (parseOpenBlock(builder)) { parseProgram(builder); PsiBuilder.Marker inverseMarker = builder.mark(); if (parseInverseChain(builder) && hasDecorator) { inverseMarker.error(HbBundle.message("hb.parsing.unexpected.decorator.inverse")); } else { inverseMarker.drop(); } parseCloseBlock(builder); blockMarker.done(BLOCK_WRAPPER); } else { return false; } return true; } } /** * mustache * : OPEN sexpr CLOSE * | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED * ; */ { if (tokenType == OPEN) { if (builder.lookAhead(1) == ELSE) { /* HB_CUSTOMIZATION */ // this is actually an `{{else` expression, not a mustache. return false; } parseMustache(builder, OPEN, CLOSE); return true; } if (tokenType == OPEN_UNESCAPED) { parseMustache(builder, OPEN_UNESCAPED, CLOSE_UNESCAPED); return true; } } /** * rawBlock * : openRawBlock CONTENT endRawBlock */ if (tokenType == OPEN_RAW_BLOCK) { PsiBuilder.Marker blockMarker = builder.mark(); if (parseOpenRawBlock(builder)) { if (builder.getTokenType() == CONTENT) { builder.advanceLexer(); // eat non-HB content } parseCloseRawBlock(builder); blockMarker.done(BLOCK_WRAPPER); } else { return false; } return true; } if (tokenType == OPEN_PARTIAL) { parsePartial(builder); return true; } /** * partialBlock + : openPartialBlock program closeBlock */ if (tokenType == OPEN_PARTIAL_BLOCK) { PsiBuilder.Marker blockMarker = builder.mark(); if (parseOpenPartialBlock(builder)) { parseProgram(builder); parseCloseBlock(builder); blockMarker.done(BLOCK_WRAPPER); } else { return false; } return true; } if (tokenType == ESCAPE_CHAR) { builder.advanceLexer(); // ignore the escape character return true; } if (tokenType == CONTENT) { builder.advanceLexer(); // eat non-HB content return true; } if (tokenType == COMMENT) { parseLeafToken(builder, COMMENT); return true; } // HB_CUSTOMIZATION: we lex UNCLOSED_COMMENT sections specially so that we can coherently mark them as errors if (tokenType == UNCLOSED_COMMENT) { PsiBuilder.Marker unclosedCommentMarker = builder.mark(); parseLeafToken(builder, UNCLOSED_COMMENT); unclosedCommentMarker.error(HbBundle.message("hb.parsing.comment.unclosed")); return true; } return false; } /** * inverseChain * : openInverseChain program inverseChain? * | inverseAndProgram */ private boolean parseInverseChain(PsiBuilder builder) { if (parseInverseAndProgram(builder)) { return true; } else if (parseOpenInverseChain(builder)) { parseProgram(builder); parseInverseChain(builder); return true; } return false; } /** * inverseAndProgram * : INVERSE program */ private boolean parseInverseAndProgram(PsiBuilder builder) { if (parseINVERSE(builder)) { parseProgram(builder); return true; } else { return false; } } /** * openRawBlock * : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK */ private boolean parseOpenRawBlock(PsiBuilder builder) { PsiBuilder.Marker openRawBlockStacheMarker = builder.mark(); if (!parseLeafToken(builder, OPEN_RAW_BLOCK)) { openRawBlockStacheMarker.drop(); return false; } if (parseHelperName(builder)) { parseParamsStartHashQuestion(builder); parseLeafTokenGreedy(builder, CLOSE_RAW_BLOCK); } openRawBlockStacheMarker.done(OPEN_BLOCK_STACHE); return true; } /** * openPartialBlock * : OPEN_PARTIAL_BLOCK partialName param* hash? CLOSE */ private boolean parseOpenPartialBlock(PsiBuilder builder) { PsiBuilder.Marker openPartialBlockStacheMarker = builder.mark(); if (!parseLeafToken(builder, OPEN_PARTIAL_BLOCK)) { openPartialBlockStacheMarker.rollbackTo(); return false; } if (parsePartialName(builder)) { parseParamsStartHashQuestion(builder); } parseLeafTokenGreedy(builder, CLOSE); openPartialBlockStacheMarker.done(OPEN_PARTIAL_BLOCK_STACHE); return true; } /** * openBlock * : OPEN_BLOCK helperName param* hash? blockParams? CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); } * ; */ private boolean parseOpenBlock(PsiBuilder builder) { PsiBuilder.Marker openBlockStacheMarker = builder.mark(); if (!parseLeafToken(builder, OPEN_BLOCK)) { openBlockStacheMarker.drop(); return false; } if(parseHelperName(builder)) { parseParamsStartHashQuestion(builder); parseBlockParams(builder); parseLeafTokenGreedy(builder, CLOSE); } openBlockStacheMarker.done(OPEN_BLOCK_STACHE); return true; } /** * openInverseChain * : OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE * ; */ private boolean parseOpenInverseChain(PsiBuilder builder) { PsiBuilder.Marker openInverseChainMarker = builder.mark(); if (!parseLeafToken(builder, OPEN) || !parseLeafToken(builder, ELSE)) { openInverseChainMarker.rollbackTo(); return false; } if (parseHelperName(builder)) { parseParamsStartHashQuestion(builder); parseBlockParams(builder); parseLeafTokenGreedy(builder, CLOSE); } openInverseChainMarker.done(OPEN_INVERSE_CHAIN); return true; } /** * openInverse * : OPEN_INVERSE helperName param* hash? blockParams? CLOSE * ; */ private boolean parseOpenInverse(PsiBuilder builder) { PsiBuilder.Marker openInverseBlockStacheMarker = builder.mark(); if (!parseLeafToken(builder, OPEN_INVERSE)) { return false; } if (parseHelperName(builder)) { parseParamsStartHashQuestion(builder); parseBlockParams(builder); parseLeafTokenGreedy(builder, CLOSE); } openInverseBlockStacheMarker.done(OPEN_INVERSE_BLOCK_STACHE); return true; } /** * closeRawBlock * : END_RAW_BLOCK helperName CLOSE_RAW_BLOCK * ; */ private boolean parseCloseRawBlock(PsiBuilder builder) { PsiBuilder.Marker closeRawBlockMarker = builder.mark(); if (!parseLeafToken(builder, END_RAW_BLOCK)) { closeRawBlockMarker.drop(); return false; } parseHelperName(builder); parseLeafTokenGreedy(builder, CLOSE_RAW_BLOCK); closeRawBlockMarker.done(CLOSE_BLOCK_STACHE); return true; } /** * closeBlock * : OPEN_ENDBLOCK helperName CLOSE { $$ = $2; } * ; */ private boolean parseCloseBlock(PsiBuilder builder) { PsiBuilder.Marker closeBlockMarker = builder.mark(); if (!parseLeafToken(builder, OPEN_ENDBLOCK)) { closeBlockMarker.drop(); return false; } parseHelperName(builder); parseLeafTokenGreedy(builder, CLOSE); closeBlockMarker.done(CLOSE_BLOCK_STACHE); return true; } /** * mustache * : OPEN helperName param* hash? CLOSE * | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED * ; */ protected void parseMustache(PsiBuilder builder, IElementType openStache, IElementType closeStache) { PsiBuilder.Marker mustacheMarker = builder.mark(); parseLeafToken(builder, openStache); if (parseHelperName(builder)) { parseParamsStartHashQuestion(builder); } parseLeafTokenGreedy(builder, closeStache); mustacheMarker.done(MUSTACHE); } /** * partial * : OPEN_PARTIAL partialName param* hash? CLOSE * ; */ protected void parsePartial(PsiBuilder builder) { PsiBuilder.Marker partialMarker = builder.mark(); parseLeafToken(builder, OPEN_PARTIAL); if (parsePartialName(builder)) { parseParamsStartHashQuestion(builder); } parseLeafTokenGreedy(builder, CLOSE); partialMarker.done(PARTIAL_STACHE); } /** * partialName * : helperName * | sexpr */ private boolean parsePartialName(PsiBuilder builder) { PsiBuilder.Marker partialNameMark = builder.mark(); if (parseHelperName(builder) || parseSexpr(builder)) { partialNameMark.done(PARTIAL_NAME); return true; } partialNameMark.error(HbBundle.message("hb.parsing.expected.partial.name")); return false; } /** * HB_CUSTOMIZATION: we don't parse an INVERSE token like the wycats/handlebars grammar since we lex "else" as * an individual token so we can highlight it distinctly. This method parses {{^}} and {{else}} * as a unit to synthesize INVERSE */ private boolean parseINVERSE(PsiBuilder builder) { PsiBuilder.Marker simpleInverseMarker = builder.mark(); boolean isSimpleInverse; // try and parse "{{^}}" PsiBuilder.Marker regularInverseMarker = builder.mark(); if (!parseLeafToken(builder, OPEN_INVERSE) || !parseLeafToken(builder, CLOSE)) { regularInverseMarker.rollbackTo(); isSimpleInverse = false; } else { regularInverseMarker.drop(); isSimpleInverse = true; } // if we didn't find "{{^}}", check for "{{else}}" PsiBuilder.Marker elseInverseMarker = builder.mark(); if (!isSimpleInverse && (!parseLeafToken(builder, OPEN) || !parseLeafToken(builder, ELSE) || !parseLeafToken(builder, CLOSE))) { elseInverseMarker.rollbackTo(); isSimpleInverse = false; } else { elseInverseMarker.drop(); isSimpleInverse = true; } if (isSimpleInverse) { simpleInverseMarker.done(SIMPLE_INVERSE); return true; } else { simpleInverseMarker.drop(); return false; } } /** * sexpr * : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR */ protected boolean parseSexpr(PsiBuilder builder) { PsiBuilder.Marker sexprMarker = builder.mark(); if (parseLeafToken(builder, OPEN_SEXPR)) { parseParamsStartHashQuestion(builder); parseLeafTokenGreedy(builder, CLOSE_SEXPR); sexprMarker.drop(); return true; } sexprMarker.rollbackTo(); return false; } /** * Helper for the common `param* hash?` expression in the grammar. * ; */ private void parseParamsStartHashQuestion(PsiBuilder builder) { PsiBuilder.Marker helpParamHashMarker = builder.mark(); // try to extend the 'path' we found to 'path hash' PsiBuilder.Marker hashMarker = builder.mark(); if (parseHash(builder)) { hashMarker.drop(); } else { // not a hash... try for 'path params', followed by an attempt at 'path params hash' hashMarker.rollbackTo(); PsiBuilder.Marker paramsMarker = builder.mark(); if (parseParams(builder)) { PsiBuilder.Marker paramsHashMarker = builder.mark(); int hashStartPos = builder.getCurrentOffset(); if (parseHash(builder)) { paramsHashMarker.drop(); } else { if (hashStartPos < builder.getCurrentOffset()) { /* HB_CUSTOMIZATION */ // managed to partially parse the hash. Don't rollback so that // we can keep the errors paramsHashMarker.drop(); } else { paramsHashMarker.rollbackTo(); } } paramsMarker.drop(); } else { paramsMarker.rollbackTo(); } } helpParamHashMarker.drop(); } /** * params * : params param * | param * ; */ private boolean parseParams(PsiBuilder builder) { PsiBuilder.Marker paramsMarker = builder.mark(); if (!parseParam(builder)) { paramsMarker.error(HbBundle.message("hb.parsing.expected.parameter")); return false; } // parse any additional params while (true) { PsiBuilder.Marker optionalParamMarker = builder.mark(); if (parseParam(builder)) { optionalParamMarker.drop(); } else { optionalParamMarker.rollbackTo(); break; } } paramsMarker.drop(); return true; } /** * param * : helperName * | sexpr * ; */ protected boolean parseParam(PsiBuilder builder) { PsiBuilder.Marker paramMarker = builder.mark(); PsiBuilder.Marker helperNameMark = builder.mark(); if (parseHelperName(builder)) { helperNameMark.drop(); paramMarker.done(PARAM); return true; } else { helperNameMark.rollbackTo(); } PsiBuilder.Marker sexprMarker = builder.mark(); if (parseSexpr(builder)) { sexprMarker.drop(); paramMarker.done(PARAM); return true; } else { sexprMarker.rollbackTo(); } paramMarker.error(HbBundle.message("hb.parsing.expected.parameter")); return false; } /** * hash * : hashSegments { $$ = new yy.HashNode($1); } * ; */ private boolean parseHash(PsiBuilder builder) { return parseHashSegments(builder); } /** * hashSegments * : hashSegments hashSegment { $1.push($2); $$ = $1; } * | hashSegment { $$ = [$1]; } * ; */ private boolean parseHashSegments(PsiBuilder builder) { PsiBuilder.Marker hashSegmentsMarker = builder.mark(); if (!parseHashSegment(builder)) { hashSegmentsMarker.error(HbBundle.message("hb.parsing.expected.hash")); return false; } // parse any additional hash segments while (true) { PsiBuilder.Marker optionalHashMarker = builder.mark(); int hashStartPos = builder.getCurrentOffset(); if (parseHashSegment(builder)) { optionalHashMarker.drop(); } else { if (hashStartPos < builder.getCurrentOffset()) { // HB_CUSTOMIZATION managed to partially parse this hash; don't roll back the errors optionalHashMarker.drop(); hashSegmentsMarker.drop(); return false; } else { optionalHashMarker.rollbackTo(); } break; } } hashSegmentsMarker.drop(); return true; } /** * hashSegment * : ID EQUALS path * | ID EQUALS STRING * | ID EQUALS NUMBER * | ID EQUALS BOOLEAN * | ID EQUALS dataName * ; * <p/> * Refactored to: * hashSegment * : ID EQUALS param */ private boolean parseHashSegment(PsiBuilder builder) { final PsiBuilder.Marker hash = builder.mark(); boolean result = parseLeafToken(builder, ID) && parseLeafToken(builder, EQUALS) && parseParam(builder); if (result) { hash.done(HASH); } else { hash.drop(); } return result; } /** * helperName * : path * | dataName * | STRING * | NUMBER * | BOOLEAN * ; */ private boolean parseHelperName(PsiBuilder builder) { PsiBuilder.Marker helperNameMarker = builder.mark(); PsiBuilder.Marker pathMarker = builder.mark(); if (parsePath(builder)) { pathMarker.drop(); helperNameMarker.done(MUSTACHE_NAME); return true; } else { pathMarker.rollbackTo(); } PsiBuilder.Marker dataNameMarker = builder.mark(); if (parseDataName(builder)) { dataNameMarker.drop(); helperNameMarker.done(MUSTACHE_NAME); return true; } else { dataNameMarker.rollbackTo(); } PsiBuilder.Marker stringMarker = builder.mark(); if (parseLeafToken(builder, STRING)) { stringMarker.drop(); helperNameMarker.done(MUSTACHE_NAME); return true; } else { stringMarker.rollbackTo(); } PsiBuilder.Marker integerMarker = builder.mark(); if (parseLeafToken(builder, NUMBER)) { integerMarker.drop(); helperNameMarker.done(MUSTACHE_NAME); return true; } else { integerMarker.rollbackTo(); } PsiBuilder.Marker booleanMarker = builder.mark(); if (parseLeafToken(builder, BOOLEAN)) { booleanMarker.drop(); helperNameMarker.done(MUSTACHE_NAME); return true; } else { booleanMarker.rollbackTo(); } helperNameMarker.error(HbBundle.message("hb.parsing.expected.path.or.data")); return false; } /** * blockParams * OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS */ private boolean parseBlockParams(PsiBuilder builder) { PsiBuilder.Marker blockParamsMarker = builder.mark(); if (parseLeafToken(builder, OPEN_BLOCK_PARAMS)) { blockParamsMarker.drop(); parseLeafToken(builder, ID); // parse any additional IDs while (true) { PsiBuilder.Marker optionalIdMarker = builder.mark(); if (parseLeafToken(builder, ID)) { optionalIdMarker.drop(); } else { optionalIdMarker.rollbackTo(); break; } } parseLeafToken(builder, CLOSE_BLOCK_PARAMS); return true; } else { blockParamsMarker.rollbackTo(); return false; } } /** * dataName * : DATA pathSegments * ; */ private boolean parseDataName(PsiBuilder builder) { PsiBuilder.Marker prefixMarker = builder.mark(); if (parseLeafToken(builder, DATA_PREFIX)) { prefixMarker.drop(); } else { prefixMarker.rollbackTo(); return false; } PsiBuilder.Marker dataMarker = builder.mark(); if (parsePathSegments(builder)) { dataMarker.done(DATA); return true; } dataMarker.rollbackTo(); return false; } /** * path * : pathSegments * ; */ protected boolean parsePath(PsiBuilder builder) { PsiBuilder.Marker pathMarker = builder.mark(); if (parsePathSegments(builder)) { pathMarker.done(PATH); return true; } pathMarker.rollbackTo(); return false; } /** * pathSegments * : pathSegments SEP ID { $1.push($3); $$ = $1; } * | ID { $$ = [$1]; } * ; * <p/> * Refactored to eliminate left recursion: * <p/> * pathSegments * : ID pathSegments' * <p/> * pathSegements' * : <epsilon> * | SEP ID pathSegments' */ protected boolean parsePathSegments(PsiBuilder builder) { PsiBuilder.Marker pathSegmentsMarker = builder.mark(); /* HB_CUSTOMIZATION: see isHashNextLookAhead docs for details */ if (isHashNextLookAhead(builder)) { pathSegmentsMarker.rollbackTo(); return false; } if (!parseLeafToken(builder, ID)) { pathSegmentsMarker.drop(); return false; } parsePathSegmentsPrime(builder); pathSegmentsMarker.drop(); return true; } /** * See {@link #parsePathSegments(com.intellij.lang.PsiBuilder)} for more info on this method */ protected void parsePathSegmentsPrime(PsiBuilder builder) { PsiBuilder.Marker pathSegmentsPrimeMarker = builder.mark(); if (!parseLeafToken(builder, SEP)) { // the epsilon case pathSegmentsPrimeMarker.rollbackTo(); return; } /* HB_CUSTOMIZATION*/ if (isHashNextLookAhead(builder)) { pathSegmentsPrimeMarker.rollbackTo(); return; } if (parseLeafToken(builder, ID)) { parsePathSegmentsPrime(builder); } pathSegmentsPrimeMarker.drop(); } /** * HB_CUSTOMIZATION: the beginnings of a 'hash' have a bad habit of looking like params * (i.e. test="what" parses as if "test" was a param, and then the builder is left pointing * at "=" which matches no rules). * <p/> * We check this in a couple of places to determine whether something should be parsed as * a param, or left alone to grabbed by the hash parser later */ protected boolean isHashNextLookAhead(PsiBuilder builder) { PsiBuilder.Marker hashLookAheadMarker = builder.mark(); boolean isHashUpcoming = parseHashSegment(builder); hashLookAheadMarker.rollbackTo(); return isHashUpcoming; } /** * Tries to parse the given token, marking an error if any other token is found */ protected boolean parseLeafToken(PsiBuilder builder, IElementType leafTokenType) { PsiBuilder.Marker leafTokenMark = builder.mark(); if (builder.getTokenType() == leafTokenType) { builder.advanceLexer(); leafTokenMark.done(leafTokenType); return true; } else if (builder.getTokenType() == INVALID) { while (!builder.eof() && builder.getTokenType() == INVALID) { builder.advanceLexer(); } recordLeafTokenError(INVALID, leafTokenMark); return false; } else { recordLeafTokenError(leafTokenType, leafTokenMark); return false; } } /** * HB_CUSTOMIZATION * <p/> * Eats tokens until it finds the expected token, marking errors along the way. * <p/> * Will also stop if it encounters a {@link #RECOVERY_SET} token */ @SuppressWarnings("SameParameterValue") // though this method is only being used for CLOSE right now, it reads better this way protected void parseLeafTokenGreedy(PsiBuilder builder, IElementType expectedToken) { // failed to parse expected token... chew up tokens marking this error until we encounter // a token which give the parser a good shot at resuming if (builder.getTokenType() != expectedToken) { PsiBuilder.Marker unexpectedTokensMarker = builder.mark(); while (!builder.eof() && builder.getTokenType() != expectedToken && !RECOVERY_SET.contains(builder.getTokenType())) { builder.advanceLexer(); } recordLeafTokenError(expectedToken, unexpectedTokensMarker); } if (!builder.eof() && builder.getTokenType() == expectedToken) { parseLeafToken(builder, expectedToken); } } private void recordLeafTokenError(IElementType expectedToken, PsiBuilder.Marker unexpectedTokensMarker) { if (expectedToken instanceof HbElementType) { unexpectedTokensMarker.error(((HbElementType)expectedToken).parseExpectedMessage()); } else { unexpectedTokensMarker.error(HbBundle.message("hb.parsing.element.expected.invalid")); } } }