blob: 7250d782e7bcac9c84a01656f4a69770d797516b [file] [log] [blame]
/*
* Copyright (C) 2014 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.TAG_DECLARE_STYLEABLE;
import static com.android.SdkConstants.TAG_RESOURCES;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
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.ResourceContext;
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.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
/**
* Ensure that resources in Gradle projects which specify a resource prefix
* conform to the given name
*
* TODO: What about id's?
*/
public class ResourcePrefixDetector extends ResourceXmlDetector implements
Detector.BinaryResourceScanner {
/** The main issue discovered by this detector */
@SuppressWarnings("unchecked")
public static final Issue ISSUE = Issue.create(
"ResourceName", //$NON-NLS-1$
"Resource with Wrong Prefix",
"In Gradle projects you can specify a resource prefix that all resources " +
"in the project must conform to. This makes it easier to ensure that you don't " +
"accidentally combine resources from different libraries, since they all end " +
"up in the same shared app namespace.",
Category.CORRECTNESS,
8,
Severity.FATAL,
new Implementation(
ResourcePrefixDetector.class,
EnumSet.of(Scope.RESOURCE_FILE, Scope.BINARY_RESOURCE_FILE),
Scope.RESOURCE_FILE_SCOPE,
Scope.BINARY_RESOURCE_FILE_SCOPE));
/** Constructs a new {@link com.android.tools.lint.checks.ResourcePrefixDetector} */
public ResourcePrefixDetector() {
}
private String mPrefix;
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(TAG_RESOURCES, TAG_DECLARE_STYLEABLE);
}
@Nullable
private static String computeResourcePrefix(@NonNull Project project) {
if (project.isGradleProject()) {
return LintUtils.computeResourcePrefix(project.getGradleProjectModel());
}
return null;
}
@Override
public void beforeCheckProject(@NonNull Context context) {
mPrefix = computeResourcePrefix(context.getProject());
}
@Override
public void beforeCheckLibraryProject(@NonNull Context context) {
// TODO: Make sure this doesn't wipe out the prefix for the remaining projects
mPrefix = computeResourcePrefix(context.getProject());
}
@Override
public void afterCheckProject(@NonNull Context context) {
mPrefix = null;
}
@Override
public void afterCheckLibraryProject(@NonNull Context context) {
mPrefix = null;
}
@Override
public void beforeCheckFile(@NonNull Context context) {
if (mPrefix != null && context instanceof XmlContext) {
XmlContext xmlContext = (XmlContext) context;
ResourceFolderType folderType = xmlContext.getResourceFolderType();
if (folderType != null && folderType != ResourceFolderType.VALUES) {
String name = LintUtils.getBaseName(context.file.getName());
if (!name.startsWith(mPrefix)) {
// Attempt to report the error on the root tag of the associated
// document to make suppressing the error with a tools:suppress
// attribute etc possible
if (xmlContext.document != null) {
Element root = xmlContext.document.getDocumentElement();
if (root != null) {
xmlContext.report(ISSUE, root, xmlContext.getLocation(root),
getErrorMessage(name));
return;
}
}
context.report(ISSUE, Location.create(context.file),
getErrorMessage(name));
}
}
}
}
private String getErrorMessage(String name) {
assert mPrefix != null && !name.startsWith(mPrefix);
return String.format("Resource named '`%1$s`' does not start "
+ "with the project's resource prefix '`%2$s`'; rename to '`%3$s`' ?",
name, mPrefix, LintUtils.computeResourceName(mPrefix, name));
}
// --- Implements XmlScanner ----
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (mPrefix == null || context.getResourceFolderType() != ResourceFolderType.VALUES) {
return;
}
for (Element item : LintUtils.getChildren(element)) {
Attr nameAttribute = item.getAttributeNode(ATTR_NAME);
if (nameAttribute != null) {
String name = nameAttribute.getValue();
if (!name.startsWith(mPrefix)) {
String message = getErrorMessage(name);
context.report(ISSUE, nameAttribute, context.getLocation(nameAttribute),
message);
}
}
}
}
// ---- Implements BinaryResourceScanner ---
@Override
public void checkBinaryResource(@NonNull ResourceContext context) {
if (mPrefix != null) {
ResourceFolderType folderType = context.getResourceFolderType();
if (folderType != null && folderType != ResourceFolderType.VALUES) {
String name = LintUtils.getBaseName(context.file.getName());
if (!name.startsWith(mPrefix)) {
Location location = Location.create(context.file);
context.report(ISSUE, location, getErrorMessage(name));
}
}
}
}
}