/*******************************************************************************
* Copyright (c) 2012 NumberFour AG
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* NumberFour AG - initial API and Implementation (Alex Panchenko)
*******************************************************************************/
package org.eclipse.dltk.internal.javascript.parser.structure;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import org.eclipse.dltk.annotations.Nullable;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.core.ISourceNode;
import org.eclipse.dltk.internal.javascript.ti.JSDocSupport;
import org.eclipse.dltk.internal.javascript.ti.JSMethod;
import org.eclipse.dltk.internal.javascript.ti.JSVariable;
import org.eclipse.dltk.javascript.ast.AbstractNavigationVisitor;
import org.eclipse.dltk.javascript.ast.BinaryOperation;
import org.eclipse.dltk.javascript.ast.BooleanLiteral;
import org.eclipse.dltk.javascript.ast.CallExpression;
import org.eclipse.dltk.javascript.ast.DecimalLiteral;
import org.eclipse.dltk.javascript.ast.Expression;
import org.eclipse.dltk.javascript.ast.FunctionStatement;
import org.eclipse.dltk.javascript.ast.GetMethod;
import org.eclipse.dltk.javascript.ast.Identifier;
import org.eclipse.dltk.javascript.ast.JSDeclaration;
import org.eclipse.dltk.javascript.ast.JSNode;
import org.eclipse.dltk.javascript.ast.ObjectInitializer;
import org.eclipse.dltk.javascript.ast.ObjectInitializerPart;
import org.eclipse.dltk.javascript.ast.PropertyExpression;
import org.eclipse.dltk.javascript.ast.PropertyInitializer;
import org.eclipse.dltk.javascript.ast.ReturnStatement;
import org.eclipse.dltk.javascript.ast.Script;
import org.eclipse.dltk.javascript.ast.SetMethod;
import org.eclipse.dltk.javascript.ast.StringLiteral;
import org.eclipse.dltk.javascript.ast.ThisExpression;
import org.eclipse.dltk.javascript.ast.VariableDeclaration;
import org.eclipse.dltk.javascript.ast.VariableStatement;
import org.eclipse.dltk.javascript.ast.VoidExpression;
import org.eclipse.dltk.javascript.parser.JSParser;
import org.eclipse.dltk.javascript.parser.JSProblemReporter;
import org.eclipse.dltk.javascript.structure.FunctionDeclaration;
import org.eclipse.dltk.javascript.structure.FunctionExpression;
import org.eclipse.dltk.javascript.structure.FunctionNode;
import org.eclipse.dltk.javascript.structure.IDeclaration;
import org.eclipse.dltk.javascript.structure.IParentNode;
import org.eclipse.dltk.javascript.structure.IScope;
import org.eclipse.dltk.javascript.structure.IStructureContext;
import org.eclipse.dltk.javascript.structure.IStructureHandler;
import org.eclipse.dltk.javascript.structure.IStructureNode;
import org.eclipse.dltk.javascript.structure.IStructureRequestor;
import org.eclipse.dltk.javascript.structure.IStructureVisitor;
import org.eclipse.dltk.javascript.structure.ObjectDeclaration;
import org.eclipse.dltk.javascript.structure.PropertyDeclaration;
import org.eclipse.dltk.javascript.structure.ScriptScope;
import org.eclipse.dltk.javascript.structure.VariableNode;
import org.eclipse.dltk.javascript.typeinference.ReferenceLocation;
import org.eclipse.dltk.javascript.typeinfo.ITypeChecker;
import org.eclipse.dltk.javascript.typeinfo.ReferenceSource;
import org.eclipse.dltk.javascript.typeinfo.TypeInfoManager;
public class StructureReporter3 extends
AbstractNavigationVisitor<IStructureNode> implements IStructureVisitor {
private final ReferenceSource referenceSource;
private final Stack<IParentNode> parents = new Stack<IParentNode>();
private final Stack<List<JSDeclaration>> declarations = new Stack<List<JSDeclaration>>();
private final IStructureHandler[] handlers;
private JSDocSupport jsdocSupport = new JSDocSupport();
private final JSProblemReporter fReporter = null;
private final ITypeChecker fTypeChecker = null;
public StructureReporter3(ReferenceSource referenceSource) {
this.referenceSource = referenceSource;
final List<IStructureHandler> extensions = TypeInfoManager
.createExtensions(referenceSource, IStructureHandler.class,
this);
this.handlers = extensions.toArray(new IStructureHandler[extensions
.size()]);
}
@Override
public IStructureNode visit(ASTNode node) {
for (IStructureHandler handler : handlers) {
final IStructureNode value = handler.handle(node);
if (value != IStructureHandler.CONTINUE) {
return addToParent(value);
}
}
return addToParent(super.visit(node));
}
private IStructureNode addToParent(@Nullable final IStructureNode value) {
if (value != null) {
if (!parents.isEmpty()) {
final IParentNode parent = parents.peek();
parent.addToScope(value);
}
}
return value;
}
public void push(IParentNode declaration) {
parents.push(declaration);
}
public IParentNode pop() {
return parents.pop();
}
public IParentNode peek() {
return parents.peek();
}
@Override
public IStructureNode visitScript(Script node) {
push(new ScriptScope());
declarations.push(node.getDeclarations());
super.visitScript(node);
declarations.pop();
return pop();
}
@Override
public IStructureNode visitFunctionStatement(FunctionStatement node) {
final JSMethod method = new JSMethod(node, referenceSource);
jsdocSupport.processMethod(node, method, fReporter, fTypeChecker);
final FunctionNode functionNode;
if (node.isDeclaration()) {
functionNode = new FunctionDeclaration(peek(), node, method);
} else {
functionNode = new FunctionExpression(peek(), node, method);
}
method.setLocation(ReferenceLocation.create(referenceSource,
node.start(), node.end(), functionNode.getNameNode()));
functionNode.buildArgumentNodes();
push(functionNode);
declarations.push(node.getDeclarations());
super.visitFunctionStatement(node);
declarations.pop();
return pop();
}
@Override
public IStructureNode visitReturnStatement(ReturnStatement node) {
if (node.getValue() != null) {
IParentNode peek = peek();
if (peek instanceof FunctionNode) {
if (node.getValue() instanceof StringLiteral) {
((FunctionNode) peek).setReturnType("String");
} else if (node.getValue() instanceof DecimalLiteral) {
((FunctionNode) peek).setReturnType("Number");
} else if (node.getValue() instanceof BooleanLiteral) {
((FunctionNode) peek).setReturnType("Boolean");
} else {
((FunctionNode) peek).setReturnType("Object");
}
}
}
return super.visitReturnStatement(node);
}
@Override
protected void processVariable(VariableDeclaration declaration) {
for (IStructureHandler handler : handlers) {
final IStructureNode value = handler.handle(declaration);
if (value != IStructureHandler.CONTINUE) {
if (value != null) {
peek().getScope().addChild(value);
}
return;
}
}
if (declaration.getInitializer() instanceof FunctionStatement) {
peek().getScope().addChild(
buildFunctionDeclarationFromAssignment(declaration,
(FunctionStatement) declaration.getInitializer(),
Collections.<Expression> singletonList(declaration
.getIdentifier())));
return;
}
final JSVariable variable = new JSVariable(
declaration.getVariableName());
variable.setLocation(declaration.getInitializer() != null ? ReferenceLocation
.create(referenceSource, declaration.start(),
declaration.end(), declaration.getIdentifier())
: ReferenceLocation.create(referenceSource,
declaration.start(), declaration.end()));
jsdocSupport.processVariable(declaration, variable, fReporter,
fTypeChecker);
final VariableNode variableNode = new VariableNode(peek(), declaration,
variable);
peek().getScope().addChild(variableNode);
final Expression initializer = declaration.getInitializer();
if (initializer != null) {
push(variableNode);
variableNode.setValue(visit(initializer));
pop();
}
}
@Override
public IStructureNode visitObjectInitializer(ObjectInitializer node) {
final ObjectDeclaration object = new ObjectDeclaration(peek());
for (ObjectInitializerPart part : node.getInitializers()) {
if (part instanceof GetMethod) {
visitMethod((GetMethod) part);
// TODO (alex) handle GetMethod
} else if (part instanceof SetMethod) {
visitMethod((SetMethod) part);
// TODO (alex) handle SetMethod
} else if (part instanceof PropertyInitializer) {
final PropertyInitializer pi = (PropertyInitializer) part;
final String name;
if (pi.getName() instanceof Identifier) {
name = ((Identifier) pi.getName()).getName();
} else if (pi.getName() instanceof StringLiteral) {
name = ((StringLiteral) pi.getName()).getValue();
} else if (pi.getName() instanceof DecimalLiteral) {
name = ((DecimalLiteral) pi.getName()).getText();
} else {
name = "";
visit(pi.getName());
}
final PropertyDeclaration propertyDeclaration = new PropertyDeclaration(
peek(), name, pi, ReferenceLocation.create(
referenceSource, pi.start(), pi.end(),
pi.getName()));
object.addChild(propertyDeclaration);
push(propertyDeclaration);
propertyDeclaration.setValue(visit(pi.getValue()));
pop();
}
}
return !object.getChildren().isEmpty() ? object : null;
}
@Override
public IStructureNode visitPropertyExpression(PropertyExpression node) {
visit(node.getObject());
final Expression property = node.getProperty();
if (property instanceof Identifier) {
final Identifier identifier = (Identifier) property;
if (isFunctionCall(node)) {
peek().addMethodReference(identifier,
getCallArgumentCount(node));
} else {
peek().addFieldReference(identifier);
}
} else {
visit(property);
}
return null;
}
@Override
public IStructureNode visitIdentifier(Identifier node) {
final String name = node.getName();
final IDeclaration resolved = peek().getScope().resolve(name);
if (resolved != null) {
if (resolved.getParent() instanceof ScriptScope) {
// TODO (alex) option to treat it always as local or not
if (resolved instanceof FunctionNode) {
peek().addMethodReference(
node,
isFunctionCall(node) ? getCallArgumentCount(node)
: 0);
} else {
peek().addFieldReference(node);
}
} else {
peek().addLocalReference(node, resolved);
}
} else {
if (isFunctionCall(node)) {
peek().addMethodReference(node, getCallArgumentCount(node));
} else {
peek().addFieldReference(node);
}
}
return null;
}
public IStructureNode visitCallExpression(CallExpression node) {
IStructureNode retValue = visit(node.getExpression());
for (ASTNode argument : node.getArguments()) {
IStructureNode visit = visit(argument);
if (visit != null) {
peek().getScope().addToScope(new ArgumentsStructureNode(visit));
}
}
return retValue;
}
@Override
public IStructureNode visitVoidExpression(VoidExpression node) {
final Expression expression = node.getExpression();
if (expression instanceof FunctionStatement
|| expression instanceof VariableStatement
|| expression instanceof BinaryOperation
&& isAssignment((BinaryOperation) expression)) {
visit(expression);
} else {
final ExpressionNode expressionNode = new ExpressionNode(peek());
push(expressionNode);
visit(expression);
pop();
}
return null;
}
private boolean isAssignment(BinaryOperation operation) {
return operation.getOperation() == JSParser.ASSIGN;
}
@Override
public IStructureNode visitBinaryOperation(BinaryOperation node) {
if (node.getOperation() == JSParser.ASSIGN) {
final Expression left = node.getLeftExpression();
final Expression right = node.getRightExpression();
if (left instanceof PropertyExpression) {
final List<Expression> path = ((PropertyExpression) left)
.getPath();
boolean thisReference = false;
if (path.get(0) instanceof ThisExpression) {
path.remove(0);
thisReference = true;
}
if (isValidPath(path)) {
if (right instanceof FunctionStatement) {
return buildFunctionDeclarationFromAssignment(node,
(FunctionStatement) right, path);
} else if (thisReference) {
final FieldNode fieldNode = new FieldNode(peek(), node,
joinPath(path), ReferenceLocation.create(
referenceSource, node.start(),
node.end(), getNameNode(path)));
push(fieldNode);
visit(right);
return pop();
}
}
} else if (left instanceof Identifier
&& right instanceof FunctionStatement) {
return buildFunctionDeclarationFromAssignment(node,
(FunctionStatement) right,
Collections.singletonList(left));
}
}
return super.visitBinaryOperation(node);
}
private IStructureNode buildFunctionDeclarationFromAssignment(JSNode node,
FunctionStatement function, final List<Expression> path) {
final JSMethod method = new JSMethod(function, ReferenceSource.UNKNOWN);
jsdocSupport.processMethod(function, method, fReporter, fTypeChecker);
final FunctionNode functionNode = new FunctionDeclarationExpressionLike(
peek(), function, method, joinPath(path), getNameNode(path));
method.setLocation(ReferenceLocation.create(referenceSource,
node.start(), node.end(), functionNode.getNameNode()));
functionNode.buildArgumentNodes();
push(functionNode);
declarations.add(function.getDeclarations());
super.visitFunctionStatement(function);
declarations.pop();
return pop();
}
private static ISourceNode getNameNode(List<Expression> path) {
if (path.size() == 1) {
return path.get(0);
}
return new NameNode(path.get(0).start(), path.get(path.size() - 1)
.end());
}
private static String joinPath(List<Expression> path) {
if (path.size() == 1) {
return ((Identifier) path.get(0)).getName();
}
final StringBuilder sb = new StringBuilder();
for (Expression expression : path) {
if (sb.length() != 0) {
sb.append('.');
}
sb.append(((Identifier) expression).getName());
}
return sb.toString();
}
private boolean isValidPath(List<Expression> path) {
for (Expression expression : path) {
if (!(expression instanceof Identifier)) {
return false;
}
}
return true;
}
private boolean isFunctionCall(Expression node) {
if (isCallExpression(node))
return true;
if (node instanceof Identifier) {
String name = ((Identifier) node).getName();
for (int i = declarations.size(); --i >= 0;) {
List<JSDeclaration> list = declarations.get(i);
for (JSDeclaration declaration : list) {
if (declaration instanceof FunctionStatement
&& name.equals(((FunctionStatement) declaration)
.getFunctionName())) {
return true;
}
}
}
}
return false;
}
public static boolean isCallExpression(Expression node) {
final ASTNode parent = node.getParent();
return parent instanceof CallExpression
&& ((CallExpression) parent).getExpression() == node;
}
public static int getCallArgumentCount(Expression node) {
if (isCallExpression(node)) {
return ((CallExpression) node.getParent()).getArguments().size();
}
return 0;
}
private static class ArgumentsStructureNode implements IStructureNode {
private IStructureNode wrapper;
public ArgumentsStructureNode(IStructureNode wrapper) {
this.wrapper = wrapper;
}
public List<? extends IStructureNode> getChildren() {
return wrapper.getChildren();
}
public IParentNode getParent() {
return wrapper.getParent();
}
public IScope getScope() {
return wrapper.getScope();
}
public int start() {
return wrapper.start();
}
public boolean isManyChildren() {
return wrapper.isManyChildren();
}
public void reportStructure(IStructureRequestor requestor,
IStructureContext context) {
context.pushMask(IStructureContext.FIELD);
wrapper.reportStructure(requestor, context);
context.popMask();
}
}
}