| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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.tools.lint.checks; |
| |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.APP_PREFIX; |
| import static com.android.SdkConstants.AUTO_URI; |
| import static com.android.SdkConstants.TOOLS_PREFIX; |
| import static com.android.SdkConstants.TOOLS_URI; |
| import static com.android.SdkConstants.URI_PREFIX; |
| import static com.android.SdkConstants.XMLNS_PREFIX; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.LayoutDetector; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.android.tools.lint.detector.api.Project; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.Speed; |
| import com.android.tools.lint.detector.api.XmlContext; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| * Checks for various issues related to XML namespaces |
| */ |
| public class NamespaceDetector extends LayoutDetector { |
| |
| @SuppressWarnings("unchecked") |
| private static final Implementation IMPLEMENTATION = new Implementation( |
| NamespaceDetector.class, |
| Scope.MANIFEST_AND_RESOURCE_SCOPE, |
| Scope.RESOURCE_FILE_SCOPE, Scope.MANIFEST_SCOPE); |
| |
| /** Typos in the namespace */ |
| public static final Issue TYPO = Issue.create( |
| "NamespaceTypo", //$NON-NLS-1$ |
| "Misspelled namespace declaration", |
| |
| "Accidental misspellings in namespace declarations can lead to some very " + |
| "obscure error messages. This check looks for potential misspellings to " + |
| "help track these down.", |
| Category.CORRECTNESS, |
| 8, |
| Severity.FATAL, |
| IMPLEMENTATION); |
| |
| /** Unused namespace declarations */ |
| public static final Issue UNUSED = Issue.create( |
| "UnusedNamespace", //$NON-NLS-1$ |
| "Unused namespace", |
| |
| "Unused namespace declarations take up space and require processing that is not " + |
| "necessary", |
| |
| Category.PERFORMANCE, |
| 1, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Using custom namespace attributes in a library project */ |
| public static final Issue CUSTOM_VIEW = Issue.create( |
| "LibraryCustomView", //$NON-NLS-1$ |
| "Custom views in libraries should use res-auto-namespace", |
| |
| "When using a custom view with custom attributes in a library project, the layout " + |
| "must use the special namespace " + AUTO_URI + " instead of a URI which includes " + |
| "the library project's own package. This will be used to automatically adjust the " + |
| "namespace of the attributes when the library resources are merged into the " + |
| "application project.", |
| Category.CORRECTNESS, |
| 6, |
| Severity.FATAL, |
| IMPLEMENTATION); |
| |
| /** Unused namespace declarations */ |
| public static final Issue RES_AUTO = Issue.create( |
| "ResAuto", //$NON-NLS-1$ |
| "Hardcoded Package in Namespace", |
| |
| "In Gradle projects, the actual package used in the final APK can vary; for example," + |
| "you can add a `.debug` package suffix in one version and not the other. " + |
| "Therefore, you should *not* hardcode the application package in the resource; " + |
| "instead, use the special namespace `http://schemas.android.com/apk/res-auto` " + |
| "which will cause the tools to figure out the right namespace for the resource " + |
| "regardless of the actual package used during the build.", |
| |
| Category.CORRECTNESS, |
| 9, |
| Severity.FATAL, |
| IMPLEMENTATION); |
| |
| /** Prefix relevant for custom namespaces */ |
| private static final String XMLNS_ANDROID = "xmlns:android"; //$NON-NLS-1$ |
| private static final String XMLNS_A = "xmlns:a"; //$NON-NLS-1$ |
| |
| private Map<String, Attr> mUnusedNamespaces; |
| private boolean mCheckUnused; |
| |
| /** Constructs a new {@link NamespaceDetector} */ |
| public NamespaceDetector() { |
| } |
| |
| @NonNull |
| @Override |
| public Speed getSpeed() { |
| return Speed.FAST; |
| } |
| |
| @Override |
| public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { |
| boolean haveCustomNamespace = false; |
| Element root = document.getDocumentElement(); |
| NamedNodeMap attributes = root.getAttributes(); |
| for (int i = 0, n = attributes.getLength(); i < n; i++) { |
| Node item = attributes.item(i); |
| String prefix = item.getNodeName(); |
| if (prefix.startsWith(XMLNS_PREFIX)) { |
| String value = item.getNodeValue(); |
| |
| if (!value.equals(ANDROID_URI)) { |
| Attr attribute = (Attr) item; |
| |
| if (value.startsWith(URI_PREFIX)) { |
| haveCustomNamespace = true; |
| if (mUnusedNamespaces == null) { |
| mUnusedNamespaces = new HashMap<String, Attr>(); |
| } |
| mUnusedNamespaces.put(prefix.substring(XMLNS_PREFIX.length()), |
| attribute); |
| } else if (value.startsWith("urn:")) { //$NON-NLS-1$ |
| continue; |
| } else if (!value.startsWith("http://")) { //$NON-NLS-1$ |
| if (context.isEnabled(TYPO)) { |
| context.report(TYPO, attribute, context.getValueLocation(attribute), |
| "Suspicious namespace: should start with `http://`"); |
| } |
| |
| continue; |
| } else if (!value.equals(AUTO_URI) && value.contains("auto") && //$NON-NLS-1$ |
| value.startsWith("http://schemas.android.com/")) { //$NON-NLS-1$ |
| context.report(RES_AUTO, attribute, context.getValueLocation(attribute), |
| "Suspicious namespace: Did you mean `" + AUTO_URI + "`?"); |
| } else if (value.equals(TOOLS_URI) && (prefix.equals(XMLNS_ANDROID) || |
| prefix.endsWith(APP_PREFIX) && prefix.equals( |
| XMLNS_PREFIX + APP_PREFIX))) { |
| context.report(TYPO, attribute, context.getValueLocation(attribute), |
| "Suspicious namespace and prefix combination"); |
| } |
| |
| if (!context.isEnabled(TYPO)) { |
| continue; |
| } |
| |
| String name = attribute.getName(); |
| if (!name.equals(XMLNS_ANDROID) && !name.equals(XMLNS_A)) { |
| // See if it looks like a typo |
| int resIndex = value.indexOf("/res/"); //$NON-NLS-1$ |
| if (resIndex != -1 && value.length() + 5 > URI_PREFIX.length()) { |
| String urlPrefix = value.substring(0, resIndex + 5); |
| if (!urlPrefix.equals(URI_PREFIX) && |
| LintUtils.editDistance(URI_PREFIX, urlPrefix) <= 3) { |
| String correctUri = URI_PREFIX + value.substring(resIndex + 5); |
| context.report(TYPO, attribute, |
| context.getValueLocation(attribute), |
| String.format( |
| "Possible typo in URL: was `\"%1$s\"`, should " + |
| "probably be `\"%2$s\"`", |
| value, correctUri)); |
| } |
| } |
| continue; |
| } |
| |
| if (name.equals(XMLNS_A)) { |
| // For the "android" prefix we always assume that the namespace prefix |
| // should be our expected prefix, but for the "a" prefix we make sure |
| // that it's at least "close"; if you're bound it to something completely |
| // different, don't complain. |
| if (LintUtils.editDistance(ANDROID_URI, value) > 4) { |
| continue; |
| } |
| } |
| |
| if (value.equalsIgnoreCase(ANDROID_URI)) { |
| context.report(TYPO, attribute, context.getValueLocation(attribute), |
| String.format( |
| "URI is case sensitive: was `\"%1$s\"`, expected `\"%2$s\"`", |
| value, ANDROID_URI)); |
| } else { |
| context.report(TYPO, attribute, context.getValueLocation(attribute), |
| String.format( |
| "Unexpected namespace URI bound to the `\"android\"` " + |
| "prefix, was `%1$s`, expected `%2$s`", value, ANDROID_URI)); |
| } |
| } else if (!prefix.equals(XMLNS_ANDROID) && |
| ((prefix.endsWith(TOOLS_PREFIX) && prefix.equals(XMLNS_PREFIX + TOOLS_PREFIX)) || |
| (prefix.endsWith(APP_PREFIX) && prefix.equals(XMLNS_PREFIX + APP_PREFIX)))) { |
| Attr attribute = (Attr) item; |
| context.report(TYPO, attribute, context.getValueLocation(attribute), |
| "Suspicious namespace and prefix combination"); |
| } |
| } |
| } |
| |
| if (haveCustomNamespace) { |
| Project project = context.getProject(); |
| boolean checkCustomAttrs = |
| context.isEnabled(CUSTOM_VIEW) && project.isLibrary() |
| || context.isEnabled(RES_AUTO) && project.isGradleProject(); |
| |
| mCheckUnused = context.isEnabled(UNUSED); |
| |
| if (checkCustomAttrs) { |
| checkCustomNamespace(context, root); |
| } |
| checkElement(root); |
| |
| if (mCheckUnused && !mUnusedNamespaces.isEmpty()) { |
| for (Map.Entry<String, Attr> entry : mUnusedNamespaces.entrySet()) { |
| String prefix = entry.getKey(); |
| Attr attribute = entry.getValue(); |
| context.report(UNUSED, attribute, context.getLocation(attribute), |
| String.format("Unused namespace `%1$s`", prefix)); |
| } |
| } |
| } |
| } |
| |
| private static void checkCustomNamespace(XmlContext context, Element element) { |
| NamedNodeMap attributes = element.getAttributes(); |
| for (int i = 0, n = attributes.getLength(); i < n; i++) { |
| Attr attribute = (Attr) attributes.item(i); |
| if (attribute.getName().startsWith(XMLNS_PREFIX)) { |
| String uri = attribute.getValue(); |
| if (uri != null && !uri.isEmpty() && uri.startsWith(URI_PREFIX) |
| && !uri.equals(ANDROID_URI)) { |
| if (context.getProject().isGradleProject()) { |
| context.report(RES_AUTO, attribute, context.getValueLocation(attribute), |
| "In Gradle projects, always use `" + AUTO_URI + "` for custom " + |
| "attributes"); |
| } else { |
| context.report(CUSTOM_VIEW, attribute, context.getValueLocation(attribute), |
| "When using a custom namespace attribute in a library project, " + |
| "use the namespace `\"" + AUTO_URI + "\"` instead."); |
| } |
| } |
| } |
| } |
| } |
| |
| private void checkElement(Node node) { |
| if (node.getNodeType() == Node.ELEMENT_NODE) { |
| if (mCheckUnused) { |
| NamedNodeMap attributes = node.getAttributes(); |
| for (int i = 0, n = attributes.getLength(); i < n; i++) { |
| Attr attribute = (Attr) attributes.item(i); |
| String prefix = attribute.getPrefix(); |
| if (prefix != null) { |
| mUnusedNamespaces.remove(prefix); |
| } |
| } |
| } |
| |
| NodeList childNodes = node.getChildNodes(); |
| for (int i = 0, n = childNodes.getLength(); i < n; i++) { |
| checkElement(childNodes.item(i)); |
| } |
| } |
| } |
| } |