| /* |
| * Copyright 2003-2007 the original author or authors. |
| * |
| * 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 org.codehaus.groovy.classgen; |
| |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.codehaus.groovy.GroovyBugError; |
| import org.codehaus.groovy.ast.ASTNode; |
| import org.codehaus.groovy.ast.ClassCodeVisitorSupport; |
| import org.codehaus.groovy.ast.ClassHelper; |
| import org.codehaus.groovy.ast.ClassNode; |
| import org.codehaus.groovy.ast.FieldNode; |
| import org.codehaus.groovy.ast.MethodNode; |
| import org.codehaus.groovy.ast.Parameter; |
| import org.codehaus.groovy.ast.PropertyNode; |
| import org.codehaus.groovy.ast.DynamicVariable; |
| import org.codehaus.groovy.ast.Variable; |
| import org.codehaus.groovy.ast.VariableScope; |
| import org.codehaus.groovy.ast.expr.ClosureExpression; |
| import org.codehaus.groovy.ast.expr.ConstantExpression; |
| import org.codehaus.groovy.ast.expr.ConstructorCallExpression; |
| import org.codehaus.groovy.ast.expr.DeclarationExpression; |
| import org.codehaus.groovy.ast.expr.Expression; |
| import org.codehaus.groovy.ast.expr.FieldExpression; |
| import org.codehaus.groovy.ast.expr.MethodCallExpression; |
| import org.codehaus.groovy.ast.expr.PropertyExpression; |
| import org.codehaus.groovy.ast.expr.VariableExpression; |
| import org.codehaus.groovy.ast.stmt.BlockStatement; |
| import org.codehaus.groovy.ast.stmt.CatchStatement; |
| import org.codehaus.groovy.ast.stmt.ForStatement; |
| import org.codehaus.groovy.control.SourceUnit; |
| |
| /** |
| * goes through an AST and initializes the scopes |
| * @author Jochen Theodorou |
| */ |
| public class VariableScopeVisitor extends ClassCodeVisitorSupport { |
| private VariableScope currentScope = null; |
| private VariableScope headScope = new VariableScope(); |
| private ClassNode currentClass=null; |
| private SourceUnit source; |
| private boolean inClosure=false; |
| private boolean inPropertyExpression = false; |
| private boolean isSpecialContructorCall = false; |
| |
| private LinkedList stateStack=new LinkedList(); |
| |
| private class StateStackElement { |
| VariableScope scope; |
| ClassNode clazz; |
| boolean closure; |
| |
| StateStackElement() { |
| scope = VariableScopeVisitor.this.currentScope; |
| clazz = VariableScopeVisitor.this.currentClass; |
| closure = VariableScopeVisitor.this.inClosure; |
| } |
| } |
| |
| public VariableScopeVisitor(SourceUnit source) { |
| this.source = source; |
| currentScope = headScope; |
| } |
| |
| |
| // ------------------------------ |
| // helper methods |
| //------------------------------ |
| |
| private void pushState(boolean isStatic) { |
| stateStack.add(new StateStackElement()); |
| currentScope = new VariableScope(currentScope); |
| currentScope.setInStaticContext(isStatic); |
| } |
| |
| private void pushState() { |
| pushState(currentScope.isInStaticContext()); |
| } |
| |
| private void popState() { |
| // a scope in a closure is never really static |
| // the checking needs this to be as the surrounding |
| // method to correctly check the access to variables. |
| // But a closure and all nested scopes are a result |
| // of calling a non static method, so the context |
| // is not static. |
| if (inClosure) currentScope.setInStaticContext(false); |
| |
| StateStackElement element = (StateStackElement) stateStack.removeLast(); |
| currentScope = element.scope; |
| currentClass = element.clazz; |
| inClosure = element.closure; |
| } |
| |
| private void declare(Parameter[] parameters, ASTNode node) { |
| for (int i = 0; i < parameters.length; i++) { |
| if (parameters[i].hasInitialExpression()) { |
| parameters[i].getInitialExpression().visit(this); |
| } |
| declare(parameters[i],node); |
| } |
| } |
| |
| private void declare(VariableExpression expr) { |
| declare(expr,expr); |
| } |
| |
| private void declare(Variable var, ASTNode expr) { |
| String scopeType = "scope"; |
| String variableType = "variable"; |
| |
| if (expr.getClass()==FieldNode.class){ |
| scopeType = "class"; |
| variableType = "field"; |
| } else if (expr.getClass()==PropertyNode.class){ |
| scopeType = "class"; |
| variableType = "property"; |
| } |
| |
| StringBuffer msg = new StringBuffer(); |
| msg.append("The current ").append(scopeType); |
| msg.append(" does already contain a ").append(variableType); |
| msg.append(" of the name ").append(var.getName()); |
| |
| if (currentScope.getDeclaredVariable(var.getName())!=null) { |
| addError(msg.toString(),expr); |
| return; |
| } |
| |
| for (VariableScope scope = currentScope.getParent(); scope!=null; scope = scope.getParent()) { |
| // if we are in a class and no variable is declared until |
| // now, then we can break the loop, because we are allowed |
| // to declare a variable of the same name as a class member |
| if (scope.getClassScope()!=null) break; |
| |
| Map declares = scope.getDeclaredVariables(); |
| if (declares.get(var.getName())!=null) { |
| // variable already declared |
| addError(msg.toString(), expr); |
| break; |
| } |
| } |
| // declare the variable even if there was an error to allow more checks |
| currentScope.getDeclaredVariables().put(var.getName(),var); |
| } |
| |
| protected SourceUnit getSourceUnit() { |
| return source; |
| } |
| |
| private Variable findClassMember(ClassNode cn, String name) { |
| if (cn == null) return null; |
| if (cn.isScript()) { |
| return new DynamicVariable(name,false); |
| } |
| List l = cn.getFields(); |
| for (Iterator iter = l.iterator(); iter.hasNext();) { |
| FieldNode f = (FieldNode) iter.next(); |
| if (f.getName().equals(name)) return f; |
| } |
| |
| l = cn.getMethods(); |
| for (Iterator iter = l.iterator(); iter.hasNext();) { |
| MethodNode f =(MethodNode) iter.next(); |
| String methodName = f.getName(); |
| String pName = getPropertyName(f); |
| if (pName == null) continue; |
| if (!pName.equals(name)) continue; |
| PropertyNode var = new PropertyNode(pName,f.getModifiers(),getPropertyType(f),cn,null,null,null); |
| return var; |
| } |
| |
| l = cn.getProperties(); |
| for (Iterator iter = l.iterator(); iter.hasNext();) { |
| PropertyNode f = (PropertyNode) iter.next(); |
| if (f.getName().equals(name)) return f; |
| } |
| |
| Variable ret = findClassMember(cn.getSuperClass(),name); |
| if (ret!=null) return ret; |
| return findClassMember(cn.getOuterClass(),name); |
| } |
| |
| private ClassNode getPropertyType(MethodNode m) { |
| String name = m.getName(); |
| if (m.getReturnType()!=ClassHelper.VOID_TYPE) { |
| return m.getReturnType(); |
| } |
| return m.getParameters()[0].getType(); |
| } |
| |
| private String getPropertyName(MethodNode m) { |
| String name = m.getName(); |
| if (!(name.startsWith("set") || name.startsWith("get"))) return null; |
| String pname = name.substring(3); |
| if (pname.length() == 0) return null; |
| String s = pname.substring(0, 1).toLowerCase(); |
| String rest = pname.substring(1); |
| pname = s + rest; |
| |
| if (name.startsWith("get") && m.getReturnType()==ClassHelper.VOID_TYPE) { |
| return null; |
| } |
| if (name.startsWith("set") && m.getParameters().length!=1) { |
| return null; |
| } |
| return pname; |
| } |
| |
| // ------------------------------- |
| // different Variable based checks |
| // ------------------------------- |
| |
| private Variable checkVariableNameForDeclaration(String name, Expression expression) { |
| if ("super".equals(name) || "this".equals(name)) return null; |
| |
| VariableScope scope = currentScope; |
| Variable var = new DynamicVariable(name,currentScope.isInStaticContext()); |
| Variable dummyStart = var; |
| // try to find a declaration of a variable |
| VariableScope dynamicScope = null; |
| while (!scope.isRoot()) { |
| if (dynamicScope==null && scope.isResolvingDynamic()) { |
| dynamicScope = scope; |
| } |
| |
| Map declares = scope.getDeclaredVariables(); |
| if (declares.get(var.getName())!=null) { |
| var = (Variable) declares.get(var.getName()); |
| break; |
| } |
| Map localReferenced = scope.getReferencedLocalVariables(); |
| if (localReferenced.get(var.getName())!=null) { |
| var = (Variable) localReferenced.get(var.getName()); |
| break; |
| } |
| |
| Map classReferenced = scope.getReferencedClassVariables(); |
| if (classReferenced.get(var.getName())!=null) { |
| var = (Variable) classReferenced.get(var.getName()); |
| break; |
| } |
| |
| ClassNode classScope = scope.getClassScope(); |
| if (classScope!=null) { |
| Variable member = findClassMember(classScope,var.getName()); |
| if (member!=null) { |
| boolean cc = currentScope.isInStaticContext() || isSpecialContructorCall; |
| boolean cm = member.isInStaticContext(); |
| // |
| // we don't allow access from dynamic context to static context |
| // |
| // cm==cc: |
| // we always allow access if the context is in both cases static |
| // or dynamic |
| // cm==true: |
| // the member is static, which means access is always allowed |
| // cm||cm==cc: |
| // is false only for the case cc==true and cm==false, which means |
| // the member is a dynamic context, but the current scope is static. |
| // |
| // One example for (cm||cm==cc)==false is a static method trying to |
| // access a non static field. |
| // |
| if (cm || cm==cc) var = member; |
| } |
| break; |
| } |
| scope = scope.getParent(); |
| } |
| |
| VariableScope end = scope; |
| |
| if (scope.isRoot() && dynamicScope==null) { |
| // no matching scope found |
| declare(var,expression); |
| addError("The variable " + var.getName() + |
| " is undefined in the current scope", expression); |
| } else if (scope.isRoot() && dynamicScope!=null) { |
| // no matching scope found, but there was a scope that |
| // resolves dynamic |
| scope = dynamicScope; |
| } |
| |
| if (!scope.isRoot()) { |
| scope = currentScope; |
| while (scope != end) { |
| Map references = null; |
| if (end.isClassScope() || end.isRoot() || |
| (end.isReferencedClassVariable(name) && end.getDeclaredVariable(name)==null)) |
| { |
| references = scope.getReferencedClassVariables(); |
| } else { |
| references = scope.getReferencedLocalVariables(); |
| var.setClosureSharedVariable(var.isClosureSharedVariable() || inClosure); |
| } |
| references.put(var.getName(),var); |
| scope = scope.getParent(); |
| } |
| if (end.isResolvingDynamic()) { |
| if (end.getDeclaredVariable(var.getName())==null) { |
| end.getDeclaredVariables().put(var.getName(),var); |
| } |
| } |
| } |
| |
| return var; |
| } |
| /** |
| * a property on "this", like this.x is transformed to a |
| * direct field access, so we need to check the |
| * static context here |
| */ |
| private void checkPropertyOnExplicitThis(PropertyExpression pe) { |
| if (!currentScope.isInStaticContext()) return; |
| Expression object = pe.getObjectExpression(); |
| if (!(object instanceof VariableExpression)) return; |
| VariableExpression ve = (VariableExpression) object; |
| if (!ve.getName().equals("this")) return; |
| String name = pe.getPropertyAsString(); |
| if (name==null) return; |
| Variable member = findClassMember(currentClass,name); |
| if (member==null) return; |
| checkVariableContextAccess(member, pe); |
| } |
| |
| private void checkVariableContextAccess(Variable v, Expression expr) { |
| if (inPropertyExpression || v.isInStaticContext() || !currentScope.isInStaticContext()) return; |
| |
| String msg = v.getName()+ |
| " is declared in a dynamic context, but you tried to"+ |
| " access it from a static context."; |
| addError(msg,expr); |
| |
| // declare a static variable to be able to continue the check |
| DynamicVariable v2 = new DynamicVariable(v.getName(),currentScope.isInStaticContext()); |
| currentScope.getDeclaredVariables().put(v.getName(),v2); |
| } |
| |
| // ------------------------------ |
| // code visit |
| // ------------------------------ |
| |
| public void visitBlockStatement(BlockStatement block) { |
| pushState(); |
| block.setVariableScope(currentScope); |
| super.visitBlockStatement(block); |
| popState(); |
| } |
| |
| public void visitForLoop(ForStatement forLoop) { |
| pushState(); |
| forLoop.setVariableScope(currentScope); |
| Parameter p = (Parameter) forLoop.getVariable(); |
| p.setInStaticContext(currentScope.isInStaticContext()); |
| if (p!=ForStatement.FOR_LOOP_DUMMY) declare(p, forLoop); |
| super.visitForLoop(forLoop); |
| popState(); |
| } |
| |
| public void visitDeclarationExpression(DeclarationExpression expression) { |
| // visit right side first to avoid the usage of a |
| // variable before its declaration |
| expression.getRightExpression().visit(this); |
| // no need to visit left side, just get the variable name |
| VariableExpression vex = expression.getVariableExpression(); |
| vex.setInStaticContext(currentScope.isInStaticContext()); |
| declare(vex); |
| vex.setAccessedVariable(vex); |
| } |
| |
| public void visitVariableExpression(VariableExpression expression) { |
| String name = expression.getName(); |
| Variable v = checkVariableNameForDeclaration(name,expression); |
| if (v==null) return; |
| expression.setAccessedVariable(v); |
| checkVariableContextAccess(v,expression); |
| } |
| |
| public void visitPropertyExpression(PropertyExpression expression) { |
| boolean ipe = inPropertyExpression; |
| inPropertyExpression = true; |
| expression.getObjectExpression().visit(this); |
| inPropertyExpression = false; |
| expression.getProperty().visit(this); |
| checkPropertyOnExplicitThis(expression); |
| inPropertyExpression = ipe; |
| } |
| |
| public void visitClosureExpression(ClosureExpression expression) { |
| pushState(); |
| |
| inClosure=true; |
| // as result of the Paris meeting Closure resolves |
| // always dynamically |
| currentScope.setDynamicResolving(true); |
| |
| expression.setVariableScope(currentScope); |
| |
| if (expression.isParameterSpecified()) { |
| Parameter[] parameters = expression.getParameters(); |
| for (int i = 0; i < parameters.length; i++) { |
| parameters[i].setInStaticContext(currentScope.isInStaticContext()); |
| declare(parameters[i],expression); |
| } |
| } else if (expression.getParameters()!=null){ |
| DynamicVariable var = new DynamicVariable("it",currentScope.isInStaticContext()); |
| currentScope.getDeclaredVariables().put("it",var); |
| } |
| |
| super.visitClosureExpression(expression); |
| popState(); |
| } |
| |
| public void visitCatchStatement(CatchStatement statement) { |
| pushState(); |
| Parameter p = (Parameter) statement.getVariable(); |
| p.setInStaticContext(currentScope.isInStaticContext()); |
| declare(p, statement); |
| super.visitCatchStatement(statement); |
| popState(); |
| } |
| |
| public void visitFieldExpression(FieldExpression expression) { |
| String name = expression.getFieldName(); |
| //TODO: change that to get the correct scope |
| Variable v = checkVariableNameForDeclaration(name,expression); |
| checkVariableContextAccess(v,expression); |
| } |
| |
| // ------------------------------ |
| // class visit |
| // ------------------------------ |
| |
| public void visitClass(ClassNode node) { |
| pushState(); |
| |
| currentClass = node; |
| boolean dynamicMode = node.isScript(); |
| currentScope.setDynamicResolving(dynamicMode); |
| currentScope.setClassScope(node); |
| |
| super.visitClass(node); |
| popState(); |
| } |
| |
| protected void visitConstructorOrMethod(MethodNode node, boolean isConstructor) { |
| pushState(node.isStatic()); |
| |
| node.setVariableScope(currentScope); |
| declare(node.getParameters(),node); |
| |
| super.visitConstructorOrMethod(node, isConstructor); |
| popState(); |
| } |
| |
| public void visitMethodCallExpression(MethodCallExpression call) { |
| if (call.isImplicitThis() && call.getMethod() instanceof ConstantExpression) { |
| Object value = ((ConstantExpression) call.getMethod()).getText(); |
| if (! (value instanceof String)) { |
| throw new GroovyBugError("tried to make a method call with an constant as"+ |
| " name, but the constant was no String."); |
| } |
| String methodName = (String) value; |
| Variable v = checkVariableNameForDeclaration(methodName,call); |
| if (v!=null && !(v instanceof DynamicVariable)) { |
| checkVariableContextAccess(v,call); |
| } |
| } |
| super.visitMethodCallExpression(call); |
| } |
| |
| public void visitConstructorCallExpression(ConstructorCallExpression call) { |
| isSpecialContructorCall = call.isSpecialCall(); |
| super.visitConstructorCallExpression(call); |
| isSpecialContructorCall = false; |
| } |
| |
| public void visitProperty(PropertyNode node) { |
| pushState(node.isStatic()); |
| super.visitProperty(node); |
| popState(); |
| } |
| |
| public void visitField(FieldNode node) { |
| pushState(node.isStatic()); |
| super.visitField(node); |
| popState(); |
| } |
| |
| } |