diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java index c37fb6bc7..20ef35578 100644 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java @@ -50,26 +50,7 @@ import org.eclipse.jdt.core.compiler.InvalidInputException; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.dom.ASTVisitor; -import org.eclipse.jdt.core.dom.ClassInstanceCreation; import org.eclipse.jdt.core.dom.CompilationUnit; -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.jdt.core.dom.rewrite.ImportRewrite; import org.eclipse.jface.text.ITextSelection; @@ -106,7 +87,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.TreeMap; /** * This refactoring extracts a string from a file and replaces it by an Android resource ID @@ -402,7 +382,8 @@ public class ExtractStringRefactoring extends Refactoring { } if (!status.isOK()) { - status.addFatalError("Selection must be inside a Java source or an Android Layout XML file."); + status.addFatalError( + "Selection must be inside a Java source or an Android Layout XML file."); } } finally { @@ -521,7 +502,8 @@ public class ExtractStringRefactoring extends Refactoring { } if (node == null) { - status.addFatalError("The selection does not match any element in the XML document."); + status.addFatalError( + "The selection does not match any element in the XML document."); return status.isOK(); } @@ -542,79 +524,10 @@ public class ExtractStringRefactoring extends Refactoring { sdoc.getRegionAtCharacterOffset(selStart); if (region != null && DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { - // The region gives us the textual representation of the XML element - // where the selection starts, split using sub-regions. We now just - // need to iterate through the sub-regions to find which one - // contains the actual selection. We're interested in an attribute - // value however when we find one we want to memorize the attribute - // name that was defined just before. - - int startInRegion = selStart - region.getStartOffset(); - - int nb = region.getNumberOfRegions(); - ITextRegionList list = region.getRegions(); - String currAttrValue = null; - - for (int i = 0; i < nb; i++) { - ITextRegion subRegion = list.get(i); - String type = subRegion.getType(); - - if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { - currAttrName = region.getText(subRegion); - - // I like to select the attribute definition and invoke - // the extract string wizard. So if the selection is on - // the attribute name part, find the value that is just - // after and use it as if it were the selection. - - if (subRegion.getStart() <= startInRegion && - startInRegion < subRegion.getTextEnd()) { - // A well-formed attribute is composed of a name, - // an equal sign and the value. There can't be any space - // in between, which makes the parsing a lot easier. - if (i <= nb - 3 && - DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals( - list.get(i + 1).getType())) { - subRegion = list.get(i + 2); - type = subRegion.getType(); - if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals( - type)) { - currAttrValue = region.getText(subRegion); - } - } - } - - } else if (subRegion.getStart() <= startInRegion && - startInRegion < subRegion.getTextEnd() && - DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { - currAttrValue = region.getText(subRegion); - } - - if (currAttrValue != null) { - // We found the value. Only accept it if not empty - // and if we found an attribute name before. - String text = currAttrValue; - - // The attribute value will contain the XML quotes. Remove them. - int len = text.length(); - if (len >= 2 && - text.charAt(0) == '"' && - text.charAt(len - 1) == '"') { - text = text.substring(1, len - 1); - } else if (len >= 2 && - text.charAt(0) == '\'' && - text.charAt(len - 1) == '\'') { - text = text.substring(1, len - 1); - } - if (text.length() > 0 && currAttrName != null) { - // Setting mTokenString to non-null marks the fact we - // accept this attribute. - mTokenString = text; - } - - break; - } - } + // Find if any sub-region representing an attribute contains the + // selection. If it does, returns the name of the attribute in + // currAttrName and returns the value in the field mTokenString. + currAttrName = findSelectionInRegion(region, selStart); if (mTokenString == null) { status.addFatalError( @@ -625,67 +538,10 @@ public class ExtractStringRefactoring extends Refactoring { if (mTokenString != null && node != null && currAttrName != null) { - UiElementNode rootUiNode = editor.getUiRootNode(); - UiElementNode currentUiNode = - rootUiNode == null ? null : rootUiNode.findXmlNode(node); - ReferenceAttributeDescriptor attrDesc = null; - - if (currentUiNode != null) { - // remove any namespace prefix from the attribute name - String name = currAttrName; - int pos = name.indexOf(':'); - if (pos > 0 && pos < name.length() - 1) { - name = name.substring(pos + 1); - } - - for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) { - if (attrNode.getDescriptor().getXmlLocalName().equals(name)) { - AttributeDescriptor desc = attrNode.getDescriptor(); - if (desc instanceof ReferenceAttributeDescriptor) { - attrDesc = (ReferenceAttributeDescriptor) desc; - } - break; - } - } - } - - // The attribute descriptor is a resource reference. It must either accept - // of any resource type or specifically accept string types. - if (attrDesc != null && - (attrDesc.getResourceType() == null || - attrDesc.getResourceType() == ResourceType.STRING)) { - // We have one more check to do: is the current string value already - // an Android XML string reference? If so, we can't edit it. - if (mTokenString.startsWith("@")) { //$NON-NLS-1$ - int pos1 = 0; - if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') { - pos1++; - } - int pos2 = mTokenString.indexOf('/'); - if (pos2 > pos1) { - String kind = mTokenString.substring(pos1 + 1, pos2); - mTokenString = null; - status.addFatalError(String.format( - "The attribute %1$s already contains a %2$s reference.", - currAttrName, - kind)); - } - } - - if (mTokenString != null) { - // We're done with all our checks. mTokenString contains the - // current attribute value. We don't memorize the region nor the - // attribute, however we memorize the textual attribute name so - // that we can offer replacement for all its occurrences. - mXmlAttributeName = currAttrName; - } - - } else { - mTokenString = null; - status.addFatalError(String.format( - "The attribute %1$s does not accept a string reference.", - currAttrName)); - } + // Validate that the attribute accepts a string reference. + // This sets mTokenString to null by side-effect when it fails and + // adds a fatal error to the status as needed. + validateSelectedAttribute(editor, node, currAttrName, status); } else { // We shouldn't get here: we're missing one of the token string, the node @@ -707,6 +563,163 @@ public class ExtractStringRefactoring extends Refactoring { return status.isOK(); } + /** + * The region gives us the textual representation of the XML element + * where the selection starts, split using sub-regions. We now just + * need to iterate through the sub-regions to find which one + * contains the actual selection. We're interested in an attribute + * value however when we find one we want to memorize the attribute + * name that was defined just before. + * + * @return When the cursor is on a valid attribute name or value, returns the string of + * attribute name. As a side-effect, returns the value of the attribute in {@link #mTokenString} + */ + private String findSelectionInRegion(IStructuredDocumentRegion region, int selStart) { + + String currAttrName = null; + + int startInRegion = selStart - region.getStartOffset(); + + int nb = region.getNumberOfRegions(); + ITextRegionList list = region.getRegions(); + String currAttrValue = null; + + for (int i = 0; i < nb; i++) { + ITextRegion subRegion = list.get(i); + String type = subRegion.getType(); + + if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { + currAttrName = region.getText(subRegion); + + // I like to select the attribute definition and invoke + // the extract string wizard. So if the selection is on + // the attribute name part, find the value that is just + // after and use it as if it were the selection. + + if (subRegion.getStart() <= startInRegion && + startInRegion < subRegion.getTextEnd()) { + // A well-formed attribute is composed of a name, + // an equal sign and the value. There can't be any space + // in between, which makes the parsing a lot easier. + if (i <= nb - 3 && + DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals( + list.get(i + 1).getType())) { + subRegion = list.get(i + 2); + type = subRegion.getType(); + if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals( + type)) { + currAttrValue = region.getText(subRegion); + } + } + } + + } else if (subRegion.getStart() <= startInRegion && + startInRegion < subRegion.getTextEnd() && + DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { + currAttrValue = region.getText(subRegion); + } + + if (currAttrValue != null) { + // We found the value. Only accept it if not empty + // and if we found an attribute name before. + String text = currAttrValue; + + // The attribute value will contain the XML quotes. Remove them. + int len = text.length(); + if (len >= 2 && + text.charAt(0) == '"' && + text.charAt(len - 1) == '"') { + text = text.substring(1, len - 1); + } else if (len >= 2 && + text.charAt(0) == '\'' && + text.charAt(len - 1) == '\'') { + text = text.substring(1, len - 1); + } + if (text.length() > 0 && currAttrName != null) { + // Setting mTokenString to non-null marks the fact we + // accept this attribute. + mTokenString = text; + } + + break; + } + } + + return currAttrName; + } + + /** + * Validates that the attribute accepts a string reference. + * This sets mTokenString to null by side-effect when it fails and + * adds a fatal error to the status as needed. + */ + private void validateSelectedAttribute(AndroidEditor editor, Node node, + String attrName, RefactoringStatus status) { + UiElementNode rootUiNode = editor.getUiRootNode(); + UiElementNode currentUiNode = + rootUiNode == null ? null : rootUiNode.findXmlNode(node); + ReferenceAttributeDescriptor attrDesc = null; + + if (currentUiNode != null) { + // remove any namespace prefix from the attribute name + String name = attrName; + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + name = name.substring(pos + 1); + } + + for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) { + if (attrNode.getDescriptor().getXmlLocalName().equals(name)) { + AttributeDescriptor desc = attrNode.getDescriptor(); + if (desc instanceof ReferenceAttributeDescriptor) { + attrDesc = (ReferenceAttributeDescriptor) desc; + } + break; + } + } + } + + // The attribute descriptor is a resource reference. It must either accept + // of any resource type or specifically accept string types. + if (attrDesc != null && + (attrDesc.getResourceType() == null || + attrDesc.getResourceType() == ResourceType.STRING)) { + // We have one more check to do: is the current string value already + // an Android XML string reference? If so, we can't edit it. + if (mTokenString.startsWith("@")) { //$NON-NLS-1$ + int pos1 = 0; + if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') { + pos1++; + } + int pos2 = mTokenString.indexOf('/'); + if (pos2 > pos1) { + String kind = mTokenString.substring(pos1 + 1, pos2); + if (ResourceType.STRING.getName().equals(kind)) { //$NON-NLS-1$ + mTokenString = null; + status.addFatalError(String.format( + "The attribute %1$s already contains a %2$s reference.", + attrName, + kind)); + } + } + } + + if (mTokenString != null) { + // We're done with all our checks. mTokenString contains the + // current attribute value. We don't memorize the region nor the + // attribute, however we memorize the textual attribute name so + // that we can offer replacement for all its occurrences. + mXmlAttributeName = attrName; + } + + } else { + mTokenString = null; + status.addFatalError(String.format( + "The attribute %1$s does not accept a string reference.", + attrName)); + } + } + /** * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit() * Might not be useful. @@ -1373,412 +1386,6 @@ public class ExtractStringRefactoring extends Refactoring { return null; } - public 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 mEditGroups; - - public ReplaceStringsVisitor(AST ast, - ASTRewrite astRewrite, - ArrayList editGroups, - String oldString, - String rQualifier, - String xmlId) { - mAst = ast; - mRewriter = astRewrite; - mEditGroups = editGroups; - mOldString = oldString; - mRQualifier = rQualifier; - mXmlId = xmlId; - } - - @SuppressWarnings("unchecked") //$NON-NLS-1$ - @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); - - Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$ - SimpleName idName = mAst.newSimpleName(mXmlId); - ASTNode newNode = mAst.newQualifiedName(qualifierName, idName); - 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. - context = mAst.newSimpleName("Context"); //$NON-NLS-1$ - } - } - - 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 TextEditGroup(title); - mEditGroups.add(editGroup); - mRewriter.replace(node, newNode, editGroup); - } - return super.visit(node); - } - - /** - * Examines if the StringLiteral is part of of an assignment to a string, - * 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; - } - - /** - * 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("unchecked") //$NON-NLS-1$ - 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); - ITypeBinding clazzType = clazz == null ? null : clazz.resolveBinding(); - return findContextFieldOrMethod(clazzType); - } - - private Expression findContextFieldOrMethod(ITypeBinding clazzType) { - TreeMap results = new TreeMap(); - 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 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 findParentClass(ASTNode node, Class clazz) { - 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; - } - } - /** * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the * work and creates a descriptor that can be used to replay that refactoring later. diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java new file mode 100755 index 000000000..dd0f9f4db --- /dev/null +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java @@ -0,0 +1,457 @@ +/* + * 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.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 mEditGroups; + + public ReplaceStringsVisitor(AST ast, + ASTRewrite astRewrite, + ArrayList editGroups, + String oldString, + String rQualifier, + String xmlId) { + mAst = ast; + mRewriter = astRewrite; + mEditGroups = editGroups; + mOldString = oldString; + mRQualifier = rQualifier; + mXmlId = xmlId; + } + + @SuppressWarnings("unchecked") //$NON-NLS-1$ + @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); + + Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$ + SimpleName idName = mAst.newSimpleName(mXmlId); + ASTNode newNode = mAst.newQualifiedName(qualifierName, idName); + 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. + context = mAst.newSimpleName("Context"); //$NON-NLS-1$ + } + } + + 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 TextEditGroup(title); + mEditGroups.add(editGroup); + mRewriter.replace(node, newNode, editGroup); + } + return super.visit(node); + } + + /** + * Examines if the StringLiteral is part of of an assignment to a string, + * 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; + } + + /** + * 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("unchecked") //$NON-NLS-1$ + 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); + ITypeBinding clazzType = clazz == null ? null : clazz.resolveBinding(); + return findContextFieldOrMethod(clazzType); + } + + private Expression findContextFieldOrMethod(ITypeBinding clazzType) { + TreeMap results = new TreeMap(); + 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 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 findParentClass(ASTNode node, Class clazz) { + 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; + } +}