// 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.js; import com.google.collide.client.code.autocomplete.CodeAnalyzer; import com.google.collide.codemirror2.Token; import com.google.collide.codemirror2.TokenType; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.TaggableLine; import javax.annotation.Nonnull; /** * JavaScript specific code analyzer. * * <p>This class calculates scope for each line of code. */ public class JsIndexUpdater implements CodeAnalyzer { public static final String TAG_SCOPE = JsIndexUpdater.class.getName() + ".scope"; private static final String TAG_STATE = JsIndexUpdater.class.getName() + ".state"; /** * Tag for storing incomplete scope name. */ private static final String TAG_NAME = JsIndexUpdater.class.getName() + ".sName"; private static final String LITERAL_FUNCTION = "function"; private static final String LITERAL_PERIOD = "."; private static final String LITERAL_ASSIGN = "="; private static final String LITERAL_COLON = ":"; private static final String LITERAL_OPEN_BRACKET = "("; private static final String LITERAL_CLOSE_BRACKET = ")"; private static final String LITERAL_COMMA = ","; private static final String LITERAL_OPEN_CURLY = "{"; private static final String LITERAL_CLOSE_CURLY = "}"; /** * Checks if the token describes some name (variable, function) or * name part (property). */ private static boolean isName(TokenType type) { return TokenType.VARIABLE == type || TokenType.VARIABLE2 == type || TokenType.DEF == type || TokenType.PROPERTY == type; } private static boolean isFunctionKeyword(TokenType tokenType, String tokenValue) { return TokenType.KEYWORD == tokenType && LITERAL_FUNCTION.equals(tokenValue); } /** * Enumeration of situations that may occur during scope parsing. */ private enum State { /** * Without particular context. */ NONE, /** * Keyword "function" met, expecting name. */ FUNCTION, /** * Context with intention to define named function. * * <p>Expecting "(" */ NAMED_FUNCTION, /** * Inside params definition. */ FUNCTION_PARAMS, /** * Function name and params are known, expecting block start. */ FULLY_QUALIFIED, /** * (Variable) name met. Expecting "=", ":", or "." */ NAME, /** * Got something like "name1.", expecting next name. */ SUBNAME, /** * Got something like "name1.name2 =", or "name1:". * * <p>Expecting "function" keyword. */ ASSIGN, } /** * Bean that holds line-parsing context. */ public static class Context { private final State state; private final String name; private final JsCodeScope scope; /** * Constructs context from saved values. */ public Context(TaggableLine line) { scope = line.getTag(TAG_SCOPE); name = line.getTag(TAG_NAME); State tempState = line.getTag(TAG_STATE); if (tempState == null) { tempState = State.NONE; } state = tempState; } private Context(State state, String name, JsCodeScope scope) { this.state = state; this.name = name; this.scope = scope; } private void saveToLine(TaggableLine line) { line.putTag(TAG_SCOPE, scope); line.putTag(TAG_NAME, name); line.putTag(TAG_STATE, state); } public JsCodeScope getScope() { return scope; } } /** * Calculates updated context based on previous line context and tokens. * * <p>Note: currently we do not create root scope, so {@code null} scope * corresponds to root. */ public static Context calculateContext(TaggableLine previousLine, JsonArray<Token> tokens) { Context context = new Context(previousLine); State state = context.state; String name = context.name; JsCodeScope scope = context.scope; int size = tokens.size(); int index = 0; while (index < size) { Token token = tokens.get(index); index++; TokenType tokenType = token.getType(); if (TokenType.WHITESPACE == tokenType || TokenType.NEWLINE == tokenType || TokenType.COMMENT == tokenType) { // TODO: Parse JsDocs. continue; } String tokenValue = token.getValue(); switch (state) { case NONE: if (isFunctionKeyword(tokenType, tokenValue)) { state = State.FUNCTION; } else if (isName(tokenType)) { name = tokenValue; state = State.NAME; } else if (LITERAL_OPEN_CURLY.equals(tokenValue)) { scope = new JsCodeScope(scope, null); name = null; } else if (LITERAL_CLOSE_CURLY.equals(tokenValue)) { if (scope != null) { scope = scope.getParent(); } } break; case FUNCTION: if (isName(tokenType)) { name = tokenValue; state = State.NAMED_FUNCTION; } else { index--; name = null; state = State.NONE; } break; case NAMED_FUNCTION: if (LITERAL_OPEN_BRACKET.equals(tokenValue)) { state = State.FUNCTION_PARAMS; } else { index--; name = null; state = State.NONE; } break; case FUNCTION_PARAMS: if (LITERAL_CLOSE_BRACKET.equals(tokenValue)) { state = State.FULLY_QUALIFIED; } else if (isName(tokenType) || LITERAL_COMMA.equals(tokenValue)) { // Do nothing. } else { index--; name = null; state = State.NONE; } break; case FULLY_QUALIFIED: if (LITERAL_OPEN_CURLY.equals(tokenValue)) { scope = new JsCodeScope(scope, name); name = null; state = State.NONE; } else { index--; name = null; state = State.NONE; } break; case NAME: if (LITERAL_PERIOD.equals(tokenValue)) { state = State.SUBNAME; } else if (LITERAL_ASSIGN.equals(tokenValue) || LITERAL_COLON.equals(tokenValue)) { state = State.ASSIGN; } else { index--; name = null; state = State.NONE; } break; case SUBNAME: if (isName(tokenType)) { name = name + "." + tokenValue; state = State.NAME; } else { index--; name = null; state = State.NONE; } break; case ASSIGN: if (isFunctionKeyword(tokenType, tokenValue)) { state = State.NAMED_FUNCTION; } else if (LITERAL_OPEN_CURLY.equals(tokenValue)) { scope = new JsCodeScope(scope, name); name = null; state = State.NONE; } else { index--; name = null; state = State.NONE; } break; default: throw new IllegalStateException("Unexpected state [" + state + "]"); } } return new Context(state, name, scope); } @Override public void onBeforeParse() { } @Override public void onParseLine( TaggableLine previousLine, TaggableLine line, @Nonnull JsonArray<Token> tokens) { calculateContext(previousLine, tokens).saveToLine(line); } @Override public void onAfterParse() { } @Override public void onLinesDeleted(JsonArray<TaggableLine> deletedLines) { } }