/* * Copyright (C) 2011 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_PKG_PREFIX; import static com.android.SdkConstants.ANDROID_SUPPORT_PKG_PREFIX; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_CLASS; import static com.android.SdkConstants.ATTR_CORE_APP; import static com.android.SdkConstants.ATTR_LAYOUT; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.ATTR_PACKAGE; import static com.android.SdkConstants.ATTR_STYLE; import static com.android.SdkConstants.AUTO_URI; import static com.android.SdkConstants.TAG_LAYOUT; import static com.android.SdkConstants.TOOLS_URI; import static com.android.SdkConstants.VIEW_TAG; import static com.android.resources.ResourceFolderType.ANIM; import static com.android.resources.ResourceFolderType.ANIMATOR; import static com.android.resources.ResourceFolderType.COLOR; import static com.android.resources.ResourceFolderType.DRAWABLE; import static com.android.resources.ResourceFolderType.INTERPOLATOR; import static com.android.resources.ResourceFolderType.LAYOUT; import static com.android.resources.ResourceFolderType.MENU; import com.android.annotations.NonNull; import com.android.resources.ResourceFolderType; 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.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.Element; import org.w3c.dom.Node; import java.util.Collection; import java.util.HashSet; import java.util.Set; /** * Detects layout attributes on builtin Android widgets that do not specify * a prefix but probably should. */ public class DetectMissingPrefix extends LayoutDetector { /** Attributes missing the android: prefix */ @SuppressWarnings("unchecked") public static final Issue MISSING_NAMESPACE = Issue.create( "MissingPrefix", //$NON-NLS-1$ "Missing Android XML namespace", "Most Android views have attributes in the Android namespace. When referencing " + "these attributes you *must* include the namespace prefix, or your attribute will " + "be interpreted by `aapt` as just a custom attribute.\n" + "\n" + "Similarly, in manifest files, nearly all attributes should be in the `android:` " + "namespace.", Category.CORRECTNESS, 6, Severity.ERROR, new Implementation( DetectMissingPrefix.class, Scope.MANIFEST_AND_RESOURCE_SCOPE, Scope.MANIFEST_SCOPE, Scope.RESOURCE_FILE_SCOPE)); private static final Set<String> NO_PREFIX_ATTRS = new HashSet<String>(); static { NO_PREFIX_ATTRS.add(ATTR_CLASS); NO_PREFIX_ATTRS.add(ATTR_STYLE); NO_PREFIX_ATTRS.add(ATTR_LAYOUT); NO_PREFIX_ATTRS.add(ATTR_PACKAGE); NO_PREFIX_ATTRS.add(ATTR_CORE_APP); } /** Constructs a new {@link DetectMissingPrefix} */ public DetectMissingPrefix() { } @Override public boolean appliesTo(@NonNull ResourceFolderType folderType) { return folderType == LAYOUT || folderType == MENU || folderType == DRAWABLE || folderType == ANIM || folderType == ANIMATOR || folderType == COLOR || folderType == INTERPOLATOR; } @NonNull @Override public Speed getSpeed() { return Speed.FAST; } @Override public Collection<String> getApplicableAttributes() { return ALL; } @Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { String uri = attribute.getNamespaceURI(); if (uri == null || uri.isEmpty()) { String name = attribute.getName(); if (name == null) { return; } if (NO_PREFIX_ATTRS.contains(name)) { return; } Element element = attribute.getOwnerElement(); if (isCustomView(element) && context.getResourceFolderType() != null) { return; } else if (context.getResourceFolderType() == ResourceFolderType.LAYOUT) { // Data binding: These look like Android framework views but // are data binding directives not in the Android namespace Element root = element.getOwnerDocument().getDocumentElement(); if (TAG_LAYOUT.equals(root.getTagName())) { return; } } if (name.indexOf(':') != -1) { // Don't flag warnings for attributes that already have a different // namespace! This doesn't usually happen when lint is run from the // command line, since (with the exception of xmlns: declaration attributes) // an attribute shouldn't have a prefix *and* have no namespace, but // when lint is run in the IDE (with a more fault-tolerant XML parser) // this can happen, and we don't want to flag erroneous/misleading lint // errors in this case. return; } context.report(MISSING_NAMESPACE, attribute, context.getLocation(attribute), "Attribute is missing the Android namespace prefix"); } else if (!ANDROID_URI.equals(uri) && !TOOLS_URI.equals(uri) && context.getResourceFolderType() == ResourceFolderType.LAYOUT && !isCustomView(attribute.getOwnerElement()) && !attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) // TODO: Consider not enforcing that the parent is a custom view // too, though in that case we should filter out views that are // layout params for the custom view parent: // ....&& !attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) && attribute.getOwnerElement().getParentNode().getNodeType() == Node.ELEMENT_NODE && !isCustomView((Element) attribute.getOwnerElement().getParentNode())) { if (context.getResourceFolderType() == ResourceFolderType.LAYOUT && AUTO_URI.equals(uri)) { // Data binding: Can add attributes like onClickListener to buttons etc. Element root = attribute.getOwnerDocument().getDocumentElement(); if (TAG_LAYOUT.equals(root.getTagName())) { return; } } context.report(MISSING_NAMESPACE, attribute, context.getLocation(attribute), String.format("Unexpected namespace prefix \"%1$s\" found for tag `%2$s`", attribute.getPrefix(), attribute.getOwnerElement().getTagName())); } } private static boolean isCustomView(Element element) { // If this is a custom view, the usage of custom attributes can be legitimate String tag = element.getTagName(); if (tag.equals(VIEW_TAG)) { // <view class="my.custom.view" ...> return true; } return tag.indexOf('.') != -1 && (!tag.startsWith(ANDROID_PKG_PREFIX) || tag.startsWith(ANDROID_SUPPORT_PKG_PREFIX)); } }