| /* |
| * 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.resources.ResourceFolderType; |
| 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.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", |
| "Checks for inconsistencies in the number of elements in arrays", |
| "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.ALL_RESOURCES_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) { |
| // 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); |
| } |
| } |
| } |
| |
| 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(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, null); |
| } |
| } |
| |
| 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()), |
| null); |
| } else { |
| String name = attribute.getValue(); |
| if (phase == 1) { |
| if (context.getProject().getReportIssues()) { |
| int childCount = LintUtils.getChildCount(element); |
| mFileToArrayCount.put(context.file, Pair.of(name, childCount)); |
| } |
| } else { |
| assert phase == 2; |
| if (mLocations.containsKey(name)) { |
| if (context.getDriver().isSuppressed(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); |
| } |
| } |
| } |
| } |
| } |