1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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.tools.lint.checks; 18 19 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI; 20 import static com.android.tools.lint.detector.api.LintConstants.ATTR_NAME; 21 import static com.android.tools.lint.detector.api.LintConstants.ATTR_REF_PREFIX; 22 import static com.android.tools.lint.detector.api.LintConstants.DOT_GIF; 23 import static com.android.tools.lint.detector.api.LintConstants.DOT_JPG; 24 import static com.android.tools.lint.detector.api.LintConstants.DOT_PNG; 25 import static com.android.tools.lint.detector.api.LintConstants.DOT_XML; 26 import static com.android.tools.lint.detector.api.LintConstants.RESOURCE_CLR_STYLEABLE; 27 import static com.android.tools.lint.detector.api.LintConstants.RESOURCE_CLZ_ARRAY; 28 import static com.android.tools.lint.detector.api.LintConstants.RESOURCE_CLZ_ID; 29 import static com.android.tools.lint.detector.api.LintConstants.RES_FOLDER; 30 import static com.android.tools.lint.detector.api.LintConstants.R_ATTR_PREFIX; 31 import static com.android.tools.lint.detector.api.LintConstants.R_CLASS; 32 import static com.android.tools.lint.detector.api.LintConstants.R_ID_PREFIX; 33 import static com.android.tools.lint.detector.api.LintConstants.R_PREFIX; 34 import static com.android.tools.lint.detector.api.LintConstants.TAG_ARRAY; 35 import static com.android.tools.lint.detector.api.LintConstants.TAG_ITEM; 36 import static com.android.tools.lint.detector.api.LintConstants.TAG_RESOURCES; 37 import static com.android.tools.lint.detector.api.LintConstants.TAG_STRING_ARRAY; 38 import static com.android.tools.lint.detector.api.LintConstants.TAG_STYLE; 39 import static com.android.tools.lint.detector.api.LintUtils.endsWith; 40 41 import com.android.resources.ResourceType; 42 import com.android.tools.lint.detector.api.Category; 43 import com.android.tools.lint.detector.api.Context; 44 import com.android.tools.lint.detector.api.Detector; 45 import com.android.tools.lint.detector.api.Issue; 46 import com.android.tools.lint.detector.api.JavaContext; 47 import com.android.tools.lint.detector.api.LintUtils; 48 import com.android.tools.lint.detector.api.Location; 49 import com.android.tools.lint.detector.api.ResourceXmlDetector; 50 import com.android.tools.lint.detector.api.Scope; 51 import com.android.tools.lint.detector.api.Severity; 52 import com.android.tools.lint.detector.api.Speed; 53 import com.android.tools.lint.detector.api.XmlContext; 54 55 import org.w3c.dom.Attr; 56 import org.w3c.dom.Element; 57 import org.w3c.dom.Node; 58 import org.w3c.dom.NodeList; 59 60 import java.io.File; 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.Collection; 64 import java.util.Collections; 65 import java.util.Comparator; 66 import java.util.EnumSet; 67 import java.util.HashMap; 68 import java.util.HashSet; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.Set; 72 73 import lombok.ast.AstVisitor; 74 import lombok.ast.ClassDeclaration; 75 import lombok.ast.ForwardingAstVisitor; 76 import lombok.ast.NormalTypeBody; 77 import lombok.ast.VariableDeclaration; 78 import lombok.ast.VariableDefinition; 79 80 /** 81 * Finds unused resources. 82 * <p> 83 * Note: This detector currently performs *string* analysis to check Java files. 84 * The Lint API needs an official Java AST API (or map to an existing one like 85 * BCEL for bytecode analysis etc) and once it does this should be updated to 86 * use it. 87 */ 88 public class UnusedResourceDetector extends ResourceXmlDetector implements Detector.JavaScanner { 89 90 /** Unused resources (other than ids). */ 91 public static final Issue ISSUE = Issue.create("UnusedResources", //$NON-NLS-1$ 92 "Looks for unused resources", 93 "Unused resources make applications larger and slow down builds.", 94 Category.PERFORMANCE, 95 3, 96 Severity.WARNING, 97 UnusedResourceDetector.class, 98 EnumSet.of(Scope.MANIFEST, Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES)); 99 /** Unused id's */ 100 public static final Issue ISSUE_IDS = Issue.create("UnusedIds", //$NON-NLS-1$ 101 "Looks for unused id's", 102 "This resource id definition appears not to be needed since it is not referenced " + 103 "from anywhere. Having id definitions, even if unused, is not necessarily a bad " + 104 "idea since they make working on layouts and menus easier, so there is not a " + 105 "strong reason to delete these.", 106 Category.PERFORMANCE, 107 1, 108 Severity.WARNING, 109 UnusedResourceDetector.class, 110 EnumSet.of(Scope.MANIFEST, Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES)) 111 .setEnabledByDefault(false); 112 113 private Set<String> mDeclarations; 114 private Set<String> mReferences; 115 private Map<String, Location> mUnused; 116 117 /** 118 * Constructs a new {@link UnusedResourceDetector} 119 */ 120 public UnusedResourceDetector() { 121 } 122 123 @Override 124 public void run(Context context) { 125 assert false; 126 } 127 128 @Override 129 public boolean appliesTo(Context context, File file) { 130 return true; 131 } 132 133 @Override 134 public void beforeCheckProject(Context context) { 135 if (context.getPhase() == 1) { 136 mDeclarations = new HashSet<String>(300); 137 mReferences = new HashSet<String>(300); 138 } 139 } 140 141 // ---- Implements JavaScanner ---- 142 143 @Override 144 public void beforeCheckFile(Context context) { 145 File file = context.file; 146 147 String fileName = file.getName(); 148 boolean isXmlFile = endsWith(fileName, DOT_XML); 149 if (isXmlFile 150 || endsWith(fileName, DOT_PNG) 151 || endsWith(fileName, DOT_JPG) 152 || endsWith(fileName, DOT_GIF)) { 153 String parentName = file.getParentFile().getName(); 154 int dash = parentName.indexOf('-'); 155 String typeName = parentName.substring(0, dash == -1 ? parentName.length() : dash); 156 ResourceType type = ResourceType.getEnum(typeName); 157 if (type != null && LintUtils.isFileBasedResourceType(type)) { 158 String baseName = fileName.substring(0, fileName.length() - DOT_XML.length()); 159 String resource = R_PREFIX + typeName + '.' + baseName; 160 if (context.getPhase() == 1) { 161 mDeclarations.add(resource); 162 } else { 163 assert context.getPhase() == 2; 164 if (mUnused.containsKey(resource)) { 165 // Check whether this is an XML document that has a tools:ignore attribute 166 // on the document element: if so don't record it as a declaration. 167 if (isXmlFile && context instanceof XmlContext) { 168 XmlContext xmlContext = (XmlContext) context; 169 if (xmlContext.document != null 170 && xmlContext.document.getDocumentElement() != null) { 171 Element root = xmlContext.document.getDocumentElement(); 172 if (xmlContext.getDriver().isSuppressed(ISSUE, root)) { 173 // Also remove it from consideration such that even the 174 // presence of this field in the R file is ignored. 175 if (mUnused != null) { 176 mUnused.remove(resource); 177 } 178 return; 179 } 180 } 181 } 182 183 recordLocation(resource, Location.create(file)); 184 } 185 } 186 } 187 } 188 } 189 190 @Override 191 public void afterCheckProject(Context context) { 192 if (context.getPhase() == 1) { 193 mDeclarations.removeAll(mReferences); 194 Set<String> unused = mDeclarations; 195 mReferences = null; 196 mDeclarations = null; 197 198 // Remove styles and attributes: they may be used, analysis isn't complete for these 199 List<String> styles = new ArrayList<String>(); 200 for (String resource : unused) { 201 // R.style.x, R.styleable.x, R.attr 202 if (resource.startsWith("R.style") //$NON-NLS-1$ 203 || resource.startsWith("R.attr")) { //$NON-NLS-1$ 204 styles.add(resource); 205 } 206 } 207 unused.removeAll(styles); 208 209 // Remove id's if the user has disabled reporting issue ids 210 if (unused.size() > 0 && !context.isEnabled(ISSUE_IDS)) { 211 // Remove all R.id references 212 List<String> ids = new ArrayList<String>(); 213 for (String resource : unused) { 214 if (resource.startsWith(R_ID_PREFIX)) { 215 ids.add(resource); 216 } 217 } 218 unused.removeAll(ids); 219 } 220 221 if (unused.size() > 0) { 222 mUnused = new HashMap<String, Location>(unused.size()); 223 for (String resource : unused) { 224 mUnused.put(resource, null); 225 } 226 227 // Request another pass, and in the second pass we'll gather location 228 // information for all declaration locations we've found 229 context.requestRepeat(this, Scope.ALL_RESOURCES_SCOPE); 230 } 231 } else { 232 assert context.getPhase() == 2; 233 234 // Report any resources that we (for some reason) could not find a declaration 235 // location for 236 if (mUnused.size() > 0) { 237 // Fill in locations for files that we didn't encounter in other ways 238 for (Map.Entry<String, Location> entry : mUnused.entrySet()) { 239 String resource = entry.getKey(); 240 Location location = entry.getValue(); 241 if (location != null) { 242 continue; 243 } 244 245 // Try to figure out the file if it's a file based resource (such as R.layout) -- 246 // in that case we can figure out the filename since it has a simple mapping 247 // from the resource name (though the presence of qualifiers like -land etc 248 // makes it a little tricky if there's no base file provided) 249 int secondDot = resource.indexOf('.', 2); 250 String typeName = resource.substring(2, secondDot); // 2: Skip R. 251 ResourceType type = ResourceType.getEnum(typeName); 252 if (type != null && LintUtils.isFileBasedResourceType(type)) { 253 String name = resource.substring(secondDot + 1); 254 255 File res = new File(context.getProject().getDir(), RES_FOLDER); 256 File[] folders = res.listFiles(); 257 if (folders != null) { 258 // Process folders in alphabetical order such that we process 259 // based folders first: we want the locations in base folder 260 // order 261 Arrays.sort(folders, new Comparator<File>() { 262 @Override 263 public int compare(File file1, File file2) { 264 return file1.getName().compareTo(file2.getName()); 265 } 266 }); 267 for (File folder : folders) { 268 if (folder.getName().startsWith(typeName)) { 269 File[] files = folder.listFiles(); 270 if (files != null) { 271 for (File file : files) { 272 String fileName = file.getName(); 273 if (fileName.startsWith(name) 274 && fileName.startsWith(".", //$NON-NLS-1$ 275 name.length())) { 276 recordLocation(resource, Location.create(file)); 277 } 278 } 279 } 280 } 281 } 282 } 283 } 284 } 285 286 List<String> sorted = new ArrayList<String>(mUnused.keySet()); 287 Collections.sort(sorted); 288 289 for (String resource : sorted) { 290 Location location = mUnused.get(resource); 291 if (location != null) { 292 // We were prepending locations, but we want to prefer the base folders 293 location = Location.reverse(location); 294 } 295 String message = String.format("The resource %1$s appears to be unused", 296 resource); 297 Issue issue = getIssue(resource); 298 // TODO: Compute applicable node scope 299 context.report(issue, location, message, resource); 300 } 301 } 302 } 303 } 304 305 private static Issue getIssue(String resource) { 306 return resource.startsWith(R_ID_PREFIX) ? ISSUE_IDS : ISSUE; 307 } 308 309 private void recordLocation(String resource, Location location) { 310 Location oldLocation = mUnused.get(resource); 311 if (oldLocation != null) { 312 location.setSecondary(oldLocation); 313 } 314 mUnused.put(resource, location); 315 } 316 317 @Override 318 public Collection<String> getApplicableAttributes() { 319 return ALL; 320 } 321 322 @Override 323 public Collection<String> getApplicableElements() { 324 return Arrays.asList( 325 TAG_STYLE, 326 TAG_RESOURCES, 327 TAG_ARRAY, 328 TAG_STRING_ARRAY 329 ); 330 } 331 332 @Override 333 public void visitElement(XmlContext context, Element element) { 334 if (TAG_RESOURCES.equals(element.getTagName())) { 335 for (Element item : LintUtils.getChildren(element)) { 336 String name = item.getAttribute(ATTR_NAME); 337 if (name.length() > 0) { 338 if (name.indexOf('.') != -1) { 339 name = name.replace('.', '_'); 340 } 341 String type = item.getTagName(); 342 if (type.equals(TAG_ITEM)) { 343 type = RESOURCE_CLZ_ID; 344 } else if (type.equals("declare-styleable")) { //$NON-NLS-1$ 345 type = RESOURCE_CLR_STYLEABLE; 346 } else if (type.contains("array")) { //$NON-NLS-1$ 347 // <string-array> etc 348 type = RESOURCE_CLZ_ARRAY; 349 } 350 String resource = R_PREFIX + type + '.' + name; 351 352 if (context.getPhase() == 1) { 353 mDeclarations.add(resource); 354 checkChildRefs(item); 355 } else { 356 assert context.getPhase() == 2; 357 if (mUnused.containsKey(resource)) { 358 if (context.getDriver().isSuppressed(getIssue(resource), item)) { 359 mUnused.remove(resource); 360 return; 361 } 362 recordLocation(resource, context.getLocation(item)); 363 } 364 } 365 } 366 } 367 } else if (mReferences != null) { 368 assert TAG_STYLE.equals(element.getTagName()) 369 || TAG_ARRAY.equals(element.getTagName()) 370 || TAG_STRING_ARRAY.equals(element.getTagName()); 371 for (Element item : LintUtils.getChildren(element)) { 372 checkChildRefs(item); 373 } 374 } 375 } 376 377 private void checkChildRefs(Element item) { 378 // Look for ?attr/ and @dimen/foo etc references in the item children 379 NodeList childNodes = item.getChildNodes(); 380 for (int i = 0, n = childNodes.getLength(); i < n; i++) { 381 Node child = childNodes.item(i); 382 if (child.getNodeType() == Node.TEXT_NODE) { 383 String text = child.getNodeValue(); 384 385 int index = text.indexOf(ATTR_REF_PREFIX); 386 if (index != -1) { 387 String name = text.substring(index + ATTR_REF_PREFIX.length()).trim(); 388 mReferences.add(R_ATTR_PREFIX + name); 389 } else { 390 index = text.indexOf('@'); 391 if (index != -1 && text.indexOf('/', index) != -1 392 && !text.startsWith("@android:", index)) { //$NON-NLS-1$ 393 // Compute R-string, e.g. @string/foo => R.string.foo 394 String token = text.substring(index + 1).trim().replace('/', '.'); 395 String r = R_PREFIX + token; 396 mReferences.add(r); 397 } 398 } 399 } 400 } 401 } 402 403 @Override 404 public void visitAttribute(XmlContext context, Attr attribute) { 405 String value = attribute.getValue(); 406 407 if (value.startsWith("@+") && !value.startsWith("@+android")) { //$NON-NLS-1$ //$NON-NLS-2$ 408 String resource = R_PREFIX + value.substring(2).replace('/', '.'); 409 // We already have the declarations when we scan the R file, but we're tracking 410 // these here to get attributes for position info 411 412 if (context.getPhase() == 1) { 413 mDeclarations.add(resource); 414 } else if (mUnused.containsKey(resource)) { 415 if (context.getDriver().isSuppressed(getIssue(resource), attribute)) { 416 mUnused.remove(resource); 417 return; 418 } 419 recordLocation(resource, context.getLocation(attribute)); 420 return; 421 } 422 } else if (mReferences != null) { 423 if (value.startsWith("@") //$NON-NLS-1$ 424 && !value.startsWith("@android:")) { //$NON-NLS-1$ 425 // Compute R-string, e.g. @string/foo => R.string.foo 426 String r = R_PREFIX + value.substring(1).replace('/', '.'); 427 mReferences.add(r); 428 } else if (value.startsWith(ATTR_REF_PREFIX)) { 429 mReferences.add(R_ATTR_PREFIX + value.substring(ATTR_REF_PREFIX.length())); 430 } 431 } 432 433 if (attribute.getNamespaceURI() != null 434 && !ANDROID_URI.equals(attribute.getNamespaceURI()) && mReferences != null) { 435 mReferences.add(R_ATTR_PREFIX + attribute.getLocalName()); 436 } 437 } 438 439 @Override 440 public Speed getSpeed() { 441 return Speed.SLOW; 442 } 443 444 @Override 445 public List<Class<? extends lombok.ast.Node>> getApplicableNodeTypes() { 446 return Collections.<Class<? extends lombok.ast.Node>>singletonList(ClassDeclaration.class); 447 } 448 449 @Override 450 public boolean appliesToResourceRefs() { 451 return true; 452 } 453 454 @Override 455 public void visitResourceReference(JavaContext context, AstVisitor visitor, 456 lombok.ast.Node node, String type, String name, boolean isFramework) { 457 if (mReferences != null && !isFramework) { 458 String reference = R_PREFIX + type + '.' + name; 459 mReferences.add(reference); 460 } 461 } 462 463 @Override 464 public AstVisitor createJavaVisitor(JavaContext context) { 465 if (mReferences != null) { 466 return new UnusedResourceVisitor(); 467 } else { 468 // Second pass, computing resource declaration locations: No need to look at Java 469 return null; 470 } 471 } 472 473 // Look for references and declarations 474 private class UnusedResourceVisitor extends ForwardingAstVisitor { 475 @Override 476 public boolean visitClassDeclaration(ClassDeclaration node) { 477 // Look for declarations of R class fields and store them in 478 // mDeclarations 479 String description = node.getDescription(); 480 if (description.equals(R_CLASS)) { 481 // This is an R class. We can process this class very deliberately. 482 // The R class has a very specific AST format: 483 // ClassDeclaration ("R") 484 // NormalTypeBody 485 // ClassDeclaration (e.g. "drawable") 486 // NormalTypeBody 487 // VariableDeclaration 488 // VariableDefinition (e.g. "ic_launcher") 489 for (lombok.ast.Node body : node.getChildren()) { 490 if (body instanceof NormalTypeBody) { 491 for (lombok.ast.Node subclass : body.getChildren()) { 492 if (subclass instanceof ClassDeclaration) { 493 String className = ((ClassDeclaration) subclass).getDescription(); 494 for (lombok.ast.Node innerBody : subclass.getChildren()) { 495 if (innerBody instanceof NormalTypeBody) { 496 for (lombok.ast.Node field : innerBody.getChildren()) { 497 if (field instanceof VariableDeclaration) { 498 for (lombok.ast.Node child : field.getChildren()) { 499 if (child instanceof VariableDefinition) { 500 VariableDefinition def = 501 (VariableDefinition) child; 502 String name = def.astVariables().first() 503 .astName().astValue(); 504 String resource = R_PREFIX + className 505 + '.' + name; 506 mDeclarations.add(resource); 507 } // Else: It could be a comment node 508 } 509 } 510 } 511 } 512 } 513 } 514 } 515 } 516 } 517 518 return true; 519 } 520 521 return false; 522 } 523 } 524 } 525