| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php |
| * |
| * 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 com.android.ide.eclipse.adt.internal.refactorings.extractstring; |
| |
| import org.eclipse.jdt.core.dom.AST; |
| import org.eclipse.jdt.core.dom.ASTNode; |
| import org.eclipse.jdt.core.dom.ASTVisitor; |
| import org.eclipse.jdt.core.dom.Assignment; |
| import org.eclipse.jdt.core.dom.ClassInstanceCreation; |
| import org.eclipse.jdt.core.dom.Expression; |
| import org.eclipse.jdt.core.dom.IMethodBinding; |
| import org.eclipse.jdt.core.dom.ITypeBinding; |
| import org.eclipse.jdt.core.dom.IVariableBinding; |
| import org.eclipse.jdt.core.dom.MethodDeclaration; |
| import org.eclipse.jdt.core.dom.MethodInvocation; |
| import org.eclipse.jdt.core.dom.Modifier; |
| import org.eclipse.jdt.core.dom.Name; |
| import org.eclipse.jdt.core.dom.SimpleName; |
| import org.eclipse.jdt.core.dom.SimpleType; |
| import org.eclipse.jdt.core.dom.SingleVariableDeclaration; |
| import org.eclipse.jdt.core.dom.StringLiteral; |
| import org.eclipse.jdt.core.dom.Type; |
| import org.eclipse.jdt.core.dom.TypeDeclaration; |
| import org.eclipse.jdt.core.dom.VariableDeclarationExpression; |
| import org.eclipse.jdt.core.dom.VariableDeclarationFragment; |
| import org.eclipse.jdt.core.dom.VariableDeclarationStatement; |
| import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; |
| import org.eclipse.text.edits.TextEditGroup; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.TreeMap; |
| |
| /** |
| * Visitor used by {@link ExtractStringRefactoring} to extract a string from an existing |
| * Java source and replace it by an Android XML string reference. |
| * |
| * @see ExtractStringRefactoring#computeJavaChanges |
| */ |
| class ReplaceStringsVisitor extends ASTVisitor { |
| |
| private static final String CLASS_ANDROID_CONTEXT = "android.content.Context"; //$NON-NLS-1$ |
| private static final String CLASS_JAVA_CHAR_SEQUENCE = "java.lang.CharSequence"; //$NON-NLS-1$ |
| private static final String CLASS_JAVA_STRING = "java.lang.String"; //$NON-NLS-1$ |
| |
| |
| private final AST mAst; |
| private final ASTRewrite mRewriter; |
| private final String mOldString; |
| private final String mRQualifier; |
| private final String mXmlId; |
| private final ArrayList<TextEditGroup> mEditGroups; |
| |
| public ReplaceStringsVisitor(AST ast, |
| ASTRewrite astRewrite, |
| ArrayList<TextEditGroup> editGroups, |
| String oldString, |
| String rQualifier, |
| String xmlId) { |
| mAst = ast; |
| mRewriter = astRewrite; |
| mEditGroups = editGroups; |
| mOldString = oldString; |
| mRQualifier = rQualifier; |
| mXmlId = xmlId; |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public boolean visit(StringLiteral node) { |
| if (node.getLiteralValue().equals(mOldString)) { |
| |
| // We want to analyze the calling context to understand whether we can |
| // just replace the string literal by the named int constant (R.id.foo) |
| // or if we should generate a Context.getString() call. |
| boolean useGetResource = false; |
| useGetResource = examineVariableDeclaration(node) || |
| examineMethodInvocation(node) || |
| examineAssignment(node); |
| |
| Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$ |
| SimpleName idName = mAst.newSimpleName(mXmlId); |
| ASTNode newNode = mAst.newQualifiedName(qualifierName, idName); |
| boolean disabledChange = false; |
| String title = "Replace string by ID"; |
| |
| if (useGetResource) { |
| Expression context = methodHasContextArgument(node); |
| if (context == null && !isClassDerivedFromContext(node)) { |
| // if we don't have a class that derives from Context and |
| // we don't have a Context method argument, then try a bit harder: |
| // can we find a method or a field that will give us a context? |
| context = findContextFieldOrMethod(node); |
| |
| if (context == null) { |
| // If not, let's write Context.getString(), which is technically |
| // invalid but makes it a good clue on how to fix it. Since these |
| // will not compile, we create a disabled change by default. |
| context = mAst.newSimpleName("Context"); //$NON-NLS-1$ |
| disabledChange = true; |
| } |
| } |
| |
| MethodInvocation mi2 = mAst.newMethodInvocation(); |
| mi2.setName(mAst.newSimpleName("getString")); //$NON-NLS-1$ |
| mi2.setExpression(context); |
| mi2.arguments().add(newNode); |
| |
| newNode = mi2; |
| title = "Replace string by Context.getString(R.string...)"; |
| } |
| |
| TextEditGroup editGroup = new EnabledTextEditGroup(title, !disabledChange); |
| mEditGroups.add(editGroup); |
| mRewriter.replace(node, newNode, editGroup); |
| } |
| return super.visit(node); |
| } |
| |
| /** |
| * Examines if the StringLiteral is part of an assignment corresponding to the |
| * a string variable declaration, e.g. String foo = id. |
| * |
| * The parent fragment is of syntax "var = expr" or "var[] = expr". |
| * We want the type of the variable, which is either held by a |
| * VariableDeclarationStatement ("type [fragment]") or by a |
| * VariableDeclarationExpression. In either case, the type can be an array |
| * but for us all that matters is to know whether the type is an int or |
| * a string. |
| */ |
| private boolean examineVariableDeclaration(StringLiteral node) { |
| VariableDeclarationFragment fragment = findParentClass(node, |
| VariableDeclarationFragment.class); |
| |
| if (fragment != null) { |
| ASTNode parent = fragment.getParent(); |
| |
| Type type = null; |
| if (parent instanceof VariableDeclarationStatement) { |
| type = ((VariableDeclarationStatement) parent).getType(); |
| } else if (parent instanceof VariableDeclarationExpression) { |
| type = ((VariableDeclarationExpression) parent).getType(); |
| } |
| |
| if (type instanceof SimpleType) { |
| return isJavaString(type.resolveBinding()); |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Examines if the StringLiteral is part of a assignment to a variable that |
| * is a string. We need to lookup the variable to find its type, either in the |
| * enclosing method or class type. |
| */ |
| private boolean examineAssignment(StringLiteral node) { |
| |
| Assignment assignment = findParentClass(node, Assignment.class); |
| if (assignment != null) { |
| Expression left = assignment.getLeftHandSide(); |
| |
| ITypeBinding typeBinding = left.resolveTypeBinding(); |
| return isJavaString(typeBinding); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * If the expression is part of a method invocation (aka a function call) or a |
| * class instance creation (aka a "new SomeClass" constructor call), we try to |
| * find the type of the argument being used. If it is a String (most likely), we |
| * want to return true (to generate a getString() call). However if there might |
| * be a similar method that takes an int, in which case we don't want to do that. |
| * |
| * This covers the case of Activity.setTitle(int resId) vs setTitle(String str). |
| */ |
| @SuppressWarnings("rawtypes") |
| private boolean examineMethodInvocation(StringLiteral node) { |
| |
| ASTNode parent = null; |
| List arguments = null; |
| IMethodBinding methodBinding = null; |
| |
| MethodInvocation invoke = findParentClass(node, MethodInvocation.class); |
| if (invoke != null) { |
| parent = invoke; |
| arguments = invoke.arguments(); |
| methodBinding = invoke.resolveMethodBinding(); |
| } else { |
| ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class); |
| if (newclass != null) { |
| parent = newclass; |
| arguments = newclass.arguments(); |
| methodBinding = newclass.resolveConstructorBinding(); |
| } |
| } |
| |
| if (parent != null && arguments != null && methodBinding != null) { |
| // We want to know which argument this is. |
| // Walk up the hierarchy again to find the immediate child of the parent, |
| // which should turn out to be one of the invocation arguments. |
| ASTNode child = null; |
| for (ASTNode n = node; n != parent; ) { |
| ASTNode p = n.getParent(); |
| if (p == parent) { |
| child = n; |
| break; |
| } |
| n = p; |
| } |
| if (child == null) { |
| // This can't happen: a parent of 'node' must be the child of 'parent'. |
| return false; |
| } |
| |
| // Find the index |
| int index = 0; |
| for (Object arg : arguments) { |
| if (arg == child) { |
| break; |
| } |
| index++; |
| } |
| |
| if (index == arguments.size()) { |
| // This can't happen: one of the arguments of 'invoke' must be 'child'. |
| return false; |
| } |
| |
| // Eventually we want to determine if the parameter is a string type, |
| // in which case a Context.getString() call must be generated. |
| boolean useStringType = false; |
| |
| // Find the type of that argument |
| ITypeBinding[] types = methodBinding.getParameterTypes(); |
| if (index < types.length) { |
| ITypeBinding type = types[index]; |
| useStringType = isJavaString(type); |
| } |
| |
| // Now that we know that this method takes a String parameter, can we find |
| // a variant that would accept an int for the same parameter position? |
| if (useStringType) { |
| String name = methodBinding.getName(); |
| ITypeBinding clazz = methodBinding.getDeclaringClass(); |
| nextMethod: for (IMethodBinding mb2 : clazz.getDeclaredMethods()) { |
| if (methodBinding == mb2 || !mb2.getName().equals(name)) { |
| continue; |
| } |
| // We found a method with the same name. We want the same parameters |
| // except that the one at 'index' must be an int type. |
| ITypeBinding[] types2 = mb2.getParameterTypes(); |
| int len2 = types2.length; |
| if (types.length == len2) { |
| for (int i = 0; i < len2; i++) { |
| if (i == index) { |
| ITypeBinding type2 = types2[i]; |
| if (!("int".equals(type2.getQualifiedName()))) { //$NON-NLS-1$ |
| // The argument at 'index' is not an int. |
| continue nextMethod; |
| } |
| } else if (!types[i].equals(types2[i])) { |
| // One of the other arguments do not match our original method |
| continue nextMethod; |
| } |
| } |
| // If we got here, we found a perfect match: a method with the same |
| // arguments except the one at 'index' is an int. In this case we |
| // don't need to convert our R.id into a string. |
| useStringType = false; |
| break; |
| } |
| } |
| } |
| |
| return useStringType; |
| } |
| return false; |
| } |
| |
| /** |
| * Examines if the StringLiteral is part of a method declaration (a.k.a. a function |
| * definition) which takes a Context argument. |
| * If such, it returns the name of the variable as a {@link SimpleName}. |
| * Otherwise it returns null. |
| */ |
| private SimpleName methodHasContextArgument(StringLiteral node) { |
| MethodDeclaration decl = findParentClass(node, MethodDeclaration.class); |
| if (decl != null) { |
| for (Object obj : decl.parameters()) { |
| if (obj instanceof SingleVariableDeclaration) { |
| SingleVariableDeclaration var = (SingleVariableDeclaration) obj; |
| if (isAndroidContext(var.getType())) { |
| return mAst.newSimpleName(var.getName().getIdentifier()); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Walks up the node hierarchy to find the class (aka type) where this statement |
| * is used and returns true if this class derives from android.content.Context. |
| */ |
| private boolean isClassDerivedFromContext(StringLiteral node) { |
| TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class); |
| if (clazz != null) { |
| // This is the class that the user is currently writing, so it can't be |
| // a Context by itself, it has to be derived from it. |
| return isAndroidContext(clazz.getSuperclassType()); |
| } |
| return false; |
| } |
| |
| private Expression findContextFieldOrMethod(StringLiteral node) { |
| TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class); |
| return clazz == null ? null : findContextFieldOrMethod(clazz.resolveBinding()); |
| } |
| |
| private Expression findContextFieldOrMethod(ITypeBinding clazzType) { |
| TreeMap<Integer, Expression> results = new TreeMap<Integer, Expression>(); |
| findContextCandidates(results, clazzType, 0 /*superType*/); |
| if (results.size() > 0) { |
| Integer bestRating = results.keySet().iterator().next(); |
| return results.get(bestRating); |
| } |
| return null; |
| } |
| |
| /** |
| * Find all method or fields that are candidates for providing a Context. |
| * There can be various choices amongst this class or its super classes. |
| * Sort them by rating in the results map. |
| * |
| * The best ever choice is to find a method with no argument that returns a Context. |
| * The second suitable choice is to find a Context field. |
| * The least desirable choice is to find a method with arguments. It's not really |
| * desirable since we can't generate these arguments automatically. |
| * |
| * Methods and fields from supertypes are ignored if they are private. |
| * |
| * The rating is reversed: the lowest rating integer is used for the best candidate. |
| * Because the superType argument is actually a recursion index, this makes the most |
| * immediate classes more desirable. |
| * |
| * @param results The map that accumulates the rating=>expression results. The lower |
| * rating number is the best candidate. |
| * @param clazzType The class examined. |
| * @param superType The recursion index. |
| * 0 for the immediate class, 1 for its super class, etc. |
| */ |
| private void findContextCandidates(TreeMap<Integer, Expression> results, |
| ITypeBinding clazzType, |
| int superType) { |
| for (IMethodBinding mb : clazzType.getDeclaredMethods()) { |
| // If we're looking at supertypes, we can't use private methods. |
| if (superType != 0 && Modifier.isPrivate(mb.getModifiers())) { |
| continue; |
| } |
| |
| if (isAndroidContext(mb.getReturnType())) { |
| // We found a method that returns something derived from Context. |
| |
| int argsLen = mb.getParameterTypes().length; |
| if (argsLen == 0) { |
| // We'll favor any method that takes no argument, |
| // That would be the best candidate ever, so we can stop here. |
| MethodInvocation mi = mAst.newMethodInvocation(); |
| mi.setName(mAst.newSimpleName(mb.getName())); |
| results.put(Integer.MIN_VALUE, mi); |
| return; |
| } else { |
| // A method with arguments isn't as interesting since we wouldn't |
| // know how to populate such arguments. We'll use it if there are |
| // no other alternatives. We'll favor the one with the less arguments. |
| Integer rating = Integer.valueOf(10000 + 1000 * superType + argsLen); |
| if (!results.containsKey(rating)) { |
| MethodInvocation mi = mAst.newMethodInvocation(); |
| mi.setName(mAst.newSimpleName(mb.getName())); |
| results.put(rating, mi); |
| } |
| } |
| } |
| } |
| |
| // A direct Context field would be more interesting than a method with |
| // arguments. Try to find one. |
| for (IVariableBinding var : clazzType.getDeclaredFields()) { |
| // If we're looking at supertypes, we can't use private field. |
| if (superType != 0 && Modifier.isPrivate(var.getModifiers())) { |
| continue; |
| } |
| |
| if (isAndroidContext(var.getType())) { |
| // We found such a field. Let's use it. |
| Integer rating = Integer.valueOf(superType); |
| results.put(rating, mAst.newSimpleName(var.getName())); |
| break; |
| } |
| } |
| |
| // Examine the super class to see if we can locate a better match |
| clazzType = clazzType.getSuperclass(); |
| if (clazzType != null) { |
| findContextCandidates(results, clazzType, superType + 1); |
| } |
| } |
| |
| /** |
| * Walks up the node hierarchy and returns the first ASTNode of the requested class. |
| * Only look at parents. |
| * |
| * Implementation note: this is a generic method so that it returns the node already |
| * casted to the requested type. |
| */ |
| @SuppressWarnings("unchecked") |
| private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) { |
| if (node != null) { |
| for (node = node.getParent(); node != null; node = node.getParent()) { |
| if (node.getClass().equals(clazz)) { |
| return (T) node; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true if the given type is or derives from android.content.Context. |
| */ |
| private boolean isAndroidContext(Type type) { |
| if (type != null) { |
| return isAndroidContext(type.resolveBinding()); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns true if the given type is or derives from android.content.Context. |
| */ |
| private boolean isAndroidContext(ITypeBinding type) { |
| for (; type != null; type = type.getSuperclass()) { |
| if (CLASS_ANDROID_CONTEXT.equals(type.getQualifiedName())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns true if this type binding represents a String or CharSequence type. |
| */ |
| private boolean isJavaString(ITypeBinding type) { |
| for (; type != null; type = type.getSuperclass()) { |
| if (CLASS_JAVA_STRING.equals(type.getQualifiedName()) || |
| CLASS_JAVA_CHAR_SEQUENCE.equals(type.getQualifiedName())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |