/* * 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.codehaus.groovy.eclipse.codebrowsing.requestor; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; import org.codehaus.groovy.ast.ClassCodeVisitorSupport; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.GenericsType; import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.InnerClassNode; 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.expr.AnnotationConstantExpression; import org.codehaus.groovy.ast.expr.ArrayExpression; import org.codehaus.groovy.ast.expr.BinaryExpression; 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.ConstantExpression; import org.codehaus.groovy.ast.expr.ConstructorCallExpression; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.FieldExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression; import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.ast.stmt.BlockStatement; import org.codehaus.groovy.ast.stmt.CatchStatement; import org.codehaus.groovy.ast.stmt.ForStatement; import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.eclipse.core.GroovyCore; import org.codehaus.groovy.eclipse.core.util.ArrayUtils; import org.codehaus.groovy.eclipse.core.util.VisitCompleteException; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.runtime.GeneratedClosure; import org.eclipse.jdt.groovy.core.util.GroovyUtils; public class ASTNodeFinder extends ClassCodeVisitorSupport { protected ModuleNode module; protected ASTNode result; protected Region sloc; public ASTNodeFinder(Region sloc) { this.sloc = sloc; } /** * The main entry point. */ public ASTNode doVisit(ModuleNode node) { module = node; try { visitPackage(node.getPackage()); visitImports(node); for (ClassNode clazz : node.getClasses()) { visitClass(clazz); } } catch (VisitCompleteException done) { } return result; } @Override public void visitPackage(PackageNode node) { if (node != null) { visitAnnotations(node); check(node); } } @Override public void visitImports(ModuleNode node) { for (ImportNode importNode : GroovyUtils.getAllImportNodes(node)) { visitImport(importNode); } } protected void visitImport(ImportNode node) { if (node.getType() != null) { visitAnnotations(node); check(node.getType()); if (node.getFieldNameExpr() != null) { check(node.getFieldNameExpr()); } if (node.getAliasExpr() != null) { check(node.getAliasExpr()); } } check(node); // for package qualifier } @Override public void visitClass(ClassNode node) { visitAnnotations(node); if (node.getNameEnd() > 0) { checkNameRange(node); // also checks generics checkSupers(node); // extends and implements } if (node.getObjectInitializerStatements() != null) { for (Statement statement : node.getObjectInitializerStatements()) { statement.visit(this); } } // visit <clinit> body because this is where static field initializers are placed // However, there is a problem in that constant fields are seen here as well. // If a match is found here, keep it for later because there may be a more appropriate match in the class body VisitCompleteException vce = null; try { MethodNode clinit = node.getMethod("<clinit>", Parameter.EMPTY_ARRAY); if (clinit != null && clinit.getCode() instanceof BlockStatement) { for (Statement statement : ((BlockStatement) clinit.getCode()).getStatements()) { statement.visit(this); } } } catch (VisitCompleteException e) { vce = e; } // visit trait members @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) { visitMethod(method); } } node.visitContents(this); // visit inner classes for (Iterator<InnerClassNode> it = node.getInnerClasses(); it.hasNext();) { ClassNode inner = it.next(); // do not look into closure classes. A closure class // looks like ParentClass$_name_closure#, where // ParentClass is the name of the containing class. // name is a name for the closure, and # is a number if (!inner.isSynthetic() || inner instanceof GeneratedClosure) { visitClass(inner); } } // if we have gotten here, then we have not found a more appropriate candidate if (vce != null) { throw vce; } } @Override public void visitField(FieldNode node) { if (node.getNameEnd() > 0) { checkNameRange(node); } // visit annotations and init expression super.visitField(node); // compute supporting type start position int start = node.getStart(), annos = node.getAnnotations().size(); if (annos > 0) start = GroovyUtils.endOffset(node.getAnnotations().get(annos - 1)); // visit type and generics check(node.getType(), start, node.getEnd() - node.getName().length()); } @Override protected void visitConstructorOrMethod(MethodNode node, boolean isConstructor) { if (node.getEnd() > 0) { if (!isConstructor && node.getGenericsTypes() != null) { checkGenerics(node.getGenericsTypes(), null); } ClassNode returnType = node.getReturnType(); if (returnType != null /*&& !returnType.isPrimitive()*/) { // allow primitives to be found to stop the visit int n, offset = -1; if (returnType.getEnd() < 1) { // constrain the return type's start offset using generics or annotations ASTNode last = ArrayUtils.lastElement(node.getGenericsTypes()); if (last != null) { offset = last.getEnd() + 1; } else if ((n = node.getAnnotations().size()) > 0) { for (int i = (n - 1); i >= 0; i -= 1) { // find the rightmost annotation with end source position info int end = GroovyUtils.endOffset(node.getAnnotations().get(i)); if (end > 0) { offset = end; break; } } } // TODO: if offset is still -1, select on modifiers shows as return type } check(returnType, Math.max(offset, node.getStart()), node.getNameStart() - 1); } if (node.getNameEnd() > 0) { checkNameRange(node); } checkParameters(node.getParameters()); checkTypes(node.getExceptions()); } // visit annotations, param annotations, and statements super.visitConstructorOrMethod(node, isConstructor); } @Override protected void visitAnnotation(AnnotationNode node) { check(node.getClassNode()); int start = node.getEnd() + 2; for (Map.Entry<String, Expression> pair : node.getMembers().entrySet()) { String name = pair.getKey(); Expression expr = pair.getValue(); check(node.getClassNode().getMethod(name, Parameter.EMPTY_ARRAY), start/*expr.getStart() - name.length() - 1*/, expr.getStart() - 1); /*expr.visit(this);*/ start = expr.getEnd() + 1; } super.visitAnnotation(node); } @Override public void visitArrayExpression(ArrayExpression expression) { ClassNode arrayClass = expression.getElementType(); if (arrayClass != arrayClass.redirect()) { check(arrayClass); } else { // synthetic ArrayExpression for referencing enum fields or collected annotations } super.visitArrayExpression(expression); } @Override public void visitBinaryExpression(BinaryExpression expression) { super.visitBinaryExpression(expression); check(expression); } @Override public void visitCastExpression(CastExpression expression) { if (expression.getEnd() > 0) { check(expression.getType()); } super.visitCastExpression(expression); } @Override public void visitClassExpression(ClassExpression expression) { // NOTE: expression.getType() may refer to ClassNode behind "this" or "super", so it may cast a very wide net if (expression.getEnd() > 0 && expression.getStart() == expression.getType().getStart()) { check(expression.getType()); } check(expression); } @Override public void visitClosureExpression(ClosureExpression expression) { checkParameters(expression.getParameters()); super.visitClosureExpression(expression); } @Override public void visitFieldExpression(FieldExpression expression) { check(expression); super.visitFieldExpression(expression); } @Override public void visitVariableExpression(VariableExpression expression) { // check the annotations, generics, and type of variable declarations -- including @Lazy fields if (expression == expression.getAccessedVariable() || expression.getName().charAt(0) == '$') { visitAnnotations(expression); // expression start and end bound the variable name; guestimate the type positions int until = expression.getStart() - 1, // assume at least one space on either side start = Math.max(0, until - expression.getOriginType().getName().length() - 1); check(expression.getOriginType(), start, until); } check(expression); } @Override public void visitConstantExpression(ConstantExpression expression) { if (expression == ConstantExpression.NULL) { // the sloc of this global variable is inexplicably set to something // so, we may erroneously find matches here return; } if (expression.getText().length() == 0 && expression.getLength() != 0) { // GRECLIPSE-1330 This is probably an empty expression in a gstring...can ignore. return; } if (expression instanceof AnnotationConstantExpression) { // example: @interface X { Y default @Y(...) }; expression is "@Y(...)" // example: @X(@Y(...)); expression is "@Y(...)" check(expression.getType()); // values have been visited } check(expression); super.visitConstantExpression(expression); } @Override public void visitConstructorCallExpression(ConstructorCallExpression call) { if (call.getEnd() > 0) { int start, until; if (call.getNameStart() > 0) { if (call.isUsingAnonymousInnerClass()) { check(call.getType().getUnresolvedSuperClass()); checkTypes(call.getType().getUnresolvedInterfaces()); } else { checkNameRange(call); } checkGenerics(call.getType()); start = call.getStart(); until = call.getNameStart() - 1; } else try { start = call.getStart() + "new ".length(); until = call.getArguments().getStart() - 1; // check call name and generics check(call.getType(), start, until); until = start; start = call.getStart(); } catch (VisitCompleteException e) { result = call; throw e; } // check the new keyword check(null, start, until); } // visit argument list super.visitConstructorCallExpression(call); // visit anonymous body if (call.isUsingAnonymousInnerClass()) { call.getType().visitContents(this); } } @Override public void visitMethodCallExpression(MethodCallExpression call) { if (call.isUsingGenerics()) { checkGenerics(call.getGenericsTypes(), null); } super.visitMethodCallExpression(call); // check for trait field re-written as call to helper method Expression expr = GroovyUtils.getTraitFieldExpression(call); if (expr != null) { check(expr); } } @Override public void visitStaticMethodCallExpression(StaticMethodCallExpression call) { // don't check here if the type reference is implicit // we know that the type is not implicit if the name // location is filled in. if (call.getOwnerType() != call.getOwnerType().redirect()) { check(call.getOwnerType()); } super.visitStaticMethodCallExpression(call); // the method itself is not an expression, but only a string // so this check call will test for open declaration on the method check(call); } @Override public void visitCatchStatement(CatchStatement statement) { checkParameter(statement.getVariable()); super.visitCatchStatement(statement); } @Override public void visitForLoop(ForStatement statement) { checkParameter(statement.getVariable()); super.visitForLoop(statement); } //-------------------------------------------------------------------------- /** * Checks if the node covers the selection. */ protected void check(ASTNode node) { if (node instanceof ClassNode) { checkGenerics((ClassNode) node); } if (node.getEnd() > 0 && sloc.regionIsCoveredByNode(node)) { completeVisitation(node, null); } } /** * Checks if the node covers the selection. If source location isn't set * for specified node, the supplied offsets will constrain it's location. */ protected void check(ASTNode node, int start, int until) { if (node != null && node.getEnd() > 0) { check(node); } else { if (node instanceof ClassNode) { checkGenerics((ClassNode) node); } if (sloc.getOffset() >= start && sloc.getEnd() <= until) { completeVisitation(node, new Region(start, until - start)); } } } /** * Checks if the name of the node covers the selection. */ protected void checkNameRange(AnnotatedNode node) { if (sloc.regionIsCoveredByNameRange(node)) { completeVisitation(node, new Region( node.getNameStart(), node.getNameEnd() - node.getNameStart())); } if (node instanceof ClassNode) { checkGenerics((ClassNode) node); } } /** * Checks if the extends or implements clause covers the selection. */ private void checkSupers(ClassNode node) { // supers can only appear after the name if (!(sloc.getOffset() > node.getNameEnd())) { return; } // anonymous inner classes cannot have extends or implements clauses if (node instanceof InnerClassNode && ((InnerClassNode) node).isAnonymous()) { return; } // set offset beyond the class name and any generics GenericsType type = ArrayUtils.lastElement(node.getGenericsTypes()); int offset = (type != null ? type.getEnd() : node.getNameEnd()) + 1; String source, src = null; try { source = (src = readClassDeclaration(node)).substring(offset - node.getStart()); } catch (Exception err) { GroovyCore.logException(String.format( "Error checking super-types at offset %d in file / index %d of:%n%s%n%s", offset, offset - node.getStart(), src, DefaultGroovyMethods.dump(node)), err); return; } ClassNode superClass = node.getUnresolvedSuperClass(); if (superClass != null) { // only check types that appear in the source code String name = GroovyUtils.splitName(superClass)[1]; if (source.indexOf(name) > 0) { int a = endIndexOf(source, EXTENDS_), b = indexOf(source, _IMPLEMENTS); if (b < 0) b = source.length(); check(superClass, offset + a, offset + b); } } ClassNode[] superTypes = node.getUnresolvedInterfaces(); if (superTypes != null && superTypes.length > 0) { for (int i = 0, n = superTypes.length; i < n; i += 1) { // only check types that appear in the source code String name = GroovyUtils.splitName(superTypes[i])[1]; if (source.indexOf(name) > 0) { int a = endIndexOf(source, IMPLEMENTS_), b = source.length(); // TODO: Set to source.indexOf(name) + name.length()? char c; // use prev and next to further constrain for (int j = i - 1; j >= 0; j -= 1) { if (superTypes[j].getEnd() > 0) { a = (superTypes[j].getEnd()) - offset; while ((c = source.charAt(a)) == ',' || Character.isWhitespace(c)) a += 1; break; } } for (int j = i + 1; j < n; j += 1) { if (superTypes[j].getStart() > 0) { b = (superTypes[j].getStart() - 1) - offset; while ((c = source.charAt(b - 1)) == ',' || Character.isWhitespace(c)) b -= 1; break; } } check(superTypes[i], offset + a, offset + b); } } } } private void checkGenerics(ClassNode node) { if (node.isUsingGenerics() && node.getGenericsTypes() != null) { checkGenerics(node.getGenericsTypes(), node.getName()); } } private void checkGenerics(GenericsType[] generics, String typeName) { for (GenericsType generic : generics) { int start = generic.getStart(), until = start + generic.getName().length(); if (generic.getType() != null && generic.getType().getName().charAt(0) != '?') { check(generic.getType(), start, until); } start = until + 1; until = generic.getEnd(); if (generic.getLowerBound() != null) { start += "super ".length(); // assume 1 space check(generic.getLowerBound(), start, until); } else if (generic.getUpperBounds() != null) { start += "extends ".length(); // assume 1 space for (ClassNode upper : generic.getUpperBounds()) { String name = upper.getName(); // handle enums where the upper bound is the same as the type if (!name.equals(typeName)) { check(upper, start, Math.min(start + name.length(), until)); if (upper.getEnd() > 0) start = upper.getEnd() + 1; } } } } } private void checkParameter(Parameter param) { if (param != null && param.getEnd() > 0) { checkNameRange(param); if (param.getInitialExpression() != null) { param.getInitialExpression().visit(this); } check(param.getOriginType(), param.getStart(), param.getNameStart() - 1); } } private void checkParameters(Parameter[] params) { if (params != null && params.length > 0) { for (Parameter p : params) { checkParameter(p); } } } private void checkTypes(ClassNode[] nodes) { if (nodes != null && nodes.length > 0) { for (ClassNode node : nodes) { check(node); } } } /** * Provides a single exit point for the various check methods. */ protected final void completeVisitation(ASTNode node, Region sloc) throws VisitCompleteException { result = node; if (sloc != null) this.sloc = sloc; throw new VisitCompleteException(); } //-------------------------------------------------------------------------- private String readClassDeclaration(ClassNode node) { String code = (String) node.getNodeMetaData("groovy.source"); if (code == null) { code = String.valueOf(GroovyUtils.readSourceRange(module.getContext(), node.getStart(), node.getLength())); node.putNodeMetaData("groovy.source", code); } return code.substring(0, code.indexOf('{')); } private static int endIndexOf(String s, Pattern p) { Matcher m = p.matcher(s); if (m.find()) { return m.start() + m.group().length(); } return -1; } private static int indexOf(String s, Pattern p) { Matcher m = p.matcher(s); if (m.find()) { return m.start(); } return -1; } private static final Pattern EXTENDS_ = Pattern.compile("\\bextends\\s+"); private static final Pattern IMPLEMENTS_ = Pattern.compile("\\bimplements\\s+"); private static final Pattern _IMPLEMENTS = Pattern.compile("\\s+implements\\b"); }