// 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.autocomplete.codegraph;
import static com.google.collide.codemirror2.Token.LITERAL_PERIOD;
import static com.google.collide.codemirror2.TokenType.NULL;
import static com.google.collide.codemirror2.TokenType.REGEXP;
import static com.google.collide.codemirror2.TokenType.STRING;
import static com.google.collide.codemirror2.TokenType.VARIABLE;
import static com.google.collide.codemirror2.TokenType.VARIABLE2;
import static com.google.collide.codemirror2.TokenType.WHITESPACE;
import com.google.collide.client.documentparser.DocumentParser;
import com.google.collide.client.documentparser.ParseResult;
import com.google.collide.codemirror2.State;
import com.google.collide.codemirror2.Token;
import com.google.collide.codemirror2.TokenType;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.base.Preconditions;
import javax.annotation.Nonnull;
/**
* Set of utilities to perform code parsing and parse results processing.
*/
public class ParseUtils {
private static final String SPACE = " ";
private static final String[] CONTEXT_START = new String[] {"[", "(", "{"};
private static final String SIMPLE_CONTEXT_END = "])";
private static final String CONTEXT_END = SIMPLE_CONTEXT_END + "}";
/**
* Collect ids interleaved with periods, omitting parenthesis groups.
*
* @param tokens source tokens array; destroyed in runtime.
* @param expectingPeriod state before parsing:
* {@code true} if period token is expected
* @param contextParts output collector; only ids are collected
* @return state after parsing: {@code true} if period token is expected
*/
static boolean buildInvocationSequenceContext(
JsonArray<Token> tokens, boolean expectingPeriod, JsonArray<String> contextParts) {
// right-to-left tokens processing loop.
while (!tokens.isEmpty()) {
Token lastToken = tokens.pop();
TokenType lastTokenType = lastToken.getType();
String lastTokenValue = lastToken.getValue();
// Omit whitespaces.
if (lastTokenType == WHITESPACE) {
continue;
}
if (expectingPeriod) {
// If we are expecting period, then no other tokens are allowed.
if ((lastTokenType != NULL) || !LITERAL_PERIOD.equals(lastTokenValue)) {
return expectingPeriod;
}
expectingPeriod = false;
} else {
// Not expecting period means that previously processed token (located
// to the right of the current token) was not id.
// That way we expect id or parenthesis group.
if (lastTokenType == VARIABLE || lastTokenType == VARIABLE2
|| lastTokenType == TokenType.PROPERTY) {
contextParts.add(lastTokenValue);
// Period is obligatory to the left of id to continue the chain.
expectingPeriod = true;
} else if ((lastTokenType == NULL) && (lastTokenValue.length() == 1)
&& SIMPLE_CONTEXT_END.contains(lastTokenValue)) {
// We are to enter parenthesis group.
if (!bypassParenthesizedGroup(tokens, lastToken)) {
// If we were unable to properly close group - exit
return expectingPeriod;
}
// After group is closed, we again expect id or parenthesis group.
} else {
// Token type we don't expect - exit
return expectingPeriod;
}
}
}
return expectingPeriod;
}
/**
* Pops tokens until context closed with lastToken is not opened,
* or inconsistency found.
*
* @return {@code true} if context was successfully removed from tokens.
*/
static boolean bypassParenthesizedGroup(JsonArray<Token> tokens, Token lastToken) {
JsonArray<String> stack = JsonCollections.createArray();
// Push char that corresponds to opening parenthesis.
stack.add(CONTEXT_START[(CONTEXT_END.indexOf(lastToken.getValue()))]);
while (!tokens.isEmpty()) {
lastToken = tokens.pop();
String lastTokenValue = lastToken.getValue();
// Bypass non-parenthesis.
if (lastToken.getType() != NULL || (lastTokenValue.length() != 1)) {
continue;
}
// Dive deeper.
if (CONTEXT_END.contains(lastTokenValue)) {
stack.add(CONTEXT_START[(CONTEXT_END.indexOf(lastToken.getValue()))]);
continue;
}
// Check if token corresponds to stack head
if (CONTEXT_START[0].equals(lastTokenValue)
|| CONTEXT_START[1].equals(lastTokenValue) || CONTEXT_START[2].equals(lastTokenValue)) {
if (!stack.peek().equals(lastTokenValue)) {
// Got opening parenthesis not matching stack -> exit with error.
return false;
}
stack.pop();
// If initial group is closed - we've finished.
if (stack.isEmpty()) {
return true;
}
}
}
return false;
}
/**
* Types of situations.
*
* <p>When expanding line with letters leads to expanding last token,
* it means that token is not finished. Strings and comments have such
* behaviour ({@link #IN_STRING}, {@link #IN_COMMENT}).
*
* <p>When parse result is {@code null}, then no further analysis can be done
* ({@link #NOT_PARSED}).
*
* <p>Otherwise (the most common and interesting situation) we suppose to be
* somewhere in code ({@link #IN_CODE}).
*/
public enum Context {
IN_STRING, IN_COMMENT, NOT_PARSED, IN_CODE
}
/**
* Bean that wraps together {@link ParseResult} and {@link Context}.
*
* @param <T> language-specific {@link State} type.
*/
public static class ExtendedParseResult<T extends State> {
private final ParseResult<T> parseResult;
private final Context context;
public ExtendedParseResult(ParseResult<T> parseResult, Context context) {
this.parseResult = parseResult;
this.context = context;
}
/**
* @return {@code null} if parsing is failed, or token list is empty.
*/
public String getLastTokenValue() {
if (parseResult == null) {
return null;
}
JsonArray<Token> tokens = parseResult.getTokens();
if (tokens.isEmpty()) {
return null;
}
Token lastToken = tokens.peek();
return lastToken.getValue();
}
ParseResult<T> getParseResult() {
return parseResult;
}
Context getContext() {
return context;
}
}
/**
* Parses the line to specified position and returns parse result.
*
* @param parser current document parser
* @param position point of interest
*/
public static <T extends State> ExtendedParseResult<T> getExtendedParseResult(
Class<T> stateClass, @Nonnull DocumentParser parser, Position position) {
int column = position.getColumn();
String text = position.getLine().getText().substring(0, column);
// Add space if we are not sure that comment/literal is finished
boolean addSpace = (column == 0)
|| text.endsWith("*/")
|| text.endsWith("'") || text.endsWith("\"");
ParseResult<T> result = parser.getState(stateClass, position, addSpace ? SPACE : null);
if (result == null) {
return new ExtendedParseResult<T>(null, Context.NOT_PARSED);
}
JsonArray<Token> tokens = result.getTokens();
Token lastToken = tokens.peek();
Preconditions.checkNotNull(lastToken,
"Last token expected to be non-null; text='%s', position=%s", text, position);
TokenType lastTokenType = lastToken.getType();
String lastTokenValue = lastToken.getValue();
if (!addSpace) {
if (lastTokenType == STRING || lastTokenType == REGEXP) {
return new ExtendedParseResult<T>(result, Context.IN_STRING);
} else if (lastTokenType == TokenType.COMMENT) {
return new ExtendedParseResult<T>(result, Context.IN_COMMENT);
}
// Python parser, for a purpose of simplicity, parses period and variable
// name as a single token. If period is not followed by identifier, parser
// states that this is and error, which is, generally, not truth.
if ((lastTokenType == TokenType.ERROR) && LITERAL_PERIOD.equals(lastTokenValue)) {
tokens.pop();
tokens.add(new Token(lastToken.getMode(), TokenType.NULL, LITERAL_PERIOD));
}
return new ExtendedParseResult<T>(result, Context.IN_CODE);
}
// Remove / shorten last token to omit added whitespace.
tokens.pop();
if (lastTokenType == STRING || lastTokenType == REGEXP || lastTokenType == TokenType.COMMENT) {
// Whitespace stuck to token - strip it.
lastTokenValue = lastTokenValue.substring(0, lastTokenValue.length() - 1);
tokens.add(new Token(lastToken.getMode(), lastTokenType, lastTokenValue));
if (lastTokenType == STRING || lastTokenType == REGEXP) {
return new ExtendedParseResult<T>(result, Context.IN_STRING);
} else {
return new ExtendedParseResult<T>(result, Context.IN_COMMENT);
}
}
// Otherwise whitespace was stated as a standalone token.
return new ExtendedParseResult<T>(result, Context.IN_CODE);
}
}