
com.android.tools.lint.checks.DetectMissingPrefix Maven / Gradle / Ivy
The newest version!
/*
* 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_SRC_COMPAT;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.CONSTRAINT_LAYOUT;
import static com.android.SdkConstants.CONSTRAINT_LAYOUT_GUIDELINE;
import static com.android.SdkConstants.TAG_LAYOUT;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import static com.android.SdkConstants.VIEW_TAG;
import static com.android.SdkConstants.XMLNS;
import static com.android.SdkConstants.XMLNS_PREFIX;
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.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.LintClient;
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.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import java.util.Collection;
import java.util.List;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
/**
* 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",
"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));
/** 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;
}
@Override
public Collection getApplicableAttributes() {
return ALL;
}
private static boolean isNoPrefixAttribute(@NonNull String attribute) {
switch (attribute) {
case ATTR_CLASS:
case ATTR_STYLE:
case ATTR_LAYOUT:
case ATTR_PACKAGE:
case ATTR_CORE_APP:
return true;
default:
return false;
}
}
@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 (isNoPrefixAttribute(name)) {
return;
}
Element element = attribute.getOwnerElement();
if (isCustomView(element) && context.getResourceFolderType() != null) {
return;
} else if (context.getResourceFolderType() == 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;
}
String elementNamespace = element.getNamespaceURI();
if (elementNamespace != null && !elementNamespace.isEmpty()) {
// For example,
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() == LAYOUT
&& !isCustomView(attribute.getOwnerElement())
&& !isFragment(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() == 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;
}
// Appcompat now encourages decorating standard views (like ImageView and
// ImageButton) with srcCompat in the app namespace
if (attribute.getLocalName().equals(ATTR_SRC_COMPAT)) {
return;
}
// Look for other app compat attributes - such as buttonTint
Project project = context.getMainProject();
LintClient client = context.getClient();
AbstractResourceRepository repository = client.getResourceRepository(project,
true, true);
if (repository != null) {
List items = repository.getResourceItem(ResourceType.ATTR,
attribute.getLocalName());
if (items != null && !items.isEmpty()) {
for (ResourceItem item : items) {
String libraryName = item.getLibraryName();
if (libraryName != null && libraryName.startsWith("appcompat-")) {
return;
}
}
}
}
}
// A namespace declaration?
String prefix = attribute.getPrefix();
if (XMLNS.equals(prefix)) {
String name = attribute.getNodeName();
// See if it's already reported on the root
Element root = attribute.getOwnerDocument().getDocumentElement();
NamedNodeMap attributes = root.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Node item = attributes.item(i);
if (name.equals(item.getNodeName())
&& attribute.getValue().equals(item.getNodeValue())) {
context.report(NamespaceDetector.UNUSED, attribute,
context.getLocation(attribute),
String.format("Unused namespace declaration %1$s; already "
+ "declared on the root element",
name));
}
}
return;
}
context.report(MISSING_NAMESPACE, attribute,
context.getLocation(attribute),
String.format("Unexpected namespace prefix \"%1$s\" found for tag `%2$s`",
prefix, attribute.getOwnerElement().getTagName()));
}
}
private static boolean isFragment(Element element) {
return VIEW_FRAGMENT.equals(element.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)) {
//
return true;
}
// For the purposes of this check, the ConstraintLayout isn't a custom view
//noinspection SimplifiableIfStatement
if (CONSTRAINT_LAYOUT.equals(tag) || CONSTRAINT_LAYOUT_GUIDELINE.equals(tag)) {
return false;
}
return tag.indexOf('.') != -1 && (!tag.startsWith(ANDROID_PKG_PREFIX)
|| tag.startsWith(ANDROID_SUPPORT_PKG_PREFIX));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy