blob: 1045cf63dab1353932f0ef2699e84dc6943b6498 [file] [log] [blame]
/*
* 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.ATTR_NAME;
import static com.android.SdkConstants.TAG_ARRAY;
import static com.android.SdkConstants.TAG_INTEGER_ARRAY;
import static com.android.SdkConstants.TAG_STRING_ARRAY;
import com.android.annotations.NonNull;
import com.android.ide.common.rendering.api.ArrayResourceValue;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
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.client.api.LintDriver;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
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.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.android.utils.Pair;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Checks for arrays with inconsistent item counts
*/
public class ArraySizeDetector extends ResourceXmlDetector {
/** Are there differences in how many array elements are declared? */
public static final Issue INCONSISTENT = Issue.create(
"InconsistentArrays", //$NON-NLS-1$
"Inconsistencies in array element counts",
"When an array is translated in a different locale, it should normally have " +
"the same number of elements as the original array. When adding or removing " +
"elements to an array, it is easy to forget to update all the locales, and this " +
"lint warning finds inconsistencies like these.\n" +
"\n" +
"Note however that there may be cases where you really want to declare a " +
"different number of array items in each configuration (for example where " +
"the array represents available options, and those options differ for " +
"different layout orientations and so on), so use your own judgement to " +
"decide if this is really an error.\n" +
"\n" +
"You can suppress this error type if it finds false errors in your project.",
Category.CORRECTNESS,
7,
Severity.WARNING,
new Implementation(
ArraySizeDetector.class,
Scope.RESOURCE_FILE_SCOPE));
private Multimap<File, Pair<String, Integer>> mFileToArrayCount;
/** Locations for each array name. Populated during phase 2, if necessary */
private Map<String, Location> mLocations;
/** Error messages for each array name. Populated during phase 2, if necessary */
private Map<String, String> mDescriptions;
/** Constructs a new {@link ArraySizeDetector} */
public ArraySizeDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_ARRAY,
TAG_STRING_ARRAY,
TAG_INTEGER_ARRAY
);
}
@Override
public void beforeCheckProject(@NonNull Context context) {
if (context.getPhase() == 1) {
mFileToArrayCount = ArrayListMultimap.create(30, 5);
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (context.getPhase() == 1) {
boolean haveAllResources = context.getScope().contains(Scope.ALL_RESOURCE_FILES);
if (!haveAllResources) {
return;
}
// Check that all arrays for the same name have the same number of translations
Set<String> alreadyReported = new HashSet<String>();
Map<String, Integer> countMap = new HashMap<String, Integer>();
Map<String, File> fileMap = new HashMap<String, File>();
// Process the file in sorted file order to ensure stable output
List<File> keys = new ArrayList<File>(mFileToArrayCount.keySet());
Collections.sort(keys);
for (File file : keys) {
Collection<Pair<String, Integer>> pairs = mFileToArrayCount.get(file);
for (Pair<String, Integer> pair : pairs) {
String name = pair.getFirst();
if (alreadyReported.contains(name)) {
continue;
}
Integer count = pair.getSecond();
Integer current = countMap.get(name);
if (current == null) {
countMap.put(name, count);
fileMap.put(name, file);
} else if (!count.equals(current)) {
alreadyReported.add(name);
if (mLocations == null) {
mLocations = new HashMap<String, Location>();
mDescriptions = new HashMap<String, String>();
}
mLocations.put(name, null);
String thisName = file.getParentFile().getName() + File.separator
+ file.getName();
File otherFile = fileMap.get(name);
String otherName = otherFile.getParentFile().getName() + File.separator
+ otherFile.getName();
String message = String.format(
"Array `%1$s` has an inconsistent number of items (%2$d in `%3$s`, %4$d in `%5$s`)",
name, count, thisName, current, otherName);
mDescriptions.put(name, message);
}
}
}
//noinspection VariableNotUsedInsideIf
if (mLocations != null) {
// Request another scan through the resources such that we can
// gather the actual locations
context.getDriver().requestRepeat(this, Scope.ALL_RESOURCES_SCOPE);
}
mFileToArrayCount = null;
} else {
if (mLocations != null) {
List<String> names = new ArrayList<String>(mLocations.keySet());
Collections.sort(names);
for (String name : names) {
Location location = mLocations.get(name);
if (location == null) {
// Suppressed; see visitElement
continue;
}
// We were prepending locations, but we want to prefer the base folders
location = Location.reverse(location);
// Make sure we still have a conflict, in case one or more of the
// elements were marked with tools:ignore
int count = -1;
LintDriver driver = context.getDriver();
boolean foundConflict = false;
Location curr;
for (curr = location; curr != null; curr = curr.getSecondary()) {
Object clientData = curr.getClientData();
if (clientData instanceof Node) {
Node node = (Node) clientData;
if (driver.isSuppressed(null, INCONSISTENT, node)) {
continue;
}
int newCount = LintUtils.getChildCount(node);
if (newCount != count) {
if (count == -1) {
count = newCount; // first number encountered
} else {
foundConflict = true;
break;
}
}
} else {
foundConflict = true;
break;
}
}
// Through one or more tools:ignore, there is no more conflict so
// ignore this element
if (!foundConflict) {
continue;
}
String message = mDescriptions.get(name);
context.report(INCONSISTENT, location, message);
}
}
mLocations = null;
mDescriptions = null;
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int phase = context.getPhase();
Attr attribute = element.getAttributeNode(ATTR_NAME);
if (attribute == null || attribute.getValue().isEmpty()) {
if (phase != 1) {
return;
}
context.report(INCONSISTENT, element, context.getLocation(element),
String.format("Missing name attribute in `%1$s` declaration",
element.getTagName()));
} else {
String name = attribute.getValue();
if (phase == 1) {
if (context.getProject().getReportIssues()) {
int childCount = LintUtils.getChildCount(element);
if (!context.getScope().contains(Scope.ALL_RESOURCE_FILES) &&
context.getClient().supportsProjectResources()) {
incrementalCheckCount(context, element, name, childCount);
return;
}
mFileToArrayCount.put(context.file, Pair.of(name, childCount));
}
} else {
assert phase == 2;
if (mLocations.containsKey(name)) {
if (context.getDriver().isSuppressed(context, INCONSISTENT, element)) {
return;
}
Location location = context.getLocation(element);
location.setClientData(element);
location.setMessage(String.format("Declaration with array size (%1$d)",
LintUtils.getChildCount(element)));
location.setSecondary(mLocations.get(name));
mLocations.put(name, location);
}
}
}
}
private static void incrementalCheckCount(@NonNull XmlContext context, @NonNull Element element,
@NonNull String name, int childCount) {
LintClient client = context.getClient();
Project project = context.getMainProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return;
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.ARRAY, name);
if (items != null) {
for (ResourceItem item : items) {
ResourceFile source = item.getSource();
if (source != null && LintUtils.isSameResourceFile(context.file,
source.getFile())) {
continue;
}
ResourceValue rv = item.getResourceValue(false);
if (rv instanceof ArrayResourceValue) {
ArrayResourceValue arv = (ArrayResourceValue) rv;
if (childCount != arv.getElementCount()) {
String thisName = context.file.getParentFile().getName() + File.separator
+ context.file.getName();
assert source != null;
File otherFile = source.getFile();
String otherName = otherFile.getParentFile().getName() + File.separator
+ otherFile.getName();
String message = String.format(
"Array `%1$s` has an inconsistent number of items (%2$d in `%3$s`, %4$d in `%5$s`)",
name, childCount, thisName, arv.getElementCount(), otherName);
context.report(INCONSISTENT, element, context.getLocation(element),
message);
}
}
}
}
}
}