/* * Copyright 2009-2017 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.eclipse.jdt.groovy.search; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import groovyjarjarasm.asm.Opcodes; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotationNode; import org.codehaus.groovy.ast.ClassCodeVisitorSupport; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ConstructorNode; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.ModuleNode; import org.codehaus.groovy.ast.PackageNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.Variable; import org.codehaus.groovy.ast.expr.AnnotationConstantExpression; import org.codehaus.groovy.ast.expr.ArgumentListExpression; import org.codehaus.groovy.ast.expr.ArrayExpression; import org.codehaus.groovy.ast.expr.AttributeExpression; import org.codehaus.groovy.ast.expr.BinaryExpression; import org.codehaus.groovy.ast.expr.BitwiseNegationExpression; import org.codehaus.groovy.ast.expr.BooleanExpression; import org.codehaus.groovy.ast.expr.CastExpression; import org.codehaus.groovy.ast.expr.ClassExpression; import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.ClosureListExpression; 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.ElvisOperatorExpression; import org.codehaus.groovy.ast.expr.EmptyExpression; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.FieldExpression; import org.codehaus.groovy.ast.expr.GStringExpression; import org.codehaus.groovy.ast.expr.ListExpression; import org.codehaus.groovy.ast.expr.MapEntryExpression; import org.codehaus.groovy.ast.expr.MapExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression; import org.codehaus.groovy.ast.expr.MethodPointerExpression; import org.codehaus.groovy.ast.expr.NamedArgumentListExpression; import org.codehaus.groovy.ast.expr.NotExpression; import org.codehaus.groovy.ast.expr.PostfixExpression; import org.codehaus.groovy.ast.expr.PrefixExpression; import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.expr.RangeExpression; import org.codehaus.groovy.ast.expr.SpreadExpression; import org.codehaus.groovy.ast.expr.SpreadMapExpression; import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; import org.codehaus.groovy.ast.expr.TernaryExpression; import org.codehaus.groovy.ast.expr.TupleExpression; import org.codehaus.groovy.ast.expr.UnaryMinusExpression; import org.codehaus.groovy.ast.expr.UnaryPlusExpression; 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.ExpressionStatement; import org.codehaus.groovy.ast.stmt.ForStatement; import org.codehaus.groovy.ast.stmt.IfStatement; import org.codehaus.groovy.ast.stmt.ReturnStatement; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.ast.tools.WideningCategories; import org.codehaus.groovy.classgen.BytecodeExpression; import org.codehaus.groovy.runtime.MetaClassHelper; import org.codehaus.groovy.syntax.Types; import org.codehaus.groovy.transform.sc.ListOfExpressionsExpression; import org.codehaus.groovy.transform.stc.StaticTypesMarker; import org.codehaus.jdt.groovy.internal.compiler.ast.JDTResolver; import org.codehaus.jdt.groovy.model.GroovyCompilationUnit; import org.codehaus.jdt.groovy.model.ModuleNodeMapper.ModuleNodeInfo; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.groovy.core.Activator; import org.eclipse.jdt.groovy.core.util.GroovyUtils; import org.eclipse.jdt.groovy.core.util.ReflectionUtils; import org.eclipse.jdt.groovy.search.ITypeRequestor.VisitStatus; import org.eclipse.jdt.groovy.search.TypeLookupResult.TypeConfidence; import org.eclipse.jdt.groovy.search.VariableScope.CallAndType; import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner; import org.eclipse.jdt.internal.core.SourceType; import org.eclipse.jdt.internal.core.util.Util; /** * Visits {@link GroovyCompilationUnit" instances to determine the type of expressions they contain. */ public class TypeInferencingVisitorWithRequestor extends ClassCodeVisitorSupport { public static class VisitCompleted extends RuntimeException { private static final long serialVersionUID = 1L; public final VisitStatus status; public VisitCompleted(VisitStatus status) { this.status = status; } } private static class Tuple { ClassNode declaringType; ASTNode declaration; Tuple(ClassNode declaringType, ASTNode declaration) { this.declaringType = declaringType; this.declaration = declaration; } } private static void log(Throwable t, String form, Object... args) { String m = "Groovy-Eclipse Type Inferencing: " + String.format(form, args); Status s = new Status(IStatus.ERROR, Activator.PLUGIN_ID, m, t); Util.log(s); } /** * Set to true if debug mode is desired. Any exceptions will be spit to syserr. Also, after a visit, there will be a sanity * check to ensure that all stacks are empty Only set to true if using a visitor that always visits the entire file */ public boolean DEBUG = false; // shared instances for several method checks below private static final String[] NO_PARAMS = CharOperation.NO_STRINGS; private static final Parameter[] NO_PARAMETERS = Parameter.EMPTY_ARRAY; /** * We hard code the list of methods that take a closure and expect to iterate over that closure */ private static final Set<String> dgmClosureMethods = new HashSet<String>(); static { dgmClosureMethods.add("find"); dgmClosureMethods.add("each"); dgmClosureMethods.add("reverseEach"); dgmClosureMethods.add("eachWithIndex"); dgmClosureMethods.add("unique"); dgmClosureMethods.add("every"); dgmClosureMethods.add("collect"); dgmClosureMethods.add("collectEntries"); dgmClosureMethods.add("collectNested"); dgmClosureMethods.add("collectMany"); dgmClosureMethods.add("findAll"); dgmClosureMethods.add("groupBy"); dgmClosureMethods.add("groupEntriesBy"); dgmClosureMethods.add("inject"); dgmClosureMethods.add("count"); dgmClosureMethods.add("countBy"); dgmClosureMethods.add("findResult"); dgmClosureMethods.add("findResults"); dgmClosureMethods.add("grep"); dgmClosureMethods.add("split"); dgmClosureMethods.add("sum"); dgmClosureMethods.add("any"); dgmClosureMethods.add("flatten"); dgmClosureMethods.add("findIndexOf"); dgmClosureMethods.add("findIndexValues"); dgmClosureMethods.add("findLastIndexOf"); dgmClosureMethods.add("collectAll"); dgmClosureMethods.add("min"); dgmClosureMethods.add("max"); dgmClosureMethods.add("eachPermutation"); dgmClosureMethods.add("sort"); dgmClosureMethods.add("withDefault"); // these don't take collections, but can be handled in the same way dgmClosureMethods.add("identity"); dgmClosureMethods.add("times"); dgmClosureMethods.add("upto"); dgmClosureMethods.add("downto"); dgmClosureMethods.add("step"); dgmClosureMethods.add("eachFile"); dgmClosureMethods.add("eachDir"); dgmClosureMethods.add("eachFileRecurse"); dgmClosureMethods.add("eachDirRecurse"); dgmClosureMethods.add("traverse"); } // These methods have a type for the closure argument that is the same as the declaring type private static final Set<String> dgmClosureIdentityMethods = new HashSet<String>(); static { dgmClosureIdentityMethods.add("with"); dgmClosureIdentityMethods.add("addShutdownHook"); } // these methods can be called with a collection or a map. // When called with a map and there are 2 closure arguments, then // the types are the key/value of the map entry private static final Set<String> dgmClosureMaybeMap = new HashSet<String>(); static { dgmClosureMaybeMap.add("any"); dgmClosureMaybeMap.add("every"); dgmClosureMaybeMap.add("each"); dgmClosureMaybeMap.add("collect"); dgmClosureMaybeMap.add("collectEntries"); dgmClosureMaybeMap.add("findResult"); dgmClosureMaybeMap.add("findResults"); dgmClosureMaybeMap.add("findAll"); dgmClosureMaybeMap.add("groupBy"); dgmClosureMaybeMap.add("groupEntriesBy"); dgmClosureMaybeMap.add("inject"); dgmClosureMaybeMap.add("withDefault"); } // These methods have a fixed type for the closure argument private static final Map<String, ClassNode> dgmClosureMethodsMap = new HashMap<String, ClassNode>(); static { dgmClosureMethodsMap.put("eachLine", VariableScope.STRING_CLASS_NODE); dgmClosureMethodsMap.put("splitEachLine", VariableScope.STRING_CLASS_NODE); dgmClosureMethodsMap.put("withObjectOutputStream", VariableScope.OBJECT_OUTPUT_STREAM); dgmClosureMethodsMap.put("withObjectInputStream", VariableScope.OBJECT_INPUT_STREAM); dgmClosureMethodsMap.put("withDataOutputStream", VariableScope.DATA_OUTPUT_STREAM_CLASS); dgmClosureMethodsMap.put("withDataInputStream", VariableScope.DATA_INPUT_STREAM_CLASS); dgmClosureMethodsMap.put("withOutputStream", VariableScope.OUTPUT_STREAM_CLASS); dgmClosureMethodsMap.put("withInputStream", VariableScope.INPUT_STREAM_CLASS); dgmClosureMethodsMap.put("withStream", VariableScope.OUTPUT_STREAM_CLASS); dgmClosureMethodsMap.put("metaClass", ClassHelper.METACLASS_TYPE); dgmClosureMethodsMap.put("eachFileMatch", VariableScope.FILE_CLASS_NODE); dgmClosureMethodsMap.put("eachDirMatch", VariableScope.FILE_CLASS_NODE); dgmClosureMethodsMap.put("withReader", VariableScope.BUFFERED_READER_CLASS_NODE); dgmClosureMethodsMap.put("withWriter", VariableScope.BUFFERED_WRITER_CLASS_NODE); dgmClosureMethodsMap.put("withWriterAppend", VariableScope.BUFFERED_WRITER_CLASS_NODE); dgmClosureMethodsMap.put("withPrintWriter", VariableScope.PRINT_WRITER_CLASS_NODE); dgmClosureMethodsMap.put("transformChar", VariableScope.STRING_CLASS_NODE); dgmClosureMethodsMap.put("transformLine", VariableScope.STRING_CLASS_NODE); dgmClosureMethodsMap.put("filterLine", VariableScope.STRING_CLASS_NODE); dgmClosureMethodsMap.put("eachMatch", VariableScope.STRING_CLASS_NODE); } private final GroovyCompilationUnit unit; private final LinkedList<VariableScope> scopes = new LinkedList<VariableScope>(); // we are going to have to be very careful about the ordering of lookups // Simple type lookup must be last because it always returns an answer // Assume that if something returns an answer, then we go with that. // Later on, should do some ordering of results private final ITypeLookup[] lookups; private ITypeRequestor requestor; private IJavaElement enclosingElement; private ASTNode enclosingDeclarationNode; private BinaryExpression enclosingAssignment; private ConstructorCallExpression enclosingConstructorCall; /** * The head of the stack is the current property/attribute/methodcall/binary expression being visited. This stack is used so we * can keep track of the type of the object expressions in these property expressions */ private final LinkedList<ASTNode> completeExpressionStack = new LinkedList<ASTNode>(); /** * Keeps track of the type of the object expression corresponding to each frame of the property expression. */ private final LinkedList<ClassNode> primaryTypeStack = new LinkedList<ClassNode>(); /** * Keeps track of the declaring type of the current dependent expression. Dependent expressions are dependent on a primary * expression to find type information. this field is only applicable for {@link PropertyExpression}s and * {@link MethodCallExpression}s. */ private final LinkedList<Tuple> dependentDeclarationStack = new LinkedList<Tuple>(); /** * Keeps track of the type of the type of the property field corresponding to each frame of the property expression. */ private final LinkedList<ClassNode> dependentTypeStack = new LinkedList<ClassNode>(); /** * Keeps track of closures types. */ private LinkedList<Map<ClosureExpression, ClassNode>> closureTypes = new LinkedList<Map<ClosureExpression, ClassNode>>(); private final JDTResolver resolver; private final AssignmentStorer assignmentStorer = new AssignmentStorer(); /** * Keeps track of local map variables contexts. */ private Map<Variable, Map<String, ClassNode>> localMapProperties = new HashMap<Variable, Map<String, ClassNode>>(); private Variable currentMapVariable; /** * Use factory to instantiate */ TypeInferencingVisitorWithRequestor(GroovyCompilationUnit unit, ITypeLookup[] lookups) { super(); this.unit = unit; this.lookups = lookups; ModuleNodeInfo info = createModuleNode(unit); this.resolver = info != null ? info.resolver : null; this.enclosingDeclarationNode = info != null ? info.module : null; } //-------------------------------------------------------------------------- public void visitCompilationUnit(ITypeRequestor requestor) { if (enclosingDeclarationNode == null) { // no module node, can't do anything return; } this.requestor = requestor; this.enclosingElement = unit; VariableScope topLevelScope = new VariableScope(null, enclosingDeclarationNode, false); scopes.add(topLevelScope); for (ITypeLookup lookup : lookups) { if (lookup instanceof ITypeResolver) { ((ITypeResolver) lookup).setResolverInformation((ModuleNode) enclosingDeclarationNode, resolver); } lookup.initialize(unit, topLevelScope); } try { visitPackage(((ModuleNode) enclosingDeclarationNode).getPackage()); visitImports((ModuleNode) enclosingDeclarationNode); for (IType type : unit.getTypes()) { visitJDT(type, requestor); } scopes.removeLast(); } catch (VisitCompleted vc) { // can ignore } catch (Exception e) { log(e, "Error visiting types for %s", unit.getElementName()); if (DEBUG) { System.err.println("Excpetion thrown from inferencing engine"); e.printStackTrace(); } } if (DEBUG) { postVisitSanityCheck(); } } public void visitJDT(IType type, ITypeRequestor requestor) { IJavaElement oldEnclosing = enclosingElement; ASTNode oldEnclosingNode = enclosingDeclarationNode; enclosingElement = type; ClassNode node = findClassNode(createName(type)); if (node == null) { // probably some sort of AST transformation is making this node invisible return; } try { scopes.add(new VariableScope(scopes.getLast(), node, false)); enclosingDeclarationNode = node; visitClassInternal(node); try { // visitJDT so that we have the proper enclosing element for (IJavaElement child : type.getChildren()) { // filter out synthetic members for enums if (type.isEnum() && shouldFilterEnumMember(child)) { continue; } switch (child.getElementType()) { case IJavaElement.METHOD: visitJDT((IMethod) child, requestor); break; case IJavaElement.FIELD: visitJDT((IField) child, requestor); break; case IJavaElement.TYPE: visitJDT((IType) child, requestor); break; } } if (!type.isEnum()) { if (node.isScript()) { // visit fields created by @Field for (FieldNode field : node.getFields()) { if (field.getEnd() > 0) { if (field.getNameEnd() <= 0) { setNameLocation(field); } visitField(field); } } } else { // visit fields and methods relocated by @Trait @SuppressWarnings("unchecked") List<FieldNode> traitFields = (List<FieldNode>) node.getNodeMetaData("trait.fields"); if (traitFields != null) { for (FieldNode field : traitFields) { visitField(field); } } @SuppressWarnings("unchecked") List<MethodNode> traitMethods = (List<MethodNode>) node.getNodeMetaData("trait.methods"); if (traitMethods != null) { for (MethodNode method : traitMethods) { visitConstructorOrMethod(method, false); } } } // visit synthetic default constructor; this is where the object initializers are stuffed // this constructor has no JDT counterpart since it doesn't exist in the source code if (!type.getMethod(type.getElementName(), NO_PARAMS).exists()) { ConstructorNode defConstructor = findDefaultConstructor(node); if (defConstructor != null) { visitConstructorOrMethod(defConstructor, true); } } } // visit relocated @Memoized method bodies for (MethodNode method : node.getMethods()) { if (method.getName().startsWith("memoizedMethodPriv$")) { visitClassCodeContainer(method.getCode()); } } } catch (JavaModelException e) { log(e, "Error visiting children of %s", type.getFullyQualifiedName()); } } catch (VisitCompleted vc) { if (vc.status == VisitStatus.STOP_VISIT) { throw vc; } } finally { enclosingElement = oldEnclosing; enclosingDeclarationNode = oldEnclosingNode; scopes.removeLast(); } } public void visitJDT(IField field, ITypeRequestor requestor) { IJavaElement oldEnclosing = enclosingElement; ASTNode oldEnclosingNode = enclosingDeclarationNode; enclosingElement = field; this.requestor = requestor; FieldNode fieldNode = findFieldNode(field); if (fieldNode == null) { // probably some sort of AST transformation is making this node invisible return; } enclosingDeclarationNode = fieldNode; scopes.add(new VariableScope(scopes.getLast(), fieldNode, fieldNode.isStatic())); try { visitField(fieldNode); } catch (VisitCompleted vc) { if (vc.status == VisitStatus.STOP_VISIT) { throw vc; } } finally { enclosingDeclarationNode = oldEnclosingNode; enclosingElement = oldEnclosing; scopes.removeLast(); } if (isLazy(fieldNode)) { MethodNode lazyMethod = getLazyMethod(field.getElementName()); if (lazyMethod != null) { enclosingDeclarationNode = lazyMethod; scopes.add(new VariableScope(scopes.getLast(), lazyMethod, lazyMethod.isStatic())); try { visitConstructorOrMethod(lazyMethod, lazyMethod instanceof ConstructorNode); } catch (VisitCompleted vc) { if (vc.status == VisitStatus.STOP_VISIT) { throw vc; } } finally { scopes.removeLast(); enclosingElement = oldEnclosing; enclosingDeclarationNode = oldEnclosingNode; } } } } public void visitJDT(IMethod method, ITypeRequestor requestor) { IJavaElement oldEnclosing = enclosingElement; ASTNode oldEnclosingNode = enclosingDeclarationNode; enclosingElement = method; MethodNode methodNode = findMethodNode(method); if (methodNode == null) { // probably some sort of AST transformation is making this node invisible return; } enclosingDeclarationNode = methodNode; this.requestor = requestor; scopes.add(new VariableScope(scopes.getLast(), methodNode, methodNode.isStatic())); try { visitConstructorOrMethod(methodNode, method.isConstructor()); // check for anonymous inner types IJavaElement[] children = method.getChildren(); for (IJavaElement child : children) { if (child.getElementType() == IJavaElement.TYPE) { visitJDT((IType) child, requestor); } } } catch (VisitCompleted vc) { if (vc.status == VisitStatus.STOP_VISIT) { throw vc; } } catch (Exception e) { log(e, "Error visiting method %s in class %s", method.getElementName(), method.getParent().getElementName()); } finally { enclosingElement = oldEnclosing; enclosingDeclarationNode = oldEnclosingNode; scopes.removeLast(); } } // @Override public void visitPackage(PackageNode node) { if (node != null) { visitAnnotations(node); TypeLookupResult result = new TypeLookupResult(null, null, node, TypeConfidence.EXACT, null); VisitStatus status = notifyRequestor(node, requestor, result); if (status == VisitStatus.STOP_VISIT) { throw new VisitCompleted(status); } } } @Override public void visitImports(ModuleNode node) { for (ImportNode imp : GroovyUtils.getAllImportNodes(node)) { IJavaElement oldEnclosingElement = enclosingElement; visitAnnotations(imp); // this will not work for static or * imports, but that's OK because // as of now, there is no reason to do that. ClassNode type = imp.getType(); if (type != null) { String importName = imp.getClassName().replace('$', '.') + (imp.getFieldName() != null ? "." + imp.getFieldName() : ""); enclosingElement = unit.getImport(importName); if (!enclosingElement.exists()) { enclosingElement = oldEnclosingElement; } } try { TypeLookupResult result = null; VariableScope scope = scopes.getLast(); scope.setPrimaryNode(false); assignmentStorer.storeImport(imp, scope); for (ITypeLookup lookup : lookups) { TypeLookupResult candidate = lookup.lookupType(imp, scope); if (candidate != null) { if (result == null || result.confidence.isLessThan(candidate.confidence)) { result = candidate; } if (result.confidence.isAtLeast(TypeConfidence.INFERRED)) { break; } } } VisitStatus status = notifyRequestor(imp, requestor, result); switch (status) { case CONTINUE: try { if (type != null) { visitClassReference(type); completeExpressionStack.add(imp); if (imp.getFieldNameExpr() != null) { primaryTypeStack.add(type); imp.getFieldNameExpr().visit(this); dependentDeclarationStack.removeLast(); dependentTypeStack.removeLast(); } completeExpressionStack.removeLast(); } } catch (VisitCompleted e) { if (e.status == VisitStatus.STOP_VISIT) { throw e; } } case CANCEL_MEMBER: continue; case CANCEL_BRANCH: // assume that import statements are not interesting return; case STOP_VISIT: throw new VisitCompleted(status); } } finally { enclosingElement = oldEnclosingElement; } } } private void visitClassInternal(ClassNode node) { if (resolver != null) { resolver.currentClass = node; } VariableScope scope = scopes.getLast(); scope.addVariable("this", node, node); visitAnnotations(node); TypeLookupResult result = new TypeLookupResult(node, node, node, TypeConfidence.EXACT, scope); VisitStatus status = notifyRequestor(node, requestor, result); switch (status) { case CONTINUE: break; case CANCEL_BRANCH: return; case CANCEL_MEMBER: case STOP_VISIT: throw new VisitCompleted(status); } if (!node.isEnum()) { visitGenericTypes(node); visitClassReference(node.getUnresolvedSuperClass()); } for (ClassNode face : node.getInterfaces()) { visitClassReference(face); } // TODO: Should all methods w/o peer in JDT model have their bodies visited? Below are two cases in particular. if (isMetaAnnotation(node)) { // visit relocated @AnnotationCollector annotations MethodNode value = node.getMethod("value", NO_PARAMETERS); if (value != null && value.getEnd() < 1) { visitClassCodeContainer(value.getCode()); } } // visit <clinit> body because this is where static field initializers are placed // only visit field initializers here. // it is important here to get the right variable scope for the initializer. // need to ensure that the field is one of the enclosing nodes MethodNode clinit = node.getMethod("<clinit>", NO_PARAMETERS); if (clinit != null && clinit.getCode() instanceof BlockStatement) { for (Statement element : (Iterable<Statement>) ((BlockStatement) clinit.getCode()).getStatements()) { // only visit the static initialization of a field if (element instanceof ExpressionStatement && ((ExpressionStatement) element).getExpression() instanceof BinaryExpression) { BinaryExpression bexpr = (BinaryExpression) ((ExpressionStatement) element).getExpression(); if (bexpr.getLeftExpression() instanceof FieldExpression) { FieldNode f = ((FieldExpression) bexpr.getLeftExpression()).getField(); if (f != null && f.isStatic() && bexpr.getRightExpression() != null) { // create the field scope so that it looks like we are visiting within the context of the field VariableScope fieldScope = new VariableScope(scope, f, true); scopes.add(fieldScope); try { bexpr.getRightExpression().visit(this); } finally { scopes.removeLast(); } } } } } } // I'm not actually sure that there will be anything here. I think these will all be moved to a constructor for (Statement element : node.getObjectInitializerStatements()) { element.visit(this); } // visit synthetic no-arg constructors because that's where the non-static initializers are for (ConstructorNode constructor : node.getDeclaredConstructors()) { if (constructor.isSynthetic() && (constructor.getParameters() == null || constructor.getParameters().length == 0)) { visitConstructor(constructor); } } // don't visit contents, the visitJDT methods are used instead } private void visitClassReference(ClassNode node) { TypeLookupResult result = null; VariableScope scope = scopes.getLast(); for (ITypeLookup lookup : lookups) { TypeLookupResult candidate = lookup.lookupType(node, scope); if (candidate != null) { if (result == null || result.confidence.isLessThan(candidate.confidence)) { result = candidate; } if (result.confidence.isAtLeast(TypeConfidence.INFERRED)) { break; } } } scope.setPrimaryNode(false); VisitStatus status = notifyRequestor(node, requestor, result); switch (status) { case CONTINUE: if (!node.isEnum()) { visitGenericTypes(node); } // fall through case CANCEL_BRANCH: return; case CANCEL_MEMBER: case STOP_VISIT: throw new VisitCompleted(status); } } // @Override public void visitAnnotation(AnnotationNode node) { TypeLookupResult result = null; VariableScope scope = scopes.getLast(); for (ITypeLookup lookup : lookups) { TypeLookupResult candidate = lookup.lookupType(node, scope); if (candidate != null) { if (result == null || result.confidence.isLessThan(candidate.confidence)) { result = candidate; } if (result.confidence.isAtLeast(TypeConfidence.INFERRED)) { break; } } } VisitStatus status = notifyRequestor(node, requestor, result); switch (status) { case CONTINUE: // visit annotation label visitClassReference(node.getClassNode()); // visit attribute values super.visitAnnotation(node); // visit attribute labels for (String name : node.getMembers().keySet()) { MethodNode meth = node.getClassNode().getMethod(name, NO_PARAMETERS); ASTNode attr; TypeLookupResult noLookup; if (meth != null) { attr = meth; // no Groovy AST node exists for name noLookup = new TypeLookupResult(meth.getReturnType(), node.getClassNode().redirect(), meth, TypeConfidence.EXACT, scope); } else { attr = new ConstantExpression(name); // this is very rough; it only works for an attribute that directly follows '(' attr.setStart(node.getEnd() + 2); attr.setEnd(attr.getStart() + name.length()); noLookup = new TypeLookupResult(ClassHelper.VOID_TYPE, node.getClassNode().redirect(), null, TypeConfidence.UNKNOWN, scope); } noLookup.enclosingAnnotation = node; // set context for requestor status = notifyRequestor(attr, requestor, noLookup); if (status != VisitStatus.CONTINUE) break; } break; case CANCEL_BRANCH: return; case CANCEL_MEMBER: case STOP_VISIT: throw new VisitCompleted(status); } } @Override public void visitArgumentlistExpression(ArgumentListExpression node) { CallAndType callAndType = scopes.getLast().getEnclosingMethodCallExpression(); boolean closureFound = false; if (callAndType != null && callAndType.declaration instanceof MethodNode) { Map<ClosureExpression, ClassNode> map = new HashMap<ClosureExpression, ClassNode>(); MethodNode methodNode = (MethodNode) callAndType.declaration; if (node.getExpressions().size() == methodNode.getParameters().length) { for (int i = 0; i < node.getExpressions().size(); i++) { if (node.getExpression(i) instanceof ClosureExpression) { map.put((ClosureExpression) node.getExpression(i), methodNode.getParameters()[i].getType()); closureFound = true; } } } if (closureFound) { closureTypes.addLast(map); } } visitTupleExpression(node); if (closureFound) { closureTypes.removeLast(); } } @Override public void visitArrayExpression(ArrayExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitArrayExpression(node); } } @Override public void visitAttributeExpression(AttributeExpression node) { visitPropertyExpression(node); } @Override public void visitBinaryExpression(BinaryExpression node) { if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } // don't visit binary expressions in a constructor that have no source location. // the reason is that these were copied from the field initializer. // we want to visit them under the field initializer, not the construcor if (node instanceof DeclarationExpression && node.getEnd() == 0) { return; } visitAnnotations(node); boolean isAssignment = node.getOperation().getType() == Types.EQUALS; BinaryExpression oldEnclosingAssignment = enclosingAssignment; if (isAssignment) { enclosingAssignment = node; } completeExpressionStack.add(node); // visit order is dependent on whether or not assignment statement Expression toVisitPrimary; Expression toVisitDependent; if (isAssignment) { toVisitPrimary = node.getRightExpression(); toVisitDependent = node.getLeftExpression(); } else { toVisitPrimary = node.getLeftExpression(); toVisitDependent = node.getRightExpression(); } toVisitPrimary.visit(this); ClassNode primaryExprType = primaryTypeStack.removeLast(); if (isAssignment) { assignmentStorer.storeAssignment(node, scopes.getLast(), primaryExprType); } toVisitDependent.visit(this); completeExpressionStack.removeLast(); // type of the entire expression ClassNode completeExprType = primaryExprType; ClassNode dependentExprType = primaryTypeStack.removeLast(); // TODO: Is it an illegal state to have either as null? assert primaryExprType != null && dependentExprType != null; if (!isAssignment && primaryExprType != null && dependentExprType != null) { // type of RHS of binary expression // find the type of the complete expression String associatedMethod = findBinaryOperatorName(node.getOperation().getText()); if (isArithmeticOperationOnNumberOrStringOrList(node.getOperation().getText(), primaryExprType, dependentExprType)) { // in 1.8 and later, Groovy will not go through the MOP for standard arithmetic operations on numbers completeExprType = dependentExprType.equals(VariableScope.STRING_CLASS_NODE) ? VariableScope.STRING_CLASS_NODE : primaryExprType; } else if (associatedMethod != null) { scopes.getLast().setMethodCallArgumentTypes(Collections.singletonList(dependentExprType)); // there is an overloadable method associated with this operation; convert to a constant expression and look it up TypeLookupResult result = lookupExpressionType(new ConstantExpression(associatedMethod), primaryExprType, false, scopes.getLast()); if (result.confidence != TypeConfidence.UNKNOWN) completeExprType = result.type; // special case DefaultGroovyMethods.getAt -- the problem is that DGM has too many variants of getAt if (associatedMethod.equals("getAt") && result.declaringType.equals(VariableScope.DGM_CLASS_NODE)) { if (primaryExprType.getName().equals("java.util.BitSet")) { completeExprType = VariableScope.BOOLEAN_CLASS_NODE; } else { GenericsType[] lhsGenericsTypes = primaryExprType.getGenericsTypes(); ClassNode elementType; if (VariableScope.MAP_CLASS_NODE.equals(primaryExprType) && lhsGenericsTypes != null && lhsGenericsTypes.length == 2) { // for maps use the value type elementType = lhsGenericsTypes[1].getType(); } else { elementType = VariableScope.extractElementType(primaryExprType); } if (dependentExprType.isArray() || dependentExprType.implementsInterface(VariableScope.LIST_CLASS_NODE) || dependentExprType.getName().equals(VariableScope.LIST_CLASS_NODE.getName())) { // if rhs is a range or list type, then result is a list of elements completeExprType = createParameterizedList(elementType); } else if (ClassHelper.isNumberType(dependentExprType)) { // if rhs is a number type, then result is a single element completeExprType = elementType; } } } } else { // no overloadable associated method completeExprType = findBinaryExpressionType(node.getOperation().getText(), primaryExprType, dependentExprType); } } handleCompleteExpression(node, completeExprType, null); enclosingAssignment = oldEnclosingAssignment; } @Override public void visitBitwiseNegationExpression(BitwiseNegationExpression node) { visitUnaryExpression(node, node.getExpression(), "~"); } @Override public void visitBlockStatement(BlockStatement block) { scopes.add(new VariableScope(scopes.getLast(), block, false)); boolean shouldContinue = handleStatement(block); if (shouldContinue) { super.visitBlockStatement(block); } scopes.removeLast(); } @Override public void visitBooleanExpression(BooleanExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitBooleanExpression(node); } } @Override public void visitBytecodeExpression(BytecodeExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitBytecodeExpression(node); } } @Override public void visitCastExpression(CastExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { visitClassReference(node.getType()); super.visitCastExpression(node); } } @Override public void visitCatchStatement(CatchStatement node) { scopes.add(new VariableScope(scopes.getLast(), node, false)); Parameter param = node.getVariable(); if (param != null) { handleParameterList(new Parameter[] {param}); } super.visitCatchStatement(node); scopes.removeLast(); } @Override public void visitClassExpression(ClassExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { visitClassReference(node.getType()); super.visitClassExpression(node); } } @Override public void visitClosureExpression(ClosureExpression node) { VariableScope parent = scopes.getLast(); VariableScope scope = new VariableScope(parent, node, false); scopes.add(scope); ClassNode[] inferredParamTypes = inferClosureParamTypes(scope, node); Parameter[] parameters = node.getParameters(); final int n; if (parameters != null && (n = parameters.length) > 0) { handleParameterList(parameters); // TODO: If this is moved above handleParameterList, the inferred type will be available to the param nodes as well. for (int i = 0; i < n; i += 1) { Parameter parameter = parameters[i]; // only reset the type of the parametrers if it is not explicitly defined if (inferredParamTypes[i] != VariableScope.OBJECT_CLASS_NODE && parameter.isDynamicTyped()) { parameter.setType(inferredParamTypes[i]); scope.addVariable(parameter); } } } else // it variable only exists if there are no explicit parameters if (inferredParamTypes[0] != VariableScope.OBJECT_CLASS_NODE && !scope.containsInThisScope("it")) { scope.addVariable("it", inferredParamTypes[0], VariableScope.OBJECT_CLASS_NODE); } if (scope.lookupNameInCurrentScope("it") == null) { inferItType(node, scope); } // Delegate is the declaring type of the enclosing call if one exists, or it is 'this' CallAndType cat = scope.getEnclosingMethodCallExpression(); if (cat != null) { ClassNode declaringType = cat.declaringType; if (cat.delegatesToClosures.containsKey(node)) { declaringType = cat.delegatesToClosures.get(node); } scope.addVariable("delegate", declaringType, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("getDelegate", declaringType, VariableScope.CLOSURE_CLASS_NODE); } else { ClassNode thisType = scope.getThis(); // GRECLIPSE-1348 someone is silly enough to have a variable named "delegate". // don't override that if (scope.lookupName("delegate") == null) { scope.addVariable("delegate", thisType, VariableScope.CLOSURE_CLASS_NODE); } scope.addVariable("getDelegate", thisType, VariableScope.CLOSURE_CLASS_NODE); } // Owner is 'this' if no enclosing closure, or 'Closure' if there is if (parent.getEnclosingClosure() != null) { scope.addVariable("getOwner", VariableScope.CLOSURE_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("owner", VariableScope.CLOSURE_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); } else { ClassNode thisType = scope.getThis(); // GRECLIPSE-1348 someone is silly enough to have a variable named "owner". // don't override that if (scope.lookupName("owner") == null) { scope.addVariable("owner", thisType, VariableScope.CLOSURE_CLASS_NODE); } scope.addVariable("getOwner", thisType, VariableScope.CLOSURE_CLASS_NODE); // only do this if we are not already in a closure; no need to add twice scope.addVariable("thisObject", VariableScope.OBJECT_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("getThisObject", VariableScope.OBJECT_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("resolveStategy", VariableScope.INTEGER_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("getResolveStategy", VariableScope.INTEGER_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("directive", VariableScope.INTEGER_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("getDirective", VariableScope.INTEGER_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("maximumNumberOfParameters", VariableScope.INTEGER_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("getMaximumNumberOfParameters", VariableScope.INTEGER_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("parameterTypes", VariableScope.CLASS_ARRAY_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); scope.addVariable("getParameterTypes", VariableScope.CLASS_ARRAY_CLASS_NODE, VariableScope.CLOSURE_CLASS_NODE); } super.visitClosureExpression(node); handleSimpleExpression(node); scopes.removeLast(); } @Override public void visitClosureListExpression(ClosureListExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitClosureListExpression(node); } } @Override public void visitConstantExpression(ConstantExpression node) { if (node instanceof AnnotationConstantExpression) { visitClassReference(node.getType()); } scopes.getLast().setCurrentNode(node); handleSimpleExpression(node); scopes.getLast().forgetCurrentNode(); super.visitConstantExpression(node); } @Override public void visitConstructorCallExpression(ConstructorCallExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { visitClassReference(node.getType()); if (node.getArguments() instanceof TupleExpression) { TupleExpression tuple = (TupleExpression) node.getArguments(); if (isNotEmpty(tuple.getExpressions())) { if ((tuple.getExpressions().size() == 1 && tuple.getExpressions().get(0) instanceof MapExpression) || tuple.getExpression(tuple.getExpressions().size() - 1) instanceof NamedArgumentListExpression) { // remember this is a map ctor call, so that field names can be inferred when visiting the map enclosingConstructorCall = node; } } } super.visitConstructorCallExpression(node); } } @Override public void visitConstructorOrMethod(MethodNode node, boolean isConstructor) { TypeLookupResult result = null; VariableScope scope = scopes.getLast(); for (ITypeLookup lookup : lookups) { TypeLookupResult candidate = lookup.lookupType(node, scope); if (candidate != null) { if (result == null || result.confidence.isLessThan(candidate.confidence)) { result = candidate; } if (result.confidence.isAtLeast(TypeConfidence.INFERRED)) { break; } } } scope.setPrimaryNode(false); VisitStatus status = notifyRequestor(node, requestor, result); switch (status) { case CONTINUE: GenericsType[] gens = node.getGenericsTypes(); if (gens != null) { for (GenericsType gen : gens) { if (gen.getLowerBound() != null) { visitClassReference(gen.getLowerBound()); } if (gen.getUpperBounds() != null) { for (ClassNode upper : gen.getUpperBounds()) { visitClassReference(upper); } } if (gen.getType() != null && gen.getType().getName().charAt(0) != '?') { visitClassReference(gen.getType()); } } } visitClassReference(node.getReturnType()); if (node.getExceptions() != null) { for (ClassNode e : node.getExceptions()) { visitClassReference(e); } } if (handleParameterList(node.getParameters())) { super.visitConstructorOrMethod(node, isConstructor); } // fall through case CANCEL_BRANCH: return; case CANCEL_MEMBER: case STOP_VISIT: throw new VisitCompleted(status); } } @Override public void visitDeclarationExpression(DeclarationExpression node) { // this is ok. the variable expression is visited appropriately visitBinaryExpression(node); } @Override public void visitEmptyExpression(EmptyExpression node) { handleSimpleExpression(node); } @Override public void visitFieldExpression(FieldExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitFieldExpression(node); } } @Override public void visitField(FieldNode node) { TypeLookupResult result = null; VariableScope scope = scopes.getLast(); assignmentStorer.storeField(node, scope); for (ITypeLookup lookup : lookups) { TypeLookupResult candidate = lookup.lookupType(node, scope); if (candidate != null) { if (result == null || result.confidence.isLessThan(candidate.confidence)) { result = candidate; } if (result.confidence.isAtLeast(TypeConfidence.INFERRED)) { break; } } } scope.setPrimaryNode(false); VisitStatus status = notifyRequestor(node, requestor, result); switch (status) { case CONTINUE: ClassNode fieldType = node.getType(); // if two values are == then that means the type // is synthetic and doesn't exist in code // probably an enum field. if (fieldType != node.getDeclaringClass()) { visitClassReference(fieldType); } visitAnnotations(node); Expression init = node.getInitialExpression(); if (init != null) { init.visit(this); } case CANCEL_BRANCH: return; case CANCEL_MEMBER: case STOP_VISIT: throw new VisitCompleted(status); } } @Override public void visitForLoop(ForStatement node) { completeExpressionStack.add(node); node.getCollectionExpression().visit(this); completeExpressionStack.removeLast(); // the type of the collection ClassNode collectionType = primaryTypeStack.removeLast(); // the loop has its own scope scopes.add(new VariableScope(scopes.getLast(), node, false)); // a three-part for loop, i.e. "for (_; _; _)", uses a dummy variable; skip it if (!(node.getCollectionExpression() instanceof ClosureListExpression)) { Parameter param = node.getVariable(); if (param.isDynamicTyped()) { // update the type of the parameter from the collection type scopes.getLast().addVariable(param.getName(), VariableScope.extractElementType(collectionType), null); } handleParameterList(new Parameter[] {param}); } node.getLoopBlock().visit(this); scopes.removeLast(); } private void visitGenericTypes(ClassNode node) { if (node.isUsingGenerics() && node.getGenericsTypes() != null) { for (GenericsType gen : node.getGenericsTypes()) { if (gen.getType() != null && gen.getName().charAt(0) != '?') { visitClassReference(gen.getType()); } if (gen.getLowerBound() != null) { visitClassReference(gen.getLowerBound()); } else if (gen.getUpperBounds() != null) { for (ClassNode upper : gen.getUpperBounds()) { // handle enums where the upper bound is the same as the type if (!upper.getName().equals(node.getName())) { visitClassReference(upper); } } } } } } @Override public void visitGStringExpression(GStringExpression node) { scopes.getLast().setCurrentNode(node); boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitGStringExpression(node); } scopes.getLast().forgetCurrentNode(); } @Override public void visitIfElse(IfStatement node) { BooleanExpression expr = node.getBooleanExpression(); expr.visit(this); VariableScope s = null; // check for "if (x instanceof y) { ... }" flow typing if (!(expr instanceof NotExpression) && expr.getExpression() instanceof BinaryExpression) { BinaryExpression b = (BinaryExpression) expr.getExpression(); if (b.getOperation().getType() == Types.KEYWORD_INSTANCEOF && b.getLeftExpression() instanceof VariableExpression) { VariableExpression v = (VariableExpression) b.getLeftExpression(); VariableScope.VariableInfo i = scopes.getLast().lookupName(v.getName()); if (i != null && GroovyUtils.isAssignable(b.getRightExpression().getType(), i.type)) { s = new VariableScope(scopes.getLast(), i.scopeNode /* use the same node */, false); s.addVariable(v.getName(), b.getRightExpression().getType(), v.getDeclaringClass()); scopes.add(s); } } } node.getIfBlock().visit(this); if (s != null) scopes.removeLast(); // TODO: else, check for "if (!(x instanceof y)) { ... } else { ... }" and adjust scope of else block node.getElseBlock().visit(this); } @Override public void visitListExpression(ListExpression node) { if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } scopes.getLast().setCurrentNode(node); completeExpressionStack.add(node); super.visitListExpression(node); ClassNode eltType; if (node.getExpressions().size() > 0) { eltType = primaryTypeStack.removeLast(); } else { eltType = VariableScope.OBJECT_CLASS_NODE; } completeExpressionStack.removeLast(); ClassNode exprType = createParameterizedList(eltType); handleCompleteExpression(node, exprType, null); scopes.getLast().forgetCurrentNode(); } @Override public void visitMapEntryExpression(MapEntryExpression node) { if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } scopes.getLast().setCurrentNode(node); completeExpressionStack.add(node); node.getKeyExpression().visit(this); ClassNode k = primaryTypeStack.removeLast(); node.getValueExpression().visit(this); ClassNode v = primaryTypeStack.removeLast(); completeExpressionStack.removeLast(); ClassNode exprType; if (isPrimaryExpression(node)) { exprType = createParameterizedMap(k, v); } else { exprType = VariableScope.OBJECT_CLASS_NODE; } handleCompleteExpression(node, exprType, null); scopes.getLast().forgetCurrentNode(); } @Override public void visitMapExpression(MapExpression node) { if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } ClassNode ctorType = null; if (enclosingConstructorCall != null) { // map expr within ctor call indicates use of map constructor or default constructor + property setters ctorType = enclosingConstructorCall.getType(); enclosingConstructorCall = null; for (ConstructorNode ctor : ctorType.getDeclaredConstructors()) { Parameter[] ctorParams = ctor.getParameters(); // TODO: What about ctorParams[0].getType().declaresInterface(VariableScope.MAP_CLASS_NODE)? // TODO: Do the generics of the Map matter? Probably should be String (or Object?) for key. if (ctorParams.length == 1 && ctorParams[0].getType().equals(VariableScope.MAP_CLASS_NODE)) { ctorType = null; // a map constructor exists; shut down key type lookups } } } scopes.getLast().setCurrentNode(node); completeExpressionStack.add(node); for (MapEntryExpression entry : node.getMapEntryExpressions()) { Expression key = entry.getKeyExpression(), val = entry.getValueExpression(); if (ctorType != null && key instanceof ConstantExpression && !"*".equals(key.getText())) { VariableScope scope = scopes.getLast(); // look for a non-synthetic setter followed by a property or field scope.setMethodCallArgumentTypes(Collections.singletonList(val.getType())); String setterName = AccessorSupport.SETTER.createAccessorName(key.getText()); TypeLookupResult result = lookupExpressionType(new ConstantExpression(setterName), ctorType, false, scope); if (result.confidence == TypeConfidence.UNKNOWN || !(result.declaration instanceof MethodNode) || ((MethodNode) result.declaration).isSynthetic()) { scope.getWormhole().put("lhs", key); scope.setMethodCallArgumentTypes(null); result = lookupExpressionType(key, ctorType, false, scope); } // pre-visit entry so keys are highlighted as keys, not fields/methods/properties ClassNode mapType = isPrimaryExpression(entry) ? createParameterizedMap(key.getType(), val.getType()) : primaryTypeStack.getLast(); scope.setCurrentNode(entry); handleCompleteExpression(entry, mapType, null); scope.forgetCurrentNode(); handleRequestor(key, ctorType, result); val.visit(this); } else { entry.visit(this); } } completeExpressionStack.removeLast(); ClassNode exprType; if (isNotEmpty(node.getMapEntryExpressions())) { exprType = primaryTypeStack.removeLast(); } else { exprType = createParameterizedMap(VariableScope.OBJECT_CLASS_NODE, VariableScope.OBJECT_CLASS_NODE); } handleCompleteExpression(node, exprType, null); scopes.getLast().forgetCurrentNode(); } @Override public void visitMethodCallExpression(MethodCallExpression node) { scopes.getLast().setCurrentNode(node); if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } completeExpressionStack.add(node); node.getObjectExpression().visit(this); if (node.isSpreadSafe()) { // must find the component type of the object expression type ClassNode objType = primaryTypeStack.removeLast(); primaryTypeStack.add(VariableScope.extractElementType(objType)); } node.getMethod().visit(this); // this is the inferred return type of this method // must pop now before visiting any other nodes ClassNode returnType = dependentTypeStack.removeLast(); // this is the inferred declaring type of this method Tuple t = dependentDeclarationStack.removeLast(); CallAndType call = new CallAndType(node, t.declaringType, t.declaration); completeExpressionStack.removeLast(); ClassNode catNode = isCategoryDeclaration(node); if (catNode != null) { scopes.getLast().setCategoryBeingDeclared(catNode); } VariableScope scope = scopes.getLast(); // remember that we are inside a method call while analyzing the arguments scope.addEnclosingMethodCall(call); node.getArguments().visit(this); scope.forgetEnclosingMethodCall(); // if this method call is the primary of a larger expression, then pass the inferred type onwards if (node.isSpreadSafe()) { returnType = createParameterizedList(returnType); } // check for trait field re-written as call to helper method Expression expr = GroovyUtils.getTraitFieldExpression(node); if (expr != null) { handleSimpleExpression(expr); postVisit(node, returnType, t.declaringType, node); } else { handleCompleteExpression(node, returnType, t.declaringType); } scopes.getLast().forgetCurrentNode(); } @Override public void visitMethodPointerExpression(MethodPointerExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { completeExpressionStack.add(node); primaryTypeStack.add(// method src node.getExpression().getType()); super.visitMethodPointerExpression(node); // clean up the stacks completeExpressionStack.removeLast(); ClassNode returnType = dependentTypeStack.removeLast(); Tuple callParamTypes = dependentDeclarationStack.removeLast(); // try to set Closure generics if (!primaryTypeStack.isEmpty() && callParamTypes.declaration != null) { GroovyUtils.updateClosureWithInferredTypes(primaryTypeStack.getLast(), returnType, ((MethodNode) callParamTypes.declaration).getParameters()); } } } @Override public void visitNotExpression(NotExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitNotExpression(node); } } @Override public void visitPostfixExpression(PostfixExpression node) { visitUnaryExpression(node, node.getExpression(), node.getOperation().getText()); } @Override public void visitPrefixExpression(PrefixExpression node) { visitUnaryExpression(node, node.getExpression(), node.getOperation().getText()); } @Override public void visitPropertyExpression(PropertyExpression node) { scopes.getLast().setCurrentNode(node); completeExpressionStack.add(node); node.getObjectExpression().visit(this); ClassNode objType; if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } if (node.isSpreadSafe()) { objType = primaryTypeStack.removeLast(); // must find the component type of the object expression type primaryTypeStack.add(objType = VariableScope.extractElementType(objType)); } else { objType = primaryTypeStack.getLast(); } if (VariableScope.MAP_CLASS_NODE.equals(objType) && node.getObjectExpression() instanceof VariableExpression && node.getProperty() instanceof ConstantExpression) { currentMapVariable = ((VariableExpression) node.getObjectExpression()).getAccessedVariable(); Map<String, ClassNode> map = localMapProperties.get(currentMapVariable); if (map == null) { map = new HashMap<String, ClassNode>(); localMapProperties.put(currentMapVariable, map); } if (enclosingAssignment != null) { //String key = ((ConstantExpression) node.getProperty()).getConstantName(); String key = (String) ((ConstantExpression) node.getProperty()).getValue(); ClassNode val = enclosingAssignment.getRightExpression().getType(); map.put(key, val); } } node.getProperty().visit(this); currentMapVariable = null; // this is the type of this property expression ClassNode exprType = dependentTypeStack.removeLast(); // don't care about either of these dependentDeclarationStack.removeLast(); completeExpressionStack.removeLast(); // if this property expression is the primary of a larger expression, // then remember the inferred type if (node.isSpreadSafe()) { // if we are dealing with a map, then a spread dot will return a list of values, // so use the type of the value. if (objType.equals(VariableScope.MAP_CLASS_NODE) && objType.getGenericsTypes() != null && objType.getGenericsTypes().length == 2) { exprType = objType.getGenericsTypes()[1].getType(); } exprType = createParameterizedList(exprType); } handleCompleteExpression(node, exprType, null); scopes.getLast().forgetCurrentNode(); } @Override public void visitRangeExpression(RangeExpression node) { if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } scopes.getLast().setCurrentNode(node); completeExpressionStack.add(node); super.visitRangeExpression(node); ClassNode eltType = primaryTypeStack.removeLast(); completeExpressionStack.removeLast(); ClassNode rangeType = createParameterizedRange(eltType); handleCompleteExpression(node, rangeType, null); scopes.getLast().forgetCurrentNode(); } @Override public void visitReturnStatement(ReturnStatement node) { boolean shouldContinue = handleStatement(node); if (shouldContinue) { ClosureExpression closure = scopes.getLast().getEnclosingClosure(); if (closure == null || closure.getNodeMetaData(StaticTypesMarker.INFERRED_TYPE) != null) { super.visitReturnStatement(node); } else { // capture return type completeExpressionStack.add(node); super.visitReturnStatement(node); completeExpressionStack.removeLast(); ClassNode returnType = primaryTypeStack.removeLast(); ClassNode closureType = (ClassNode) closure.putNodeMetaData("returnType", returnType); if (closureType != null && !closureType.equals(returnType)) { closure.putNodeMetaData("returnType", WideningCategories.lowestUpperBound(closureType, returnType)); } } } } @Override public void visitShortTernaryExpression(ElvisOperatorExpression node) { if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } // arbitrarily, we choose the if clause to be the type of this expression completeExpressionStack.add(node); node.getTrueExpression().visit(this); // the declaration itself is the property node ClassNode exprType = primaryTypeStack.removeLast(); completeExpressionStack.removeLast(); node.getFalseExpression().visit(this); handleCompleteExpression(node, exprType, null); } @Override public void visitSpreadExpression(SpreadExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitSpreadExpression(node); } } @Override public void visitSpreadMapExpression(SpreadMapExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { super.visitSpreadMapExpression(node); } } @Override public void visitStaticMethodCallExpression(StaticMethodCallExpression node) { if (isPrimaryExpression(node)) { visitMethodCallExpression(new MethodCallExpression( new ClassExpression(node.getOwnerType()), node.getMethod(), node.getArguments())); } boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue) { boolean isPresentInSource = (node.getEnd() > 0); if (isPresentInSource) visitClassReference(node.getOwnerType()); if (isPresentInSource || isEnumInit(node)) super.visitStaticMethodCallExpression(node); } } @Override public void visitTernaryExpression(TernaryExpression node) { if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } completeExpressionStack.add(node); node.getBooleanExpression().visit(this); // arbitrarily, we choose the if clause to be the type of this expression node.getTrueExpression().visit(this); // arbirtrarily choose the 'true' expression // to hold the type of the ternary expression ClassNode exprType = primaryTypeStack.removeLast(); node.getFalseExpression().visit(this); completeExpressionStack.removeLast(); // if the ternary expression is a primary expression // of a larger expression, use the exprType as the // primary of the next expression handleCompleteExpression(node, exprType, null); } /** * <ul> * <li> argument list (named or positional) * <li> chained assignment, as in: {@code a = b = 1} * <li> multi-assignment, as in: {@code def (a, b) = [1, 2]} * </ul> */ @Override public void visitTupleExpression(TupleExpression node) { boolean shouldContinue = handleSimpleExpression(node); if (shouldContinue && isNotEmpty(node.getExpressions())) { // prevent revisit of statically-compiled chained assignment nodes if (node instanceof ArgumentListExpression || node.getExpression(node.getExpressions().size() - 1) instanceof NamedArgumentListExpression) { super.visitTupleExpression(node); } } } private void visitUnaryExpression(Expression node, Expression expression, String operation) { scopes.getLast().setCurrentNode(node); completeExpressionStack.add(node); if (isDependentExpression(node)) { primaryTypeStack.removeLast(); } expression.visit(this); ClassNode primaryType = primaryTypeStack.removeLast(); // now infer the type of the operator. It could have been overloaded String associatedMethod = findUnaryOperatorName(operation); ClassNode completeExprType; if (associatedMethod == null && primaryType.equals(VariableScope.NUMBER_CLASS_NODE) || ClassHelper.getWrapper(primaryType).isDerivedFrom(VariableScope.NUMBER_CLASS_NODE)) { completeExprType = primaryType; } else { // there is an overloadable method associated with this operation // convert to a constant expression and infer type TypeLookupResult result = lookupExpressionType( new ConstantExpression(associatedMethod), primaryType, false, scopes.getLast()); completeExprType = result.type; } completeExpressionStack.removeLast(); handleCompleteExpression(node, completeExprType, null); } @Override public void visitUnaryMinusExpression(UnaryMinusExpression node) { visitUnaryExpression(node, node.getExpression(), "-"); } @Override public void visitUnaryPlusExpression(UnaryPlusExpression node) { visitUnaryExpression(node, node.getExpression(), "+"); } @Override public void visitVariableExpression(VariableExpression node) { scopes.getLast().setCurrentNode(node); visitAnnotations(node); if (node.getAccessedVariable() == node) { // this is a declaration visitClassReference(node.getOriginType()); } handleSimpleExpression(node); scopes.getLast().forgetCurrentNode(); } //-------------------------------------------------------------------------- private boolean handleStatement(Statement node) { // don't check the lookups because statements have no type. // but individual requestors may choose to end the visit here VariableScope scope = scopes.getLast(); ClassNode declaring = scope.getDelegateOrThis(); scope.setPrimaryNode(false); if (node instanceof BlockStatement) { for (ITypeLookup lookup : lookups) { if (lookup instanceof ITypeLookupExtension) { // must ensure that declaring type information at the start of the block is invoked ((ITypeLookupExtension) lookup).lookupInBlock((BlockStatement) node, scope); } } } TypeLookupResult noLookup = new TypeLookupResult(declaring, declaring, declaring, TypeConfidence.EXACT, scope); VisitStatus status = notifyRequestor(node, requestor, noLookup); switch (status) { case CONTINUE: return true; case CANCEL_BRANCH: return false; case CANCEL_MEMBER: case STOP_VISIT: default: throw new VisitCompleted(status); } } private boolean handleParameterList(Parameter[] params) { if (params != null) { VariableScope scope = scopes.getLast(); scope.setPrimaryNode(false); for (Parameter param : params) { if (!scope.containsInThisScope(param.getName())) { scope.addVariable(param); } TypeLookupResult result = null; for (ITypeLookup lookup : lookups) { TypeLookupResult candidate = lookup.lookupType(param, scope); if (candidate != null) { if (result == null || result.confidence.isLessThan(candidate.confidence)) { result = candidate; } if (result.confidence.isAtLeast(TypeConfidence.INFERRED)) { break; } } } TypeLookupResult parameterResult = new TypeLookupResult(result.type, result.declaringType, param, TypeConfidence.EXACT, scope); VisitStatus status = notifyRequestor(param, requestor, parameterResult); switch (status) { case CONTINUE: break; case CANCEL_BRANCH: return false; case CANCEL_MEMBER: case STOP_VISIT: throw new VisitCompleted(status); } // visit the parameter type visitClassReference(param.getOriginType()); visitAnnotations(param); Expression init = param.getInitialExpression(); if (init != null) { init.visit(this); } } } return true; } private boolean handleSimpleExpression(Expression node) { ClassNode primaryType; boolean isStatic; VariableScope scope = scopes.getLast(); if (!isDependentExpression(node)) { primaryType = null; isStatic = false; scope.setMethodCallArgumentTypes(getMethodCallArgumentTypes(node)); scope.setMethodCallGenericsTypes(getMethodCallGenericsTypes(node)); } else { primaryType = primaryTypeStack.removeLast(); // implicit this expressions do not have a primary type if (completeExpressionStack.getLast() instanceof MethodCallExpression && ((MethodCallExpression) completeExpressionStack.getLast()).isImplicitThis()) { primaryType = null; } isStatic = hasStaticObjectExpression(node); scope.setMethodCallArgumentTypes(getMethodCallArgumentTypes(completeExpressionStack.getLast())); scope.setMethodCallGenericsTypes(getMethodCallGenericsTypes(completeExpressionStack.getLast())); } scope.setPrimaryNode(primaryType == null); TypeLookupResult result = lookupExpressionType(node, primaryType, isStatic, scope); return handleRequestor(node, primaryType, result); } private void handleCompleteExpression(Expression node, ClassNode exprType, ClassNode declaringType) { VariableScope scope = scopes.getLast(); scope.setPrimaryNode(false); handleRequestor(node, declaringType, new TypeLookupResult(exprType, declaringType, node, TypeConfidence.EXACT, scope)); } private boolean handleRequestor(Expression node, ClassNode primaryType, TypeLookupResult result) { result.enclosingAssignment = enclosingAssignment; VisitStatus status = requestor.acceptASTNode(node, result, enclosingElement); VariableScope scope = scopes.getLast(); scope.setMethodCallArgumentTypes(null); scope.setMethodCallGenericsTypes(null); // when there is a category method, we don't want to store it // as the declaring type since this will mess things up inside closures ClassNode rememberedDeclaringType = result.declaringType; if (scope.getCategoryNames().contains(rememberedDeclaringType)) { rememberedDeclaringType = primaryType != null ? primaryType : scope.getDelegateOrThis(); } if (rememberedDeclaringType == null) { rememberedDeclaringType = VariableScope.OBJECT_CLASS_NODE; } switch (status) { case CONTINUE: postVisit(node, result.type, rememberedDeclaringType, result.declaration); return true; case CANCEL_BRANCH: postVisit(node, result.type, rememberedDeclaringType, result.declaration); return false; case CANCEL_MEMBER: case STOP_VISIT: throw new VisitCompleted(status); } // won't get here return false; } // private ClassNode findClassNode(String name) { for (ClassNode clazz : findModuleNode().getClasses()) { if (clazz.getNameWithoutPackage().equals(name)) { return clazz; } } return null; } private FieldNode findFieldNode(IField field) { ClassNode clazz = findClassNode(createName(field.getDeclaringType())); FieldNode fieldNode = clazz.getField(field.getElementName()); if (fieldNode == null) { // GRECLIPSE-578 might be @Lazy. Name is changed fieldNode = clazz.getField("$" + field.getElementName()); } return fieldNode; } private MethodNode findMethodNode(IMethod method) { // FIXADE TODO pass this in as a parameter ClassNode clazz = findClassNode(createName(method.getDeclaringType())); try { if (method.isConstructor()) { List<ConstructorNode> constructors = clazz.getDeclaredConstructors(); if (constructors == null || constructors.isEmpty()) { return null; } String[] jdtParamTypes = method.getParameterTypes() == null ? NO_PARAMS : method.getParameterTypes(); outer: for (ConstructorNode constructorNode : constructors) { Parameter[] groovyParams = constructorNode.getParameters() == null ? NO_PARAMETERS : constructorNode.getParameters(); if (groovyParams != null && groovyParams.length > 0) { int implicitParamCount = 0; if (method.getDeclaringType().isEnum()) implicitParamCount = 2; if (groovyParams[0].getName().startsWith("$")) implicitParamCount = 1; // ignore implicit constructor parameters of constructors for enums or inner types if (implicitParamCount > 0) { Parameter[] newGroovyParams = new Parameter[groovyParams.length - implicitParamCount]; System.arraycopy(groovyParams, implicitParamCount, newGroovyParams, 0, newGroovyParams.length); groovyParams = newGroovyParams; } } if (groovyParams.length != jdtParamTypes.length) { continue; } inner: for (int i = 0, n = groovyParams.length; i < n; i += 1) { String simpleGroovyClassType = GroovyUtils.getTypeSignature(groovyParams[i].getType(), false, false); if (simpleGroovyClassType.equals(jdtParamTypes[i])) { continue inner; } String groovyClassType = GroovyUtils.getTypeSignature(groovyParams[i].getType(), true, false); if (!groovyClassType.equals(jdtParamTypes[i])) { continue outer; } } return constructorNode; } String params = ""; if (jdtParamTypes.length > 0) { params = Arrays.toString(jdtParamTypes); params = params.substring(1, params.length() - 1); } System.err.printf("%s.findMethodNode: no match found for %s(%s)%n", getClass().getSimpleName(), clazz.getName(), params); // no match found, just return the first return constructors.get(0); } else { List<MethodNode> methods = clazz.getMethods(method.getElementName()); if (methods.isEmpty()) { return null; } String[] jdtParamTypes = method.getParameterTypes() == null ? NO_PARAMS : method.getParameterTypes(); outer: for (MethodNode methodNode : methods) { Parameter[] groovyParams = methodNode.getParameters() == null ? NO_PARAMETERS : methodNode.getParameters(); if (groovyParams.length != jdtParamTypes.length) { continue; } inner: for (int i = 0, n = groovyParams.length; i < n; i += 1) { String simpleGroovyClassType = GroovyUtils.getTypeSignature(groovyParams[i].getType(), false, false); if (simpleGroovyClassType.equals(jdtParamTypes[i])) { continue inner; } String groovyClassType = GroovyUtils.getTypeSignature(groovyParams[i].getType(), true, false); if (!groovyClassType.equals(jdtParamTypes[i])) { continue outer; } } return methodNode; } String params = ""; if (jdtParamTypes.length > 0) { params = Arrays.toString(jdtParamTypes); params = params.substring(1, params.length() - 1); } System.err.printf("%s.findMethodNode: no match found for %s.%s(%s)%n", getClass().getSimpleName(), clazz.getName(), method.getElementName(), params); // no match found, just return the first return methods.get(0); } } catch (JavaModelException e) { log(e, "Error finding method %s in class %s", method.getElementName(), clazz.getName()); } // probably happened due to a syntax error in the code or an AST transformation return null; } private ModuleNode findModuleNode(/*declaration*/) { if (enclosingDeclarationNode instanceof ModuleNode) { return (ModuleNode) enclosingDeclarationNode; } else if (enclosingDeclarationNode instanceof ClassNode) { return ((ClassNode) enclosingDeclarationNode).getModule(); } else if (enclosingDeclarationNode instanceof MethodNode) { return ((MethodNode) enclosingDeclarationNode).getDeclaringClass().getModule(); } else if (enclosingDeclarationNode instanceof FieldNode) { return ((FieldNode) enclosingDeclarationNode).getDeclaringClass().getModule(); } else { throw new IllegalArgumentException("Invalid enclosing declaration node: " + enclosingDeclarationNode); } } private MethodNode getLazyMethod(String fieldName) { ClassNode classNode = (ClassNode) enclosingDeclarationNode; return classNode.getDeclaredMethod("get" + MetaClassHelper.capitalize(fieldName), NO_PARAMETERS); } private List<ClassNode> getMethodCallArgumentTypes(ASTNode node) { // TODO: Check for MethodCall once 2.1 is the minimum supported Groovy runtime Expression arguments = null; if (node instanceof MethodCallExpression) { arguments = ((MethodCallExpression) node).getArguments(); } else if (node instanceof ConstructorCallExpression) { arguments = ((ConstructorCallExpression) node).getArguments(); } else if (node instanceof StaticMethodCallExpression) { arguments = ((StaticMethodCallExpression) node).getArguments(); } if (arguments != null) { if (arguments instanceof ArgumentListExpression) { List<Expression> expressions = ((ArgumentListExpression) arguments).getExpressions(); if (!expressions.isEmpty()) { List<ClassNode> types = new ArrayList<ClassNode>(expressions.size()); for (Expression expression : expressions) { if (expression instanceof ConstantExpression && ((ConstantExpression) expression).isNullExpression()) { types.add(VariableScope.NULL_TYPE); // sentinel value } else { scopes.getLast().setMethodCallArgumentTypes(getMethodCallArgumentTypes(expression)); TypeLookupResult tlr = lookupExpressionType(expression, null, false, scopes.getLast()); types.add(tlr.type); } } return types; } } // TODO: Might be useful to look into TupleExpression return Collections.emptyList(); } return null; } private GenericsType[] getMethodCallGenericsTypes(ASTNode node) { GenericsType[] generics = null; if (node instanceof MethodCallExpression) { generics = ((MethodCallExpression) node).getGenericsTypes(); }/* else if (node instanceof ConstructorCallExpression) { generics = ((ConstructorCallExpression) node).getGenericsTypes(); } else if (node instanceof StaticMethodCallExpression) { generics = ((StaticMethodCallExpression) node).getGenericsTypes(); }*/ return generics; } private void inferItType(ClosureExpression node, VariableScope scope) { if (closureTypes.isEmpty()) { return; } ClassNode closureType = closureTypes.getLast().get(node); if (closureType != null) { // Try to find single abstract method with single parameter MethodNode method = null; for (MethodNode methodNode : closureType.getMethods()) { if (methodNode.isAbstract() && methodNode.getParameters().length == 1) { if (method != null) { return; } else { method = methodNode; } } } if (method != null) { ClassNode inferredType = method.getParameters()[0].getType(); scope.addVariable("it", inferredType, VariableScope.OBJECT_CLASS_NODE); } } } private ClassNode isCategoryDeclaration(MethodCallExpression node) { String methodAsString = node.getMethodAsString(); if (methodAsString != null && methodAsString.equals("use")) { Expression exprs = node.getArguments(); if (exprs instanceof ArgumentListExpression) { ArgumentListExpression args = (ArgumentListExpression) exprs; if (args.getExpressions().size() >= 2 && args.getExpressions().get(1) instanceof ClosureExpression) { // really, should be doing inference on the first expression and seeing if it // is a class node, but looking up in scope is good enough for now Expression expr = args.getExpressions().get(0); if (expr instanceof ClassExpression) { return expr.getType(); } else if (expr instanceof VariableExpression && expr.getText() != null) { VariableScope.VariableInfo info = scopes.getLast().lookupName(expr.getText()); if (info != null) { // info.type should be Class<Category> return info.type.getGenericsTypes()[0].getType(); } } } } } return null; } /** * Dependent expressions are expressions whose type depends on another expression. * * Dependent expressions are: * <ul> * <li>right part of a non-assignment binary expression * <li>left part of a assignment expression * <li>propery (ie- right part) of a property expression * <li>method (ie- right part) of a method call expression * <li>property/field (ie- right part) of an attribute expression * </ul> * * Note that for statements and ternary expressions do not have any dependent * expression even though they have primary expressions. * * @param node expression node to check * @return true iff the node is the primary expression in an expression pair. */ private boolean isDependentExpression(Expression node) { if (!completeExpressionStack.isEmpty()) { ASTNode complete = completeExpressionStack.getLast(); if (complete instanceof PropertyExpression) { PropertyExpression prop = (PropertyExpression) complete; return prop.getProperty() == node; } else if (complete instanceof MethodCallExpression) { MethodCallExpression call = (MethodCallExpression) complete; return call.getMethod() == node; } else if (complete instanceof MethodPointerExpression) { MethodPointerExpression ref = (MethodPointerExpression) complete; return ref.getMethodName() == node; } else if (complete instanceof ImportNode) { ImportNode imp = (ImportNode) complete; return imp.getAliasExpr() == node || imp.getFieldNameExpr() == node; } } return false; } /** * Primary expressions are: * <ul> * <li>left part of a non-assignment binary expression * <li>right part of a assignment expression * <li>object (ie- left part) of a property expression * <li>object (ie- left part) of a method call expression * <li>object (ie- left part) of an attribute expression * <li>collection expression (ie- right part) of a for statement * <li>true expression (ie- right part) of a ternary expression * <li>first element of a list expression (the first element is assumed to be representative of all list elements) * <li>first element of a range expression (the first element is assumed to be representative of the range) * <li>Either the key OR the value expression of a {@link MapEntryExpression} * <li>The first {@link MapEntryExpression} of a {@link MapExpression} * <li>The expression of a {@link PrefixExpression}, a {@link PostfixExpression}, a {@link UnaryMinusExpression}, a * {@link UnaryPlusExpression}, or a {@link BitwiseNegationExpression} * </ul> * * @param node expression node to check * @return true iff the node is the primary expression in an expression pair. */ private boolean isPrimaryExpression(Expression node) { if (!completeExpressionStack.isEmpty()) { ASTNode complete = completeExpressionStack.getLast(); if (complete instanceof PropertyExpression) { return ((PropertyExpression) complete).getObjectExpression() == node; } else if (complete instanceof MethodCallExpression) { return ((MethodCallExpression) complete).getObjectExpression() == node; } else if (complete instanceof BinaryExpression) { BinaryExpression expr = (BinaryExpression) complete; // both sides of the binary expression are primary since we need // access to both of them when inferring binary expression types if (expr.getLeftExpression() == node || expr.getRightExpression() == node) { return true; } // statically-compiled assignment chains need a little help if (!(node instanceof TupleExpression) && expr.getRightExpression() instanceof ListOfExpressionsExpression) { @SuppressWarnings("unchecked") List<Expression> list = (List<Expression>) ReflectionUtils.getPrivateField( ListOfExpressionsExpression.class, "expressions", expr.getRightExpression()); // list.get(0) should be TemporaryVariableExpression return (node != list.get(1) && list.get(1) instanceof MethodCallExpression); } } else if (complete instanceof AttributeExpression) { return ((AttributeExpression) complete).getObjectExpression() == node; } else if (complete instanceof TernaryExpression) { return ((TernaryExpression) complete).getTrueExpression() == node; } else if (complete instanceof ForStatement) { // this check is used to store the type of the collection expression // so that it can be assigned to the for loop variable return ((ForStatement) complete).getCollectionExpression() == node; } else if (complete instanceof ReturnStatement) { return ((ReturnStatement) complete).getExpression() == node; } else if (complete instanceof ListExpression) { return isNotEmpty(((ListExpression) complete).getExpressions()) && ((ListExpression) complete).getExpression(0) == node; } else if (complete instanceof RangeExpression) { return ((RangeExpression) complete).getFrom() == node; } else if (complete instanceof MapEntryExpression) { return ((MapEntryExpression) complete).getKeyExpression() == node || ((MapEntryExpression) complete).getValueExpression() == node; } else if (complete instanceof MapExpression) { return isNotEmpty(((MapExpression) complete).getMapEntryExpressions()) && ((MapExpression) complete).getMapEntryExpressions().get(0) == node; } else if (complete instanceof PrefixExpression) { return ((PrefixExpression) complete).getExpression() == node; } else if (complete instanceof PostfixExpression) { return ((PostfixExpression) complete).getExpression() == node; } else if (complete instanceof UnaryPlusExpression) { return ((UnaryPlusExpression) complete).getExpression() == node; } else if (complete instanceof UnaryMinusExpression) { return ((UnaryMinusExpression) complete).getExpression() == node; } else if (complete instanceof BitwiseNegationExpression) { return ((BitwiseNegationExpression) complete).getExpression() == node; } } return false; } /** * @return true iff the object expression associated with node is a static reference to a class declaration */ private boolean hasStaticObjectExpression(Expression node) { boolean staticObjectExpression = false; if (!completeExpressionStack.isEmpty()) { ASTNode maybeProperty = completeExpressionStack.getLast(); // need to call getObjectExpression and isImplicitThis w/o common interface if (maybeProperty instanceof PropertyExpression) { PropertyExpression prop = (PropertyExpression) maybeProperty; if (prop.getObjectExpression() instanceof ClassExpression) { staticObjectExpression = true; } else if (prop.isImplicitThis()) { staticObjectExpression = scopes.getLast().isStatic(); } } else if (maybeProperty instanceof MethodCallExpression) { MethodCallExpression prop = (MethodCallExpression) maybeProperty; if (prop.getObjectExpression() instanceof ClassExpression) { staticObjectExpression = true; } else if (prop.isImplicitThis()) { staticObjectExpression = scopes.getLast().isStatic(); } } } return staticObjectExpression; } private TypeLookupResult lookupExpressionType(Expression node, ClassNode objExprType, boolean isStatic, VariableScope scope) { TypeLookupResult result = null; for (ITypeLookup lookup : lookups) { TypeLookupResult candidate; if (lookup instanceof ITypeLookupExtension) { candidate = ((ITypeLookupExtension) lookup).lookupType(node, scope, objExprType, isStatic); } else { candidate = lookup.lookupType(node, scope, objExprType); } if (candidate != null) { if (result == null || result.confidence.isLessThan(candidate.confidence)) { result = candidate; } if (result.confidence.isAtLeast(TypeConfidence.INFERRED)) { break; } } } if (TypeConfidence.UNKNOWN == result.confidence && VariableScope.MAP_CLASS_NODE.equals(result.declaringType)) { ClassNode inferredType = VariableScope.OBJECT_CLASS_NODE; if (currentMapVariable != null && node instanceof ConstantExpression) { // recover inferred type from property map (see visitPropertyExpression) Map<String, ClassNode> map = localMapProperties.get(currentMapVariable); //String key = ((ConstantExpression) node).getConstantName(); String key = (String) ((ConstantExpression) node).getValue(); ClassNode val = map.get(key); if (val != null) inferredType = val; } TypeLookupResult tlr = new TypeLookupResult(inferredType, result.declaringType, result.declaration, TypeConfidence.INFERRED, result.scope, result.extraDoc); tlr.enclosingAnnotation = result.enclosingAnnotation; tlr.enclosingAssignment = result.enclosingAssignment; tlr.isGroovy = result.isGroovy; result = tlr; } return result.resolveTypeParameterization(objExprType, isStatic); } private VisitStatus notifyRequestor(ASTNode node, ITypeRequestor requestor, TypeLookupResult result) { // result is never null because SimpleTypeLookup always returns non-null return requestor.acceptASTNode(node, result, enclosingElement); } private void postVisit(Expression node, ClassNode type, ClassNode declaringType, ASTNode declaration) { if (isPrimaryExpression(node)) { assert type != null; primaryTypeStack.add(type); } else if (isDependentExpression(node)) { // TODO: null has been seen here for type; is that okay? dependentTypeStack.add(type); dependentDeclarationStack.add(new Tuple(declaringType, declaration)); } } /** * For testing only, ensures that after a visit is complete, */ private void postVisitSanityCheck() { Assert.isTrue(completeExpressionStack.isEmpty(), String.format(SANITY_CHECK_MESSAGE, "Expression")); Assert.isTrue(primaryTypeStack.isEmpty(), String.format(SANITY_CHECK_MESSAGE, "Primary type")); Assert.isTrue(dependentDeclarationStack.isEmpty(), String.format(SANITY_CHECK_MESSAGE, "Declaration")); Assert.isTrue(dependentTypeStack.isEmpty(), String.format(SANITY_CHECK_MESSAGE, "Dependent type")); Assert.isTrue(scopes.isEmpty(), String.format(SANITY_CHECK_MESSAGE, "Variable scope")); } private static final String SANITY_CHECK_MESSAGE = "Inferencing engine in invalid state after visitor completed. %s stack should be empty after visit completed."; private void setNameLocation(FieldNode fieldNode) throws JavaModelException { fieldNode.setNameEnd(fieldNode.getEnd()); int nameLength = fieldNode.getName().length(); Expression init = fieldNode.getInitialExpression(); if (init != null && !(init instanceof EmptyExpression)) { String fieldText = unit.getSource().substring(fieldNode.getStart(), init.getStart()); int nameStart = fieldNode.getStart() + fieldText.lastIndexOf(fieldNode.getName()); if (nameStart < fieldNode.getStart()) throw new JavaModelException(null, 980); fieldNode.setNameEnd(nameStart + nameLength); } fieldNode.setNameStart(fieldNode.getNameEnd() - nameLength); fieldNode.setNameEnd(fieldNode.getNameEnd() - 1); // name end index is inclusive... not sure why } //-------------------------------------------------------------------------- /** * Get the module node. Potentially forces creation of a new module node if the working copy owner is non-default. This is * necessary because a non-default working copy owner implies that this may be a search related to refactoring and therefore, * the ModuleNode must be based on the most recent working copies. */ private static ModuleNodeInfo createModuleNode(GroovyCompilationUnit unit) { if (unit.getOwner() == null || unit.owner == DefaultWorkingCopyOwner.PRIMARY) { return unit.getModuleInfo(true); } else { return unit.getNewModuleInfo(); } } /** * Creates type name taking into account inner types. */ private static String createName(IType type) { StringBuilder sb = new StringBuilder(); while (type != null) { if (sb.length() > 0) { sb.insert(0, '$'); } if (type instanceof SourceType && type.getElementName().length() < 1) { int count; try { count = (Integer) ReflectionUtils.throwableGetPrivateField(SourceType.class, "localOccurrenceCount", (SourceType) type); } catch (Exception e) { // localOccurrenceCount does not exist in 3.7 count = type.getOccurrenceCount(); } sb.insert(0, count); } else { sb.insert(0, type.getElementName()); } type = (IType) type.getParent().getAncestor(IJavaElement.TYPE); } return sb.toString(); } /** * @return a list parameterized by propType */ private static ClassNode createParameterizedList(ClassNode propType) { ClassNode list = VariableScope.clonedList(); list.getGenericsTypes()[0].setType(propType); list.getGenericsTypes()[0].setName(propType.getName()); return list; } /** * @return a list parameterized by propType */ private static ClassNode createParameterizedMap(ClassNode k, ClassNode v) { ClassNode map = VariableScope.clonedMap(); map.getGenericsTypes()[0].setType(k); map.getGenericsTypes()[0].setName(k.getName()); map.getGenericsTypes()[1].setType(v); map.getGenericsTypes()[1].setName(v.getName()); return map; } /** * @return a list parameterized by propType */ private static ClassNode createParameterizedRange(ClassNode propType) { ClassNode range = VariableScope.clonedRange(); range.getGenericsTypes()[0].setType(propType); range.getGenericsTypes()[0].setName(propType.getName()); return range; } /** * Only handle operations that are not handled in {@link #findBinaryOperatorName(String)} * * @param operation the operation of this binary expression * @param lhs the type of the lhs of the binary expression * @param rhs the type of the rhs of the binary expression * @return the determined type of the binary expression */ private static ClassNode findBinaryExpressionType(String operation, ClassNode lhs, ClassNode rhs) { char op = operation.charAt(0); switch (op) { case '*': if (operation.equals("*.") || operation.equals("*.@")) { // can we do better and parameterize the list? return VariableScope.clonedList(); } case '~': // regex pattern return VariableScope.STRING_CLASS_NODE; case '!': // includes != and !== and !! case '<': case '>': if (operation.length() > 1) { if (operation.equals("<=>")) { return VariableScope.INTEGER_CLASS_NODE; } } // all booleans return VariableScope.BOOLEAN_CLASS_NODE; case 'i': if (operation.equals("is") || operation.equals("in")) { return VariableScope.BOOLEAN_CLASS_NODE; } else { // unknown return rhs; } case '.': if (operation.equals(".&")) { return VariableScope.CLOSURE_CLASS_NODE; } else { // includes ".", "?:", "?.", ".@" return rhs; } case '=': if (operation.length() > 1) { if (operation.charAt(1) == '=') { return VariableScope.BOOLEAN_CLASS_NODE; } else if (operation.charAt(1) == '~') { // consider regex to be string return VariableScope.MATCHER_CLASS_NODE; } } // drop through default: // "as" // rhs by default return rhs; } } /** * @return the method name associated with this binary operator */ private static String findBinaryOperatorName(String text) { char op = text.charAt(0); switch (op) { case '+': return "plus"; case '-': return "minus"; case '*': if (text.length() > 1 && text.equals("**")) { return "power"; } return "multiply"; case '/': return "div"; case '%': return "mod"; case '&': return "and"; case '|': return "or"; case '^': return "xor"; case '>': if (text.length() > 1 && text.equals(">>")) { return "rightShift"; } break; case '<': if (text.length() > 1 && text.equals("<<")) { return "leftShift"; } break; case '[': return "getAt"; } return null; } private static ConstructorNode findDefaultConstructor(ClassNode node) { List<ConstructorNode> constructors = node.getDeclaredConstructors(); for (ConstructorNode constructor : constructors) { if (constructor.getParameters() == null || constructor.getParameters().length == 0) { return constructor; } } return null; } /** * @return the method name associated with this unary operator */ private static String findUnaryOperatorName(String text) { char op = text.charAt(0); switch (op) { case '+': if (text.length() > 1 && text.equals("++")) { return "next"; } return "positive"; case '-': if (text.length() > 1 && text.equals("--")) { return "previous"; } return "negative"; case ']': return "putAt"; case '~': return "bitwiseNegate"; } return null; } /** * Makes assumption that no one has overloaded the basic arithmetic operations on numbers. * These operations will bypass the mop in most situations anyway. */ private static boolean isArithmeticOperationOnNumberOrStringOrList(String text, ClassNode lhs, ClassNode rhs) { if (text.length() != 1) { return false; } lhs = ClassHelper.getWrapper(lhs); switch (text.charAt(0)) { case '+': case '-': // lists, numbers or string return VariableScope.STRING_CLASS_NODE.equals(lhs) || lhs.isDerivedFrom(VariableScope.NUMBER_CLASS_NODE) || VariableScope.NUMBER_CLASS_NODE.equals(lhs) || VariableScope.LIST_CLASS_NODE.equals(lhs) || lhs.implementsInterface(VariableScope.LIST_CLASS_NODE); case '*': case '/': case '%': // numbers or string return VariableScope.STRING_CLASS_NODE.equals(lhs) || lhs.isDerivedFrom(VariableScope.NUMBER_CLASS_NODE) || VariableScope.NUMBER_CLASS_NODE.equals(lhs); default: return false; } } /** * Determines if the parameter type can be implicitly determined. We look for * DGM method calls that take closures and see what kind of type they expect. * * @return array of {@link ClassNode}s specifying the inferred types of the closure's parameters */ private static ClassNode[] inferClosureParamTypes(VariableScope scope, ClosureExpression closure) { int paramCount = closure.getParameters() == null ? 0 : closure.getParameters().length; if (paramCount == 0) { // implicit parameter paramCount += 1; } ClassNode[] inferredTypes = new ClassNode[paramCount]; // TODO: Could this use the Closure annotations to determine the type? CallAndType call = scope.getEnclosingMethodCallExpression(); if (call != null) { ClassNode delegateType = call.declaringType; String methodName = call.call.getMethodAsString(); ClassNode inferredType; if (dgmClosureMethods.contains(methodName)) { inferredType = VariableScope.extractElementType(delegateType); } else if (dgmClosureIdentityMethods.contains(methodName)) { inferredType = VariableScope.clone(delegateType); } else { // inferredType might be null inferredType = dgmClosureMethodsMap.get(methodName); } if (inferredType != null) { Arrays.fill(inferredTypes, inferredType); // special cases: eachWithIndex has last element an integer if (methodName.equals("eachWithIndex") && inferredTypes.length > 1) { inferredTypes[inferredTypes.length - 1] = VariableScope.INTEGER_CLASS_NODE; } // if declaring type is a map and if (delegateType.getName().equals(VariableScope.MAP_CLASS_NODE.getName())) { if ((dgmClosureMaybeMap.contains(methodName) && paramCount == 2) || (methodName.equals("eachWithIndex") && paramCount == 3)) { GenericsType[] typeParams = inferredType.getGenericsTypes(); if (typeParams != null && typeParams.length == 2) { inferredTypes[0] = typeParams[0].getType(); inferredTypes[1] = typeParams[1].getType(); } } } return inferredTypes; } } Arrays.fill(inferredTypes, VariableScope.OBJECT_CLASS_NODE); return inferredTypes; } private static boolean isEnumInit(StaticMethodCallExpression node) { int typeModifiers = node.getOwnerType().getModifiers(); return ((typeModifiers & Opcodes.ACC_ENUM) > 0 && node.getMethod().equals("$INIT")); } private static boolean isLazy(FieldNode fieldNode) { for (AnnotationNode annotation : fieldNode.getAnnotations()) { if (annotation.getClassNode().getName().equals("groovy.lang.Lazy")) { return true; } } return false; } private static boolean isMetaAnnotation(ClassNode node) { if (node.isAnnotated() && node.hasMethod("value", NO_PARAMETERS)) { for (AnnotationNode annotation : node.getAnnotations()) { if (annotation.getClassNode().getName().equals("groovy.transform.AnnotationCollector")) { return true; } } } return false; } private static boolean isNotEmpty(List<?> list) { return list != null && !list.isEmpty(); } private static boolean shouldFilterEnumMember(IJavaElement child) { int type = child.getElementType(); String name = child.getElementName(); if (name.indexOf('$') >= 0) { return true; } else if (type == IJavaElement.METHOD) { if ((name.equals("next") || name.equals("previous")) && ((IMethod) child).getNumberOfParameters() == 0) { return true; } } else if (type == IJavaElement.METHOD) { if (name.equals("MIN_VALUE") || name.equals("MAX_VALUE")) { return true; } } return false; } }