/* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.template.soy.soyparse; import static com.google.template.soy.base.internal.BaseUtils.formatParseExceptionDetails; import com.google.common.collect.ImmutableSet; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.base.internal.LegacyInternalSyntaxException; import com.google.template.soy.error.ErrorReporter; import com.google.template.soy.error.SoyErrorKind; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Helpers for interpreting parse errors as soy errors. */ final class ParseErrors { private static final Pattern EXTRACT_LOCATION = Pattern.compile("at line (\\d+), column (\\d+)."); private static final SoyErrorKind BAD_PHNAME_VALUE = SoyErrorKind.of("Found ''phname'' attribute that is not a valid identifier"); private static final SoyErrorKind INVALID_STRING_LITERAL = SoyErrorKind.of("Invalid string literal found in Soy command."); private static final SoyErrorKind LEGACY_AND_ERROR = SoyErrorKind.of("Found use of ''&&'' instead of the ''and'' operator"); private static final SoyErrorKind LEGACY_OR_ERROR = SoyErrorKind.of("Found use of ''||'' instead of the ''or'' operator"); private static final SoyErrorKind LEGACY_NOT_ERROR = SoyErrorKind.of("Found use of ''!'' instead of the ''not'' operator"); private static final SoyErrorKind LEGACY_DOUBLE_QUOTED_STRING = SoyErrorKind.of("Found use of double quotes, Soy strings use single quotes"); private static final SoyErrorKind UNEXPECTED_EOF = SoyErrorKind.of( "Unexpected end of file. Did you forget to close an attribute value or a comment?"); private static final SoyErrorKind UNEXPECTED_PARAM_DECL = SoyErrorKind.of( "Unexpected parameter declaration. Param declarations must come before any code in " + "your template."); private static final SoyErrorKind UNEXPECTED_RIGHT_BRACE = SoyErrorKind.of("Unexpected ''}''; did you mean '''{'rb'}'''?"); private static final SoyErrorKind UNEXPECTED_TOKEN_MGR_ERROR = SoyErrorKind.of( "Unexpected fatal Soy error. Please file a bug with your Soy file and " + "we''ll take a look. {0}"); private ParseErrors() {} static void reportSoyFileParseException( ErrorReporter reporter, String filePath, ParseException e) { Token currentToken = e.currentToken; // currentToken is the 'last successfully consumed token', but the error is usually due to the // first unsuccessful token. use that for the source location Token errorToken = (currentToken.next != null) ? currentToken.next : currentToken; SourceLocation location = Tokens.createSrcLoc(filePath, errorToken); // handle a few special cases. switch (errorToken.kind) { case SoyFileParserConstants.XXX_BRACE_INVALID: reporter.report(location, UNEXPECTED_RIGHT_BRACE); return; case SoyFileParserConstants.XXX_INVALID_STRING_LITERAL: reporter.report(location, INVALID_STRING_LITERAL); return; case SoyFileParserConstants.XXX_CMD_TEXT_PHNAME_NOT_IDENT: reporter.report(location, BAD_PHNAME_VALUE); return; case SoyFileParserConstants.DECL_BEGIN_PARAM: case SoyFileParserConstants.DECL_BEGIN_OPT_PARAM: case SoyFileParserConstants.DECL_BEGIN_INJECT_PARAM: case SoyFileParserConstants.DECL_BEGIN_OPT_INJECT_PARAM: reporter.report(location, UNEXPECTED_PARAM_DECL); return; case SoyFileParserConstants.LEGACY_AND: reporter.report(location, LEGACY_AND_ERROR); return; case SoyFileParserConstants.LEGACY_OR: reporter.report(location, LEGACY_OR_ERROR); return; case SoyFileParserConstants.LEGACY_NOT: reporter.report(location, LEGACY_NOT_ERROR); return; case SoyFileParserConstants.DOUBLE_QUOTE: reporter.report(location, LEGACY_DOUBLE_QUOTED_STRING); return; default: //fall-through } ImmutableSet.Builder<String> expectedTokenImages = ImmutableSet.builder(); for (int[] expected : e.expectedTokenSequences) { // We only display the first token of any expected sequence expectedTokenImages.add(getSoyFileParserTokenDisplayName(expected[0])); } reporter.report( location, SoyErrorKind.of("{0}"), formatParseExceptionDetails(errorToken.image, expectedTokenImages.build().asList())); } /** * Returns a human friendly display name for tokens. By default we use the generated token image * which is appropriate for literal tokens. */ private static String getSoyFileParserTokenDisplayName(int tokenId) { switch (tokenId) { case SoyFileParserConstants.ATTRIBUTE_VALUE: return "attribute-value"; // File-level tokens: case SoyFileParserConstants.DELTEMPLATE_OPEN: return "{deltemplate"; case SoyFileParserConstants.TEMPLATE_OPEN: return "{template"; // Template tokens: case SoyFileParserConstants.CMD_BEGIN_CALL: return "{call"; case SoyFileParserConstants.CMD_CLOSE_CALL: return "{/call}"; case SoyFileParserConstants.CMD_BEGIN_DELCALL: return "{delcall"; case SoyFileParserConstants.CMD_CLOSE_DELCALL: return "{/delcall}"; case SoyFileParserConstants.NAME: case SoyFileParserConstants.T_NAME: return "identifier"; case SoyFileParserConstants.EOF: return "eof"; // TODO(slaks): Gather all CMD_BEGIN* constants using Reflection & string manipulation? case SoyFileParserConstants.CMD_BEGIN_PARAM: return "{param"; case SoyFileParserConstants.CMD_BEGIN_MSG: return "{msg"; case SoyFileParserConstants.CMD_BEGIN_FALLBACK_MSG: return "{fallbackmsg"; case SoyFileParserConstants.CMD_BEGIN_PRINT: return "{print"; case SoyFileParserConstants.CMD_BEGIN_XID: return "{xid"; case SoyFileParserConstants.CMD_BEGIN_CSS: return "{css"; case SoyFileParserConstants.CMD_BEGIN_IF: return "{if"; case SoyFileParserConstants.CMD_BEGIN_ELSEIF: return "{elseif"; case SoyFileParserConstants.CMD_BEGIN_LET: return "{let"; case SoyFileParserConstants.CMD_BEGIN_FOR: return "{for"; case SoyFileParserConstants.CMD_BEGIN_PLURAL: return "{plural"; case SoyFileParserConstants.CMD_BEGIN_SELECT: return "{select"; case SoyFileParserConstants.CMD_BEGIN_SWITCH: return "{switch"; case SoyFileParserConstants.CMD_BEGIN_CASE: return "{case"; case SoyFileParserConstants.CMD_BEGIN_FOREACH: return "{foreach"; case SoyFileParserConstants.CMD_OPEN_LITERAL: return "{literal"; case SoyFileParserConstants.CMD_FULL_SP: case SoyFileParserConstants.CMD_FULL_NIL: case SoyFileParserConstants.CMD_FULL_CR: case SoyFileParserConstants.CMD_FULL_LF: case SoyFileParserConstants.CMD_FULL_TAB: case SoyFileParserConstants.CMD_FULL_LB: case SoyFileParserConstants.CMD_FULL_RB: case SoyFileParserConstants.TOKEN_NOT_WS: return "text"; case SoyFileParserConstants.TOKEN_WS: return "whitespace"; case SoyFileParserConstants.HEX_INTEGER: case SoyFileParserConstants.DEC_INTEGER: case SoyFileParserConstants.FLOAT: return "number"; case SoyFileParserConstants.STRING: return "string"; case SoyFileParserConstants.IDENT: return "an identifier"; case SoyFileParserConstants.DOLLAR_IDENT: return "variable"; case SoyFileParserConstants.UNEXPECTED_TOKEN: throw new AssertionError("we should never expect the unexpected token"); default: return SoyFileParserConstants.tokenImage[tokenId]; } } static void report( ErrorReporter reporter, String filePath, LegacyInternalSyntaxException exception) { SourceLocation sourceLocation = exception.getSourceLocation(); if (!sourceLocation.isKnown()) { sourceLocation = new SourceLocation(filePath); } reporter.report(sourceLocation, SoyErrorKind.of("{0}"), exception.getOriginalMessage()); } static void reportTokenMgrError( ErrorReporter reporter, String filePath, TokenMgrError exception) { // If the file is terminated in the middle of an attribute value or a multiline comment a // TokenMgrError will be thrown (due to our use of MORE productions). The only way to tell is // to test the message for "<EOF>". The suggested workaround for this is to submit the // generated TokenMgrError code into source control and rewrite the constructor. This would // also allow us to avoid using a regex to extract line number information. String message = exception.getMessage(); if (exception.errorCode == TokenMgrError.LEXICAL_ERROR && message.contains("<EOF>")) { Matcher line = EXTRACT_LOCATION.matcher(message); if (line.find()) { int startLine = Integer.parseInt(line.group(1)); // javacc's column numbers are 0-based, while Soy's are 1-based int column = Integer.parseInt(line.group(2)) + 1; reporter.report( new SourceLocation(filePath, startLine, column, startLine, column), UNEXPECTED_EOF); return; } } reporter.report(new SourceLocation(filePath), UNEXPECTED_TOKEN_MGR_ERROR, message); } }