blob: 6ed318f9ab0cac462de55b12479831062a165939 [file] [log] [blame]
/*
* 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.ATTR_NAME;
import static com.android.SdkConstants.ATTR_REF_PREFIX;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.RESOURCE_CLR_STYLEABLE;
import static com.android.SdkConstants.RESOURCE_CLZ_ARRAY;
import static com.android.SdkConstants.RESOURCE_CLZ_ID;
import static com.android.SdkConstants.TAG_ARRAY;
import static com.android.SdkConstants.TAG_INTEGER_ARRAY;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_PLURALS;
import static com.android.SdkConstants.TAG_RESOURCES;
import static com.android.SdkConstants.TAG_STRING_ARRAY;
import static com.android.SdkConstants.TAG_STYLE;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.tools.lint.detector.api.LintUtils.getBaseName;
import static com.android.utils.SdkUtils.getResourceFieldName;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.MavenCoordinates;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
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 com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiElement;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Check which looks for access of private resources.
*/
public class PrivateResourceDetector extends ResourceXmlDetector implements
JavaPsiScanner {
/** Attribute for overriding a resource */
private static final String ATTR_OVERRIDE = "override";
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
PrivateResourceDetector.class,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.JAVA_FILE_SCOPE,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"PrivateResource", //$NON-NLS-1$
"Using private resources",
"Private resources should not be referenced; the may not be present everywhere, and " +
"even where they are they may disappear without notice.\n" +
"\n" +
"To fix this, copy the resource into your own project instead.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new detector */
public PrivateResourceDetector() {
}
// ---- Implements JavaScanner ----
@Override
public boolean appliesToResourceRefs() {
return true;
}
@Override
public void visitResourceReference(@NonNull JavaContext context,
@Nullable JavaElementVisitor visitor, @NonNull PsiElement node,
@NonNull ResourceType resourceType, @NonNull String name, boolean isFramework) {
if (context.getProject().isGradleProject() && !isFramework) {
Project project = context.getProject();
if (project.getGradleProjectModel() != null && project.getCurrentVariant() != null) {
if (isPrivate(context, resourceType, name)) {
String message = createUsageErrorMessage(context, resourceType, name);
context.report(ISSUE, node, context.getLocation(node), message);
}
}
}
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableAttributes() {
return ALL;
}
/** Check resource references: accessing a private resource from an upstream library? */
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String value = attribute.getNodeValue();
if (context.getProject().isGradleProject()) {
ResourceUrl url = ResourceUrl.parse(value);
if (isPrivate(context, url)) {
String message = createUsageErrorMessage(context, url.type, url.name);
context.report(ISSUE, attribute, context.getValueLocation(attribute), message);
}
}
}
/** Check resource definitions: overriding a private resource from an upstream library? */
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_STYLE,
TAG_RESOURCES,
TAG_ARRAY,
TAG_STRING_ARRAY,
TAG_INTEGER_ARRAY,
TAG_PLURALS
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (TAG_RESOURCES.equals(element.getTagName())) {
for (Element item : LintUtils.getChildren(element)) {
Attr nameAttribute = item.getAttributeNode(ATTR_NAME);
if (nameAttribute != null) {
String name = getResourceFieldName(nameAttribute.getValue());
String type = item.getTagName();
if (type.equals(TAG_ITEM)) {
type = item.getAttribute(ATTR_TYPE);
if (type == null || type.isEmpty()) {
type = RESOURCE_CLZ_ID;
}
} else if (type.equals("declare-styleable")) { //$NON-NLS-1$
type = RESOURCE_CLR_STYLEABLE;
} else if (type.contains("array")) { //$NON-NLS-1$
// <string-array> etc
type = RESOURCE_CLZ_ARRAY;
}
ResourceType t = ResourceType.getEnum(type);
if (t != null && isPrivate(context, t, name) &&
!VALUE_TRUE.equals(item.getAttributeNS(TOOLS_URI, ATTR_OVERRIDE))) {
String message = createOverrideErrorMessage(context, t, name);
Location location = context.getValueLocation(nameAttribute);
context.report(ISSUE, nameAttribute, location, message);
}
}
}
} else {
assert TAG_STYLE.equals(element.getTagName())
|| TAG_ARRAY.equals(element.getTagName())
|| TAG_PLURALS.equals(element.getTagName())
|| TAG_INTEGER_ARRAY.equals(element.getTagName())
|| TAG_STRING_ARRAY.equals(element.getTagName());
for (Element item : LintUtils.getChildren(element)) {
checkChildRefs(context, item);
}
}
}
private static boolean isPrivate(Context context, ResourceType type, String name) {
if (type == ResourceType.ID) {
// No need to complain about "overriding" id's. There's no harm
// in doing so. (This avoids warning about cases like for example
// appcompat's (private) @id/title resource, which would otherwise
// flag any attempt to create a resource named title in the user's
// project.
return false;
}
if (context.getProject().isGradleProject()) {
ResourceVisibilityLookup lookup = context.getProject().getResourceVisibility();
return lookup.isPrivate(type, name);
}
return false;
}
private static boolean isPrivate(@NonNull Context context, @Nullable ResourceUrl url) {
return url != null && !url.framework && isPrivate(context, url.type, url.name);
}
private static void checkChildRefs(@NonNull XmlContext context, Element item) {
// Look for ?attr/ and @dimen/foo etc references in the item children
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
int index = text.indexOf(ATTR_REF_PREFIX);
if (index != -1) {
String name = text.substring(index + ATTR_REF_PREFIX.length()).trim();
if (isPrivate(context, ResourceType.ATTR, name)) {
String message = createUsageErrorMessage(context, ResourceType.ATTR, name);
context.report(ISSUE, item, context.getLocation(child), message);
}
} else {
for (int j = 0, m = text.length(); j < m; j++) {
char c = text.charAt(j);
if (c == '@') {
ResourceUrl url = ResourceUrl.parse(text.trim());
if (isPrivate(context, url)) {
String message = createUsageErrorMessage(context, url.type,
url.name);
context.report(ISSUE, item, context.getLocation(child), message);
}
break;
} else if (!Character.isWhitespace(c)) {
break;
}
}
}
}
}
}
@Override
public void beforeCheckFile(@NonNull Context context) {
File file = context.file;
boolean isXmlFile = LintUtils.isXmlFile(file);
if (!isXmlFile && !LintUtils.isBitmapFile(file)) {
return;
}
String parentName = file.getParentFile().getName();
int dash = parentName.indexOf('-');
if (dash != -1 || FD_RES_VALUES.equals(parentName)) {
return;
}
ResourceFolderType folderType = ResourceFolderType.getFolderType(parentName);
if (folderType == null) {
return;
}
List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType);
if (types.isEmpty()) {
return;
}
ResourceType type = types.get(0);
String resourceName = getResourceFieldName(getBaseName(file.getName()));
if (isPrivate(context, type, resourceName)) {
String message = createOverrideErrorMessage(context, type, resourceName);
Location location = Location.create(file);
context.report(ISSUE, location, message);
}
}
private static String createOverrideErrorMessage(@NonNull Context context,
@NonNull ResourceType type, @NonNull String name) {
String libraryName = getLibraryName(context, type, name);
return String.format("Overriding `@%1$s/%2$s` which is marked as private in %3$s. If "
+ "deliberate, use tools:override=\"true\", otherwise pick a "
+ "different name.", type, name, libraryName);
}
private static String createUsageErrorMessage(@NonNull Context context,
@NonNull ResourceType type, @NonNull String name) {
String libraryName = getLibraryName(context, type, name);
return String.format("The resource `@%1$s/%2$s` is marked as private in %3$s", type,
name, libraryName);
}
/** Pick a suitable name to describe the library defining the private resource */
@Nullable
private static String getLibraryName(@NonNull Context context, @NonNull ResourceType type,
@NonNull String name) {
ResourceVisibilityLookup lookup = context.getProject().getResourceVisibility();
AndroidLibrary library = lookup.getPrivateIn(type, name);
if (library != null) {
String libraryName = library.getProject();
if (libraryName != null) {
return libraryName;
}
MavenCoordinates coordinates = library.getResolvedCoordinates();
if (coordinates != null) {
return coordinates.getGroupId() + ':' + coordinates.getArtifactId();
}
}
return "the library";
}
}