blob: 175e8ad09df57639bc2fd04827d807d0e06f4de7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2007 IBM Corporation and others.
* 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
*
*******************************************************************************/
package org.eclipse.dltk.ruby.internal.parser.mixin;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import org.eclipse.core.runtime.Assert;
import org.eclipse.dltk.ast.ASTListNode;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.ASTVisitor;
import org.eclipse.dltk.ast.Modifiers;
import org.eclipse.dltk.ast.declarations.MethodDeclaration;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.ast.declarations.TypeDeclaration;
import org.eclipse.dltk.ast.expressions.CallExpression;
import org.eclipse.dltk.ast.references.ConstantReference;
import org.eclipse.dltk.ast.references.SimpleReference;
import org.eclipse.dltk.ast.references.VariableReference;
import org.eclipse.dltk.core.IField;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.mixin.IMixinRequestor;
import org.eclipse.dltk.core.mixin.MixinModel;
import org.eclipse.dltk.core.mixin.IMixinRequestor.ElementInfo;
import org.eclipse.dltk.internal.core.ModelElement;
import org.eclipse.dltk.ruby.ast.RubyAliasExpression;
import org.eclipse.dltk.ruby.ast.RubyAssignment;
import org.eclipse.dltk.ruby.ast.RubyCallArgument;
import org.eclipse.dltk.ruby.ast.RubyClassDeclaration;
import org.eclipse.dltk.ruby.ast.RubyColonExpression;
import org.eclipse.dltk.ruby.ast.RubyConstantDeclaration;
import org.eclipse.dltk.ruby.ast.RubyMethodArgument;
import org.eclipse.dltk.ruby.ast.RubySelfReference;
import org.eclipse.dltk.ruby.ast.RubySingletonClassDeclaration;
import org.eclipse.dltk.ruby.ast.RubySingletonMethodDeclaration;
import org.eclipse.dltk.ruby.core.model.FakeField;
import org.eclipse.dltk.ruby.core.model.FakeMethod;
import org.eclipse.dltk.ruby.internal.parser.visitors.RubyAttributeHandler;
public class RubyMixinBuildVisitor extends ASTVisitor {
private static final String INSTANCE_SUFFIX = RubyMixin.INSTANCE_SUFFIX;
private static final String VIRTUAL_SUFFIX = RubyMixin.VIRTUAL_SUFFIX;
private static final String SEPARATOR = MixinModel.SEPARATOR;
private final ISourceModule sourceModule;
private final boolean moduleAvailable;
private final IMixinRequestor requestor;
private final HashSet allReportedKeys = new HashSet();
private abstract class Scope {
private final ASTNode node;
public Scope(ASTNode node) {
super();
this.node = node;
}
public ASTNode getNode() {
return node;
}
public abstract String reportMethod(String name, IMethod object);
public abstract String reportVariable(String name, IField object);
public abstract String reportType(String name, IType object,
boolean module);
public abstract String reportInclude(String object);
public abstract String reportExtend(String object);
public abstract String getClassKey();
public abstract String getKey();
}
private class SourceModuleScope extends Scope {
public SourceModuleScope(ModuleDeclaration node) {
super(node);
}
public String getClassKey() {
return "Object"; //$NON-NLS-1$
}
public String reportMethod(String name, IMethod object) {
return report(name, RubyMixinElementInfo.createMethod(object));
}
public String reportType(String name, IType object, boolean module) {
RubyMixinElementInfo obj = null;
if (module)
obj = RubyMixinElementInfo.createModule(object);
else
obj = RubyMixinElementInfo.createClass(object);
report(name + INSTANCE_SUFFIX, obj);
return report(name, obj);
}
public String reportVariable(String name, IField object) {
RubyMixinElementInfo info = (name.endsWith(VIRTUAL_SUFFIX)) ? RubyMixinElementInfo
.createVirtualClass()
: RubyMixinElementInfo.createVariable(object);
if (name.startsWith("$")) //$NON-NLS-1$
return report(name, info);
if (name.startsWith("@") || Character.isUpperCase(name.charAt(0))) //$NON-NLS-1$
return report("Object" + SEPARATOR + name, info); //$NON-NLS-1$
else {
if (info.getKind() == RubyMixinElementInfo.K_VIRTUAL)
return report(name, info);
return name; // no top-level vars
}
}
public String getKey() {
return "Object"; //$NON-NLS-1$
}
public String reportInclude(String object) {
return null;
}
public String reportExtend(String object) {
return null;
}
}
private class ClassScope extends Scope {
private final String classKey;
public ClassScope(ASTNode node, String classKey) {
super(node);
this.classKey = classKey;
}
public String reportMethod(String name, IMethod object) {
String key = classKey + INSTANCE_SUFFIX + SEPARATOR + name;
return report(key, RubyMixinElementInfo.createMethod(object));
}
public String reportType(String name, IType obj, boolean module) {
RubyMixinElementInfo object = null;
if (module)
object = RubyMixinElementInfo.createModule(obj);
else
object = RubyMixinElementInfo.createClass(obj);
String key = classKey + SEPARATOR + name;
report(key + INSTANCE_SUFFIX, object);
return report(key, object);
}
public String reportVariable(String name, IField object) {
RubyMixinElementInfo info = (name.endsWith(VIRTUAL_SUFFIX)) ? RubyMixinElementInfo
.createVirtualClass()
: RubyMixinElementInfo.createVariable(object);
if (name.startsWith("$")) //$NON-NLS-1$
return report(name, info);
RubyMixinElementInfo obj = info;
String key = null;
if (name.startsWith("@@")) { //$NON-NLS-1$
key = classKey + SEPARATOR + name;
report(classKey + INSTANCE_SUFFIX + SEPARATOR + name, obj);
return report(key, obj);
} else if (name.startsWith("@")) { //$NON-NLS-1$
key = classKey + SEPARATOR + name;
return report(key, obj);
} else {
key = classKey + SEPARATOR + name;
return report(key, obj);
}
}
public String getClassKey() {
return classKey;
}
public String getKey() {
return classKey;
}
public String reportInclude(String object) {
return report(classKey + INSTANCE_SUFFIX, new RubyMixinElementInfo(
RubyMixinElementInfo.K_INCLUDE, object));
}
public String reportExtend(String object) {
return report(classKey + INSTANCE_SUFFIX, new RubyMixinElementInfo(
RubyMixinElementInfo.K_EXTEND, object));
}
}
private class MetaClassScope extends Scope {
private final String classKey;
public MetaClassScope(ASTNode node, String classKey) {
super(node);
this.classKey = classKey;
}
public String reportMethod(String name, IMethod object) {
return report(classKey + SEPARATOR + name, RubyMixinElementInfo
.createMethod(object));
}
public String reportType(String name, IType object, boolean module) {
RubyMixinElementInfo obj = null;
if (module)
obj = RubyMixinElementInfo.createModule(object);
else
obj = RubyMixinElementInfo.createClass(object);
report(classKey + SEPARATOR + name + INSTANCE_SUFFIX, obj);
return report(classKey + SEPARATOR + name, obj);
}
public String reportVariable(String name, IField object) {
RubyMixinElementInfo info = (name.endsWith(VIRTUAL_SUFFIX)) ? RubyMixinElementInfo
.createVirtualClass()
: RubyMixinElementInfo.createVariable(object);
if (name.startsWith("$")) //$NON-NLS-1$
return report(name, info);
RubyMixinElementInfo obj = info;
if (name.startsWith("@@")) { //$NON-NLS-1$
report(classKey + INSTANCE_SUFFIX + SEPARATOR + name, obj);
return report(classKey + SEPARATOR + name, obj);
} else {
return report(classKey + SEPARATOR + name, obj);
}
}
public String getClassKey() {
return classKey;
}
public String getKey() {
return classKey;
}
public String reportInclude(String object) {
return report(classKey, new RubyMixinElementInfo(
RubyMixinElementInfo.K_INCLUDE, object));
}
public String reportExtend(String object) {
return report(classKey, new RubyMixinElementInfo(
RubyMixinElementInfo.K_EXTEND, object));
}
}
private class MethodScope extends Scope {
private final Scope classScope;
private final String methodKey;
public MethodScope(ASTNode node, Scope classScope, String methodKey) {
super(node);
this.classScope = classScope;
this.methodKey = methodKey;
}
public String reportMethod(String name, IMethod object) {
return classScope.reportMethod(name, object);
}
public String reportType(String name, IType obj, boolean module) {
// throw new RuntimeException();
return null;
}
public String reportVariable(String name, IField obj) {
RubyMixinElementInfo info = (name.endsWith(VIRTUAL_SUFFIX)) ? RubyMixinElementInfo
.createVirtualClass()
: RubyMixinElementInfo.createVariable(obj);
if (name.startsWith("$")) //$NON-NLS-1$
return report(name, info);
RubyMixinElementInfo object = info;
if (name.startsWith("@@")) { //$NON-NLS-1$
String key = classScope.getKey() + SEPARATOR + name;
report(
classScope.getKey() + INSTANCE_SUFFIX + SEPARATOR
+ name, object);
return report(key, object);
}
if (name.startsWith("@")) { //$NON-NLS-1$
String key;
if (classScope instanceof ClassScope) {
key = classScope.getKey() + INSTANCE_SUFFIX + SEPARATOR
+ name;
} else {
key = classScope.getKey() + SEPARATOR + name;
}
return report(key, object);
} else {
return report(methodKey + SEPARATOR + name, object);
}
}
public String getClassKey() {
return classScope.getClassKey();
}
public String getKey() {
return methodKey;
}
public String reportInclude(String object) {
return classScope.reportInclude(object);
}
public String reportExtend(String object) {
return classScope.reportExtend(object);
}
}
private Stack scopes = new Stack();
private final ModuleDeclaration module;
public RubyMixinBuildVisitor(ModuleDeclaration module,
ISourceModule sourceModule, boolean moduleAvailable,
IMixinRequestor requestor) {
super();
this.module = module;
this.sourceModule = sourceModule;
this.moduleAvailable = moduleAvailable;
this.requestor = requestor;
}
private Scope peekScope() {
return (Scope) scopes.peek();
}
public boolean visit(ModuleDeclaration s) throws Exception {
this.scopes.add(new SourceModuleScope(s));
return true;
}
public boolean visit(MethodDeclaration decl) throws Exception {
IMethod obj = null;
String name = decl.getName();
if (moduleAvailable) {
IModelElement element = findModelElementFor(decl);
obj = (IMethod) element;
}
if (decl instanceof RubySingletonMethodDeclaration) {
RubySingletonMethodDeclaration singl = (RubySingletonMethodDeclaration) decl;
ASTNode receiver = singl.getReceiver();
if (receiver instanceof RubySelfReference) {
Scope scope = peekScope();
MetaClassScope metaScope = new MetaClassScope(scope.getNode(),
scope.getClassKey());
String method = metaScope.reportMethod(name, obj);
scopes.push(new MethodScope(decl, metaScope, method));
} else if (receiver instanceof ConstantReference
|| receiver instanceof RubyColonExpression) {
String evaluatedClassKey = evaluateClassKey(receiver);
if (evaluatedClassKey != null) {
MetaClassScope metaScope = new MetaClassScope(decl,
evaluatedClassKey);
String method = metaScope.reportMethod(name, obj);
scopes.push(new MethodScope(decl, metaScope, method));
}
} else {
// TODO
}
} else {
Scope scope = peekScope();
String method = scope.reportMethod(name, obj);
scopes.push(new MethodScope(decl, scope, method));
}
return true;
}
private IModelElement findModelElementFor(ASTNode decl)
throws ModelException {
// return null;
return sourceModule.getElementAt(decl.sourceStart() + 1);
}
public boolean visitGeneral(ASTNode s) throws Exception {
if (s instanceof RubyMethodArgument) {
RubyMethodArgument argument = (RubyMethodArgument) s;
String name = argument.getName();
Scope scope = peekScope();
IField obj = null;
if (sourceModule != null) {
obj = new FakeField((ModelElement) sourceModule, name, s
.sourceStart(), s.sourceEnd() - s.sourceStart());
}
scope.reportVariable(name, obj);
} else if (s instanceof RubyAssignment) {
RubyAssignment assignment = (RubyAssignment) s;
ASTNode left = assignment.getLeft();
if (left instanceof VariableReference) {
VariableReference ref = (VariableReference) left;
String name = ref.getName();
Scope scope = peekScope();
IField obj = null;
if (sourceModule != null)
obj = new FakeField((ModelElement) sourceModule, name, ref
.sourceStart(), ref.sourceEnd() - ref.sourceStart());
scope.reportVariable(name, obj);
}
} else if (s instanceof CallExpression) {
visit((CallExpression) s);
} else if (s instanceof RubyConstantDeclaration) {
RubyConstantDeclaration constantDeclaration = (RubyConstantDeclaration) s;
SimpleReference name2 = constantDeclaration.getName();
String name = name2.getName();
boolean closeScope = false;
if (constantDeclaration.getPath() instanceof RubyColonExpression) {
RubyColonExpression colon = (RubyColonExpression) constantDeclaration
.getPath();
String classKey = evaluateClassKey(colon.getLeft());
if (classKey != null) {
this.scopes.add(new ClassScope(colon, classKey));
closeScope = true;
}
}
Scope scope = peekScope();
IField obj = null;
if (sourceModule != null)
obj = new FakeField((ModelElement) sourceModule, name, name2
.sourceStart(), name2.sourceEnd() - name2.sourceStart());
scope.reportVariable(name, obj);
if (closeScope)
this.scopes.pop();
} else if (s instanceof RubyAliasExpression) {
RubyAliasExpression alias = (RubyAliasExpression) s;
String oldValue = alias.getOldValue();
if (!oldValue.startsWith("$")) { //$NON-NLS-1$
String newValue = alias.getNewValue();
String nkey = peekScope().reportMethod(newValue, null);
report(nkey, new RubyMixinElementInfo(
RubyMixinElementInfo.K_ALIAS, alias));
}
}
return true;
}
public boolean visit(CallExpression call) throws Exception {
if (call.getReceiver() == null
&& call.getName().equals("include") && call.getArgs().getChilds().size() > 0) { //$NON-NLS-1$
ASTNode expr = (ASTNode) call.getArgs().getChilds().get(0);
if (expr instanceof RubyCallArgument)
expr = ((RubyCallArgument) expr).getValue();
Scope scope = peekScope();
String incl = evaluateClassKey(expr);
if (incl != null)
scope.reportInclude(incl);
return false;
} else if (call.getReceiver() == null
&& call.getName().equals("extend") //$NON-NLS-1$
&& call.getArgs().getChilds().size() > 0) {
ASTNode expr = (ASTNode) call.getArgs().getChilds().get(0);
if (expr instanceof RubyCallArgument)
expr = ((RubyCallArgument) expr).getValue();
scopes.push(new MetaClassScope(call, peekScope().getClassKey()));
Scope scope = peekScope();
String ext = evaluateClassKey(expr);
if (ext != null)
scope.reportExtend(ext);
scopes.pop();
return false;
} else if (RubyAttributeHandler.isAttributeCreationCall(call)
&& sourceModule != null) {
Scope scope = peekScope();
RubyAttributeHandler info = new RubyAttributeHandler(call);
List readers = info.getReaders();
for (Iterator iterator = readers.iterator(); iterator.hasNext();) {
ASTNode n = (ASTNode) iterator.next();
String attr = RubyAttributeHandler.getText(n);
if (attr == null)
continue;
FakeMethod fakeMethod = new FakeMethod(
(ModelElement) sourceModule, attr, n.sourceStart(),
attr.length(), n.sourceStart(), attr.length());
fakeMethod.setFlags(Modifiers.AccPublic);
scope.reportMethod(attr, fakeMethod);
}
List writers = info.getWriters();
for (Iterator iterator = writers.iterator(); iterator.hasNext();) {
ASTNode n = (ASTNode) iterator.next();
String attr = RubyAttributeHandler.getText(n);
if (attr == null)
continue;
FakeMethod fakeMethod = new FakeMethod(
(ModelElement) sourceModule, attr + "=", n //$NON-NLS-1$
.sourceStart(), attr.length(), n.sourceStart(),
attr.length());
fakeMethod.setFlags(Modifiers.AccPublic);
fakeMethod.setParameters(new String[] { attr });
scope.reportMethod(attr + "=", fakeMethod); //$NON-NLS-1$
}
return false;
}
return true;
}
public boolean visit(TypeDeclaration decl) throws Exception {
IType obj = null;
if (moduleAvailable) {
IModelElement elementFor = findModelElementFor(decl);
if (!(elementFor instanceof IType)) {
elementFor = findModelElementFor(decl);
}
obj = (IType) elementFor;
}
boolean module = (decl.getModifiers() & Modifiers.AccModule) != 0;
if (decl instanceof RubySingletonClassDeclaration) {
RubySingletonClassDeclaration declaration = (RubySingletonClassDeclaration) decl;
ASTNode receiver = declaration.getReceiver();
if (receiver instanceof RubySelfReference) {
Scope scope = peekScope();
scopes.push(new MetaClassScope(decl, scope.getClassKey()));
return true;
} else if (receiver instanceof ConstantReference
|| receiver instanceof RubyColonExpression) {
String evaluatedClassKey = evaluateClassKey(receiver);
if (evaluatedClassKey != null) {
MetaClassScope metaScope = new MetaClassScope(decl,
evaluatedClassKey);
scopes.push(metaScope);
return true;
}
} else if (receiver instanceof VariableReference) {
VariableReference ref = (VariableReference) receiver;
Scope scope = peekScope();
String key = scope.reportVariable(ref.getName(), null);
if (key != null) {
key += VIRTUAL_SUFFIX;
report(key, new RubyMixinElementInfo(RubyMixinElementInfo.K_VIRTUAL, obj));
scopes.push(new MetaClassScope(decl, key));
return true;
}
} else {
// TODO: add common method for singletons resolving
}
} else if (decl instanceof RubyClassDeclaration) {
RubyClassDeclaration declaration = (RubyClassDeclaration) decl;
ASTNode className = declaration.getClassName();
if (className instanceof ConstantReference) {
String name = ((ConstantReference) className).getName();
Scope scope = peekScope();
String newKey = scope.reportType(name, obj, module);
scopes.push(new ClassScope(decl, newKey));
} else {
String name = evaluateClassKey(className);
if (name != null) {
report(name, RubyMixinElementInfo.createClass(obj));
scopes.push(new ClassScope(decl, name));
}
}
ASTListNode superClasses = declaration.getSuperClasses();
if (superClasses != null && superClasses.getChilds().size() == 1) {
ASTNode s = (ASTNode) superClasses.getChilds().get(0);
// if (this.sourceModule != null) {
SuperclassReferenceInfo ref = new SuperclassReferenceInfo(s,
this.module, sourceModule);
Scope scope = peekScope();
report(scope.getKey() + INSTANCE_SUFFIX,
new RubyMixinElementInfo(RubyMixinElementInfo.K_SUPER,
ref));
report(scope.getKey(), new RubyMixinElementInfo(
RubyMixinElementInfo.K_SUPER, ref));
// }
}
return true;
} else {
String name = decl.getName();
Scope scope = peekScope();
String newKey = scope.reportType(name, obj, module);
scopes.push(new ClassScope(decl, newKey));
return true;
}
return false;
}
private String report(String key, RubyMixinElementInfo object) {
RubyMixinModel.getRawInstance().clearKeysCashe(key);
ElementInfo info = new IMixinRequestor.ElementInfo();
info.key = key;
info.object = object;
if (requestor != null) {
requestor.reportElement(info);
// System.out.println("Mixin reported: " + key);
}
allReportedKeys.add(key);
return key;
}
private String evaluateClassKey(ASTNode expr) {
if (expr instanceof RubyColonExpression) {
RubyColonExpression colonExpression = (RubyColonExpression) expr;
if (colonExpression.isFull()) {
return colonExpression.getName();
} else {
String key = evaluateClassKey(colonExpression.getLeft());
if (key != null) {
return key + SEPARATOR + colonExpression.getName();
}
}
} else if (expr instanceof ConstantReference) {
ConstantReference constantReference = (ConstantReference) expr;
// simple heuristic
int size = this.scopes.size();
for (int i = size - 1; i >= 0; i--) {
String possibleKey = ""; //$NON-NLS-1$
if (i > 0) {
Scope s = (Scope) this.scopes.get(i);
possibleKey = s.getKey() + SEPARATOR
+ constantReference.getName();
} else
possibleKey = constantReference.getName();
if (this.allReportedKeys.contains(possibleKey))
return possibleKey;
}
return constantReference.getName();
}
return null;
}
public void endvisitGeneral(ASTNode node) throws Exception {
Scope scope = (Scope) scopes.peek();
if (scope.getNode() == node) {
scopes.pop();
}
super.endvisitGeneral(node);
}
public static String[] restoreScopesByNodes(ASTNode[] nodes) {
Assert.isLegal(nodes != null);
Assert.isLegal(nodes.length > 0);
String[] keys = new String[nodes.length];
RubyMixinBuildVisitor visitor = new RubyMixinBuildVisitor(
(ModuleDeclaration) nodes[0], null, false, null);
for (int i = 0; i < nodes.length; i++) {
try {
if (nodes[i] instanceof ModuleDeclaration) {
visitor.visit((ModuleDeclaration) nodes[i]);
} else if (nodes[i] instanceof TypeDeclaration) {
visitor.visit((TypeDeclaration) nodes[i]);
} else if (nodes[i] instanceof MethodDeclaration) {
visitor.visit((MethodDeclaration) nodes[i]);
} else {
visitor.visit(nodes[i]);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
Scope scope = visitor.peekScope();
keys[i] = scope.getKey();
}
return keys;
}
}