/* * Contributions to FindBugs * Copyright (C) 2006, Institut for Software * An Institut of the University of Applied Sciences Rapperswil * * Author: Thierry Wyss, Marco Busarello * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package edu.umd.cs.findbugs.plugin.eclipse.quickfix.util; import static edu.umd.cs.findbugs.plugin.eclipse.quickfix.util.ConditionCheck.checkForNull; import static org.eclipse.core.runtime.Assert.isTrue; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.SortedSet; import java.util.TreeSet; import javax.annotation.CheckForNull; import org.eclipse.core.runtime.Assert; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ArrayType; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.FieldDeclaration; import org.eclipse.jdt.core.dom.ImportDeclaration; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.PackageDeclaration; import org.eclipse.jdt.core.dom.ParameterizedType; import org.eclipse.jdt.core.dom.PrimitiveType; import org.eclipse.jdt.core.dom.QualifiedType; import org.eclipse.jdt.core.dom.SimpleType; import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.eclipse.jdt.core.dom.Statement; import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.VariableDeclarationFragment; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import org.eclipse.jdt.core.dom.rewrite.ListRewrite; import edu.umd.cs.findbugs.ClassAnnotation; import edu.umd.cs.findbugs.FieldAnnotation; import edu.umd.cs.findbugs.MethodAnnotation; import edu.umd.cs.findbugs.SourceLineAnnotation; import edu.umd.cs.findbugs.plugin.eclipse.quickfix.exception.ASTNodeNotFoundException; import edu.umd.cs.findbugs.plugin.eclipse.quickfix.exception.FieldDeclarationNotFoundException; import edu.umd.cs.findbugs.plugin.eclipse.quickfix.exception.MethodDeclarationNotFoundException; import edu.umd.cs.findbugs.plugin.eclipse.quickfix.exception.StatementNotFoundException; import edu.umd.cs.findbugs.plugin.eclipse.quickfix.exception.TypeDeclarationNotFoundException; /** * The <CODE>ASTUtil</CODE> provides some usefull methods to transform * <CODE>PackageMemberAnnotations</CODE> into <CODE>BodyDeclarations</CODE>. * Normally this methods should be used to get a type, field or method * declaration for a class, field or method annotation. * * @see ASTUtil#getTypeDeclaration(CompilationUnit, ClassAnnotation) * @see ASTUtil#getFieldDeclaration(TypeDeclaration, FieldAnnotation) * @see ASTUtil#getMethodDeclaration(TypeDeclaration, MethodAnnotation) * @author <a href="mailto:twyss@hsr.ch">Thierry Wyss</a> * @author <a href="mailto:mbusarel@hsr.ch">Marco Busarello</a> * @author <a href="mailto:g1zgragg@hsr.ch">Guido Zgraggen</a> * @version 1.0 */ public class ASTUtil { private static Comparator<? super ImportDeclaration> defaultImportComparator; private static Map<String, Class<?>> primitiveTypes; static { defaultImportComparator = new ImportDeclarationComparator<ImportDeclaration>(); primitiveTypes = new HashMap<String, Class<?>>(); primitiveTypes.put("B", byte.class); primitiveTypes.put("C", char.class); primitiveTypes.put("S", short.class); primitiveTypes.put("I", int.class); primitiveTypes.put("J", long.class); primitiveTypes.put("F", float.class); primitiveTypes.put("D", double.class); } public static void addImports(ASTRewrite rewrite, CompilationUnit compilationUnit, String... imports) { addImports(rewrite, compilationUnit, false, imports); } public static void addStaticImports(ASTRewrite rewrite, CompilationUnit compilationUnit, String... imports) { addImports(rewrite, compilationUnit, true, imports); } public static void addImports(ASTRewrite rewrite, CompilationUnit compilationUnit, boolean staticImports, String... imports) { addImports(rewrite, compilationUnit, defaultImportComparator, staticImports, imports); } public static void addImports(ASTRewrite rewrite, CompilationUnit compilationUnit, Comparator<? super ImportDeclaration> comparator, boolean staticImports, String... imports) { checkForNull(comparator, "import comparator"); checkForNull(imports, "imports"); final AST ast = rewrite.getAST(); SortedSet<ImportDeclaration> importDeclarations = new TreeSet<ImportDeclaration>(comparator); for (String importName : imports) { ImportDeclaration importDeclaration = ast.newImportDeclaration(); importDeclaration.setName(ast.newName(importName)); importDeclaration.setStatic(staticImports); importDeclarations.add(importDeclaration); } addImports(rewrite, compilationUnit, importDeclarations); } /** * Adds <CODE>ImportDeclaration</CODE>s to the list of imports in the * specified <CODE>CompilationUnit</CODE>. If an import already exists, the * import will not be inserted. The imports are inserted in an ordered way. * The <CODE>Comparator</CODE> of the <CODE>SortedSet</CODE> is used to sort * the imports. * * @param rewrite * the <CODE>ASTRewrite</CODE>, that stores the edits. * @param compilationUnit * the <CODE>CompilationUnit</CODE>. * @param imports * the new <CODE>ImportDeclaration</CODE>s to add. */ public static void addImports(ASTRewrite rewrite, CompilationUnit compilationUnit, SortedSet<ImportDeclaration> imports) { checkForNull(rewrite, "ast-rewrite"); checkForNull(compilationUnit, "compilation-unit"); checkForNull(imports, "imports"); ListRewrite importRewrite = rewrite.getListRewrite(compilationUnit, CompilationUnit.IMPORTS_PROPERTY); addImports(importRewrite, imports.comparator(), imports.iterator()); } private static void addImports(ListRewrite importRewrite, Comparator<? super ImportDeclaration> comparator, Iterator<ImportDeclaration> newImports) { try { ImportDeclaration newImport = newImports.next(); List<?> imports = importRewrite.getRewrittenList(); for (Object importObj : imports) { ImportDeclaration anImport = (ImportDeclaration) importObj; int comp = comparator.compare(newImport, anImport); if (comp > 0) { continue; } if (comp < 0) { importRewrite.insertBefore(newImport, anImport, null); } newImport = newImports.next(); } importRewrite.insertLast(newImport, null); while (newImports.hasNext()) { importRewrite.insertLast(newImports.next(), null); } } catch (NoSuchElementException e) { // do nothing } } public static ASTNode getASTNode(CompilationUnit compilationUnit, SourceLineAnnotation sourceLineAnno) throws ASTNodeNotFoundException { checkForNull(sourceLineAnno, "source line annotation"); return getASTNode(compilationUnit, sourceLineAnno.getStartLine(), sourceLineAnno.getEndLine()); } /** * Searchs the first <CODE>ASTNode</CODE> between the specified * <CODE>startLine</CODE> and <CODE>endLine</CODE>. If the source line * doesn't contain an <CODE>ASTNode</CODE>, a * <CODE>ASTNodeNotFoundException</CODE> is thrown. * * @param compilationUnit * the <CODE>CompilationUnit</CODE>, that contains the * <CODE>ASTNode</CODE>. * @param startLine * the starting source line number. * @param endLine * the ending source line number. * @throws ASTNodeNotFoundException * if no <CODE>ASTNode</CODE> found between the specifed start * and end line. */ public static ASTNode getASTNode(CompilationUnit compilationUnit, int startLine, int endLine) throws ASTNodeNotFoundException { checkForNull(compilationUnit, "compilation unit"); ASTNode node = searchASTNode(compilationUnit, startLine, endLine); if (node == null) { throw new ASTNodeNotFoundException("No ast node found between " + startLine + " and " + endLine + "."); } return node; } /** * Returns the <CODE>TypeDeclaration</CODE> for the specified * <CODE>ClassAnnotation</CODE>. The type has to be declared in the * specified <CODE>CompilationUnit</CODE>. * * @param compilationUnit * The <CODE>CompilationUnit</CODE>, where the * <CODE>TypeDeclaration</CODE> is declared in. * @param classAnno * The <CODE>ClassAnnotation</CODE>, which contains the class * name of the <CODE>TypeDeclaration</CODE>. * @return the <CODE>TypeDeclaration</CODE> found in the specified * <CODE>CompilationUnit</CODE>. * @throws TypeDeclarationNotFoundException * if no matching <CODE>TypeDeclaration</CODE> was found. */ public static TypeDeclaration getTypeDeclaration(CompilationUnit compilationUnit, ClassAnnotation classAnno) throws TypeDeclarationNotFoundException { checkForNull(classAnno, "class annotation"); return getTypeDeclaration(compilationUnit, classAnno.getClassName()); } /** * Returns the <CODE>TypeDeclaration</CODE> for the specified type name. The * type has to be declared in the specified <CODE>CompilationUnit</CODE>. * * @param compilationUnit * The <CODE>CompilationUnit</CODE>, where the * <CODE>TypeDeclaration</CODE> is declared in. * @param typeName * The qualified class name to search for. * @return the <CODE>TypeDeclaration</CODE> found in the specified * <CODE>CompilationUnit</CODE>. * @throws TypeDeclarationNotFoundException * if no matching <CODE>TypeDeclaration</CODE> was found. */ public static TypeDeclaration getTypeDeclaration(CompilationUnit compilationUnit, String typeName) throws TypeDeclarationNotFoundException { checkForNull(compilationUnit, "compilation unit"); checkForNull(typeName, "class name"); int index = typeName.lastIndexOf('.'); String packageName = index > 0 ? typeName.substring(0, index) : ""; if (!matchesPackage(compilationUnit.getPackage(), packageName)) { throw new TypeDeclarationNotFoundException(compilationUnit, typeName, "The package '" + packageName + "' doesn't match the package of the compilation unit."); } TypeDeclaration type = searchTypeDeclaration(compilationUnit.types(), typeName.substring(index + 1)); if (type == null) { throw new TypeDeclarationNotFoundException(compilationUnit, typeName); } return type; } /** * Returns the <CODE>FieldDeclaration</CODE> for the specified * <CODE>FieldAnnotation</CODE>. The field has to be declared in the * specified <CODE>TypeDeclaration</CODE>. * * @param type * The <CODE>TypeDeclaration</CODE>, where the * <CODE>FieldDeclaration</CODE> is declared in. * @param fieldAnno * The <CODE>FieldAnnotation</CODE>, which contains the field * name of the <CODE>FieldDeclaration</CODE>. * @return the <CODE>FieldDeclaration</CODE> found in the specified * <CODE>TypeDeclaration</CODE>. * @throws FieldDeclarationNotFoundException * if no matching <CODE>FieldDeclaration</CODE> was found. */ public static FieldDeclaration getFieldDeclaration(TypeDeclaration type, FieldAnnotation fieldAnno) throws FieldDeclarationNotFoundException { checkForNull(fieldAnno, "field annotation"); return getFieldDeclaration(type, fieldAnno.getFieldName()); } /** * Returns the <CODE>FieldDeclaration</CODE> for the specified field name. * The field has to be declared in the specified * <CODE>TypeDeclaration</CODE>. * * @param type * The <CODE>TypeDeclaration</CODE>, where the * <CODE>FieldDeclaration</CODE> is declared in. * @param fieldName * The simple field name to search for. * @return the <CODE>FieldDeclaration</CODE> found in the specified * <CODE>TypeDeclaration</CODE>. * @throws FieldDeclarationNotFoundException * if no matching <CODE>FieldDeclaration</CODE> was found. */ public static FieldDeclaration getFieldDeclaration(TypeDeclaration type, String fieldName) throws FieldDeclarationNotFoundException { checkForNull(type, "type declaration"); checkForNull(fieldName, "field name"); for (FieldDeclaration field : type.getFields()) { for (Object fragObj : field.fragments()) { VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragObj; if (fieldName.equals(fragment.getName().getIdentifier())) { return field; } } } throw new FieldDeclarationNotFoundException(type, fieldName); } /** * Returns the <CODE>MethodDeclaration</CODE> for the specified * <CODE>MethodAnnotation</CODE>. The method has to be declared in the * specified <CODE>TypeDeclaration</CODE>. * * @param type * The <CODE>TypeDeclaration</CODE>, where the * <CODE>MethodDeclaration</CODE> is declared in. * @param methodAnno * The <CODE>MethodAnnotation</CODE>, which contains the method * name and signature of the <CODE>MethodDeclaration</CODE> * @return the <CODE>MethodDeclaration</CODE> found in the specified * <CODE>TypeDeclaration</CODE>. * @throws MethodDeclarationNotFoundException * if no matching <CODE>MethodDeclaration</CODE> was found. */ public static MethodDeclaration getMethodDeclaration(TypeDeclaration type, MethodAnnotation methodAnno) throws MethodDeclarationNotFoundException { checkForNull(methodAnno, "method annotation"); return getMethodDeclaration(type, methodAnno.getMethodName(), methodAnno.getMethodSignature()); } /** * Returns the <CODE>MethodDeclaration</CODE> for the specified method name * and signature. The method has to be declared in the specified * <CODE>TypeDeclaration</CODE>. * * @param type * The <CODE>TypeDeclaration</CODE>, where the * <CODE>MethodDeclaration</CODE> is declared in. * @param methodName * The method name to search for. * @param methodSignature * The method signature to search for. * @return the <CODE>MethodDeclaration</CODE> found in the specified * <CODE>TypeDeclaration</CODE>. * @throws MethodDeclarationNotFoundException * if no matching <CODE>MethodDeclaration</CODE> was found. */ public static MethodDeclaration getMethodDeclaration(TypeDeclaration type, String methodName, String methodSignature) throws MethodDeclarationNotFoundException { checkForNull(type, "type declaration"); checkForNull(methodName, "method name"); checkForNull(methodSignature, "method signature"); MethodDeclaration method = searchMethodDeclaration(type.getAST(), type.getMethods(), methodName, methodSignature); if (method == null) { throw new MethodDeclarationNotFoundException(type, methodName, methodSignature); } return method; } public static Statement getStatement(CompilationUnit compilationUnit, MethodDeclaration method, SourceLineAnnotation sourceLineAnno) throws StatementNotFoundException { checkForNull(sourceLineAnno, "source line annotation"); return getStatement(compilationUnit, method, sourceLineAnno.getStartLine(), sourceLineAnno.getEndLine()); } /** * Return the first <CODE>Statement</CODE> found, that is between the * specified start and end line. * * @param compilationUnit * @param method * @param startLine * @param endLine * @return * @throws StatementNotFoundException * @throws StatementNotFoundException */ public static Statement getStatement(CompilationUnit compilationUnit, MethodDeclaration method, int startLine, int endLine) throws StatementNotFoundException { checkForNull(compilationUnit, "compilation unit"); checkForNull(method, "method declaration"); Statement statement = searchStatement(compilationUnit, method.getBody().statements(), startLine, endLine); if (statement == null) { throw new StatementNotFoundException(compilationUnit, startLine, endLine); } return statement; } @CheckForNull protected static ASTNode searchASTNode(CompilationUnit compilationUnit, int startLine, int endLine) { Assert.isNotNull(compilationUnit); isTrue(startLine <= endLine); SourceLineVisitor visitor = new SourceLineVisitor(compilationUnit, startLine, endLine); compilationUnit.accept(visitor); return visitor.getASTNode(); } @CheckForNull protected static TypeDeclaration searchTypeDeclaration(List<?> declarations, String typeName) { Assert.isNotNull(declarations); Assert.isNotNull(typeName); int index = typeName.indexOf('$'); String innerClassName = null; if (index >= 0) { innerClassName = typeName.substring(index + 1); typeName = typeName.substring(0, index); } for (Object declaration : declarations) { if (!(declaration instanceof TypeDeclaration)) { continue; } TypeDeclaration type = (TypeDeclaration) declaration; if (!typeName.equals(type.getName().getFullyQualifiedName())) { continue; } if (index < 0) { return type; } return searchTypeDeclaration(type.bodyDeclarations(), innerClassName); } return null; } @CheckForNull protected static MethodDeclaration searchMethodDeclaration(AST ast, MethodDeclaration[] methods, String methodName, String methodSignature) { Assert.isNotNull(methods); Assert.isNotNull(methodName); Assert.isNotNull(methodSignature); String[] parameters = parseParameters(methodSignature); for (MethodDeclaration method : methods) { if (!methodName.equals(method.getName().getFullyQualifiedName())) { continue; } if (!matchesParams(method.parameters(), parameters)) { continue; } return method; } return null; } @CheckForNull protected static Statement searchStatement(CompilationUnit compilationUnit, List<?> statements, int startLine, int endLine) { Assert.isNotNull(compilationUnit); Assert.isNotNull(statements); for (Object statementObj : statements) { Statement statement = (Statement) statementObj; int lineNumber = compilationUnit.getLineNumber(statement.getStartPosition()); if (startLine <= lineNumber && lineNumber <= endLine) { return statement; } } return null; } protected static String[] parseParameters(String methodSignature) { Assert.isNotNull(methodSignature); int leftParenthesis = methodSignature.indexOf('('); int rightParenthesis = methodSignature.indexOf(')'); methodSignature = methodSignature.substring(leftParenthesis + 1, rightParenthesis); if (methodSignature.length() == 0) { return new String[0]; } String[] parameters = methodSignature.split(";"); for (int i = 0; i < parameters.length; i++) { parameters[i] = normalizeParameter(parameters[i]); } return parameters; } protected static String normalizeParameter(String parameter) { Assert.isNotNull(parameter); Class<?> primitiveClass = primitiveTypes.get(parameter); if (primitiveClass != null) { return primitiveClass.getName(); } if (parameter.startsWith("L")) { return parameter.substring(1).replaceAll("[/$]", "."); } if (parameter.startsWith("[")) { return normalizeParameter(parameter.substring(1)) + "[]"; } throw new IllegalStateException("Unknown parameter type '" + parameter + "'."); } private static boolean matchesPackage(PackageDeclaration apackage, String packageName) { return apackage != null && packageName.equals(apackage.getName().getFullyQualifiedName()) || packageName.length() == 0; } private static boolean matchesParams(List<?> methodParams, String[] paramTypeNames) { return matchesParams(methodParams.toArray(new SingleVariableDeclaration[methodParams.size()]), paramTypeNames); } private static boolean matchesParams(SingleVariableDeclaration[] methodParams, String[] paramTypeNames) { if (methodParams.length != paramTypeNames.length) { return false; } for (int i = 0; i < methodParams.length; i++) { String typeName = getPrettyTypeName(methodParams[i].getType()); if (!typeName.equals(paramTypeNames[i])) { return false; } } return true; } private static String getPrettyTypeName(Type type) { if (type.isArrayType()) { return getPrettyTypeName((ArrayType) type); } else if (type.isParameterizedType()) { return getPrettyTypeName((ParameterizedType) type); } else if (type.isPrimitiveType()) { return getPrettyTypeName((PrimitiveType) type); } else if (type.isQualifiedType()) { return getPrettyTypeName((QualifiedType) type); } else if (type.isSimpleType()) { return getPrettyTypeName((SimpleType) type); } else { return ""; } } private static String getPrettyTypeName(ArrayType type) { return getPrettyTypeName(type.getComponentType()) + "[]"; } private static String getPrettyTypeName(PrimitiveType type) { return type.getPrimitiveTypeCode().toString(); } private static String getPrettyTypeName(ParameterizedType type) { String typeName = type.resolveBinding().getQualifiedName(); return typeName.substring(0, typeName.indexOf('<')); } private static String getPrettyTypeName(QualifiedType type) { return type.resolveBinding().getQualifiedName(); } private static String getPrettyTypeName(SimpleType type) { return type.resolveBinding().getQualifiedName(); } }