Home | History | Annotate | Download | only in checks
      1 /*
      2  * Copyright (C) 2012 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_PKG_PREFIX;
     20 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI;
     21 import static com.android.tools.lint.detector.api.LintConstants.AUTO_URI;
     22 import static com.android.tools.lint.detector.api.LintConstants.URI_PREFIX;
     23 import static com.android.tools.lint.detector.api.LintConstants.XMLNS_PREFIX;
     24 
     25 import com.android.tools.lint.detector.api.Category;
     26 import com.android.tools.lint.detector.api.Issue;
     27 import com.android.tools.lint.detector.api.LayoutDetector;
     28 import com.android.tools.lint.detector.api.LintUtils;
     29 import com.android.tools.lint.detector.api.Scope;
     30 import com.android.tools.lint.detector.api.Severity;
     31 import com.android.tools.lint.detector.api.Speed;
     32 import com.android.tools.lint.detector.api.XmlContext;
     33 
     34 import org.w3c.dom.Attr;
     35 import org.w3c.dom.Document;
     36 import org.w3c.dom.Element;
     37 import org.w3c.dom.NamedNodeMap;
     38 import org.w3c.dom.Node;
     39 import org.w3c.dom.NodeList;
     40 
     41 import java.util.HashMap;
     42 import java.util.Map;
     43 
     44 /**
     45  * Checks for various issues related to XML namespaces
     46  */
     47 public class NamespaceDetector extends LayoutDetector {
     48     /** Typos in the namespace */
     49     public static final Issue TYPO = Issue.create(
     50             "NamespaceTypo", //$NON-NLS-1$
     51             "Looks for misspellings in namespace declarations",
     52 
     53             "Accidental misspellings in namespace declarations can lead to some very " +
     54             "obscure error messages. This check looks for potential misspellings to " +
     55             "help track these down.",
     56             Category.CORRECTNESS,
     57             8,
     58             Severity.WARNING,
     59             NamespaceDetector.class,
     60             Scope.RESOURCE_FILE_SCOPE);
     61 
     62     /** Unused namespace declarations */
     63     public static final Issue UNUSED = Issue.create(
     64             "UnusedNamespace", //$NON-NLS-1$
     65             "Finds unused namespaces in XML documents",
     66 
     67             "Unused namespace declarations take up space and require processing that is not " +
     68             "necessary",
     69 
     70             Category.CORRECTNESS,
     71             1,
     72             Severity.WARNING,
     73             NamespaceDetector.class,
     74             Scope.RESOURCE_FILE_SCOPE);
     75 
     76     /** Using custom namespace attributes in a library project */
     77     public static final Issue CUSTOMVIEW = Issue.create(
     78             "LibraryCustomView", //$NON-NLS-1$
     79             "Flags custom attributes in libraries, which must use the res-auto-namespace instead",
     80 
     81             "When using a custom view with custom attributes in a library project, the layout " +
     82             "must use the special namespace " + AUTO_URI + " instead of a URI which includes " +
     83             "the library project's own package. This will be used to automatically adjust the " +
     84             "namespace of the attributes when the library resources are merged into the " +
     85             "application project.",
     86             Category.CORRECTNESS,
     87             6,
     88             Severity.ERROR,
     89             NamespaceDetector.class,
     90             Scope.RESOURCE_FILE_SCOPE);
     91 
     92     /** Prefix relevant for custom namespaces */
     93     private static final String XMLNS_ANDROID = "xmlns:android";                    //$NON-NLS-1$
     94     private static final String XMLNS_A = "xmlns:a";                                //$NON-NLS-1$
     95 
     96     private Map<String, Attr> mUnusedNamespaces;
     97     private boolean mCheckUnused;
     98     private boolean mCheckCustomAttrs;
     99 
    100     /** Constructs a new {@link NamespaceDetector} */
    101     public NamespaceDetector() {
    102     }
    103 
    104     @Override
    105     public Speed getSpeed() {
    106         return Speed.FAST;
    107     }
    108 
    109     @Override
    110     public void visitDocument(XmlContext context, Document document) {
    111         boolean haveCustomNamespace = false;
    112         Element root = document.getDocumentElement();
    113         NamedNodeMap attributes = root.getAttributes();
    114         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    115             Node item = attributes.item(i);
    116             if (item.getNodeName().startsWith(XMLNS_PREFIX)) {
    117                 String value = item.getNodeValue();
    118 
    119                 if (!value.equals(ANDROID_URI)) {
    120                     Attr attribute = (Attr) item;
    121 
    122                     if (value.startsWith(URI_PREFIX)) {
    123                         haveCustomNamespace = true;
    124                         if (mUnusedNamespaces == null) {
    125                             mUnusedNamespaces = new HashMap<String, Attr>();
    126                         }
    127                         mUnusedNamespaces.put(item.getNodeName().substring(XMLNS_PREFIX.length()),
    128                                 attribute);
    129                     }
    130 
    131                     String name = attribute.getName();
    132                     if (!name.equals(XMLNS_ANDROID) && !name.equals(XMLNS_A)) {
    133                         continue;
    134                     }
    135 
    136                     if (!context.isEnabled(TYPO)) {
    137                         continue;
    138                     }
    139 
    140                     if (name.equals(XMLNS_A)) {
    141                         // For the "android" prefix we always assume that the namespace prefix
    142                         // should be our expected prefix, but for the "a" prefix we make sure
    143                         // that it's at least "close"; if you're bound it to something completely
    144                         // different, don't complain.
    145                         if (LintUtils.editDistance(ANDROID_URI, value) > 4) {
    146                             continue;
    147                         }
    148                     }
    149 
    150                     if (value.equalsIgnoreCase(ANDROID_URI)) {
    151                         context.report(TYPO, attribute, context.getLocation(attribute),
    152                                 String.format(
    153                                     "URI is case sensitive: was \"%1$s\", expected \"%2$s\"",
    154                                     value, ANDROID_URI), null);
    155                     } else {
    156                         context.report(TYPO, attribute, context.getLocation(attribute),
    157                                 String.format(
    158                                     "Unexpected namespace URI bound to the \"android\" " +
    159                                     "prefix, was %1$s, expected %2$s", value, ANDROID_URI),
    160                                 null);
    161                     }
    162                 }
    163             }
    164         }
    165 
    166         if (haveCustomNamespace) {
    167             mCheckCustomAttrs = context.isEnabled(CUSTOMVIEW) && context.getProject().isLibrary();
    168             mCheckUnused = context.isEnabled(UNUSED);
    169             checkElement(context, document.getDocumentElement());
    170 
    171             if (mCheckUnused && mUnusedNamespaces.size() > 0) {
    172                 for (Map.Entry<String, Attr> entry : mUnusedNamespaces.entrySet()) {
    173                     String prefix = entry.getKey();
    174                     Attr attribute = entry.getValue();
    175                     context.report(UNUSED, attribute, context.getLocation(attribute),
    176                             String.format("Unused namespace %1$s", prefix), null);
    177                 }
    178             }
    179         }
    180     }
    181 
    182     private void checkElement(XmlContext context, Node node) {
    183         if (node.getNodeType() == Node.ELEMENT_NODE) {
    184             if (mCheckCustomAttrs) {
    185                 String tag = node.getNodeName();
    186                 if (tag.indexOf('.') != -1
    187                         // Don't consider android.support.* and android.app.FragmentBreadCrumbs etc
    188                         && !tag.startsWith(ANDROID_PKG_PREFIX)) {
    189                     NamedNodeMap attributes = ((Element) node).getAttributes();
    190                     for (int i = 0, n = attributes.getLength(); i < n; i++) {
    191                         Attr attribute = (Attr) attributes.item(i);
    192                         String uri = attribute.getNamespaceURI();
    193                         if (uri != null && uri.length() > 0 && uri.startsWith(URI_PREFIX)
    194                                 && !uri.equals(ANDROID_URI)) {
    195                             context.report(CUSTOMVIEW, attribute, context.getLocation(attribute),
    196                                 "When using a custom namespace attribute in a library project, " +
    197                                 "use the namespace \"" + AUTO_URI + "\" instead.", null);
    198                         }
    199                     }
    200                 }
    201             }
    202 
    203             if (mCheckUnused) {
    204                 NamedNodeMap attributes = ((Element) node).getAttributes();
    205                 for (int i = 0, n = attributes.getLength(); i < n; i++) {
    206                     Attr attribute = (Attr) attributes.item(i);
    207                     String prefix = attribute.getPrefix();
    208                     if (prefix != null) {
    209                         mUnusedNamespaces.remove(prefix);
    210                     }
    211                 }
    212             }
    213 
    214             NodeList childNodes = node.getChildNodes();
    215             for (int i = 0, n = childNodes.getLength(); i < n; i++) {
    216                 checkElement(context, childNodes.item(i));
    217             }
    218         }
    219     }
    220 }
    221