ADT XML String Refactoring: fix refusing to edit @+id/blah.
It correctly only refuses to edit @string/blah now. This CL also does a bit of refactoring; I extracted some methods and a class to make it a bit easier to read. BUG 2066460 Change-Id: I95a34d28d6390ab0cc075f05ea83ceec04993ae9
This commit is contained in:
@@ -50,26 +50,7 @@ import org.eclipse.jdt.core.compiler.InvalidInputException;
|
|||||||
import org.eclipse.jdt.core.dom.AST;
|
import org.eclipse.jdt.core.dom.AST;
|
||||||
import org.eclipse.jdt.core.dom.ASTNode;
|
import org.eclipse.jdt.core.dom.ASTNode;
|
||||||
import org.eclipse.jdt.core.dom.ASTParser;
|
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.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.ASTRewrite;
|
||||||
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
|
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
|
||||||
import org.eclipse.jface.text.ITextSelection;
|
import org.eclipse.jface.text.ITextSelection;
|
||||||
@@ -106,7 +87,6 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This refactoring extracts a string from a file and replaces it by an Android resource ID
|
* 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()) {
|
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 {
|
} finally {
|
||||||
@@ -521,7 +502,8 @@ public class ExtractStringRefactoring extends Refactoring {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node == null) {
|
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();
|
return status.isOK();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,12 +524,59 @@ public class ExtractStringRefactoring extends Refactoring {
|
|||||||
sdoc.getRegionAtCharacterOffset(selStart);
|
sdoc.getRegionAtCharacterOffset(selStart);
|
||||||
if (region != null &&
|
if (region != null &&
|
||||||
DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
|
DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
|
||||||
// The region gives us the textual representation of the XML element
|
// Find if any sub-region representing an attribute contains the
|
||||||
// where the selection starts, split using sub-regions. We now just
|
// selection. If it does, returns the name of the attribute in
|
||||||
// need to iterate through the sub-regions to find which one
|
// currAttrName and returns the value in the field mTokenString.
|
||||||
// contains the actual selection. We're interested in an attribute
|
currAttrName = findSelectionInRegion(region, selStart);
|
||||||
// value however when we find one we want to memorize the attribute
|
|
||||||
// name that was defined just before.
|
if (mTokenString == null) {
|
||||||
|
status.addFatalError(
|
||||||
|
"The selection is not inside an actual XML attribute value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mTokenString != null && node != null && currAttrName != null) {
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// or the attribute name. All of them have been checked earlier so don't
|
||||||
|
// set any specific error.
|
||||||
|
mTokenString = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (smodel != null) {
|
||||||
|
smodel.releaseFromRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
monitor.worked(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 startInRegion = selStart - region.getStartOffset();
|
||||||
|
|
||||||
@@ -616,15 +645,16 @@ public class ExtractStringRefactoring extends Refactoring {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mTokenString == null) {
|
return currAttrName;
|
||||||
status.addFatalError(
|
|
||||||
"The selection is not inside an actual XML attribute value.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mTokenString != null && node != null && currAttrName != null) {
|
/**
|
||||||
|
* 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 rootUiNode = editor.getUiRootNode();
|
||||||
UiElementNode currentUiNode =
|
UiElementNode currentUiNode =
|
||||||
rootUiNode == null ? null : rootUiNode.findXmlNode(node);
|
rootUiNode == null ? null : rootUiNode.findXmlNode(node);
|
||||||
@@ -632,7 +662,7 @@ public class ExtractStringRefactoring extends Refactoring {
|
|||||||
|
|
||||||
if (currentUiNode != null) {
|
if (currentUiNode != null) {
|
||||||
// remove any namespace prefix from the attribute name
|
// remove any namespace prefix from the attribute name
|
||||||
String name = currAttrName;
|
String name = attrName;
|
||||||
int pos = name.indexOf(':');
|
int pos = name.indexOf(':');
|
||||||
if (pos > 0 && pos < name.length() - 1) {
|
if (pos > 0 && pos < name.length() - 1) {
|
||||||
name = name.substring(pos + 1);
|
name = name.substring(pos + 1);
|
||||||
@@ -664,47 +694,30 @@ public class ExtractStringRefactoring extends Refactoring {
|
|||||||
int pos2 = mTokenString.indexOf('/');
|
int pos2 = mTokenString.indexOf('/');
|
||||||
if (pos2 > pos1) {
|
if (pos2 > pos1) {
|
||||||
String kind = mTokenString.substring(pos1 + 1, pos2);
|
String kind = mTokenString.substring(pos1 + 1, pos2);
|
||||||
|
if (ResourceType.STRING.getName().equals(kind)) { //$NON-NLS-1$
|
||||||
mTokenString = null;
|
mTokenString = null;
|
||||||
status.addFatalError(String.format(
|
status.addFatalError(String.format(
|
||||||
"The attribute %1$s already contains a %2$s reference.",
|
"The attribute %1$s already contains a %2$s reference.",
|
||||||
currAttrName,
|
attrName,
|
||||||
kind));
|
kind));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mTokenString != null) {
|
if (mTokenString != null) {
|
||||||
// We're done with all our checks. mTokenString contains the
|
// We're done with all our checks. mTokenString contains the
|
||||||
// current attribute value. We don't memorize the region nor the
|
// current attribute value. We don't memorize the region nor the
|
||||||
// attribute, however we memorize the textual attribute name so
|
// attribute, however we memorize the textual attribute name so
|
||||||
// that we can offer replacement for all its occurrences.
|
// that we can offer replacement for all its occurrences.
|
||||||
mXmlAttributeName = currAttrName;
|
mXmlAttributeName = attrName;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
mTokenString = null;
|
mTokenString = null;
|
||||||
status.addFatalError(String.format(
|
status.addFatalError(String.format(
|
||||||
"The attribute %1$s does not accept a string reference.",
|
"The attribute %1$s does not accept a string reference.",
|
||||||
currAttrName));
|
attrName));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
// We shouldn't get here: we're missing one of the token string, the node
|
|
||||||
// or the attribute name. All of them have been checked earlier so don't
|
|
||||||
// set any specific error.
|
|
||||||
mTokenString = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (smodel != null) {
|
|
||||||
smodel.releaseFromRead();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
monitor.worked(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return status.isOK();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1373,412 +1386,6 @@ public class ExtractStringRefactoring extends Refactoring {
|
|||||||
return null;
|
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
|
* 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.
|
* work and creates a descriptor that can be used to replay that refactoring later.
|
||||||
|
|||||||
@@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user