001    /*
002     $Id: VariableScopeVisitor.java 4607 2006-12-22 22:19:01Z blackdrag $
003    
004     Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005    
006     Redistribution and use of this software and associated documentation
007     ("Software"), with or without modification, are permitted provided
008     that the following conditions are met:
009    
010     1. Redistributions of source code must retain copyright
011        statements and notices.  Redistributions must also contain a
012        copy of this document.
013    
014     2. Redistributions in binary form must reproduce the
015        above copyright notice, this list of conditions and the
016        following disclaimer in the documentation and/or other
017        materials provided with the distribution.
018    
019     3. The name "groovy" must not be used to endorse or promote
020        products derived from this Software without prior written
021        permission of The Codehaus.  For written permission,
022        please contact info@codehaus.org.
023    
024     4. Products derived from this Software may not be called "groovy"
025        nor may "groovy" appear in their names without prior written
026        permission of The Codehaus. "groovy" is a registered
027        trademark of The Codehaus.
028    
029     5. Due credit should be given to The Codehaus -
030        http://groovy.codehaus.org/
031    
032     THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033     ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034     NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035     FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
036     THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042     ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043     OF THE POSSIBILITY OF SUCH DAMAGE.
044    
045     */
046    package org.codehaus.groovy.classgen;
047    
048    import java.util.Iterator;
049    import java.util.LinkedList;
050    import java.util.List;
051    import java.util.Map;
052    
053    import org.codehaus.groovy.GroovyBugError;
054    import org.codehaus.groovy.ast.ASTNode;
055    import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
056    import org.codehaus.groovy.ast.ClassHelper;
057    import org.codehaus.groovy.ast.ClassNode;
058    import org.codehaus.groovy.ast.FieldNode;
059    import org.codehaus.groovy.ast.MethodNode;
060    import org.codehaus.groovy.ast.Parameter;
061    import org.codehaus.groovy.ast.PropertyNode;
062    import org.codehaus.groovy.ast.DynamicVariable;
063    import org.codehaus.groovy.ast.Variable;
064    import org.codehaus.groovy.ast.VariableScope;
065    import org.codehaus.groovy.ast.expr.ClosureExpression;
066    import org.codehaus.groovy.ast.expr.ConstantExpression;
067    import org.codehaus.groovy.ast.expr.DeclarationExpression;
068    import org.codehaus.groovy.ast.expr.Expression;
069    import org.codehaus.groovy.ast.expr.FieldExpression;
070    import org.codehaus.groovy.ast.expr.MethodCallExpression;
071    import org.codehaus.groovy.ast.expr.VariableExpression;
072    import org.codehaus.groovy.ast.stmt.BlockStatement;
073    import org.codehaus.groovy.ast.stmt.CatchStatement;
074    import org.codehaus.groovy.ast.stmt.ForStatement;
075    import org.codehaus.groovy.control.SourceUnit;
076    
077    /**
078     * goes through an AST and initializes the scopes 
079     * @author Jochen Theodorou
080     */
081    public class VariableScopeVisitor extends ClassCodeVisitorSupport {
082        private VariableScope currentScope = null;
083        private VariableScope headScope = new VariableScope();
084        private ClassNode currentClass=null;
085        private SourceUnit source;
086        private boolean inClosure=false;
087        
088        private LinkedList stateStack=new LinkedList();
089        
090        private class StateStackElement {
091            VariableScope scope;
092            ClassNode clazz;
093            boolean dynamic;
094            boolean closure;
095            
096            StateStackElement() {
097                scope = VariableScopeVisitor.this.currentScope;
098                clazz = VariableScopeVisitor.this.currentClass;
099                closure = VariableScopeVisitor.this.inClosure;
100            }
101        }
102        
103        public VariableScopeVisitor(SourceUnit source) {
104            this.source = source;
105            currentScope  = headScope;
106        }
107        
108        
109        // ------------------------------
110        // helper methods   
111        //------------------------------
112        
113        private void pushState(boolean isStatic) {
114            stateStack.add(new StateStackElement());
115            currentScope = new VariableScope(currentScope);
116            currentScope.setInStaticContext(isStatic);
117        }
118        
119        private void pushState() {
120            pushState(currentScope.isInStaticContext());
121        }
122        
123        private void popState() {
124            // a scope in a closure is never really static
125            // the checking needs this to be as the surrounding
126            // method to correctly check the access to variables.
127            // But a closure and all nested scopes are a result
128            // of calling a non static method, so the context
129            // is not static.
130            if (inClosure) currentScope.setInStaticContext(false);
131            
132            StateStackElement element = (StateStackElement) stateStack.removeLast();
133            currentScope = element.scope;
134            currentClass = element.clazz;
135            inClosure = element.closure;
136        }
137        
138        private void declare(Parameter[] parameters, ASTNode node) {
139            for (int i = 0; i < parameters.length; i++) {
140                if (parameters[i].hasInitialExpression()) {
141                    parameters[i].getInitialExpression().visit(this);
142                }
143                declare(parameters[i],node);
144            }
145        }        
146        
147        private void declare(VariableExpression expr) {
148            declare(expr,expr);
149        }
150        
151        private void declare(Variable var, ASTNode expr) {
152            String scopeType = "scope";
153            String variableType = "variable";
154            
155            if (expr.getClass()==FieldNode.class){
156                scopeType = "class"; 
157                variableType = "field";
158            } else if (expr.getClass()==PropertyNode.class){
159                scopeType = "class"; 
160                variableType = "property";
161            }
162            
163            StringBuffer msg = new StringBuffer();
164            msg.append("The current ").append(scopeType);
165            msg.append(" does already contain a ").append(variableType);
166            msg.append(" of the name ").append(var.getName());
167            
168            if (currentScope.getDeclaredVariable(var.getName())!=null) {
169                addError(msg.toString(),expr);
170                return;
171            }
172            
173            for (VariableScope scope = currentScope.getParent(); scope!=null; scope = scope.getParent()) {
174                // if we are in a class and no variable is declared until
175                // now, then we can break the loop, because we are allowed
176                // to declare a variable of the same name as a class member
177                if (scope.getClassScope()!=null) break;
178                
179                Map declares = scope.getDeclaredVariables();
180                if (declares.get(var.getName())!=null) {
181                    // variable already declared
182                    addError(msg.toString(), expr);
183                    break;
184                }
185            }
186            // declare the variable even if there was an error to allow more checks
187            currentScope.getDeclaredVariables().put(var.getName(),var);
188        }
189        
190        protected SourceUnit getSourceUnit() {
191            return source;
192        }
193        
194        private Variable findClassMember(ClassNode cn, String name) {
195            if (cn == null) return null;
196            if (cn.isScript()) {
197                return new DynamicVariable(name,false);
198            }
199            List l = cn.getFields();
200            for (Iterator iter = l.iterator(); iter.hasNext();) {
201                FieldNode f = (FieldNode) iter.next();
202                if (f.getName().equals(name)) return f;
203            }
204    
205            l = cn.getMethods();
206            for (Iterator iter = l.iterator(); iter.hasNext();) {
207                MethodNode f =(MethodNode) iter.next();
208                String methodName = f.getName();
209                String pName = getPropertyName(f);
210                if (pName == null) continue; 
211                if (!pName.equals(name)) continue;
212                PropertyNode var = new PropertyNode(pName,f.getModifiers(),getPropertyType(f),cn,null,null,null);
213                return var;
214            }
215    
216            l = cn.getProperties();
217            for (Iterator iter = l.iterator(); iter.hasNext();) {
218                PropertyNode f = (PropertyNode) iter.next();
219                if (f.getName().equals(name)) return f;
220            }
221            
222            Variable ret = findClassMember(cn.getSuperClass(),name);
223            if (ret!=null) return ret;
224            return findClassMember(cn.getOuterClass(),name); 
225        }
226        
227        private ClassNode getPropertyType(MethodNode m) {
228            String name = m.getName();
229            if (m.getReturnType()!=ClassHelper.VOID_TYPE) {
230                return m.getReturnType();
231            }
232            return m.getParameters()[0].getType();
233        }
234    
235        private String getPropertyName(MethodNode m) {
236            String name = m.getName();
237            if (!(name.startsWith("set") || name.startsWith("get"))) return null;
238            String pname = name.substring(3);
239            if (pname.length() == 0) return null;
240            String s = pname.substring(0, 1).toLowerCase();
241            String rest = pname.substring(1);
242            pname = s + rest;
243            
244            if (name.startsWith("get") && m.getReturnType()==ClassHelper.VOID_TYPE) {
245                return null;
246            }
247            if (name.startsWith("set") && m.getParameters().length!=1) {
248                return null;
249            }
250            return pname;
251        }     
252        
253        // -------------------------------
254        // different Variable based checks  
255        // -------------------------------
256        
257        private Variable checkVariableNameForDeclaration(String name, Expression expression) {
258            if ("super".equals(name) || "this".equals(name)) return null;
259    
260            VariableScope scope = currentScope;
261            Variable var = new DynamicVariable(name,currentScope.isInStaticContext());
262            Variable dummyStart = var;
263            // try to find a declaration of a variable
264            VariableScope dynamicScope = null;
265            while (!scope.isRoot()) {
266                if (dynamicScope==null && scope.isResolvingDynamic()) {
267                    dynamicScope = scope;
268                }
269                
270                Map declares = scope.getDeclaredVariables();
271                if (declares.get(var.getName())!=null) {
272                    var = (Variable) declares.get(var.getName());
273                    break;
274                }
275                Map localReferenced = scope.getReferencedLocalVariables(); 
276                if (localReferenced.get(var.getName())!=null) {
277                    var = (Variable) localReferenced.get(var.getName());
278                    break;
279                }
280    
281                Map classReferenced = scope.getReferencedClassVariables(); 
282                if (classReferenced.get(var.getName())!=null) {
283                    var = (Variable) classReferenced.get(var.getName());
284                    break;
285                }
286                
287                ClassNode classScope = scope.getClassScope();
288                if (classScope!=null) {
289                    Variable member = findClassMember(classScope,var.getName());
290                    if (member!=null && (currentScope.isInStaticContext() ^ member instanceof DynamicVariable)) var = member;
291                    break;
292                }            
293                scope = scope.getParent();
294            }
295    
296            VariableScope end = scope;
297    
298            if (scope.isRoot() && dynamicScope==null) {
299                // no matching scope found
300                declare(var,expression);
301                addError("The variable " + var.getName() +
302                         " is undefined in the current scope", expression);
303            } else if (scope.isRoot() && dynamicScope!=null) {
304                // no matching scope found, but there was a scope that
305                // resolves dynamic
306                scope = dynamicScope;
307            } 
308            
309            if (!scope.isRoot()) {
310                scope = currentScope;
311                while (scope != end) {
312                    Map references = null;
313                    if (end.isClassScope() || end.isRoot() || 
314                            (end.isReferencedClassVariable(name) && end.getDeclaredVariable(name)==null)) 
315                    {
316                        references = scope.getReferencedClassVariables();
317                    } else {
318                        references = scope.getReferencedLocalVariables();
319                        var.setClosureSharedVariable(var.isClosureSharedVariable() || inClosure);
320                    }
321                    references.put(var.getName(),var);
322                    scope = scope.getParent();
323                }
324                if (end.isResolvingDynamic()) {
325                    if (end.getDeclaredVariable(var.getName())==null) {
326                        end.getDeclaredVariables().put(var.getName(),var);
327                    }
328                }
329            }
330            
331            return var;
332        }
333        
334        private void checkVariableContextAccess(Variable v, Expression expr) {
335            if (v.isInStaticContext() || !currentScope.isInStaticContext()) return;        
336            
337            String msg =  v.getName()+
338                          " is declared in a dynamic context, but you tried to"+
339                          " access it from a static context.";
340            addError(msg,expr);
341            
342            // declare a static variable to be able to continue the check
343            DynamicVariable v2 = new DynamicVariable(v.getName(),currentScope.isInStaticContext());
344            currentScope.getDeclaredVariables().put(v.getName(),v2);
345        }
346        
347        // ------------------------------
348        // code visit  
349        // ------------------------------
350        
351        public void visitBlockStatement(BlockStatement block) {
352            pushState();
353            block.setVariableScope(currentScope);
354            super.visitBlockStatement(block);
355            popState();
356        }
357        
358        public void visitForLoop(ForStatement forLoop) {
359            pushState();
360            forLoop.setVariableScope(currentScope);
361            Parameter p = (Parameter) forLoop.getVariable();
362            p.setInStaticContext(currentScope.isInStaticContext());
363            declare(p, forLoop);        
364            super.visitForLoop(forLoop);
365            popState();
366        }
367    
368        public void visitDeclarationExpression(DeclarationExpression expression) {
369            // visit right side first to avoid the usage of a 
370            // variable before its declaration
371            expression.getRightExpression().visit(this);
372            // no need to visit left side, just get the variable name
373            VariableExpression vex = expression.getVariableExpression();
374            vex.setInStaticContext(currentScope.isInStaticContext());
375            declare(vex);
376            vex.setAccessedVariable(vex);
377        }
378        
379        public void visitVariableExpression(VariableExpression expression) {
380            String name = expression.getName();
381            Variable v = checkVariableNameForDeclaration(name,expression);
382            if (v==null) return;
383            expression.setAccessedVariable(v);
384            checkVariableContextAccess(v,expression);
385        }
386        
387        public void visitClosureExpression(ClosureExpression expression) {
388            pushState();
389    
390            inClosure=true;
391            // as result of the Paris meeting Closure resolves
392            // always dynamically
393            currentScope.setDynamicResolving(true);
394            
395            expression.setVariableScope(currentScope);
396    
397            if (expression.isParameterSpecified()) {
398                Parameter[] parameters = expression.getParameters();
399                for (int i = 0; i < parameters.length; i++) {
400                    parameters[i].setInStaticContext(currentScope.isInStaticContext());
401                    declare(parameters[i],expression);
402                }
403            } else if (expression.getParameters()!=null){
404                DynamicVariable var = new DynamicVariable("it",currentScope.isInStaticContext());
405                currentScope.getDeclaredVariables().put("it",var);
406            }
407    
408            super.visitClosureExpression(expression);
409            popState();
410        }
411        
412        public void visitCatchStatement(CatchStatement statement) {
413            pushState();
414            Parameter p = (Parameter) statement.getVariable();
415            p.setInStaticContext(currentScope.isInStaticContext());
416            declare(p, statement);
417            super.visitCatchStatement(statement);
418            popState();
419        }
420        
421        public void visitFieldExpression(FieldExpression expression) {
422            String name = expression.getFieldName();
423            //TODO: change that to get the correct scope
424            Variable v = checkVariableNameForDeclaration(name,expression);
425            checkVariableContextAccess(v,expression);  
426        }
427        
428        // ------------------------------
429        // class visit  
430        // ------------------------------
431        
432        public void visitClass(ClassNode node) {
433            pushState();
434            boolean dynamicMode = node.isScript();
435            currentScope.setDynamicResolving(dynamicMode);
436            currentScope.setClassScope(node);
437            
438            super.visitClass(node);
439            popState();
440        }
441    
442        protected void visitConstructorOrMethod(MethodNode node, boolean isConstructor) {
443            pushState(node.isStatic());
444            
445            node.setVariableScope(currentScope);
446            declare(node.getParameters(),node);
447            
448            super.visitConstructorOrMethod(node, isConstructor);
449            popState();
450        }
451        
452        public void visitMethodCallExpression(MethodCallExpression call) {
453            if (call.isImplicitThis() && call.getMethod() instanceof ConstantExpression) {
454                Object value = ((ConstantExpression) call.getMethod()).getText();
455                if (! (value instanceof String)) {
456                    throw new GroovyBugError("tried to make a method call with an constant as"+
457                                             " name, but the constant was no String.");
458                }
459                String methodName = (String) value;
460                    Variable v = checkVariableNameForDeclaration(methodName,call);
461                    if (v!=null && !(v instanceof DynamicVariable)) {
462                        checkVariableContextAccess(v,call);
463                    }
464            }
465            super.visitMethodCallExpression(call);
466        }
467    }