| /* |
| * 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 org.codehaus.groovy.GroovyBugError; |
| import org.codehaus.groovy.ast.*; |
| import org.codehaus.groovy.ast.Variable; |
| import org.codehaus.groovy.ast.expr.*; |
| 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; |
| |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * 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 isSpecialConstructorCall = 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(" already contains 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; |
| |
| if (scope.getDeclaredVariable(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.putDeclaredVariable(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; |
| } |
| |
| Variable var1; |
| var1 = scope.getDeclaredVariable(var.getName()); |
| |
| if (var1 != null) { |
| var = var1; |
| break; |
| } |
| |
| var1 = (Variable) scope.getReferencedLocalVariable(var.getName()); |
| if (var1 != null) { |
| var = var1; |
| break; |
| } |
| |
| var1 = scope.getReferencedClassVariable(var.getName()); |
| if (var1 != null) { |
| var = var1; |
| break; |
| } |
| |
| ClassNode classScope = scope.getClassScope(); |
| if (classScope != null) { |
| Variable member = findClassMember(classScope, var.getName()); |
| if (member != null) { |
| boolean cc = currentScope.isInStaticContext() || isSpecialConstructorCall; |
| 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)) { |
| scope.putReferencedClassVariable(var); |
| } else { |
| var.setClosureSharedVariable(var.isClosureSharedVariable() || inClosure); |
| scope.putReferencedLocalVariable(var); |
| } |
| scope = scope.getParent(); |
| } |
| if (end.isResolvingDynamic()) { |
| if (end.getDeclaredVariable(var.getName()) == null) { |
| end.putDeclaredVariable(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.putDeclaredVariable(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()); |
| if (parameters[i].hasInitialExpression()) { |
| parameters[i].getInitialExpression().visit(this); |
| } |
| declare(parameters[i], expression); |
| } |
| } else if (expression.getParameters() != null) { |
| DynamicVariable var = new DynamicVariable("it", currentScope.isInStaticContext()); |
| currentScope.putDeclaredVariable(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); |
| |
| // GROOVY-2156 |
| Parameter[] parameters = node.getParameters(); |
| for (int i = 0; i < parameters.length; i++) { |
| Parameter parameter = parameters[i]; |
| visitAnnotations(parameter); |
| } |
| |
| 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 a non-String constant method name."); |
| } |
| 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) { |
| isSpecialConstructorCall = call.isSpecialCall(); |
| super.visitConstructorCallExpression(call); |
| isSpecialConstructorCall = 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(); |
| } |
| |
| } |