Home | History | Annotate | Download | only in checks
      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