1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.refactorings.extractstring; 18 19 import org.eclipse.jdt.core.dom.AST; 20 import org.eclipse.jdt.core.dom.ASTNode; 21 import org.eclipse.jdt.core.dom.ASTVisitor; 22 import org.eclipse.jdt.core.dom.Assignment; 23 import org.eclipse.jdt.core.dom.ClassInstanceCreation; 24 import org.eclipse.jdt.core.dom.Expression; 25 import org.eclipse.jdt.core.dom.IMethodBinding; 26 import org.eclipse.jdt.core.dom.ITypeBinding; 27 import org.eclipse.jdt.core.dom.IVariableBinding; 28 import org.eclipse.jdt.core.dom.MethodDeclaration; 29 import org.eclipse.jdt.core.dom.MethodInvocation; 30 import org.eclipse.jdt.core.dom.Modifier; 31 import org.eclipse.jdt.core.dom.Name; 32 import org.eclipse.jdt.core.dom.SimpleName; 33 import org.eclipse.jdt.core.dom.SimpleType; 34 import org.eclipse.jdt.core.dom.SingleVariableDeclaration; 35 import org.eclipse.jdt.core.dom.StringLiteral; 36 import org.eclipse.jdt.core.dom.Type; 37 import org.eclipse.jdt.core.dom.TypeDeclaration; 38 import org.eclipse.jdt.core.dom.VariableDeclarationExpression; 39 import org.eclipse.jdt.core.dom.VariableDeclarationFragment; 40 import org.eclipse.jdt.core.dom.VariableDeclarationStatement; 41 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; 42 import org.eclipse.text.edits.TextEditGroup; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.TreeMap; 47 48 /** 49 * Visitor used by {@link ExtractStringRefactoring} to extract a string from an existing 50 * Java source and replace it by an Android XML string reference. 51 * 52 * @see ExtractStringRefactoring#computeJavaChanges 53 */ 54 class ReplaceStringsVisitor extends ASTVisitor { 55 56 private static final String CLASS_ANDROID_CONTEXT = "android.content.Context"; //$NON-NLS-1$ 57 private static final String CLASS_JAVA_CHAR_SEQUENCE = "java.lang.CharSequence"; //$NON-NLS-1$ 58 private static final String CLASS_JAVA_STRING = "java.lang.String"; //$NON-NLS-1$ 59 60 61 private final AST mAst; 62 private final ASTRewrite mRewriter; 63 private final String mOldString; 64 private final String mRQualifier; 65 private final String mXmlId; 66 private final ArrayList<TextEditGroup> mEditGroups; 67 ReplaceStringsVisitor(AST ast, ASTRewrite astRewrite, ArrayList<TextEditGroup> editGroups, String oldString, String rQualifier, String xmlId)68 public ReplaceStringsVisitor(AST ast, 69 ASTRewrite astRewrite, 70 ArrayList<TextEditGroup> editGroups, 71 String oldString, 72 String rQualifier, 73 String xmlId) { 74 mAst = ast; 75 mRewriter = astRewrite; 76 mEditGroups = editGroups; 77 mOldString = oldString; 78 mRQualifier = rQualifier; 79 mXmlId = xmlId; 80 } 81 82 @SuppressWarnings("unchecked") 83 @Override visit(StringLiteral node)84 public boolean visit(StringLiteral node) { 85 if (node.getLiteralValue().equals(mOldString)) { 86 87 // We want to analyze the calling context to understand whether we can 88 // just replace the string literal by the named int constant (R.id.foo) 89 // or if we should generate a Context.getString() call. 90 boolean useGetResource = false; 91 useGetResource = examineVariableDeclaration(node) || 92 examineMethodInvocation(node) || 93 examineAssignment(node); 94 95 Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$ 96 SimpleName idName = mAst.newSimpleName(mXmlId); 97 ASTNode newNode = mAst.newQualifiedName(qualifierName, idName); 98 boolean disabledChange = false; 99 String title = "Replace string by ID"; 100 101 if (useGetResource) { 102 Expression context = methodHasContextArgument(node); 103 if (context == null && !isClassDerivedFromContext(node)) { 104 // if we don't have a class that derives from Context and 105 // we don't have a Context method argument, then try a bit harder: 106 // can we find a method or a field that will give us a context? 107 context = findContextFieldOrMethod(node); 108 109 if (context == null) { 110 // If not, let's write Context.getString(), which is technically 111 // invalid but makes it a good clue on how to fix it. Since these 112 // will not compile, we create a disabled change by default. 113 context = mAst.newSimpleName("Context"); //$NON-NLS-1$ 114 disabledChange = true; 115 } 116 } 117 118 MethodInvocation mi2 = mAst.newMethodInvocation(); 119 mi2.setName(mAst.newSimpleName("getString")); //$NON-NLS-1$ 120 mi2.setExpression(context); 121 mi2.arguments().add(newNode); 122 123 newNode = mi2; 124 title = "Replace string by Context.getString(R.string...)"; 125 } 126 127 TextEditGroup editGroup = new EnabledTextEditGroup(title, !disabledChange); 128 mEditGroups.add(editGroup); 129 mRewriter.replace(node, newNode, editGroup); 130 } 131 return super.visit(node); 132 } 133 134 /** 135 * Examines if the StringLiteral is part of an assignment corresponding to the 136 * a string variable declaration, e.g. String foo = id. 137 * 138 * The parent fragment is of syntax "var = expr" or "var[] = expr". 139 * We want the type of the variable, which is either held by a 140 * VariableDeclarationStatement ("type [fragment]") or by a 141 * VariableDeclarationExpression. In either case, the type can be an array 142 * but for us all that matters is to know whether the type is an int or 143 * a string. 144 */ examineVariableDeclaration(StringLiteral node)145 private boolean examineVariableDeclaration(StringLiteral node) { 146 VariableDeclarationFragment fragment = findParentClass(node, 147 VariableDeclarationFragment.class); 148 149 if (fragment != null) { 150 ASTNode parent = fragment.getParent(); 151 152 Type type = null; 153 if (parent instanceof VariableDeclarationStatement) { 154 type = ((VariableDeclarationStatement) parent).getType(); 155 } else if (parent instanceof VariableDeclarationExpression) { 156 type = ((VariableDeclarationExpression) parent).getType(); 157 } 158 159 if (type instanceof SimpleType) { 160 return isJavaString(type.resolveBinding()); 161 } 162 } 163 164 return false; 165 } 166 167 /** 168 * Examines if the StringLiteral is part of a assignment to a variable that 169 * is a string. We need to lookup the variable to find its type, either in the 170 * enclosing method or class type. 171 */ examineAssignment(StringLiteral node)172 private boolean examineAssignment(StringLiteral node) { 173 174 Assignment assignment = findParentClass(node, Assignment.class); 175 if (assignment != null) { 176 Expression left = assignment.getLeftHandSide(); 177 178 ITypeBinding typeBinding = left.resolveTypeBinding(); 179 return isJavaString(typeBinding); 180 } 181 182 return false; 183 } 184 185 /** 186 * If the expression is part of a method invocation (aka a function call) or a 187 * class instance creation (aka a "new SomeClass" constructor call), we try to 188 * find the type of the argument being used. If it is a String (most likely), we 189 * want to return true (to generate a getString() call). However if there might 190 * be a similar method that takes an int, in which case we don't want to do that. 191 * 192 * This covers the case of Activity.setTitle(int resId) vs setTitle(String str). 193 */ 194 @SuppressWarnings("rawtypes") examineMethodInvocation(StringLiteral node)195 private boolean examineMethodInvocation(StringLiteral node) { 196 197 ASTNode parent = null; 198 List arguments = null; 199 IMethodBinding methodBinding = null; 200 201 MethodInvocation invoke = findParentClass(node, MethodInvocation.class); 202 if (invoke != null) { 203 parent = invoke; 204 arguments = invoke.arguments(); 205 methodBinding = invoke.resolveMethodBinding(); 206 } else { 207 ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class); 208 if (newclass != null) { 209 parent = newclass; 210 arguments = newclass.arguments(); 211 methodBinding = newclass.resolveConstructorBinding(); 212 } 213 } 214 215 if (parent != null && arguments != null && methodBinding != null) { 216 // We want to know which argument this is. 217 // Walk up the hierarchy again to find the immediate child of the parent, 218 // which should turn out to be one of the invocation arguments. 219 ASTNode child = null; 220 for (ASTNode n = node; n != parent; ) { 221 ASTNode p = n.getParent(); 222 if (p == parent) { 223 child = n; 224 break; 225 } 226 n = p; 227 } 228 if (child == null) { 229 // This can't happen: a parent of 'node' must be the child of 'parent'. 230 return false; 231 } 232 233 // Find the index 234 int index = 0; 235 for (Object arg : arguments) { 236 if (arg == child) { 237 break; 238 } 239 index++; 240 } 241 242 if (index == arguments.size()) { 243 // This can't happen: one of the arguments of 'invoke' must be 'child'. 244 return false; 245 } 246 247 // Eventually we want to determine if the parameter is a string type, 248 // in which case a Context.getString() call must be generated. 249 boolean useStringType = false; 250 251 // Find the type of that argument 252 ITypeBinding[] types = methodBinding.getParameterTypes(); 253 if (index < types.length) { 254 ITypeBinding type = types[index]; 255 useStringType = isJavaString(type); 256 } 257 258 // Now that we know that this method takes a String parameter, can we find 259 // a variant that would accept an int for the same parameter position? 260 if (useStringType) { 261 String name = methodBinding.getName(); 262 ITypeBinding clazz = methodBinding.getDeclaringClass(); 263 nextMethod: for (IMethodBinding mb2 : clazz.getDeclaredMethods()) { 264 if (methodBinding == mb2 || !mb2.getName().equals(name)) { 265 continue; 266 } 267 // We found a method with the same name. We want the same parameters 268 // except that the one at 'index' must be an int type. 269 ITypeBinding[] types2 = mb2.getParameterTypes(); 270 int len2 = types2.length; 271 if (types.length == len2) { 272 for (int i = 0; i < len2; i++) { 273 if (i == index) { 274 ITypeBinding type2 = types2[i]; 275 if (!("int".equals(type2.getQualifiedName()))) { //$NON-NLS-1$ 276 // The argument at 'index' is not an int. 277 continue nextMethod; 278 } 279 } else if (!types[i].equals(types2[i])) { 280 // One of the other arguments do not match our original method 281 continue nextMethod; 282 } 283 } 284 // If we got here, we found a perfect match: a method with the same 285 // arguments except the one at 'index' is an int. In this case we 286 // don't need to convert our R.id into a string. 287 useStringType = false; 288 break; 289 } 290 } 291 } 292 293 return useStringType; 294 } 295 return false; 296 } 297 298 /** 299 * Examines if the StringLiteral is part of a method declaration (a.k.a. a function 300 * definition) which takes a Context argument. 301 * If such, it returns the name of the variable as a {@link SimpleName}. 302 * Otherwise it returns null. 303 */ methodHasContextArgument(StringLiteral node)304 private SimpleName methodHasContextArgument(StringLiteral node) { 305 MethodDeclaration decl = findParentClass(node, MethodDeclaration.class); 306 if (decl != null) { 307 for (Object obj : decl.parameters()) { 308 if (obj instanceof SingleVariableDeclaration) { 309 SingleVariableDeclaration var = (SingleVariableDeclaration) obj; 310 if (isAndroidContext(var.getType())) { 311 return mAst.newSimpleName(var.getName().getIdentifier()); 312 } 313 } 314 } 315 } 316 return null; 317 } 318 319 /** 320 * Walks up the node hierarchy to find the class (aka type) where this statement 321 * is used and returns true if this class derives from android.content.Context. 322 */ isClassDerivedFromContext(StringLiteral node)323 private boolean isClassDerivedFromContext(StringLiteral node) { 324 TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class); 325 if (clazz != null) { 326 // This is the class that the user is currently writing, so it can't be 327 // a Context by itself, it has to be derived from it. 328 return isAndroidContext(clazz.getSuperclassType()); 329 } 330 return false; 331 } 332 findContextFieldOrMethod(StringLiteral node)333 private Expression findContextFieldOrMethod(StringLiteral node) { 334 TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class); 335 return clazz == null ? null : findContextFieldOrMethod(clazz.resolveBinding()); 336 } 337 findContextFieldOrMethod(ITypeBinding clazzType)338 private Expression findContextFieldOrMethod(ITypeBinding clazzType) { 339 TreeMap<Integer, Expression> results = new TreeMap<Integer, Expression>(); 340 findContextCandidates(results, clazzType, 0 /*superType*/); 341 if (results.size() > 0) { 342 Integer bestRating = results.keySet().iterator().next(); 343 return results.get(bestRating); 344 } 345 return null; 346 } 347 348 /** 349 * Find all method or fields that are candidates for providing a Context. 350 * There can be various choices amongst this class or its super classes. 351 * Sort them by rating in the results map. 352 * 353 * The best ever choice is to find a method with no argument that returns a Context. 354 * The second suitable choice is to find a Context field. 355 * The least desirable choice is to find a method with arguments. It's not really 356 * desirable since we can't generate these arguments automatically. 357 * 358 * Methods and fields from supertypes are ignored if they are private. 359 * 360 * The rating is reversed: the lowest rating integer is used for the best candidate. 361 * Because the superType argument is actually a recursion index, this makes the most 362 * immediate classes more desirable. 363 * 364 * @param results The map that accumulates the rating=>expression results. The lower 365 * rating number is the best candidate. 366 * @param clazzType The class examined. 367 * @param superType The recursion index. 368 * 0 for the immediate class, 1 for its super class, etc. 369 */ findContextCandidates(TreeMap<Integer, Expression> results, ITypeBinding clazzType, int superType)370 private void findContextCandidates(TreeMap<Integer, Expression> results, 371 ITypeBinding clazzType, 372 int superType) { 373 for (IMethodBinding mb : clazzType.getDeclaredMethods()) { 374 // If we're looking at supertypes, we can't use private methods. 375 if (superType != 0 && Modifier.isPrivate(mb.getModifiers())) { 376 continue; 377 } 378 379 if (isAndroidContext(mb.getReturnType())) { 380 // We found a method that returns something derived from Context. 381 382 int argsLen = mb.getParameterTypes().length; 383 if (argsLen == 0) { 384 // We'll favor any method that takes no argument, 385 // That would be the best candidate ever, so we can stop here. 386 MethodInvocation mi = mAst.newMethodInvocation(); 387 mi.setName(mAst.newSimpleName(mb.getName())); 388 results.put(Integer.MIN_VALUE, mi); 389 return; 390 } else { 391 // A method with arguments isn't as interesting since we wouldn't 392 // know how to populate such arguments. We'll use it if there are 393 // no other alternatives. We'll favor the one with the less arguments. 394 Integer rating = Integer.valueOf(10000 + 1000 * superType + argsLen); 395 if (!results.containsKey(rating)) { 396 MethodInvocation mi = mAst.newMethodInvocation(); 397 mi.setName(mAst.newSimpleName(mb.getName())); 398 results.put(rating, mi); 399 } 400 } 401 } 402 } 403 404 // A direct Context field would be more interesting than a method with 405 // arguments. Try to find one. 406 for (IVariableBinding var : clazzType.getDeclaredFields()) { 407 // If we're looking at supertypes, we can't use private field. 408 if (superType != 0 && Modifier.isPrivate(var.getModifiers())) { 409 continue; 410 } 411 412 if (isAndroidContext(var.getType())) { 413 // We found such a field. Let's use it. 414 Integer rating = Integer.valueOf(superType); 415 results.put(rating, mAst.newSimpleName(var.getName())); 416 break; 417 } 418 } 419 420 // Examine the super class to see if we can locate a better match 421 clazzType = clazzType.getSuperclass(); 422 if (clazzType != null) { 423 findContextCandidates(results, clazzType, superType + 1); 424 } 425 } 426 427 /** 428 * Walks up the node hierarchy and returns the first ASTNode of the requested class. 429 * Only look at parents. 430 * 431 * Implementation note: this is a generic method so that it returns the node already 432 * casted to the requested type. 433 */ 434 @SuppressWarnings("unchecked") findParentClass(ASTNode node, Class<T> clazz)435 private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) { 436 if (node != null) { 437 for (node = node.getParent(); node != null; node = node.getParent()) { 438 if (node.getClass().equals(clazz)) { 439 return (T) node; 440 } 441 } 442 } 443 return null; 444 } 445 446 /** 447 * Returns true if the given type is or derives from android.content.Context. 448 */ isAndroidContext(Type type)449 private boolean isAndroidContext(Type type) { 450 if (type != null) { 451 return isAndroidContext(type.resolveBinding()); 452 } 453 return false; 454 } 455 456 /** 457 * Returns true if the given type is or derives from android.content.Context. 458 */ isAndroidContext(ITypeBinding type)459 private boolean isAndroidContext(ITypeBinding type) { 460 for (; type != null; type = type.getSuperclass()) { 461 if (CLASS_ANDROID_CONTEXT.equals(type.getQualifiedName())) { 462 return true; 463 } 464 } 465 return false; 466 } 467 468 /** 469 * Returns true if this type binding represents a String or CharSequence type. 470 */ isJavaString(ITypeBinding type)471 private boolean isJavaString(ITypeBinding type) { 472 for (; type != null; type = type.getSuperclass()) { 473 if (CLASS_JAVA_STRING.equals(type.getQualifiedName()) || 474 CLASS_JAVA_CHAR_SEQUENCE.equals(type.getQualifiedName())) { 475 return true; 476 } 477 } 478 return false; 479 } 480 } 481