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