org.fife.rsta.ac.js.resolver.JavaScriptCompletionResolver Maven / Gradle / Ivy
package org.fife.rsta.ac.js.resolver;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import org.fife.rsta.ac.java.classreader.ClassFile;
import org.fife.rsta.ac.js.JavaScriptHelper;
import org.fife.rsta.ac.js.JavaScriptParser;
import org.fife.rsta.ac.js.Logger;
import org.fife.rsta.ac.js.SourceCompletionProvider;
import org.fife.rsta.ac.js.ast.JavaScriptFunctionDeclaration;
import org.fife.rsta.ac.js.ast.jsType.JavaScriptType;
import org.fife.rsta.ac.js.ast.type.TypeDeclaration;
import org.fife.rsta.ac.js.completion.JSCompletion;
import org.fife.rsta.ac.js.completion.JSMethodData;
import org.mozilla.javascript.CompilerEnvirons;
import org.mozilla.javascript.Parser;
import org.mozilla.javascript.Token;
import org.mozilla.javascript.ast.AstNode;
import org.mozilla.javascript.ast.AstRoot;
import org.mozilla.javascript.ast.ExpressionStatement;
import org.mozilla.javascript.ast.FunctionCall;
import org.mozilla.javascript.ast.Name;
import org.mozilla.javascript.ast.NodeVisitor;
import org.mozilla.javascript.ast.PropertyGet;
/**
* Compiles the entered text using Rhino and tries to resolve the JavaScriptType
* from the AstRoot e.g.
*
* var a = ""; "" --> String JavaScriptType var b =
* a.toString() a.toString --> String JavaScriptType
*
* etc.
*
* Note, will resolve any type added to JavaScriptTypesFactory
*
*/
public class JavaScriptCompletionResolver extends JavaScriptResolver {
protected JavaScriptType lastJavaScriptType;
protected String lastLookupName = null;
/**
* Standard ECMA JavaScript resolver
* @param provider
*/
public JavaScriptCompletionResolver(SourceCompletionProvider provider) {
super(provider);
}
/**
* Compiles Text and resolves the type.
* e.g.
* "Hello World".length; //resolve as a Number
*
* @param text to compile and resolve
*/
@Override
public JavaScriptType compileText(String text) {
CompilerEnvirons env = JavaScriptParser.createCompilerEnvironment(new JavaScriptParser.JSErrorReporter(), provider.getLanguageSupport());
String parseText = JavaScriptHelper.removeLastDotFromText(text);
int charIndex = JavaScriptHelper.findIndexOfFirstOpeningBracket(parseText);
env.setRecoverFromErrors(true);
Parser parser = new Parser(env);
AstRoot root = parser.parse(parseText, null, 0);
CompilerNodeVisitor visitor = new CompilerNodeVisitor(charIndex == 0);
root.visitAll(visitor);
return lastJavaScriptType;
}
/**
* Resolve node type to TypeDeclaration. Called instead of #compileText(String text) when document is already parsed
* @param text The node to resolve
* @return TypeDeclaration for node or null if not found.
*/
@Override
public TypeDeclaration resolveParamNode(String text) {
if(text != null) {
CompilerEnvirons env = JavaScriptParser.createCompilerEnvironment(new JavaScriptParser.JSErrorReporter(), provider.getLanguageSupport());
int charIndex = JavaScriptHelper.findIndexOfFirstOpeningBracket(text);
env.setRecoverFromErrors(true);
Parser parser = new Parser(env);
AstRoot root = parser.parse(text, null, 0);
CompilerNodeVisitor visitor = new CompilerNodeVisitor(charIndex == 0);
root.visitAll(visitor);
}
return lastJavaScriptType != null ? lastJavaScriptType.getType()
: provider.getTypesFactory().getDefaultTypeDeclaration();
}
/**
* Resolve node type to TypeDeclaration. Called instead of #compileText(String text) when document is already parsed
* @param node AstNode to resolve
* @return TypeDeclaration for node or null if not found.
*/
@Override
public TypeDeclaration resolveNode(AstNode node) {
if(node == null) return provider.getTypesFactory().getDefaultTypeDeclaration();
CompilerNodeVisitor visitor = new CompilerNodeVisitor(true);
node.visit(visitor);
return lastJavaScriptType != null ? lastJavaScriptType.getType()
: provider.getTypesFactory().getDefaultTypeDeclaration();
}
/**
* Resolve node type to TypeDeclaration
* N.B called from CompilerNodeVisitor.visit()
*
* @param node AstNode to resolve
* @return TypeDeclaration for node or null if not found.
*/
@Override
protected TypeDeclaration resolveNativeType(AstNode node)
{
TypeDeclaration dec = JavaScriptHelper.tokenToNativeTypeDeclaration(node, provider);
if(dec == null) {
dec = testJavaStaticType(node);
}
return dec;
}
/**
* Test whether the node can be resolved as a static Java class.
* Only looks for Token.NAME nodes to test
* @param node node to test
* @return The type declaration.
*/
protected TypeDeclaration testJavaStaticType(AstNode node) {
switch (node.getType()) {
case Token.NAME:
return findJavaStaticType(node);
}
return null;
}
/**
* Try to resolve the Token.NAME AstNode and return a TypeDeclaration
* @param node node to resolve
* @return TypeDeclaration if the name can be resolved as a Java Class else null
*/
protected TypeDeclaration findJavaStaticType(AstNode node) {
// check parent is of type property get
String testName = node.toSource();
if (testName != null) {
TypeDeclaration dec = JavaScriptHelper.getTypeDeclaration(testName, provider);
if(dec != null)
{
ClassFile cf = provider.getJavaScriptTypesFactory().getClassFile(
provider.getJarManager(), dec);
if (cf != null) {
return provider.getJavaScriptTypesFactory()
.createNewTypeDeclaration(cf, true, false);
}
}
}
return null;
}
// TODO not sure how right this is, but is very tricky problem resolving
// complex completions
private class CompilerNodeVisitor implements NodeVisitor {
private boolean ignoreParams;
private HashSet paramNodes = new HashSet<>();
private CompilerNodeVisitor(boolean ignoreParams) {
this.ignoreParams = ignoreParams;
}
@Override
public boolean visit(AstNode node) {
Logger.log(JavaScriptHelper.convertNodeToSource(node));
Logger.log(node.shortName());
if(!validNode(node))
{
//invalid node found, set last completion invalid and stop processing
lastJavaScriptType = null;
return false;
}
if (ignore(node, ignoreParams))
return true;
JavaScriptType jsType;
TypeDeclaration dec;
//only resolve native type if last type is null
//otherwise it can be assumed that this is part of multi depth - e.g. "".length.toString()
if(lastJavaScriptType == null) {
dec = resolveNativeType(node);
if(dec == null && node.getType() == Token.NAME) {
lastJavaScriptType = null;
return false;
}
}
else {
dec = resolveTypeFromLastJavaScriptType(node);
}
if (dec != null) {
// lookup JavaScript completions type
jsType = provider.getJavaScriptTypesFactory().getCachedType(
dec, provider.getJarManager(), provider,
JavaScriptHelper.convertNodeToSource(node));
if (jsType != null) {
lastJavaScriptType = jsType;
// stop here
return false;
}
}
else if (lastJavaScriptType != null) {
if (node.getType() == Token.NAME) {
// lookup from source name
jsType = lookupFromName(node, lastJavaScriptType);
if (jsType == null) {
// lookup name through the functions of
// lastJavaScriptType
jsType = lookupFunctionCompletion(node, lastJavaScriptType);
}
lastJavaScriptType = jsType;
}
}
else if(node instanceof FunctionCall)
{
FunctionCall fn = (FunctionCall) node;
String lookupText = createLookupString(fn);
JavaScriptFunctionDeclaration funcDec = provider.getVariableResolver().findFunctionDeclaration(lookupText);
if(funcDec != null)
{
jsType = provider.getJavaScriptTypesFactory().getCachedType(
funcDec.getTypeDeclaration(), provider.getJarManager(), provider,
JavaScriptHelper.convertNodeToSource(node));
if (jsType != null) {
lastJavaScriptType = jsType;
// stop here
return false;
}
}
}
return true;
}
private boolean validNode(AstNode node)
{
switch(node.getType())
{
case Token.NAME: return ((Name) node).getIdentifier() != null && ((Name) node).getIdentifier().length() > 0;
}
return true;
}
private String createLookupString(FunctionCall fn)
{
StringBuilder sb = new StringBuilder();
String name = "";
switch(fn.getTarget().getType())
{
case Token.NAME : name = ((Name) fn.getTarget()).getIdentifier();
break;
}
sb.append(name);
sb.append("(");
Iterator i = fn.getArguments().iterator();
while (i.hasNext())
{
i.next();
sb.append("p");
if(i.hasNext())
sb.append(",");
}
sb.append(")");
return sb.toString();
}
/**
* Test node to check whether to ignore resolving, this is for
* parameters
*
* @param node node to test
* @return true to ignore
*/
private boolean ignore(AstNode node, boolean ignoreParams) {
switch (node.getType()) {
// ignore errors e.g. if statement - if(a. //no closing brace
case Token.EXPR_VOID:
case Token.EXPR_RESULT:
return ((ExpressionStatement) node).getExpression()
.getType() == Token.ERROR;
case Token.ERROR:
case Token.GETPROP:
case Token.SCRIPT:
return true;
default: {
if (isParameter(node)) {
collectAllNodes(node); // everything within this node
// is a parameter
return ignoreParams;
}
break;
}
}
//if (JavaScriptHelper.isInfixOnly(node))
// return true;
return false;
}
/**
* Get all nodes within AstNode and add to an ArrayList
*
* @param node
*/
private void collectAllNodes(AstNode node) {
if (node.getType() == Token.CALL) {
// collect all argument nodes
FunctionCall call = (FunctionCall) node;
for (AstNode arg : call.getArguments()) {
VisitorAll all = new VisitorAll();
arg.visit(all);
paramNodes.addAll(all.getAllNodes());
}
}
}
/**
* Check the function that a name may belong to contains this actual
* parameter
*
* @param node Node to check
* @return true if the function contains the parameter
*/
private boolean isParameter(AstNode node) {
if (paramNodes.contains(node))
return true;
// get all params from this function too
FunctionCall fc = JavaScriptHelper.findFunctionCallFromNode(node);
if (fc != null && !(node == fc)) {
collectAllNodes(fc);
if (paramNodes.contains(node)) {
return true;
}
}
return false;
}
}
/**
* Lookup the name of the node within the last JavaScript type. e.g. var a =
* 1; var b = a.MAX_VALUE; looks up MAX_VALUE within NumberLiteral a where a
* is resolve before as a JavaScript Number;
*
* @param node
* @param lastJavaScriptType
* @return The type.
*/
protected JavaScriptType lookupFromName(AstNode node,
JavaScriptType lastJavaScriptType) {
JavaScriptType javaScriptType = null;
if (lastJavaScriptType != null) {
String lookupText = null;
switch (node.getType()) {
case Token.NAME:
lookupText = ((Name) node).getIdentifier();
break;
}
if (lookupText == null) {
// just try the source
lookupText = node.toSource();
}
javaScriptType = lookupJavaScriptType(lastJavaScriptType,
lookupText);
}
return javaScriptType;
}
/**
* Lookup the function name of the node within the last JavaScript type. e.g.
* var a = ""; var b = a.toString(); looks up toString() within
* StringLiteral a where a is resolve before as a JavaScript String;
*
* @param node
* @param lastJavaScriptType
* @return The type.
*/
protected JavaScriptType lookupFunctionCompletion(AstNode node,
JavaScriptType lastJavaScriptType) {
JavaScriptType javaScriptType = null;
if (lastJavaScriptType != null) {
String lookupText = JavaScriptHelper.getFunctionNameLookup(node, provider);
javaScriptType = lookupJavaScriptType(lastJavaScriptType,
lookupText);
}
// return last type
return javaScriptType;
}
@Override
public String getLookupText(JSMethodData method, String name) {
StringBuilder sb = new StringBuilder(name);
sb.append('(');
int count = method.getParameterCount();
for (int i = 0; i < count; i++) {
sb.append("p");
if (i < count - 1) {
sb.append(",");
}
}
sb.append(')');
return sb.toString();
}
@Override
public String getFunctionNameLookup(FunctionCall call,
SourceCompletionProvider provider) {
if (call != null) {
StringBuilder sb = new StringBuilder();
if (call.getTarget() instanceof PropertyGet) {
PropertyGet get = (PropertyGet) call.getTarget();
sb.append(get.getProperty().getIdentifier());
}
sb.append("(");
int count = call.getArguments().size();
for (int i = 0; i < count; i++) {
sb.append("p");
if (i < count - 1) {
sb.append(",");
}
}
sb.append(")");
return sb.toString();
}
return null;
}
private JavaScriptType lookupJavaScriptType(
JavaScriptType lastJavaScriptType, String lookupText) {
JavaScriptType javaScriptType = null;
if (lookupText != null && !lookupText.equals(lastLookupName)) {
// look up JSCompletion
JSCompletion completion = lastJavaScriptType
.getCompletion(lookupText, provider);
if (completion != null) {
String type = completion.getType(true);
if (type != null) {
TypeDeclaration newType = provider.getTypesFactory()
.getTypeDeclaration(type);
if (newType != null) {
javaScriptType = provider.getJavaScriptTypesFactory()
.getCachedType(newType,
provider.getJarManager(), provider,
lookupText);
}
else {
javaScriptType = createNewTypeDeclaration(provider,
type, lookupText);
}
}
}
}
lastLookupName = lookupText;
return javaScriptType;
}
/**
* Creates a new JavaScriptType based on the String type
* @param provider SourceCompletionProvider
* @param type type of JavaScript type to create e.g. java.sql.Connection
* @param text Text entered from the user to resolve the node. This will be null if resolveNode(AstNode node) is called
* @return
*/
private JavaScriptType createNewTypeDeclaration(
SourceCompletionProvider provider, String type, String text) {
if (provider.getJavaScriptTypesFactory() != null) {
ClassFile cf = provider.getJarManager().getClassEntry(type);
TypeDeclaration newType;
if (cf != null) {
newType = provider.getJavaScriptTypesFactory()
.createNewTypeDeclaration(cf, false);
return provider.getJavaScriptTypesFactory()
.getCachedType(newType, provider.getJarManager(),
provider, text);
}
}
return null;
}
/**
* Method called if the lastJavaScriptType is not null. i.e. has gone through one iteration at least.
* Resolves TypeDeclaration for parts of a variable past the first part. e.g. "".toString() //resolve toString()
* In some circumstances this is useful to resolve this. e.g. for Custom Object completions
* @param node Node to resolve
* @return Type Declaration
*
*/
protected TypeDeclaration resolveTypeFromLastJavaScriptType(AstNode node) {
return null;
}
/**
* Visit all nodes in the AstNode tree and all to a single list
*/
private static class VisitorAll implements NodeVisitor {
private ArrayList all = new ArrayList<>();
@Override
public boolean visit(AstNode node) {
all.add(node);
return true;
}
public ArrayList getAllNodes() {
return all;
}
}
}