// Copyright 2012 Google Inc. All Rights Reserved. // // 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.collide.client.code.debugging; import com.google.collide.client.documentparser.DocumentParser; import com.google.collide.client.documentparser.ParseResult; import com.google.collide.codemirror2.JsState; import com.google.collide.codemirror2.Token; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.document.LineInfo; import com.google.collide.shared.document.Position; import com.google.collide.shared.util.StringUtils; import com.google.common.annotations.VisibleForTesting; import javax.annotation.Nullable; /** * Encapsulates an algorithm to find a shortest evaluable JavaScript expression * by a given position in the text. * */ class EvaluableExpressionFinder { /** * Represents the result of the evaluable expression search. */ interface Result { /** * @return column of the first expression's character (inclusive) */ int getStartColumn(); /** * @return column of the last expression's character (inclusive) */ int getEndColumn(); /** * @return the expression found */ String getExpression(); } /** * @see #find(LineInfo, int, DocumentParser) */ @VisibleForTesting Result find(String text, int column) { if (column < 0 || column >= text.length()) { return null; } int left = -1; int right = -1; char ch = text.charAt(column); // A special case of pointing to a quote character that is next to a square bracket. if (StringUtils.isQuote(ch)) { if (column > 0 && text.charAt(column - 1) == '[') { ch = '['; --column; } else if (column + 1 < text.length() && text.charAt(column + 1) == ']') { ch = ']'; ++column; } else { return null; } } if (ch == '.' || isValidCharacterForJavaScriptName(ch)) { left = expandLeftBorder(text, column); right = expandRightBorder(text, column); } else if (ch == '[') { right = expandRightBracket(text, column); if (right != -1) { left = expandLeftBorder(text, column); } } else if (ch == ']') { right = column; left = expandLeftBracket(text, column); if (left != -1) { left = expandLeftBorder(text, left); } } // A special case of pointing to a numeric array index inside square brackets. if (left != -1 && right != -1 && isOnlyDigits(text, left, right)) { if (left > 0 && text.charAt(left - 1) == '[' && right + 1 < text.length() && text.charAt(right + 1) == ']') { left = expandLeftBorder(text, left - 1); ++right; } else { return null; } } if (left != -1 && right != -1) { final int startColumn = left; final int endColumn = right; final String expression = text.substring(left, right + 1); return new Result() { @Override public int getStartColumn() { return startColumn; } @Override public int getEndColumn() { return endColumn; } @Override public String getExpression() { return expression; } }; } return null; } private static int expandLeftBorder(String text, int column) { while (column > 0) { char ch = text.charAt(column - 1); if (ch == '.' || isValidCharacterForJavaScriptName(ch)) { --column; } else if (ch == ']') { column = expandLeftBracket(text, column - 1); } else { break; } } return column; } private static int expandLeftBracket(String text, int column) { int bracketLevel = 1; for (--column; column >= 0; --column) { char ch = text.charAt(column); if (StringUtils.isQuote(ch)) { column = expandLeftQuote(text, column); if (column == -1) { return -1; } } else if (ch == ']') { ++bracketLevel; } else if (ch == '[') { --bracketLevel; if (bracketLevel == 0) { return column; } } else if (ch != '.' && !isValidCharacterForJavaScriptName(ch)) { return -1; } } return -1; } private static int expandLeftQuote(String text, int column) { char quote = text.charAt(column); if (!StringUtils.isQuote(quote)) { return -1; } for (--column; column >= 0; --column) { char ch = text.charAt(column); if (ch == quote) { // Check for escape chars. boolean escapeChar = false; for (int i = column - 1;; --i) { if (i >= 0 && text.charAt(i) == '\\') { escapeChar = !escapeChar; } else { if (!escapeChar) { return column; } column = i + 1; break; } } } } return -1; } private static int expandRightBorder(String text, int column) { while (column + 1 < text.length()) { char ch = text.charAt(column + 1); if (isValidCharacterForJavaScriptName(ch)) { ++column; } else { break; } } return column; } private static int expandRightBracket(String text, int column) { int bracketLevel = 1; for (++column; column < text.length(); ++column) { char ch = text.charAt(column); if (StringUtils.isQuote(ch)) { column = expandRightQuote(text, column); if (column == -1) { return -1; } } else if (ch == '[') { ++bracketLevel; } else if (ch == ']') { --bracketLevel; if (bracketLevel == 0) { return column; } } else if (ch != '.' && !isValidCharacterForJavaScriptName(ch)) { return -1; } } return -1; } private static int expandRightQuote(String text, int column) { char quote = text.charAt(column); if (!StringUtils.isQuote(quote)) { return -1; } boolean escapeChar = false; for (++column; column < text.length(); ++column) { char ch = text.charAt(column); if (ch == '\\') { escapeChar = !escapeChar; } else { if (ch == quote && !escapeChar) { return column; } escapeChar = false; } } return -1; } private static boolean isOnlyDigits(String text, int left, int right) { for (int i = left; i <= right; ++i) { if (!StringUtils.isNumeric(text.charAt(i))) { return false; } } return true; } private static boolean isValidCharacterForJavaScriptName(char ch) { return StringUtils.isAlphaNumOrUnderscore(ch) || ch == '$'; } /** * Finds a shortest evaluable JavaScript expression around a given position * in the document. * * @param lineInfo the line to examine * @param column the seed position to start with * @param parser document parser * @return a new instance of {@link Result}, or {@code null} if no expression * was found */ Result find(LineInfo lineInfo, int column, @Nullable DocumentParser parser) { Result result = find(lineInfo.line().getText(), column); if (result == null || parser == null) { return result; } // Use the parser information to determine if we are inside a comment // or a string or any other place that does not make sense to evaluate. Position endPosition = new Position(lineInfo, result.getEndColumn() + 1); ParseResult<JsState> parseResult = parser.getState(JsState.class, endPosition, null); if (parseResult == null) { return result; } JsonArray<Token> tokens = parseResult.getTokens(); Token lastToken = tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); if (lastToken != null) { switch (lastToken.getType()) { case ATOM: case COMMENT: case KEYWORD: case NUMBER: case STRING: case REGEXP: return null; } } return result; } }