aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java480
1 files changed, 480 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java
new file mode 100644
index 000000000..e058ce1ba
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java
@@ -0,0 +1,480 @@
+/*
+ * 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;
+ }
+}