package org.angularjs.lang.parser; import com.intellij.lang.PsiBuilder; import com.intellij.lang.javascript.*; import com.intellij.lang.javascript.parsing.*; import com.intellij.psi.tree.IElementType; import org.angularjs.lang.lexer.AngularJSTokenTypes; /** * @author Dennis.Ushakov */ public class AngularJSParser extends JavaScriptParser<AngularJSParser.AngularJSExpressionParser, StatementParser, FunctionParser, JSPsiTypeParser> { public AngularJSParser(PsiBuilder builder) { super(JavaScriptSupportLoader.JAVASCRIPT_1_5, builder); myExpressionParser = new AngularJSExpressionParser(); myStatementParser = new StatementParser<AngularJSParser>(this) { @Override protected void doParseStatement(boolean canHaveClasses) { final IElementType firstToken = builder.getTokenType(); if (firstToken == JSTokenTypes.LBRACE) { parseExpressionStatement(); checkForSemicolon(); return; } if (isIdentifierToken(firstToken)) { final IElementType nextToken = builder.lookAhead(1); if (nextToken == JSTokenTypes.IN_KEYWORD) { parseInStatement(); return; } } if (firstToken == JSTokenTypes.LET_KEYWORD) { if (builder.lookAhead(2) != JSTokenTypes.EQ) { parseNgForStatement(); return; } parseExpressionStatement(); return; } if (builder.getTokenType() == JSTokenTypes.LPAR) { if (parseInStatement()) { return; } } if (tryParseNgIfStatement()) { return; } super.doParseStatement(canHaveClasses); } private void parseNgForStatement() { PsiBuilder.Marker statement = builder.mark(); if (!getExpressionParser().parseForExpression()) { statement.drop(); return; } checkForSemicolon(); statement.done(JSElementTypes.EXPRESSION_STATEMENT); } private boolean parseInStatement() { PsiBuilder.Marker statement = builder.mark(); if (!getExpressionParser().parseInExpression()) { statement.drop(); return false; } statement.done(JSElementTypes.EXPRESSION_STATEMENT); return true; } private boolean tryParseNgIfStatement() { PsiBuilder.Marker ngIf = builder.mark(); getExpressionParser().parseExpression(); if (builder.getTokenType() != JSTokenTypes.SEMICOLON) { ngIf.rollbackTo(); return false; } builder.advanceLexer(); if (builder.getTokenType() != AngularJSTokenTypes.THEN && builder.getTokenType() != JSTokenTypes.ELSE_KEYWORD) { ngIf.rollbackTo(); return false; } parseBranch(AngularJSTokenTypes.THEN); parseBranch(JSTokenTypes.ELSE_KEYWORD); ngIf.done(JSElementTypes.IF_STATEMENT); checkForSemicolon(); if (builder.getTokenType() == JSTokenTypes.LET_KEYWORD) { getExpressionParser().parseHashDefinition(); } return true; } private void parseBranch(IElementType branchType) { if (builder.getTokenType() == branchType) { builder.advanceLexer(); if (builder.getTokenType() == JSTokenTypes.IDENTIFIER) { buildTokenElement(JSElementTypes.REFERENCE_EXPRESSION); } else { builder.error(JSBundle.message("javascript.parser.message.expected.identifier")); } } } }; } @Override public boolean isIdentifierName(IElementType firstToken) { return super.isIdentifierName(firstToken) || firstToken == AngularJSTokenTypes.THEN; } public void parseAngular(IElementType root) { final PsiBuilder.Marker rootMarker = builder.mark(); while (!builder.eof()) { getStatementParser().parseStatement(); } rootMarker.done(root); } protected class AngularJSExpressionParser extends ExpressionParser<AngularJSParser> { private final AngularJSMessageFormatParser myAngularJSMessageFormatParser; public AngularJSExpressionParser() { super(AngularJSParser.this); myAngularJSMessageFormatParser = new AngularJSMessageFormatParser(myJavaScriptParser); } @Override protected boolean parseUnaryExpression() { final IElementType tokenType = builder.getTokenType(); if (tokenType == JSTokenTypes.OR) { builder.advanceLexer(); if (!parseFilter()) { builder.error("expected filter"); } return true; } if (tokenType == AngularJSTokenTypes.ONE_TIME_BINDING) { final PsiBuilder.Marker expr = builder.mark(); builder.advanceLexer(); if (!super.parseUnaryExpression()) { builder.error(JSBundle.message("javascript.parser.message.expected.expression")); } expr.done(JSElementTypes.PREFIX_EXPRESSION); return true; } return super.parseUnaryExpression(); } @Override public boolean parsePrimaryExpression() { final IElementType firstToken = builder.getTokenType(); if (firstToken == JSTokenTypes.STRING_LITERAL) { return parseStringLiteral(firstToken); } if (firstToken == JSTokenTypes.LET_KEYWORD) { parseHashDefinition(); return true; } if (isIdentifierToken(firstToken) && myAngularJSMessageFormatParser.parseMessage()) { return true; } if (parseAsExpression()) { return true; } return super.parsePrimaryExpression(); } private boolean parseAsExpression() { PsiBuilder.Marker expr = builder.mark(); if (!parseQualifiedTypeName(false)) { expr.rollbackTo(); return false; } if (builder.getTokenType() != JSTokenTypes.AS_KEYWORD) { expr.rollbackTo(); return false; } builder.advanceLexer(); parseExplicitIdentifierWithError(); expr.done(AngularJSElementTypes.AS_EXPRESSION); return true; } private void parseExplicitIdentifierWithError() { if (isIdentifierToken(builder.getTokenType())) { parseExplicitIdentifier(); } else { builder.error(JSBundle.message("javascript.parser.message.expected.identifier")); } } @Override protected boolean isReferenceQualifierSeparator(IElementType tokenType) { return tokenType == AngularJSTokenTypes.ELVIS || super.isReferenceQualifierSeparator(tokenType); } @Override protected int getCurrentBinarySignPriority(boolean allowIn, boolean advance) { if (builder.getTokenType() == JSTokenTypes.OR) return 10; return super.getCurrentBinarySignPriority(allowIn, advance); } private boolean parseFilter() { final PsiBuilder.Marker mark = builder.mark(); buildTokenElement(JSElementTypes.REFERENCE_EXPRESSION); PsiBuilder.Marker arguments = null; while (builder.getTokenType() == JSTokenTypes.COLON) { arguments = arguments == null ? builder.mark() : arguments; builder.advanceLexer(); if (!super.parseUnaryExpression()) { builder.error(JSBundle.message("javascript.parser.message.expected.expression")); } } if (arguments != null) { arguments.done(JSElementTypes.ARGUMENT_LIST); } mark.done(AngularJSElementTypes.FILTER_EXPRESSION); return true; } private boolean parseStringLiteral(IElementType firstToken) { final PsiBuilder.Marker mark = builder.mark(); IElementType currentToken = firstToken; StringBuilder literal = new StringBuilder(); while (currentToken == JSTokenTypes.STRING_LITERAL || currentToken == AngularJSTokenTypes.ESCAPE_SEQUENCE || currentToken == AngularJSTokenTypes.INVALID_ESCAPE_SEQUENCE) { literal.append(builder.getTokenText()); builder.advanceLexer(); currentToken = builder.getTokenType(); } mark.done(JSStubElementTypes.LITERAL_EXPRESSION); final String errorMessage = validateLiteralText(literal.toString()); if (errorMessage != null) { builder.error(errorMessage); } return true; } public boolean parseForExpression() { final PsiBuilder.Marker expr = builder.mark(); parseHashDefinition(); if (builder.getTokenType() != JSTokenTypes.OF_KEYWORD) { builder.error("'of' expected"); } else { builder.advanceLexer(); } parseExpression(); if (builder.lookAhead(1) == AngularJSTokenTypes.TRACK_BY_KEYWORD) { builder.advanceLexer(); builder.advanceLexer(); if (builder.getTokenType() != JSTokenTypes.COLON) { builder.error(JSBundle.message("javascript.parser.message.expected.colon")); } else { builder.advanceLexer(); } parseExpression(); } expr.done(AngularJSElementTypes.FOR_EXPRESSION); return true; } protected void parseHashDefinition() { final PsiBuilder.Marker def = builder.mark(); builder.advanceLexer(); if (builder.getTokenType() != JSTokenTypes.IDENTIFIER) { builder.error(JSBundle.message("javascript.parser.message.expected.identifier")); } else { buildTokenElement(JSStubElementTypes.VARIABLE); } def.done(JSStubElementTypes.VAR_STATEMENT); } public boolean parseInExpression() { final PsiBuilder.Marker expr = builder.mark(); if (isIdentifierToken(builder.getTokenType())) { PsiBuilder.Marker statement = builder.mark(); buildTokenElement(JSStubElementTypes.VARIABLE); statement.done(JSStubElementTypes.VAR_STATEMENT); } else { final PsiBuilder.Marker keyValue = builder.mark(); parseKeyValue(); if (builder.getTokenType() != JSTokenTypes.IN_KEYWORD) { expr.rollbackTo(); return false; } else { keyValue.done(JSElementTypes.PARENTHESIZED_EXPRESSION); } } builder.advanceLexer(); parseExpression(); if (builder.getTokenType() == AngularJSTokenTypes.TRACK_BY_KEYWORD) { builder.advanceLexer(); parseExpression(); } expr.done(AngularJSElementTypes.REPEAT_EXPRESSION); return true; } private void parseKeyValue() { builder.advanceLexer(); final PsiBuilder.Marker comma = builder.mark(); if (isIdentifierToken(builder.getTokenType())) { buildTokenElement(JSStubElementTypes.VARIABLE); } else { builder.error(JSBundle.message("javascript.parser.message.expected.identifier")); } if (builder.getTokenType() == JSTokenTypes.COMMA) { builder.advanceLexer(); } else { builder.error(JSBundle.message("javascript.parser.message.expected.comma")); } if (isIdentifierToken(builder.getTokenType())) { buildTokenElement(JSStubElementTypes.VARIABLE); } else { builder.error(JSBundle.message("javascript.parser.message.expected.identifier")); } comma.done(JSStubElementTypes.VAR_STATEMENT); if (builder.getTokenType() == JSTokenTypes.RPAR) { builder.advanceLexer(); } else { builder.error(JSBundle.message("javascript.parser.message.expected.rparen")); } } private void parseExplicitIdentifier() { final PsiBuilder.Marker def = builder.mark(); buildTokenElement(JSElementTypes.REFERENCE_EXPRESSION); def.done(JSStubElementTypes.DEFINITION_EXPRESSION); } } }