| /* |
| * 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); |
| } |
| } |
| } |
| } |
| } |
| } |