| /* |
| * 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.ResourceNamespace; |
| import com.android.ide.common.rendering.api.ResourceValue; |
| import com.android.ide.common.resources.ResourceItem; |
| import com.android.ide.common.resources.ResourceRepository; |
| import com.android.ide.common.util.PathString; |
| 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.Lint; |
| 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 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; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| /** 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", |
| "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 beforeCheckRootProject(@NonNull Context context) { |
| if (context.getPhase() == 1) { |
| mFileToArrayCount = ArrayListMultimap.create(30, 5); |
| } |
| } |
| |
| @Override |
| public void afterCheckRootProject(@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 |
| |
| LintClient client = context.getClient(); |
| Set<String> alreadyReported = new HashSet<>(); |
| Map<String, Integer> countMap = new HashMap<>(); |
| Map<String, File> fileMap = new HashMap<>(); |
| |
| // Process the file in sorted file order to ensure stable output |
| List<File> keys = new ArrayList<>(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<>(); |
| mDescriptions = new HashMap<>(); |
| } |
| mLocations.put(name, null); |
| |
| String thisName = Lint.getFileNameWithParent(client, file); |
| File otherFile = fileMap.get(name); |
| String otherName = Lint.getFileNameWithParent(client, otherFile); |
| 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<>(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 = Lint.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 = Lint.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.setData(element); |
| location.setMessage( |
| String.format( |
| "Declaration with array size (%1$d)", |
| Lint.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(); |
| ResourceRepository resources = client.getResourceRepository(project, true, false); |
| if (resources == null) { |
| return; |
| } |
| List<ResourceItem> items = |
| resources.getResources(ResourceNamespace.TODO(), ResourceType.ARRAY, name); |
| for (ResourceItem item : items) { |
| PathString source = item.getSource(); |
| if (source != null && Lint.isSameResourceFile(context.file, source.toFile())) { |
| continue; |
| } |
| ResourceValue rv = item.getResourceValue(); |
| if (rv instanceof ArrayResourceValue) { |
| ArrayResourceValue arv = (ArrayResourceValue) rv; |
| if (childCount != arv.getElementCount()) { |
| String thisName = Lint.getFileNameWithParent(client, context.file); |
| assert source != null; |
| String otherName = Lint.getFileNameWithParent(client, source); |
| 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); |
| } |
| } |
| } |
| } |
| } |