Lint: Remove diagnostics unrelated to uast.

All these diagnostics are already present in the Android plugin, so there's no need in them here.
This commit is contained in:
Yan Zhulanow
2016-03-15 17:17:45 +03:00
parent ed10829091
commit 18e963422b
69 changed files with 5 additions and 19983 deletions
@@ -1,132 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION;
import static com.android.SdkConstants.ATTR_HINT;
import static com.android.SdkConstants.ATTR_IMPORTANT_FOR_ACCESSIBILITY;
import static com.android.SdkConstants.IMAGE_BUTTON;
import static com.android.SdkConstants.IMAGE_VIEW;
import static com.android.SdkConstants.VALUE_NO;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
/**
* Check which looks for accessibility problems like missing content descriptions
* <p>
* TODO: Resolve styles and don't warn where styles are defining the content description
* (though this seems unusual; content descriptions are not typically generic enough to
* put in styles)
*/
public class AccessibilityDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"ContentDescription", //$NON-NLS-1$
"Image without `contentDescription`",
"Non-textual widgets like ImageViews and ImageButtons should use the " +
"`contentDescription` attribute to specify a textual description of " +
"the widget such that screen readers and other accessibility tools " +
"can adequately describe the user interface.\n" +
"\n" +
"Note that elements in application screens that are purely decorative " +
"and do not provide any content or enable a user action should not " +
"have accessibility content descriptions. In this case, just suppress the " +
"lint warning with a tools:ignore=\"ContentDescription\" attribute.\n" +
"\n" +
"Note that for text fields, you should not set both the `hint` and the " +
"`contentDescription` attributes since the hint will never be shown. Just " +
"set the `hint`. See " +
"http://developer.android.com/guide/topics/ui/accessibility/checklist.html#special-cases.",
Category.A11Y,
3,
Severity.WARNING,
new Implementation(
AccessibilityDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link AccessibilityDetector} */
public AccessibilityDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
IMAGE_BUTTON,
IMAGE_VIEW
);
}
@Override
@Nullable
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_CONTENT_DESCRIPTION);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
Element element = attribute.getOwnerElement();
if (element.hasAttributeNS(ANDROID_URI, ATTR_HINT)) {
context.report(ISSUE, element, context.getLocation(attribute),
"Do not set both `contentDescription` and `hint`: the `contentDescription` " +
"will mask the `hint`");
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (!element.hasAttributeNS(ANDROID_URI, ATTR_CONTENT_DESCRIPTION)) {
// Ignore views that are explicitly not important for accessibility
if (VALUE_NO.equals(element.getAttributeNS(ANDROID_URI,
ATTR_IMPORTANT_FOR_ACCESSIBILITY))) {
return;
}
context.report(ISSUE, element, context.getLocation(element),
"[Accessibility] Missing `contentDescription` attribute on image");
} else {
Attr attributeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_CONTENT_DESCRIPTION);
String attribute = attributeNode.getValue();
if (attribute.isEmpty() || attribute.equals("TODO")) { //$NON-NLS-1$
context.report(ISSUE, attributeNode, context.getLocation(attributeNode),
"[Accessibility] Empty `contentDescription` attribute on image");
}
}
}
}
@@ -1,287 +0,0 @@
/*
* Copyright (C) 2015 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_HOST;
import static com.android.SdkConstants.ATTR_PATH;
import static com.android.SdkConstants.ATTR_PATH_PATTERN;
import static com.android.SdkConstants.ATTR_PATH_PREFIX;
import static com.android.SdkConstants.ATTR_SCHEME;
import static com.android.xml.AndroidManifest.ATTRIBUTE_MIME_TYPE;
import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME;
import static com.android.xml.AndroidManifest.ATTRIBUTE_PORT;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import static com.android.xml.AndroidManifest.NODE_CATEGORY;
import static com.android.xml.AndroidManifest.NODE_DATA;
import static com.android.xml.AndroidManifest.NODE_INTENT;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Check if the usage of App Indexing is correct.
*/
public class AppIndexingApiDetector extends Detector implements Detector.XmlScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
AppIndexingApiDetector.class, Scope.MANIFEST_SCOPE);
public static final Issue ISSUE_ERROR = Issue.create("AppIndexingError", //$NON-NLS-1$
"Wrong Usage of App Indexing",
"Ensures the app can correctly handle deep links and integrate with " +
"App Indexing for Google search.",
Category.USABILITY, 5, Severity.ERROR, IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing");
public static final Issue ISSUE_WARNING = Issue.create("AppIndexingWarning", //$NON-NLS-1$
"Missing App Indexing Support",
"Ensures the app can correctly handle deep links and integrate with " +
"App Indexing for Google search.",
Category.USABILITY, 5, Severity.WARNING, IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing");
private static final String[] PATH_ATTR_LIST = new String[]{ATTR_PATH_PREFIX, ATTR_PATH,
ATTR_PATH_PATTERN};
@Override
@Nullable
public Collection<String> getApplicableElements() {
return Collections.singletonList(NODE_INTENT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element intent) {
boolean actionView = hasActionView(intent);
boolean browsable = isBrowsable(intent);
boolean isHttp = false;
boolean hasScheme = false;
boolean hasHost = false;
boolean hasPort = false;
boolean hasPath = false;
boolean hasMimeType = false;
Element firstData = null;
NodeList children = intent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(NODE_DATA)) {
Element data = (Element) child;
if (firstData == null) {
firstData = data;
}
if (isHttpSchema(data)) {
isHttp = true;
}
checkSingleData(context, data);
for (String name : PATH_ATTR_LIST) {
if (data.hasAttributeNS(ANDROID_URI, name)) {
hasPath = true;
}
}
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
hasScheme = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTR_HOST)) {
hasHost = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
hasPort = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_MIME_TYPE)) {
hasMimeType = true;
}
}
}
// In data field, a URL is consisted by
// <scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
// Each part of the URL should not have illegal character.
if ((hasPath || hasHost || hasPort) && !hasScheme) {
context.report(ISSUE_ERROR, firstData, context.getLocation(firstData),
"android:scheme missing");
}
if ((hasPath || hasPort) && !hasHost) {
context.report(ISSUE_ERROR, firstData, context.getLocation(firstData),
"android:host missing");
}
if (actionView && browsable) {
if (firstData == null) {
// If this activity is an ACTION_VIEW action with category BROWSABLE, but doesn't
// have data node, it may be a mistake and we will report error.
context.report(ISSUE_ERROR, intent, context.getLocation(intent),
"Missing data node?");
} else if (!hasScheme && !hasMimeType) {
// If this activity is an action view, is browsable, but has neither a
// URL nor mimeType, it may be a mistake and we will report error.
context.report(ISSUE_ERROR, firstData, context.getLocation(firstData),
"Missing URL for the intent filter?");
}
}
// If this activity is an ACTION_VIEW action, has a http URL but doesn't have
// BROWSABLE, it may be a mistake and and we will report warning.
if (actionView && isHttp && !browsable) {
context.report(ISSUE_WARNING, intent, context.getLocation(intent),
"Activity supporting ACTION_VIEW is not set as BROWSABLE");
}
}
private static boolean hasActionView(Element intent) {
NodeList children = intent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE &&
child.getNodeName().equals(NODE_ACTION)) {
Element action = (Element) child;
if (action.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = action.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
if (attr.getValue().equals("android.intent.action.VIEW")) {
return true;
}
}
}
}
return false;
}
private static boolean isBrowsable(Element intent) {
NodeList children = intent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE &&
child.getNodeName().equals(NODE_CATEGORY)) {
Element e = (Element) child;
if (e.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = e.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
if (attr.getNodeValue().equals("android.intent.category.BROWSABLE")) {
return true;
}
}
}
}
return false;
}
private static boolean isHttpSchema(Element data) {
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
String value = data.getAttributeNodeNS(ANDROID_URI, ATTR_SCHEME).getValue();
if (value.equalsIgnoreCase("http") || value.equalsIgnoreCase("https")) {
return true;
}
}
return false;
}
private static void checkSingleData(XmlContext context, Element data) {
// path, pathPrefix and pathPattern should starts with /.
for (String name : PATH_ATTR_LIST) {
if (data.hasAttributeNS(ANDROID_URI, name)) {
Attr attr = data.getAttributeNodeNS(ANDROID_URI, name);
String path = replaceUrlWithValue(context, attr.getValue());
if (!path.startsWith("/") && !path.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) {
context.report(ISSUE_ERROR, attr, context.getLocation(attr),
"android:" + name + " attribute should start with '/', but it is : "
+ path);
}
}
}
// port should be a legal number.
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
Attr attr = data.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_PORT);
try {
String port = replaceUrlWithValue(context, attr.getValue());
Integer.parseInt(port);
} catch (NumberFormatException e) {
context.report(ISSUE_ERROR, attr, context.getLocation(attr),
"android:port is not a legal number");
}
}
// Each field should be non empty.
NamedNodeMap attrs = data.getAttributes();
for (int i = 0; i < attrs.getLength(); i++) {
Node item = attrs.item(i);
if (item.getNodeType() == Node.ATTRIBUTE_NODE) {
Attr attr = (Attr) attrs.item(i);
if (attr.getValue().isEmpty()) {
context.report(ISSUE_ERROR, attr, context.getLocation(attr),
attr.getName() + " cannot be empty");
}
}
}
}
private static String replaceUrlWithValue(@NonNull XmlContext context,
@NonNull String str) {
Project project = context.getProject();
LintClient client = context.getClient();
if (!client.supportsProjectResources()) {
return str;
}
ResourceUrl style = ResourceUrl.parse(str);
if (style == null || style.type != ResourceType.STRING || style.framework) {
return str;
}
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return str;
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.STRING, style.name);
if (items == null || items.isEmpty()) {
return str;
}
ResourceValue resourceValue = items.get(0).getResourceValue(false);
if (resourceValue == null) {
return str;
}
return resourceValue.getValue() == null ? str : resourceValue.getValue();
}
}
@@ -1,324 +0,0 @@
/*
* 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.klint.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.klint.client.api.LintClient;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.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);
}
}
}
}
}
}
@@ -35,7 +35,6 @@ public class BuiltinIssueRegistry extends IssueRegistry {
static {
List<Issue> issues = new ArrayList<Issue>(INITIAL_CAPACITY);
issues.add(AccessibilityDetector.ISSUE);
issues.add(AddJavascriptInterfaceDetector.ISSUE);
issues.add(AlarmDetector.ISSUE);
issues.add(AlwaysShowActionDetector.ISSUE);
@@ -46,54 +45,20 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(ApiDetector.UNUSED);
issues.add(AppCompatCallDetector.ISSUE);
issues.add(AppCompatResourceDetector.ISSUE);
issues.add(AppIndexingApiDetector.ISSUE_ERROR);
issues.add(AppIndexingApiDetector.ISSUE_WARNING);
issues.add(ArraySizeDetector.INCONSISTENT);
issues.add(AssertDetector.ISSUE);
issues.add(ButtonDetector.BACK_BUTTON);
issues.add(ButtonDetector.CASE);
issues.add(ButtonDetector.ORDER);
issues.add(ButtonDetector.STYLE);
issues.add(ByteOrderMarkDetector.BOM);
issues.add(CallSuperDetector.ISSUE);
issues.add(ChildCountDetector.ADAPTER_VIEW_ISSUE);
issues.add(ChildCountDetector.SCROLLVIEW_ISSUE);
issues.add(CipherGetInstanceDetector.ISSUE);
issues.add(CleanupDetector.COMMIT_FRAGMENT);
issues.add(CleanupDetector.RECYCLE_RESOURCE);
issues.add(ClickableViewAccessibilityDetector.ISSUE);
issues.add(CommentDetector.EASTER_EGG);
issues.add(CommentDetector.STOP_SHIP);
issues.add(CustomViewDetector.ISSUE);
issues.add(CutPasteDetector.ISSUE);
issues.add(DateFormatDetector.DATE_FORMAT);
issues.add(DeprecationDetector.ISSUE);
issues.add(DetectMissingPrefix.MISSING_NAMESPACE);
issues.add(DosLineEndingDetector.ISSUE);
issues.add(DuplicateIdDetector.CROSS_LAYOUT);
issues.add(DuplicateIdDetector.WITHIN_LAYOUT);
issues.add(DuplicateResourceDetector.ISSUE);
issues.add(DuplicateResourceDetector.TYPE_MISMATCH);
issues.add(ExtraTextDetector.ISSUE);
issues.add(FieldGetterDetector.ISSUE);
issues.add(FullBackupContentDetector.ISSUE);
issues.add(FragmentDetector.ISSUE);
issues.add(GetSignaturesDetector.ISSUE);
issues.add(GradleDetector.COMPATIBILITY);
issues.add(GradleDetector.GRADLE_PLUGIN_COMPATIBILITY);
issues.add(GradleDetector.DEPENDENCY);
issues.add(GradleDetector.DEPRECATED);
issues.add(GradleDetector.GRADLE_GETTER);
issues.add(GradleDetector.IDE_SUPPORT);
issues.add(GradleDetector.PATH);
issues.add(GradleDetector.PLUS);
issues.add(GradleDetector.STRING_INTEGER);
issues.add(GradleDetector.REMOTE_VERSION);
issues.add(GradleDetector.ACCIDENTAL_OCTAL);
issues.add(GridLayoutDetector.ISSUE);
issues.add(HandlerDetector.ISSUE);
issues.add(HardcodedDebugModeDetector.ISSUE);
issues.add(HardcodedValuesDetector.ISSUE);
issues.add(IconDetector.DUPLICATES_CONFIGURATIONS);
issues.add(IconDetector.DUPLICATES_NAMES);
issues.add(IconDetector.GIF_USAGE);
@@ -108,93 +73,29 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(IconDetector.ICON_MIX_9PNG);
issues.add(IconDetector.ICON_NODPI);
issues.add(IconDetector.ICON_XML_AND_PNG);
issues.add(IncludeDetector.ISSUE);
issues.add(InefficientWeightDetector.BASELINE_WEIGHTS);
issues.add(InefficientWeightDetector.INEFFICIENT_WEIGHT);
issues.add(InefficientWeightDetector.NESTED_WEIGHTS);
issues.add(InefficientWeightDetector.ORIENTATION);
issues.add(InefficientWeightDetector.WRONG_0DP);
issues.add(InvalidPackageDetector.ISSUE);
issues.add(JavaPerformanceDetector.PAINT_ALLOC);
issues.add(JavaPerformanceDetector.USE_SPARSE_ARRAY);
issues.add(JavaPerformanceDetector.USE_VALUE_OF);
issues.add(JavaScriptInterfaceDetector.ISSUE);
issues.add(LabelForDetector.ISSUE);
issues.add(LayoutConsistencyDetector.INCONSISTENT_IDS);
issues.add(LayoutInflationDetector.ISSUE);
issues.add(LocaleDetector.STRING_LOCALE);
issues.add(LocaleFolderDetector.DEPRECATED_CODE);
issues.add(LocaleFolderDetector.INVALID_FOLDER);
issues.add(LocaleFolderDetector.WRONG_REGION);
issues.add(LocaleFolderDetector.USE_ALPHA_2);
issues.add(LogDetector.CONDITIONAL);
issues.add(LogDetector.LONG_TAG);
issues.add(LogDetector.WRONG_TAG);
issues.add(ManifestDetector.ALLOW_BACKUP);
issues.add(ManifestDetector.APPLICATION_ICON);
issues.add(ManifestDetector.DEVICE_ADMIN);
issues.add(ManifestDetector.DUPLICATE_ACTIVITY);
issues.add(ManifestDetector.DUPLICATE_USES_FEATURE);
issues.add(ManifestDetector.GRADLE_OVERRIDES);
issues.add(ManifestDetector.ILLEGAL_REFERENCE);
issues.add(ManifestDetector.MIPMAP);
issues.add(ManifestDetector.MOCK_LOCATION);
issues.add(ManifestDetector.MULTIPLE_USES_SDK);
issues.add(ManifestDetector.ORDER);
issues.add(ManifestDetector.SET_VERSION);
issues.add(ManifestDetector.TARGET_NEWER);
issues.add(ManifestDetector.UNIQUE_PERMISSION);
issues.add(ManifestDetector.USES_SDK);
issues.add(ManifestDetector.WRONG_PARENT);
issues.add(ManifestTypoDetector.ISSUE);
issues.add(MathDetector.ISSUE);
issues.add(MergeRootFrameLayoutDetector.ISSUE);
issues.add(MissingClassDetector.INNERCLASS);
issues.add(MissingClassDetector.INSTANTIATABLE);
issues.add(MissingClassDetector.MISSING);
issues.add(MissingIdDetector.ISSUE);
issues.add(NamespaceDetector.CUSTOM_VIEW);
issues.add(NamespaceDetector.RES_AUTO);
issues.add(NamespaceDetector.TYPO);
issues.add(NamespaceDetector.UNUSED);
issues.add(NegativeMarginDetector.ISSUE);
issues.add(NestedScrollingWidgetDetector.ISSUE);
issues.add(NfcTechListDetector.ISSUE);
issues.add(NonInternationalizedSmsDetector.ISSUE);
issues.add(ObsoleteLayoutParamsDetector.ISSUE);
issues.add(OnClickDetector.ISSUE);
issues.add(OverdrawDetector.ISSUE);
issues.add(OverrideDetector.ISSUE);
issues.add(OverrideConcreteDetector.ISSUE);
issues.add(ParcelDetector.ISSUE);
issues.add(PluralsDetector.EXTRA);
issues.add(PluralsDetector.MISSING);
issues.add(PluralsDetector.IMPLIED_QUANTITY);
issues.add(PreferenceActivityDetector.ISSUE);
issues.add(PrivateKeyDetector.ISSUE);
issues.add(PrivateResourceDetector.ISSUE);
issues.add(ProguardDetector.SPLIT_CONFIG);
issues.add(ProguardDetector.WRONG_KEEP);
issues.add(PropertyFileDetector.ESCAPE);
issues.add(PropertyFileDetector.HTTP);
issues.add(PxUsageDetector.DP_ISSUE);
issues.add(PxUsageDetector.IN_MM_ISSUE);
issues.add(PxUsageDetector.PX_ISSUE);
issues.add(PxUsageDetector.SMALL_SP_ISSUE);
issues.add(RegistrationDetector.ISSUE);
issues.add(RelativeOverlapDetector.ISSUE);
issues.add(RequiredAttributeDetector.ISSUE);
issues.add(ResourceCycleDetector.CRASH);
issues.add(ResourceCycleDetector.CYCLE);
issues.add(ResourcePrefixDetector.ISSUE);
issues.add(RtlDetector.COMPAT);
issues.add(RtlDetector.ENABLED);
issues.add(RtlDetector.SYMMETRY);
issues.add(RtlDetector.USE_START);
issues.add(ScrollViewChildDetector.ISSUE);
issues.add(SdCardDetector.ISSUE);
issues.add(SecureRandomDetector.ISSUE);
issues.add(SecureRandomGeneratorDetector.ISSUE);
issues.add(SecurityDetector.EXPORTED_PROVIDER);
issues.add(SecurityDetector.EXPORTED_RECEIVER);
issues.add(SecurityDetector.EXPORTED_SERVICE);
@@ -204,9 +105,7 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(ServiceCastDetector.ISSUE);
issues.add(SetJavaScriptEnabledDetector.ISSUE);
issues.add(SharedPrefsDetector.ISSUE);
issues.add(SignatureOrSystemDetector.ISSUE);
issues.add(SQLiteDetector.ISSUE);
issues.add(StateListDetector.ISSUE);
issues.add(StringFormatDetector.ARG_COUNT);
issues.add(StringFormatDetector.ARG_TYPES);
issues.add(StringFormatDetector.INVALID);
@@ -219,42 +118,15 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(SupportAnnotationDetector.RESOURCE_TYPE);
issues.add(SupportAnnotationDetector.THREAD);
issues.add(SupportAnnotationDetector.TYPE_DEF);
issues.add(SystemPermissionsDetector.ISSUE);
issues.add(TextFieldDetector.ISSUE);
issues.add(TextViewDetector.ISSUE);
issues.add(TextViewDetector.SELECTABLE);
issues.add(TitleDetector.ISSUE);
issues.add(ToastDetector.ISSUE);
issues.add(TooManyViewsDetector.TOO_DEEP);
issues.add(TooManyViewsDetector.TOO_MANY);
issues.add(TranslationDetector.EXTRA);
issues.add(TranslationDetector.MISSING);
issues.add(TypoDetector.ISSUE);
issues.add(TypographyDetector.DASHES);
issues.add(TypographyDetector.ELLIPSIS);
issues.add(TypographyDetector.FRACTIONS);
issues.add(TypographyDetector.OTHER);
issues.add(TypographyDetector.QUOTES);
issues.add(UnusedResourceDetector.ISSUE);
issues.add(UnusedResourceDetector.ISSUE_IDS);
issues.add(UseCompoundDrawableDetector.ISSUE);
issues.add(UselessViewDetector.USELESS_LEAF);
issues.add(UselessViewDetector.USELESS_PARENT);
issues.add(Utf8Detector.ISSUE);
issues.add(ViewConstructorDetector.ISSUE);
issues.add(ViewHolderDetector.ISSUE);
issues.add(ViewTagDetector.ISSUE);
issues.add(ViewTypeDetector.ISSUE);
issues.add(WakelockDetector.ISSUE);
issues.add(WebViewDetector.ISSUE);
issues.add(WrongCallDetector.ISSUE);
issues.add(WrongCaseDetector.WRONG_CASE);
issues.add(WrongIdDetector.INVALID);
issues.add(WrongIdDetector.NOT_SIBLING);
issues.add(WrongIdDetector.UNKNOWN_ID);
issues.add(WrongIdDetector.UNKNOWN_ID_LAYOUT);
issues.add(WrongImportDetector.ISSUE);
issues.add(WrongLocationDetector.ISSUE);
sIssues = Collections.unmodifiableList(issues);
}
@@ -1,784 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_STRING_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_ORIENTATION;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.STRING_PREFIX;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.VALUE_SELECTABLE_ITEM_BACKGROUND;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.SdkConstants.VALUE_VERTICAL;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Check which looks at the order of buttons in dialogs and makes sure that
* "the dismissive action of a dialog is always on the left whereas the affirmative actions
* are on the right."
* <p>
* This only looks for the affirmative and dismissive actions named "OK" and "Cancel";
* "Cancel" usually works, but the affirmative action often has many other names -- "Done",
* "Send", "Go", etc.
* <p>
* TODO: Perhaps we should look for Yes/No dialogs and suggested they be rephrased as
* Cancel/OK dialogs? Similarly, consider "Abort" a synonym for "Cancel" ?
*/
public class ButtonDetector extends ResourceXmlDetector {
/** Name of cancel value ("Cancel") */
private static final String CANCEL_LABEL = "Cancel";
/** Name of OK value ("Cancel") */
private static final String OK_LABEL = "OK";
/** Name of Back value ("Back") */
private static final String BACK_LABEL = "Back";
/** Yes */
private static final String YES_LABEL = "Yes";
/** No */
private static final String NO_LABEL = "No";
/** Layout text attribute reference to {@code @android:string/ok} */
private static final String ANDROID_OK_RESOURCE =
ANDROID_STRING_PREFIX + "ok"; //$NON-NLS-1$
/** Layout text attribute reference to {@code @android:string/cancel} */
private static final String ANDROID_CANCEL_RESOURCE =
ANDROID_STRING_PREFIX + "cancel"; //$NON-NLS-1$
/** Layout text attribute reference to {@code @android:string/yes} */
private static final String ANDROID_YES_RESOURCE =
ANDROID_STRING_PREFIX + "yes"; //$NON-NLS-1$
/** Layout text attribute reference to {@code @android:string/no} */
private static final String ANDROID_NO_RESOURCE =
ANDROID_STRING_PREFIX + "no"; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
ButtonDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue ORDER = Issue.create(
"ButtonOrder", //$NON-NLS-1$
"Button order",
"According to the Android Design Guide,\n" +
"\n" +
"\"Action buttons are typically Cancel and/or OK, with OK indicating the preferred " +
"or most likely action. However, if the options consist of specific actions such " +
"as Close or Wait rather than a confirmation or cancellation of the action " +
"described in the content, then all the buttons should be active verbs. As a rule, " +
"the dismissive action of a dialog is always on the left whereas the affirmative " +
"actions are on the right.\"\n" +
"\n" +
"This check looks for button bars and buttons which look like cancel buttons, " +
"and makes sure that these are on the left.",
Category.USABILITY,
8,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/design/building-blocks/dialogs.html"); //$NON-NLS-1$
/** The main issue discovered by this detector */
public static final Issue STYLE = Issue.create(
"ButtonStyle", //$NON-NLS-1$
"Button should be borderless",
"Button bars typically use a borderless style for the buttons. Set the " +
"`style=\"?android:attr/buttonBarButtonStyle\"` attribute " +
"on each of the buttons, and set `style=\"?android:attr/buttonBarStyle\"` on " +
"the parent layout",
Category.USABILITY,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/design/building-blocks/buttons.html"); //$NON-NLS-1$
/** The main issue discovered by this detector */
public static final Issue BACK_BUTTON = Issue.create(
"BackButton", //$NON-NLS-1$
"Back button",
// TODO: Look for ">" as label suffixes as well
"According to the Android Design Guide,\n" +
"\n" +
"\"Other platforms use an explicit back button with label to allow the user " +
"to navigate up the application's hierarchy. Instead, Android uses the main " +
"action bar's app icon for hierarchical navigation and the navigation bar's " +
"back button for temporal navigation.\"" +
"\n" +
"This check is not very sophisticated (it just looks for buttons with the " +
"label \"Back\"), so it is disabled by default to not trigger on common " +
"scenarios like pairs of Back/Next buttons to paginate through screens.",
Category.USABILITY,
6,
Severity.WARNING,
IMPLEMENTATION)
.setEnabledByDefault(false)
.addMoreInfo(
"http://developer.android.com/design/patterns/pure-android.html"); //$NON-NLS-1$
/** The main issue discovered by this detector */
public static final Issue CASE = Issue.create(
"ButtonCase", //$NON-NLS-1$
"Cancel/OK dialog button capitalization",
"The standard capitalization for OK/Cancel dialogs is \"OK\" and \"Cancel\". " +
"To ensure that your dialogs use the standard strings, you can use " +
"the resource strings @android:string/ok and @android:string/cancel.",
Category.USABILITY,
2,
Severity.WARNING,
IMPLEMENTATION);
/** Set of resource names whose value was either OK or Cancel */
private Set<String> mApplicableResources;
/**
* Map of resource names we'd like resolved into strings in phase 2. The
* values should be filled in with the actual string contents.
*/
private Map<String, String> mKeyToLabel;
/**
* Set of elements we've already warned about. If we've already complained
* about a cancel button, don't also report the OK button (since it's listed
* for the warnings on OK buttons).
*/
private Set<Element> mIgnore;
/** Constructs a new {@link ButtonDetector} */
public ButtonDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(BUTTON, TAG_STRING);
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
}
@Override
public void afterCheckProject(@NonNull Context context) {
int phase = context.getPhase();
if (phase == 1 && mApplicableResources != null) {
// We found resources for the string "Cancel"; perform a second pass
// where we check layout text attributes against these strings.
context.getDriver().requestRepeat(this, Scope.RESOURCE_FILE_SCOPE);
}
}
private static String stripLabel(String text) {
text = text.trim();
if (text.length() > 2
&& (text.charAt(0) == '"' || text.charAt(0) == '\'')
&& (text.charAt(0) == text.charAt(text.length() - 1))) {
text = text.substring(1, text.length() - 1);
}
return text;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
// This detector works in two passes.
// In pass 1, it looks in layout files for hardcoded strings of "Cancel", or
// references to @string/cancel or @android:string/cancel.
// It also looks in values/ files for strings whose value is "Cancel",
// and if found, stores the corresponding keys in a map. (This is necessary
// since value files are processed after layout files).
// Then, if at the end of phase 1 any "Cancel" string resources were
// found in the value files, then it requests a *second* phase,
// where it looks only for <Button>'s whose text matches one of the
// cancel string resources.
int phase = context.getPhase();
String tagName = element.getTagName();
if (phase == 1 && tagName.equals(TAG_STRING)) {
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
for (int j = 0, len = text.length(); j < len; j++) {
char c = text.charAt(j);
if (!Character.isWhitespace(c)) {
if (c == '"' || c == '\'') {
continue;
}
if (LintUtils.startsWith(text, CANCEL_LABEL, j)) {
String label = stripLabel(text);
if (label.equalsIgnoreCase(CANCEL_LABEL)) {
String name = element.getAttribute(ATTR_NAME);
foundResource(context, name, element);
if (!label.equals(CANCEL_LABEL)
&& LintUtils.isEnglishResource(context, true)
&& context.isEnabled(CASE)) {
assert label.equalsIgnoreCase(CANCEL_LABEL);
context.report(CASE, child, context.getLocation(child),
String.format(
"The standard Android way to capitalize %1$s " +
"is \"Cancel\" (tip: use `@android:string/cancel` instead)",
label));
}
}
} else if (LintUtils.startsWith(text, OK_LABEL, j)) {
String label = stripLabel(text);
if (label.equalsIgnoreCase(OK_LABEL)) {
String name = element.getAttribute(ATTR_NAME);
foundResource(context, name, element);
if (!label.equals(OK_LABEL)
&& LintUtils.isEnglishResource(context, true)
&& context.isEnabled(CASE)) {
assert text.trim().equalsIgnoreCase(OK_LABEL) : text;
context.report(CASE, child, context.getLocation(child),
String.format(
"The standard Android way to capitalize %1$s " +
"is \"OK\" (tip: use `@android:string/ok` instead)",
label));
}
}
} else if (LintUtils.startsWith(text, BACK_LABEL, j) &&
stripLabel(text).equalsIgnoreCase(BACK_LABEL)) {
String name = element.getAttribute(ATTR_NAME);
foundResource(context, name, element);
}
break;
}
}
}
}
} else if (tagName.equals(BUTTON)) {
if (phase == 1) {
if (isInButtonBar(element)
&& !element.hasAttribute(ATTR_STYLE)
&& !VALUE_SELECTABLE_ITEM_BACKGROUND.equals(
element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND))
&& (context.getProject().getMinSdk() >= 11
|| context.getFolderVersion() >= 11)
&& context.isEnabled(STYLE)
&& !parentDefinesSelectableItem(element)) {
context.report(STYLE, element, context.getLocation(element),
"Buttons in button bars should be borderless; use " +
"`style=\"?android:attr/buttonBarButtonStyle\"` (and " +
"`?android:attr/buttonBarStyle` on the parent)");
}
}
String text = element.getAttributeNS(ANDROID_URI, ATTR_TEXT);
if (phase == 2) {
if (mApplicableResources.contains(text)) {
String key = text;
if (key.startsWith(STRING_PREFIX)) {
key = key.substring(STRING_PREFIX.length());
}
String label = mKeyToLabel.get(key);
boolean isCancel = CANCEL_LABEL.equalsIgnoreCase(label);
if (isCancel) {
if (isWrongCancelPosition(element)) {
reportCancelPosition(context, element);
}
} else if (OK_LABEL.equalsIgnoreCase(label)) {
if (isWrongOkPosition(element)) {
reportOkPosition(context, element);
}
} else {
assert BACK_LABEL.equalsIgnoreCase(label) : label + ':' + context.file;
Location location = context.getLocation(element);
if (context.isEnabled(BACK_BUTTON)) {
context.report(BACK_BUTTON, element, location,
"Back buttons are not standard on Android; see design guide's " +
"navigation section");
}
}
}
} else if (text.equals(CANCEL_LABEL) || text.equals(ANDROID_CANCEL_RESOURCE)) {
if (isWrongCancelPosition(element)) {
reportCancelPosition(context, element);
}
} else if (text.equals(OK_LABEL) || text.equals(ANDROID_OK_RESOURCE)) {
if (isWrongOkPosition(element)) {
reportOkPosition(context, element);
}
} else {
boolean isYes = text.equals(ANDROID_YES_RESOURCE);
if (isYes || text.equals(ANDROID_NO_RESOURCE)) {
Attr attribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_TEXT);
Location location = context.getLocation(attribute);
String message = String.format("%1$s actually returns \"%2$s\", not \"%3$s\"; "
+ "use %4$s instead or create a local string resource for %5$s",
text,
isYes ? OK_LABEL : CANCEL_LABEL,
isYes ? YES_LABEL : NO_LABEL,
isYes ? ANDROID_OK_RESOURCE : ANDROID_CANCEL_RESOURCE,
isYes ? YES_LABEL : NO_LABEL);
context.report(CASE, element, location, message);
}
}
}
}
private static boolean parentDefinesSelectableItem(Element element) {
String background = element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
if (VALUE_SELECTABLE_ITEM_BACKGROUND.equals(background)) {
return true;
}
Node parent = element.getParentNode();
if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) {
return parentDefinesSelectableItem((Element) parent);
}
return false;
}
/** Report the given OK button as being in the wrong position */
private void reportOkPosition(XmlContext context, Element element) {
report(context, element, false /*isCancel*/);
}
/** Report the given Cancel button as being in the wrong position */
private void reportCancelPosition(XmlContext context, Element element) {
report(context, element, true /*isCancel*/);
}
/**
* We've found a resource reference to some label we're interested in ("OK",
* "Cancel", "Back", ...). Record the corresponding name such that in the
* next pass through the layouts we can check the context (for OK/Cancel the
* button order etc).
*/
private void foundResource(XmlContext context, String name, Element element) {
if (!LintUtils.isEnglishResource(context, true)) {
return;
}
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mApplicableResources == null) {
mApplicableResources = new HashSet<String>();
}
mApplicableResources.add(STRING_PREFIX + name);
// ALSO record all the other string resources in this file to pick up other
// labels. If you define "OK" in one resource file and "Cancel" in another
// this won't work, but that's probably not common and has lower overhead.
Node parentNode = element.getParentNode();
List<Element> items = LintUtils.getChildren(parentNode);
if (mKeyToLabel == null) {
mKeyToLabel = new HashMap<String, String>(items.size());
}
for (Element item : items) {
String itemName = item.getAttribute(ATTR_NAME);
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = stripLabel(child.getNodeValue());
if (!text.isEmpty()) {
mKeyToLabel.put(itemName, text);
break;
}
}
}
}
}
/** Report the given OK/Cancel button as being in the wrong position */
private void report(XmlContext context, Element element, boolean isCancel) {
if (!context.isEnabled(ORDER)) {
return;
}
if (mIgnore != null && mIgnore.contains(element)) {
return;
}
int target = context.getProject().getTargetSdk();
if (target < 14) {
// If you're only targeting pre-ICS UI's, this is not an issue
return;
}
boolean mustCreateIcsLayout = false;
if (context.getProject().getMinSdk() < 14) {
// If you're *also* targeting pre-ICS UIs, then this reverse button
// order is correct for layouts intended for pre-ICS and incorrect for
// ICS layouts.
//
// Therefore, we need to know if this layout is an ICS layout or
// a pre-ICS layout.
boolean isIcsLayout = context.getFolderVersion() >= 14;
if (!isIcsLayout) {
// This layout is not an ICS layout. However, there *must* also be
// an ICS layout here, or this button order will be wrong:
File res = context.file.getParentFile().getParentFile();
File[] resFolders = res.listFiles();
String fileName = context.file.getName();
if (resFolders != null) {
for (File folder : resFolders) {
String folderName = folder.getName();
if (folderName.startsWith(SdkConstants.FD_RES_LAYOUT)
&& folderName.contains("-v14")) { //$NON-NLS-1$
File layout = new File(folder, fileName);
if (layout.exists()) {
// Yes, a v14 specific layout is available so this pre-ICS
// layout order is not a problem
return;
}
}
}
}
mustCreateIcsLayout = true;
}
}
List<Element> buttons = LintUtils.getChildren(element.getParentNode());
if (mIgnore == null) {
mIgnore = new HashSet<Element>();
}
for (Element button : buttons) {
// Mark all the siblings in the ignore list to ensure that we don't
// report *both* the Cancel and the OK button in "OK | Cancel"
mIgnore.add(button);
}
String message;
if (isCancel) {
message = "Cancel button should be on the left";
} else {
message = "OK button should be on the right";
}
if (mustCreateIcsLayout) {
message = String.format(
"Layout uses the wrong button order for API >= 14: Create a " +
"`layout-v14/%1$s` file with opposite order: %2$s",
context.file.getName(), message);
}
// Show existing button order? We can only do that for LinearLayouts
// since in for example a RelativeLayout the order of the elements may
// not be the same as the visual order
String layout = element.getParentNode().getNodeName();
if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
List<String> labelList = getLabelList(buttons);
String wrong = describeButtons(labelList);
sortButtons(labelList);
String right = describeButtons(labelList);
message += String.format(" (was \"%1$s\", should be \"%2$s\")", wrong, right);
}
Location location = context.getLocation(element);
context.report(ORDER, element, location, message);
}
/**
* Sort a list of label buttons into the expected order (Cancel on the left,
* OK on the right
*/
private static void sortButtons(List<String> labelList) {
for (int i = 0, n = labelList.size(); i < n; i++) {
String label = labelList.get(i);
if (label.equalsIgnoreCase(CANCEL_LABEL) && i > 0) {
swap(labelList, 0, i);
} else if (label.equalsIgnoreCase(OK_LABEL) && i < n - 1) {
swap(labelList, n - 1, i);
}
}
}
/** Swaps the strings at positions i and j */
private static void swap(List<String> strings, int i, int j) {
if (i != j) {
String temp = strings.get(i);
strings.set(i, strings.get(j));
strings.set(j, temp);
}
}
/** Creates a display string for a list of button labels, such as "Cancel | OK" */
private static String describeButtons(List<String> labelList) {
StringBuilder sb = new StringBuilder(80);
for (String label : labelList) {
if (sb.length() > 0) {
sb.append(" | "); //$NON-NLS-1$
}
sb.append(label);
}
return sb.toString();
}
/** Returns the ordered list of button labels */
private List<String> getLabelList(List<Element> views) {
List<String> labels = new ArrayList<String>();
if (mIgnore == null) {
mIgnore = new HashSet<Element>();
}
for (Element view : views) {
if (view.getTagName().equals(BUTTON)) {
String text = view.getAttributeNS(ANDROID_URI, ATTR_TEXT);
String label = getLabel(text);
labels.add(label);
// Mark all the siblings in the ignore list to ensure that we don't
// report *both* the Cancel and the OK button in "OK | Cancel"
mIgnore.add(view);
}
}
return labels;
}
private String getLabel(String key) {
String label = null;
if (key.startsWith(ANDROID_STRING_PREFIX)) {
if (key.equals(ANDROID_OK_RESOURCE)) {
label = OK_LABEL;
} else if (key.equals(ANDROID_CANCEL_RESOURCE)) {
label = CANCEL_LABEL;
}
} else if (mKeyToLabel != null) {
if (key.startsWith(STRING_PREFIX)) {
label = mKeyToLabel.get(key.substring(STRING_PREFIX.length()));
}
}
if (label == null) {
label = key;
}
if (label.indexOf(' ') != -1 && label.indexOf('"') == -1) {
label = '"' + label + '"';
}
return label;
}
/** Is the cancel button in the wrong position? It has to be on the left. */
private static boolean isWrongCancelPosition(Element element) {
return isWrongPosition(element, true /*isCancel*/);
}
/** Is the OK button in the wrong position? It has to be on the right. */
private static boolean isWrongOkPosition(Element element) {
return isWrongPosition(element, false /*isCancel*/);
}
private static boolean isInButtonBar(Element element) {
assert element.getTagName().equals(BUTTON) : element.getTagName();
Node parentNode = element.getParentNode();
if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
return false;
}
Element parent = (Element) parentNode;
String style = parent.getAttribute(ATTR_STYLE);
if (style != null && style.contains("buttonBarStyle")) { //$NON-NLS-1$
return true;
}
// Don't warn about single Cancel / OK buttons
if (LintUtils.getChildCount(parent) < 2) {
return false;
}
String layout = parent.getTagName();
if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
if (VALUE_VERTICAL.equals(orientation)) {
return false;
}
} else {
return false;
}
// Ensure that all the children are buttons
Node n = parent.getFirstChild();
while (n != null) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
if (!BUTTON.equals(n.getNodeName())) {
return false;
}
}
n = n.getNextSibling();
}
return true;
}
/** Is the given button in the wrong position? */
private static boolean isWrongPosition(Element element, boolean isCancel) {
Node parentNode = element.getParentNode();
if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
return false;
}
Element parent = (Element) parentNode;
// Don't warn about single Cancel / OK buttons
if (LintUtils.getChildCount(parent) < 2) {
return false;
}
String layout = parent.getTagName();
if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
if (VALUE_VERTICAL.equals(orientation)) {
return false;
}
if (isCancel) {
Node n = element.getPreviousSibling();
while (n != null) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
return true;
}
n = n.getPreviousSibling();
}
} else {
Node n = element.getNextSibling();
while (n != null) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
return true;
}
n = n.getNextSibling();
}
}
return false;
} else if (layout.equals(RELATIVE_LAYOUT)) {
// In RelativeLayouts, look for attachments which look like a clear sign
// that the OK or Cancel buttons are out of order:
// -- a left attachment on a Cancel button (where the left attachment
// is a button; we don't want to complain if it's pointing to a spacer
// or image or progress indicator etc)
// -- a right-side parent attachment on a Cancel button (unless it's also
// attached on the left, e.g. a cancel button stretching across the
// layout)
// etc.
if (isCancel) {
if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF)
&& isButtonId(parent, element.getAttributeNS(ANDROID_URI,
ATTR_LAYOUT_TO_RIGHT_OF))) {
return true;
}
if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT) &&
!isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
return true;
}
} else {
if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF)
&& isButtonId(parent, element.getAttributeNS(ANDROID_URI,
ATTR_LAYOUT_TO_RIGHT_OF))) {
return true;
}
if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT) &&
!isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
return true;
}
}
return false;
} else {
// TODO: Consider other button layouts - GridLayouts, custom views extending
// LinearLayout etc?
return false;
}
}
/**
* Returns true if the given attribute (in the Android namespace) is set to
* true on the given element
*/
private static boolean isTrue(Element element, String attribute) {
return VALUE_TRUE.equals(element.getAttributeNS(ANDROID_URI, attribute));
}
/** Is the given target id the id of a {@code <Button>} within this RelativeLayout? */
private static boolean isButtonId(Element parent, String targetId) {
for (Element child : LintUtils.getChildren(parent)) {
String id = child.getAttributeNS(ANDROID_URI, ATTR_ID);
if (LintUtils.idReferencesMatch(id, targetId)) {
return child.getTagName().equals(BUTTON);
}
}
return false;
}
}
@@ -1,90 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ATTR_NAME;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import java.util.Collection;
import java.util.Collections;
/**
* Checks that byte order marks do not appear in resource names
*/
public class ByteOrderMarkDetector extends ResourceXmlDetector {
/** Detects BOM characters in the middle of files */
public static final Issue BOM = Issue.create(
"ByteOrderMark", //$NON-NLS-1$
"Byte order mark inside files",
"Lint will flag any byte-order-mark (BOM) characters it finds in the middle " +
"of a file. Since we expect files to be encoded with UTF-8 (see the EnforceUTF8 " +
"issue), the BOM characters are not necessary, and they are not handled correctly " +
"by all tools. For example, if you have a BOM as part of a resource name in one " +
"particular translation, that name will not be considered identical to the base " +
"resource's name and the translation will not be used.",
Category.I18N,
8,
Severity.FATAL,
new Implementation(
ByteOrderMarkDetector.class,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("http://en.wikipedia.org/wiki/Byte_order_mark");
/** Constructs a new {@link ByteOrderMarkDetector} */
public ByteOrderMarkDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Nullable
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_NAME);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String name = attribute.getValue();
for (int i = 0, n = name.length(); i < n; i++) {
char c = name.charAt(i);
if (c == '\uFEFF') {
Location location = context.getLocation(attribute);
String message = "Found byte-order-mark in the middle of a file";
context.report(BOM, null, location, message);
break;
}
}
}
}
@@ -1,131 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.REQUEST_FOCUS;
import static com.android.SdkConstants.SCROLL_VIEW;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Arrays;
import java.util.Collection;
/**
* Check which makes sure that views have the expected number of declared
* children (e.g. at most one in ScrollViews and none in AdapterViews)
*/
public class ChildCountDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
ChildCountDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue SCROLLVIEW_ISSUE = Issue.create(
"ScrollViewCount", //$NON-NLS-1$
"ScrollViews can have only one child",
"ScrollViews can only have one child widget. If you want more children, wrap them " +
"in a container layout.",
Category.CORRECTNESS,
8,
Severity.WARNING,
IMPLEMENTATION);
/** The main issue discovered by this detector */
public static final Issue ADAPTER_VIEW_ISSUE = Issue.create(
"AdapterViewChildren", //$NON-NLS-1$
"AdapterViews cannot have children in XML",
"AdapterViews such as ListViews must be configured with data from Java code, " +
"such as a ListAdapter.",
Category.CORRECTNESS,
10,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/reference/android/widget/AdapterView.html"); //$NON-NLS-1$
/** Constructs a new {@link ChildCountDetector} */
public ChildCountDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
SCROLL_VIEW,
HORIZONTAL_SCROLL_VIEW,
LIST_VIEW,
GRID_VIEW
// TODO: Shouldn't Spinner be in this list too? (Was not there in layoutopt)
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int childCount = LintUtils.getChildCount(element);
String tagName = element.getTagName();
if (tagName.equals(SCROLL_VIEW) || tagName.equals(HORIZONTAL_SCROLL_VIEW)) {
if (childCount > 1 && getAccurateChildCount(element) > 1) {
context.report(SCROLLVIEW_ISSUE, element,
context.getLocation(element), "A scroll view can have only one child");
}
} else {
// Adapter view
if (childCount > 0 && getAccurateChildCount(element) > 0) {
context.report(ADAPTER_VIEW_ISSUE, element,
context.getLocation(element),
"A list/grid should have no children declared in XML");
}
}
}
/** Counts the number of children, but skips certain tags like {@code <requestFocus>} */
private static int getAccurateChildCount(Element element) {
NodeList childNodes = element.getChildNodes();
int childCount = 0;
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE &&
!REQUEST_FOCUS.equals(child.getNodeName())) {
childCount++;
}
}
return childCount;
}
}
@@ -1,265 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_VIEW_VIEW;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.List;
import java.util.ListIterator;
/**
* Checks that views that override View#onTouchEvent also implement View#performClick
* and call performClick when click detection occurs.
*/
public class ClickableViewAccessibilityDetector extends Detector implements Detector.ClassScanner {
public static final Issue ISSUE = Issue.create(
"ClickableViewAccessibility", //$NON-NLS-1$
"Accessibility in Custom Views",
"If a `View` that overrides `onTouchEvent` or uses an `OnTouchListener` does not also "
+ "implement `performClick` and call it when clicks are detected, the `View` "
+ "may not handle accessibility actions properly. Logic handling the click "
+ "actions should ideally be placed in `View#performClick` as some "
+ "accessibility services invoke `performClick` when a click action "
+ "should occur.",
Category.A11Y,
6,
Severity.WARNING,
new Implementation(
ClickableViewAccessibilityDetector.class,
Scope.CLASS_FILE_SCOPE));
private static final String ON_TOUCH_EVENT = "onTouchEvent"; //$NON-NLS-1$
private static final String ON_TOUCH_EVENT_SIG = "(Landroid/view/MotionEvent;)Z"; //$NON-NLS-1$
private static final String PERFORM_CLICK = "performClick"; //$NON-NLS-1$
private static final String PERFORM_CLICK_SIG = "()Z"; //$NON-NLS-1$
private static final String SET_ON_TOUCH_LISTENER = "setOnTouchListener"; //$NON-NLS-1$
private static final String SET_ON_TOUCH_LISTENER_SIG = "(Landroid/view/View$OnTouchListener;)V"; //$NON-NLS-1$
private static final String ON_TOUCH = "onTouch"; //$NON-NLS-1$
private static final String ON_TOUCH_SIG = "(Landroid/view/View;Landroid/view/MotionEvent;)Z"; //$NON-NLS-1$
private static final String ON_TOUCH_LISTENER = "android/view/View$OnTouchListener"; //$NON-NLS-1$
/** Constructs a new {@link ClickableViewAccessibilityDetector} */
public ClickableViewAccessibilityDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements ClassScanner ----
@Override
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
scanForAndCheckSetOnTouchListenerCalls(context, classNode);
// Ignore abstract classes.
if ((classNode.access & Opcodes.ACC_ABSTRACT) != 0) {
return;
}
if (context.getDriver().isSubclassOf(classNode, ANDROID_VIEW_VIEW)) {
checkView(context, classNode);
}
if (implementsOnTouchListener(classNode)) {
checkOnTouchListener(context, classNode);
}
}
@SuppressWarnings("unchecked") // ASM API
public static void scanForAndCheckSetOnTouchListenerCalls(
ClassContext context,
ClassNode classNode) {
List<MethodNode> methods = classNode.methods;
for (MethodNode methodNode : methods) {
ListIterator<AbstractInsnNode> iterator = methodNode.instructions.iterator();
while (iterator.hasNext()) {
AbstractInsnNode abstractInsnNode = iterator.next();
if (abstractInsnNode.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode methodInsnNode = (MethodInsnNode) abstractInsnNode;
if (methodInsnNode.name.equals(SET_ON_TOUCH_LISTENER)
&& methodInsnNode.desc.equals(SET_ON_TOUCH_LISTENER_SIG)) {
checkSetOnTouchListenerCall(context, methodNode, methodInsnNode);
}
}
}
}
}
@SuppressWarnings("unchecked") // ASM API
public static void checkSetOnTouchListenerCall(
@NonNull ClassContext context,
@NonNull MethodNode method,
@NonNull MethodInsnNode call) {
String owner = call.owner;
// Ignore the call if it was called on a non-view.
ClassNode ownerClass = context.getDriver().findClass(context, owner, 0);
if(ownerClass == null
|| !context.getDriver().isSubclassOf(ownerClass, ANDROID_VIEW_VIEW)) {
return;
}
MethodNode performClick = findMethod(ownerClass.methods, PERFORM_CLICK, PERFORM_CLICK_SIG);
//noinspection VariableNotUsedInsideIf
if (performClick == null) {
String message = String.format(
"Custom view `%1$s` has `setOnTouchListener` called on it but does not "
+ "override `performClick`", ownerClass.name);
context.report(ISSUE, method, call, context.getLocation(call), message);
}
}
@SuppressWarnings("unchecked") // ASM API
private static void checkOnTouchListener(ClassContext context, ClassNode classNode) {
MethodNode onTouchNode =
findMethod(
classNode.methods,
ON_TOUCH,
ON_TOUCH_SIG);
if (onTouchNode != null) {
AbstractInsnNode performClickInsnNode = findMethodCallInstruction(
onTouchNode.instructions,
ANDROID_VIEW_VIEW,
PERFORM_CLICK,
PERFORM_CLICK_SIG);
if (performClickInsnNode == null) {
String message = String.format(
"`%1$s#onTouch` should call `View#performClick` when a click is detected",
classNode.name);
context.report(
ISSUE,
onTouchNode,
null,
context.getLocation(onTouchNode, classNode),
message);
}
}
}
@SuppressWarnings("unchecked") // ASM API
private static void checkView(ClassContext context, ClassNode classNode) {
MethodNode onTouchEvent = findMethod(classNode.methods, ON_TOUCH_EVENT, ON_TOUCH_EVENT_SIG);
MethodNode performClick = findMethod(classNode.methods, PERFORM_CLICK, PERFORM_CLICK_SIG);
// Check if we override onTouchEvent.
if (onTouchEvent != null) {
// Ensure that we also override performClick.
//noinspection VariableNotUsedInsideIf
if (performClick == null) {
String message = String.format(
"Custom view `%1$s` overrides `onTouchEvent` but not `performClick`",
classNode.name);
context.report(ISSUE, onTouchEvent, null,
context.getLocation(onTouchEvent, classNode), message);
} else {
// If we override performClick, ensure that it is called inside onTouchEvent.
AbstractInsnNode performClickInOnTouchEventInsnNode = findMethodCallInstruction(
onTouchEvent.instructions,
classNode.name,
PERFORM_CLICK,
PERFORM_CLICK_SIG);
if (performClickInOnTouchEventInsnNode == null) {
String message = String.format(
"`%1$s#onTouchEvent` should call `%1$s#performClick` when a click is detected",
classNode.name);
context.report(ISSUE, onTouchEvent, null,
context.getLocation(onTouchEvent, classNode), message);
}
}
}
// Ensure that, if performClick is implemented, performClick calls super.performClick.
if (performClick != null) {
AbstractInsnNode superPerformClickInPerformClickInsnNode = findMethodCallInstruction(
performClick.instructions,
classNode.superName,
PERFORM_CLICK,
PERFORM_CLICK_SIG);
if (superPerformClickInPerformClickInsnNode == null) {
String message = String.format(
"`%1$s#performClick` should call `super#performClick`",
classNode.name);
context.report(ISSUE, performClick, null,
context.getLocation(performClick, classNode), message);
}
}
}
@Nullable
private static MethodNode findMethod(
@NonNull List<MethodNode> methods,
@NonNull String name,
@NonNull String desc) {
for (MethodNode method : methods) {
if (name.equals(method.name)
&& desc.equals(method.desc)) {
return method;
}
}
return null;
}
@SuppressWarnings("unchecked") // ASM API
@Nullable
private static AbstractInsnNode findMethodCallInstruction(
@NonNull InsnList instructions,
@NonNull String owner,
@NonNull String name,
@NonNull String desc) {
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
while (iterator.hasNext()) {
AbstractInsnNode insnNode = iterator.next();
if (insnNode.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode;
if ((methodInsnNode.owner.equals(owner))
&& (methodInsnNode.name.equals(name))
&& (methodInsnNode.desc.equals(desc))) {
return methodInsnNode;
}
}
}
return null;
}
private static boolean implementsOnTouchListener(ClassNode classNode) {
return (classNode.interfaces != null) && (classNode.interfaces.contains(ON_TOUCH_LISTENER));
}
}
@@ -1,177 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_AUTO_TEXT;
import static com.android.SdkConstants.ATTR_CAPITALIZE;
import static com.android.SdkConstants.ATTR_EDITABLE;
import static com.android.SdkConstants.ATTR_ENABLED;
import static com.android.SdkConstants.ATTR_INPUT_METHOD;
import static com.android.SdkConstants.ATTR_NUMERIC;
import static com.android.SdkConstants.ATTR_PASSWORD;
import static com.android.SdkConstants.ATTR_PHONE_NUMBER;
import static com.android.SdkConstants.ATTR_SINGLE_LINE;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.VALUE_TRUE;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
/**
* Check which looks for usage of deprecated tags, attributes, etc.
*/
public class DeprecationDetector extends LayoutDetector {
/** Usage of deprecated views or attributes */
public static final Issue ISSUE = Issue.create(
"Deprecated", //$NON-NLS-1$
"Using deprecated resources",
"Deprecated views, attributes and so on are deprecated because there " +
"is a better way to do something. Do it that new way. You've been warned.",
Category.CORRECTNESS,
2,
Severity.WARNING,
new Implementation(
DeprecationDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link DeprecationDetector} */
public DeprecationDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(
ABSOLUTE_LAYOUT
);
}
@Override
public Collection<String> getApplicableAttributes() {
return Arrays.asList(
// TODO: fill_parent is deprecated as of API 8.
// We could warn about it, but it will probably be very noisy
// and make people disable the deprecation check; let's focus on
// some older flags for now
//"fill_parent",
ATTR_EDITABLE,
ATTR_INPUT_METHOD,
ATTR_AUTO_TEXT,
ATTR_CAPITALIZE,
// This flag is still used a lot and is still properly handled by TextView
// so in the interest of not being too noisy and make people ignore all the
// output, keep quiet about this one -for now-.
//ATTR_SINGLE_LINE,
// This attribute is marked deprecated in android.R.attr but apparently
// using the suggested replacement of state_enabled doesn't work, see issue 27613
//ATTR_ENABLED,
ATTR_NUMERIC,
ATTR_PHONE_NUMBER,
ATTR_PASSWORD
// These attributes are also deprecated; not yet enabled until we
// know the API level to apply the deprecation for:
// "ignored as of ICS (but deprecated earlier)"
//"fadingEdge",
// "This attribute is not used by the Android operating system."
//"restoreNeedsApplication",
// "This will create a non-standard UI appearance, because the search bar UI is
// changing to use only icons for its buttons."
//"searchButtonText",
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
context.report(ISSUE, element, context.getLocation(element),
String.format("`%1$s` is deprecated", element.getTagName()));
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (!ANDROID_URI.equals(attribute.getNamespaceURI())) {
return;
}
String name = attribute.getLocalName();
String fix;
int minSdk = 1;
if (name.equals(ATTR_EDITABLE)) {
if (!EDIT_TEXT.equals(attribute.getOwnerElement().getTagName())) {
fix = "Use an `<EditText>` to make it editable";
} else {
if (VALUE_TRUE.equals(attribute.getValue())) {
fix = "`<EditText>` is already editable";
} else {
fix = "Use `inputType` instead";
}
}
} else if (name.equals(ATTR_ENABLED)) {
fix = "Use `state_enabled` instead";
} else if (name.equals(ATTR_SINGLE_LINE)) {
fix = "Use `maxLines=\"1\"` instead";
} else {
assert name.equals(ATTR_INPUT_METHOD)
|| name.equals(ATTR_CAPITALIZE)
|| name.equals(ATTR_NUMERIC)
|| name.equals(ATTR_PHONE_NUMBER)
|| name.equals(ATTR_PASSWORD)
|| name.equals(ATTR_AUTO_TEXT);
fix = "Use `inputType` instead";
// The inputType attribute was introduced in API 3 so don't warn about
// deprecation if targeting older platforms
minSdk = 3;
}
if (context.getProject().getMinSdk() < minSdk) {
return;
}
context.report(ISSUE, attribute, context.getLocation(attribute),
String.format("`%1$s` is deprecated: %2$s",
attribute.getName(), fix));
}
}
@@ -1,196 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
import static com.android.SdkConstants.ANDROID_SUPPORT_PKG_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ATTR_CORE_APP;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_PACKAGE;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.TAG_LAYOUT;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.VIEW_TAG;
import static com.android.resources.ResourceFolderType.ANIM;
import static com.android.resources.ResourceFolderType.ANIMATOR;
import static com.android.resources.ResourceFolderType.COLOR;
import static com.android.resources.ResourceFolderType.DRAWABLE;
import static com.android.resources.ResourceFolderType.INTERPOLATOR;
import static com.android.resources.ResourceFolderType.LAYOUT;
import static com.android.resources.ResourceFolderType.MENU;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* Detects layout attributes on builtin Android widgets that do not specify
* a prefix but probably should.
*/
public class DetectMissingPrefix extends LayoutDetector {
/** Attributes missing the android: prefix */
@SuppressWarnings("unchecked")
public static final Issue MISSING_NAMESPACE = Issue.create(
"MissingPrefix", //$NON-NLS-1$
"Missing Android XML namespace",
"Most Android views have attributes in the Android namespace. When referencing " +
"these attributes you *must* include the namespace prefix, or your attribute will " +
"be interpreted by `aapt` as just a custom attribute.\n" +
"\n" +
"Similarly, in manifest files, nearly all attributes should be in the `android:` " +
"namespace.",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(
DetectMissingPrefix.class,
Scope.MANIFEST_AND_RESOURCE_SCOPE,
Scope.MANIFEST_SCOPE, Scope.RESOURCE_FILE_SCOPE));
private static final Set<String> NO_PREFIX_ATTRS = new HashSet<String>();
static {
NO_PREFIX_ATTRS.add(ATTR_CLASS);
NO_PREFIX_ATTRS.add(ATTR_STYLE);
NO_PREFIX_ATTRS.add(ATTR_LAYOUT);
NO_PREFIX_ATTRS.add(ATTR_PACKAGE);
NO_PREFIX_ATTRS.add(ATTR_CORE_APP);
}
/** Constructs a new {@link DetectMissingPrefix} */
public DetectMissingPrefix() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == LAYOUT
|| folderType == MENU
|| folderType == DRAWABLE
|| folderType == ANIM
|| folderType == ANIMATOR
|| folderType == COLOR
|| folderType == INTERPOLATOR;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableAttributes() {
return ALL;
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String uri = attribute.getNamespaceURI();
if (uri == null || uri.isEmpty()) {
String name = attribute.getName();
if (name == null) {
return;
}
if (NO_PREFIX_ATTRS.contains(name)) {
return;
}
Element element = attribute.getOwnerElement();
if (isCustomView(element) && context.getResourceFolderType() != null) {
return;
} else if (context.getResourceFolderType() == ResourceFolderType.LAYOUT) {
// Data binding: These look like Android framework views but
// are data binding directives not in the Android namespace
Element root = element.getOwnerDocument().getDocumentElement();
if (TAG_LAYOUT.equals(root.getTagName())) {
return;
}
}
if (name.indexOf(':') != -1) {
// Don't flag warnings for attributes that already have a different
// namespace! This doesn't usually happen when lint is run from the
// command line, since (with the exception of xmlns: declaration attributes)
// an attribute shouldn't have a prefix *and* have no namespace, but
// when lint is run in the IDE (with a more fault-tolerant XML parser)
// this can happen, and we don't want to flag erroneous/misleading lint
// errors in this case.
return;
}
context.report(MISSING_NAMESPACE, attribute,
context.getLocation(attribute),
"Attribute is missing the Android namespace prefix");
} else if (!ANDROID_URI.equals(uri)
&& !TOOLS_URI.equals(uri)
&& context.getResourceFolderType() == ResourceFolderType.LAYOUT
&& !isCustomView(attribute.getOwnerElement())
&& !attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
// TODO: Consider not enforcing that the parent is a custom view
// too, though in that case we should filter out views that are
// layout params for the custom view parent:
// ....&& !attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
&& attribute.getOwnerElement().getParentNode().getNodeType() == Node.ELEMENT_NODE
&& !isCustomView((Element) attribute.getOwnerElement().getParentNode())) {
if (context.getResourceFolderType() == ResourceFolderType.LAYOUT
&& AUTO_URI.equals(uri)) {
// Data binding: Can add attributes like onClickListener to buttons etc.
Element root = attribute.getOwnerDocument().getDocumentElement();
if (TAG_LAYOUT.equals(root.getTagName())) {
return;
}
}
context.report(MISSING_NAMESPACE, attribute,
context.getLocation(attribute),
String.format("Unexpected namespace prefix \"%1$s\" found for tag `%2$s`",
attribute.getPrefix(), attribute.getOwnerElement().getTagName()));
}
}
private static boolean isCustomView(Element element) {
// If this is a custom view, the usage of custom attributes can be legitimate
String tag = element.getTagName();
if (tag.equals(VIEW_TAG)) {
// <view class="my.custom.view" ...>
return true;
}
return tag.indexOf('.') != -1 && (!tag.startsWith(ANDROID_PKG_PREFIX)
|| tag.startsWith(ANDROID_SUPPORT_PKG_PREFIX));
}
}
@@ -1,117 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Document;
/**
* Checks that the line endings in DOS files are consistent
*/
public class DosLineEndingDetector extends LayoutDetector {
/** Detects mangled DOS line ending documents */
public static final Issue ISSUE = Issue.create(
"MangledCRLF", //$NON-NLS-1$
"Mangled file line endings",
"On Windows, line endings are typically recorded as carriage return plus " +
"newline: \\r\\n.\n" +
"\n" +
"This detector looks for invalid line endings with repeated carriage return " +
"characters (without newlines). Previous versions of the ADT plugin could " +
"accidentally introduce these into the file, and when editing the file, the " +
"editor could produce confusing visual artifacts.",
Category.CORRECTNESS,
2,
Severity.ERROR,
new Implementation(
DosLineEndingDetector.class,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("https://bugs.eclipse.org/bugs/show_bug.cgi?id=375421"); //$NON-NLS-1$
/** Constructs a new {@link DosLineEndingDetector} */
public DosLineEndingDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
String contents = context.getContents();
if (contents == null) {
return;
}
// We could look for *consistency* and complain if you mix \n and \r\n too,
// but that isn't really a problem (most editors handle it) so let's
// not complain needlessly.
char prev = 0;
for (int i = 0, n = contents.length(); i < n; i++) {
char c = contents.charAt(i);
if (c == '\r' && prev == '\r') {
String message = "Incorrect line ending: found carriage return (`\\r`) without " +
"corresponding newline (`\\n`)";
// Mark the whole line as the error range, since pointing just to the
// line ending makes the error invisible in IDEs and error reports etc
// Find the most recent non-blank line
boolean blankLine = true;
for (int index = i - 2; index < i; index++) {
char d = contents.charAt(index);
if (!Character.isWhitespace(d)) {
blankLine = false;
}
}
int lineBegin = i;
for (int index = i - 2; index >= 0; index--) {
char d = contents.charAt(index);
if (d == '\n') {
lineBegin = index + 1;
if (!blankLine) {
break;
}
} else if (!Character.isWhitespace(d)) {
blankLine = false;
}
}
int lineEnd = Math.min(contents.length(), i + 1);
Location location = Location.create(context.file, contents, lineBegin, lineEnd);
context.report(ISSUE, document.getDocumentElement(), location, message);
return;
}
prev = c;
}
}
}
@@ -1,674 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.VIEW_INCLUDE;
import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
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 org.w3c.dom.NodeList;
import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Checks for duplicate ids within a layout and within an included layout
*/
public class DuplicateIdDetector extends LayoutDetector {
private Set<String> mIds;
private Map<File, Set<String>> mFileToIds;
private Map<File, List<String>> mIncludes;
// Data structures used for location collection in phase 2
// Map from include files to include names to pairs of message and location
// Map from file defining id, to the id to be defined, to a pair of location and message
private Multimap<File, Multimap<String, Occurrence>> mLocations;
private List<Occurrence> mErrors;
private static final Implementation IMPLEMENTATION = new Implementation(
DuplicateIdDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue WITHIN_LAYOUT = Issue.create(
"DuplicateIds", //$NON-NLS-1$
"Duplicate ids within a single layout",
"Within a layout, id's should be unique since otherwise `findViewById()` can " +
"return an unexpected view.",
Category.CORRECTNESS,
7,
Severity.FATAL,
IMPLEMENTATION);
/** The main issue discovered by this detector */
public static final Issue CROSS_LAYOUT = Issue.create(
"DuplicateIncludedIds", //$NON-NLS-1$
"Duplicate ids across layouts combined with include tags",
"It's okay for two independent layouts to use the same ids. However, if " +
"layouts are combined with include tags, then the id's need to be unique " +
"within any chain of included layouts, or `Activity#findViewById()` can " +
"return an unexpected view.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a duplicate id check */
public DuplicateIdDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.MENU;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_ID);
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(VIEW_INCLUDE);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
if (context.getPhase() == 1) {
mIds = new HashSet<String>();
}
}
@Override
public void afterCheckFile(@NonNull Context context) {
if (context.getPhase() == 1) {
// Store this layout's set of ids for full project analysis in afterCheckProject
mFileToIds.put(context.file, mIds);
mIds = null;
}
}
@Override
public void beforeCheckProject(@NonNull Context context) {
if (context.getPhase() == 1) {
mFileToIds = new HashMap<File, Set<String>>();
mIncludes = new HashMap<File, List<String>>();
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (context.getPhase() == 1) {
// Look for duplicates
if (!mIncludes.isEmpty()) {
// Traverse all the include chains and ensure that there are no duplicates
// across.
if (context.isEnabled(CROSS_LAYOUT)
&& context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
IncludeGraph graph = new IncludeGraph(context);
graph.check();
}
}
} else {
assert context.getPhase() == 2;
if (mErrors != null) {
for (Occurrence occurrence : mErrors) {
//assert location != null : occurrence;
Location location = occurrence.location;
if (location == null) {
location = Location.create(occurrence.file);
} else {
Object clientData = location.getClientData();
if (clientData instanceof Node) {
Node node = (Node) clientData;
if (context.getDriver().isSuppressed(null, CROSS_LAYOUT, node)) {
continue;
}
}
}
List<Occurrence> sorted = new ArrayList<Occurrence>();
Occurrence curr = occurrence.next;
while (curr != null) {
sorted.add(curr);
curr = curr.next;
}
Collections.sort(sorted);
Location prev = location;
for (Occurrence o : sorted) {
if (o.location != null) {
prev.setSecondary(o.location);
prev = o.location;
}
}
context.report(CROSS_LAYOUT, location, occurrence.message);
}
}
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
// Record include graph such that we can look for inter-layout duplicates after the
// project has been fully checked
String layout = element.getAttribute(ATTR_LAYOUT); // NOTE: Not in android: namespace
if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts
layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
if (context.getPhase() == 1) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
List<String> to = mIncludes.get(context.file);
if (to == null) {
to = new ArrayList<String>();
mIncludes.put(context.file, to);
}
to.add(layout);
} else {
assert context.getPhase() == 2;
Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file);
if (maps != null && !maps.isEmpty()) {
for (Multimap<String, Occurrence> map : maps) {
if (!maps.isEmpty()) {
Collection<Occurrence> occurrences = map.get(layout);
if (occurrences != null && !occurrences.isEmpty()) {
for (Occurrence occurrence : occurrences) {
Location location = context.getLocation(element);
location.setClientData(element);
location.setMessage(occurrence.message);
location.setSecondary(occurrence.location);
occurrence.location = location;
}
}
}
}
}
}
}
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
assert attribute.getName().equals(ATTR_ID) || ATTR_ID.equals(attribute.getLocalName());
String id = attribute.getValue();
if (context.getPhase() == 1) {
if (mIds.contains(id)) {
Location location = context.getLocation(attribute);
Attr first = findIdAttribute(attribute.getOwnerDocument(), id);
if (first != null && first != attribute) {
Location secondLocation = context.getLocation(first);
secondLocation.setMessage(String.format("`%1$s` originally defined here", id));
location.setSecondary(secondLocation);
}
context.report(WITHIN_LAYOUT, attribute, location,
String.format("Duplicate id `%1$s`, already defined earlier in this layout",
id));
} else if (id.startsWith(NEW_ID_PREFIX)) {
// Skip id's on include tags
if (attribute.getOwnerElement().getTagName().equals(VIEW_INCLUDE)) {
return;
}
mIds.add(id);
}
} else {
Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file);
if (maps != null && !maps.isEmpty()) {
for (Multimap<String, Occurrence> map : maps) {
if (!maps.isEmpty()) {
Collection<Occurrence> occurrences = map.get(id);
if (occurrences != null && !occurrences.isEmpty()) {
for (Occurrence occurrence : occurrences) {
if (context.getDriver().isSuppressed(context, CROSS_LAYOUT,
attribute)) {
return;
}
Location location = context.getLocation(attribute);
location.setClientData(attribute);
location.setMessage(occurrence.message);
location.setSecondary(occurrence.location);
occurrence.location = location;
}
}
}
}
}
}
}
/** Find the first id attribute with the given value below the given node */
private static Attr findIdAttribute(Node node, String targetValue) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
Attr attribute = ((Element) node).getAttributeNodeNS(ANDROID_URI, ATTR_ID);
if (attribute != null && attribute.getValue().equals(targetValue)) {
return attribute;
}
}
NodeList children = node.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
Attr result = findIdAttribute(child, targetValue);
if (result != null) {
return result;
}
}
return null;
}
/** Include Graph Node */
private static class Layout {
private final File mFile;
private final Set<String> mIds;
private List<Layout> mIncludes;
private List<Layout> mIncludedBy;
Layout(File file, Set<String> ids) {
mFile = file;
mIds = ids;
}
Set<String> getIds() {
return mIds;
}
String getLayoutName() {
return LintUtils.getLayoutName(mFile);
}
String getDisplayName() {
return mFile.getParentFile().getName() + File.separator + mFile.getName();
}
void include(Layout target) {
if (mIncludes == null) {
mIncludes = new ArrayList<Layout>();
}
mIncludes.add(target);
if (target.mIncludedBy == null) {
target.mIncludedBy = new ArrayList<Layout>();
}
target.mIncludedBy.add(this);
}
boolean isIncluded() {
return mIncludedBy != null && !mIncludedBy.isEmpty();
}
File getFile() {
return mFile;
}
List<Layout> getIncludes() {
return mIncludes;
}
@Override
public String toString() {
return getDisplayName();
}
}
private class IncludeGraph {
private final Context mContext;
private final Map<File, Layout> mFileToLayout;
public IncludeGraph(Context context) {
mContext = context;
// Produce a DAG of the files to be included, and compute edges to all eligible
// includes.
// Then visit the DAG and whenever you find a duplicate emit a warning about the
// include path which reached it.
mFileToLayout = new HashMap<File, Layout>(2 * mIncludes.size());
for (File file : mIncludes.keySet()) {
if (!mFileToLayout.containsKey(file)) {
mFileToLayout.put(file, new Layout(file, mFileToIds.get(file)));
}
}
for (File file : mFileToIds.keySet()) {
Set<String> ids = mFileToIds.get(file);
if (ids != null && !ids.isEmpty()) {
if (!mFileToLayout.containsKey(file)) {
mFileToLayout.put(file, new Layout(file, ids));
}
}
}
Multimap<String, Layout> nameToLayout =
ArrayListMultimap.create(mFileToLayout.size(), 4);
for (File file : mFileToLayout.keySet()) {
String name = LintUtils.getLayoutName(file);
nameToLayout.put(name, mFileToLayout.get(file));
}
// Build up the DAG
for (File file : mIncludes.keySet()) {
Layout from = mFileToLayout.get(file);
assert from != null : file;
List<String> includedLayouts = mIncludes.get(file);
for (String name : includedLayouts) {
Collection<Layout> layouts = nameToLayout.get(name);
if (layouts != null && !layouts.isEmpty()) {
if (layouts.size() == 1) {
from.include(layouts.iterator().next());
} else {
// See if we have an obvious match
File folder = from.getFile().getParentFile();
File candidate = new File(folder, name + DOT_XML);
Layout candidateLayout = mFileToLayout.get(candidate);
if (candidateLayout != null) {
from.include(candidateLayout);
} else if (mFileToIds.containsKey(candidate)) {
// We had an entry in mFileToIds, but not a layout: this
// means that the file exists, but had no includes or ids.
// This can't be a valid match: there is a layout that we know
// the include will pick, but it has no includes (to other layouts)
// and no ids, so no need to look at it
continue;
} else {
for (Layout to : layouts) {
// Decide if the two targets are compatible
if (isCompatible(from, to)) {
from.include(to);
}
}
}
}
} else {
// The layout is including some layout which has no ids or other includes
// so it's not relevant for a duplicate id search
continue;
}
}
}
}
/** Determine whether two layouts are compatible. They are not if they (for example)
* specify conflicting qualifiers such as {@code -land} and {@code -port}.
* @param from the include from
* @param to the include to
* @return true if the two are compatible */
boolean isCompatible(Layout from, Layout to) {
File fromFolder = from.mFile.getParentFile();
File toFolder = to.mFile.getParentFile();
if (fromFolder.equals(toFolder)) {
return true;
}
Iterable<String> fromQualifiers = QUALIFIER_SPLITTER.split(fromFolder.getName());
Iterable<String> toQualifiers = QUALIFIER_SPLITTER.split(toFolder.getName());
return isPortrait(fromQualifiers) == isPortrait(toQualifiers);
}
private boolean isPortrait(Iterable<String> qualifiers) {
for (String qualifier : qualifiers) {
if (qualifier.equals("port")) { //$NON-NLS-1$
return true;
} else if (qualifier.equals("land")) { //$NON-NLS-1$
return false;
}
}
return true; // it's the default
}
public void check() {
// Visit the DAG, looking for conflicts
for (Layout layout : mFileToLayout.values()) {
if (!layout.isIncluded()) { // Only check from "root" nodes
Deque<Layout> stack = new ArrayDeque<Layout>();
getIds(layout, stack, new HashSet<Layout>());
}
}
}
/**
* Computes the cumulative set of ids used in a given layout. We can't
* just depth-first-search the graph and check the set of ids
* encountered along the way, because we need to detect when multiple
* includes contribute the same ids. For example, if a file is included
* more than once, that would result in duplicates.
*/
private Set<String> getIds(Layout layout, Deque<Layout> stack, Set<Layout> seen) {
seen.add(layout);
Set<String> layoutIds = layout.getIds();
List<Layout> includes = layout.getIncludes();
if (includes != null) {
Set<String> ids = new HashSet<String>();
if (layoutIds != null) {
ids.addAll(layoutIds);
}
stack.push(layout);
Multimap<String, Set<String>> nameToIds =
ArrayListMultimap.create(includes.size(), 4);
for (Layout included : includes) {
if (seen.contains(included)) {
continue;
}
Set<String> includedIds = getIds(included, stack, seen);
if (includedIds != null) {
String layoutName = included.getLayoutName();
idCheck:
for (String id : includedIds) {
if (ids.contains(id)) {
Collection<Set<String>> idSets = nameToIds.get(layoutName);
if (idSets != null) {
for (Set<String> siblingIds : idSets) {
if (siblingIds.contains(id)) {
// The id reference was added by a sibling,
// so no need to complain (again)
continue idCheck;
}
}
}
// Duplicate! Record location request for new phase.
if (mLocations == null) {
mErrors = new ArrayList<Occurrence>();
mLocations = ArrayListMultimap.create();
mContext.getDriver().requestRepeat(DuplicateIdDetector.this,
Scope.ALL_RESOURCES_SCOPE);
}
Map<Layout, Occurrence> occurrences =
new HashMap<Layout, Occurrence>();
findId(layout, id, new ArrayDeque<Layout>(), occurrences,
new HashSet<Layout>());
assert occurrences.size() >= 2;
// Stash a request to find the given include
Collection<Occurrence> values = occurrences.values();
List<Occurrence> sorted = new ArrayList<Occurrence>(values);
Collections.sort(sorted);
String msg = String.format(
"Duplicate id %1$s, defined or included multiple " +
"times in %2$s: %3$s",
id, layout.getDisplayName(),
sorted.toString());
// Store location request for the <include> tag
Occurrence primary = new Occurrence(layout.getFile(), msg, null);
Multimap<String, Occurrence> m = ArrayListMultimap.create();
m.put(layoutName, primary);
mLocations.put(layout.getFile(), m);
mErrors.add(primary);
Occurrence prev = primary;
// Now store all the included occurrences of the id
for (Occurrence occurrence : values) {
if (occurrence.file.equals(layout.getFile())) {
occurrence.message = "Defined here";
} else {
occurrence.message = String.format(
"Defined here, included via %1$s",
occurrence.includePath);
}
m = ArrayListMultimap.create();
m.put(id, occurrence);
mLocations.put(occurrence.file, m);
// Link locations together
prev.next = occurrence;
prev = occurrence;
}
}
ids.add(id);
}
// Store these ids such that on a conflict, we can tell when
// an id was added by a single variation of this file
nameToIds.put(layoutName, includedIds);
}
}
Layout visited = stack.pop();
assert visited == layout;
return ids;
} else {
return layoutIds;
}
}
private void findId(Layout layout, String id, Deque<Layout> stack,
Map<Layout, Occurrence> occurrences, Set<Layout> seen) {
seen.add(layout);
Set<String> layoutIds = layout.getIds();
if (layoutIds != null && layoutIds.contains(id)) {
StringBuilder path = new StringBuilder(80);
if (!stack.isEmpty()) {
Iterator<Layout> iterator = stack.descendingIterator();
while (iterator.hasNext()) {
path.append(iterator.next().getDisplayName());
path.append(" => ");
}
}
path.append(layout.getDisplayName());
path.append(" defines ");
path.append(id);
assert occurrences.get(layout) == null : id + ',' + layout;
occurrences.put(layout, new Occurrence(layout.getFile(), null, path.toString()));
}
List<Layout> includes = layout.getIncludes();
if (includes != null) {
stack.push(layout);
for (Layout included : includes) {
if (!seen.contains(included)) {
findId(included, id, stack, occurrences, seen);
}
}
Layout visited = stack.pop();
assert visited == layout;
}
}
}
private static class Occurrence implements Comparable<Occurrence> {
public final File file;
public final String includePath;
public Occurrence next;
public Location location;
public String message;
public Occurrence(File file, String message, String includePath) {
this.file = file;
this.message = message;
this.includePath = includePath;
}
@Override
public String toString() {
return includePath != null ? includePath : message;
}
@Override
public int compareTo(@NonNull Occurrence other) {
// First sort by length, then sort by name
int delta = toString().length() - other.toString().length();
if (delta != 0) {
return delta;
}
return toString().compareTo(other.toString());
}
}
}
@@ -1,295 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.utils.SdkUtils.getResourceFieldName;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Location.Handle;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This detector identifies cases where a resource is defined multiple times in the
* same resource folder
*/
public class DuplicateResourceDetector extends ResourceXmlDetector {
/** The main issue discovered by this detector */
@SuppressWarnings("unchecked")
public static final Issue ISSUE = Issue.create(
"DuplicateDefinition", //$NON-NLS-1$
"Duplicate definitions of resources",
"You can define a resource multiple times in different resource folders; that's how " +
"string translations are done, for example. However, defining the same resource " +
"more than once in the same resource folder is likely an error, for example " +
"attempting to add a new resource without realizing that the name is already used, " +
"and so on.",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(
DuplicateResourceDetector.class,
// We should be able to do this incrementally!
Scope.ALL_RESOURCES_SCOPE,
Scope.RESOURCE_FILE_SCOPE));
/** Wrong resource value type */
public static final Issue TYPE_MISMATCH = Issue.create(
"ReferenceType", //$NON-NLS-1$
"Incorrect reference types",
"When you generate a resource alias, the resource you are pointing to must be " +
"of the same type as the alias",
Category.CORRECTNESS,
8,
Severity.FATAL,
new Implementation(
DuplicateResourceDetector.class,
Scope.RESOURCE_FILE_SCOPE));
private static final String PRODUCT = "product"; //$NON-NLS-1$
private Map<ResourceType, Set<String>> mTypeMap;
private Map<ResourceType, List<Pair<String, Location.Handle>>> mLocations;
private File mParent;
/** Constructs a new {@link DuplicateResourceDetector} */
public DuplicateResourceDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
@Nullable
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_NAME);
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
@Override
public void beforeCheckFile(@NonNull Context context) {
File parent = context.file.getParentFile();
if (!parent.equals(mParent)) {
mParent = parent;
mTypeMap = Maps.newEnumMap(ResourceType.class);
mLocations = Maps.newEnumMap(ResourceType.class);
}
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
Element element = attribute.getOwnerElement();
if (element.hasAttribute(PRODUCT)) {
return;
}
String tag = element.getTagName();
String typeString = tag;
if (tag.equals(TAG_ITEM)) {
typeString = element.getAttribute(ATTR_TYPE);
if (typeString == null || typeString.isEmpty()) {
if (element.getParentNode().getNodeName().equals(
ResourceType.STYLE.getName()) && isFirstElementChild(element)) {
checkUniqueNames(context, (Element) element.getParentNode());
}
return;
}
}
ResourceType type = ResourceType.getEnum(typeString);
if (type == null) {
return;
}
if (type == ResourceType.ATTR
&& element.getParentNode().getNodeName().equals(
ResourceType.DECLARE_STYLEABLE.getName())) {
if (isFirstElementChild(element)) {
checkUniqueNames(context, (Element) element.getParentNode());
}
return;
}
NodeList children = element.getChildNodes();
int childCount = children.getLength();
for (int i = 0; i < childCount; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
for (int j = 0, length = text.length(); j < length; j++) {
char c = text.charAt(j);
if (c == '@') {
if (!text.regionMatches(false, j + 1, typeString, 0,
typeString.length()) && context.isEnabled(TYPE_MISMATCH)) {
ResourceUrl url = ResourceUrl.parse(text.trim());
if (url != null && url.type != type &&
// colors and mipmaps can apparently be used as drawables
!(type == ResourceType.DRAWABLE
&& (url.type == ResourceType.COLOR
|| url.type == ResourceType.MIPMAP))) {
String message = "Unexpected resource reference type; "
+ "expected value of type `@" + type + "/`";
context.report(TYPE_MISMATCH, element,
context.getLocation(child),
message);
}
}
break;
} else if (!Character.isWhitespace(c)) {
break;
}
}
break;
}
}
Set<String> names = mTypeMap.get(type);
if (names == null) {
names = Sets.newHashSetWithExpectedSize(40);
mTypeMap.put(type, names);
}
String name = attribute.getValue();
String originalName = name;
// AAPT will flatten the namespace, turning dots, dashes and colons into _
name = getResourceFieldName(name);
if (names.contains(name)) {
String message = String.format("`%1$s` has already been defined in this folder", name);
if (!name.equals(originalName)) {
message += " (`" + name + "` is equivalent to `" + originalName + "`)";
}
Location location = context.getLocation(attribute);
List<Pair<String, Handle>> list = mLocations.get(type);
for (Pair<String, Handle> pair : list) {
if (name.equals(pair.getFirst())) {
Location secondary = pair.getSecond().resolve();
secondary.setMessage("Previously defined here");
location.setSecondary(secondary);
}
}
context.report(ISSUE, attribute, location, message);
} else {
names.add(name);
List<Pair<String, Handle>> list = mLocations.get(type);
if (list == null) {
list = Lists.newArrayList();
mLocations.put(type, list);
}
Location.Handle handle = context.createLocationHandle(attribute);
list.add(Pair.of(name, handle));
}
}
private static void checkUniqueNames(XmlContext context, Element parent) {
List<Element> items = LintUtils.getChildren(parent);
if (items.size() > 1) {
Set<String> names = Sets.newHashSet();
for (Element item : items) {
Attr nameNode = item.getAttributeNode(ATTR_NAME);
if (nameNode != null) {
String name = nameNode.getValue();
if (names.contains(name) && context.isEnabled(ISSUE)) {
Location location = context.getLocation(nameNode);
for (Element prevItem : items) {
Attr attribute = item.getAttributeNode(ATTR_NAME);
if (attribute != null && name.equals(attribute.getValue())) {
assert prevItem != item;
Location prev = context.getLocation(prevItem);
prev.setMessage("Previously defined here");
location.setSecondary(prev);
break;
}
}
String message = String.format(
"`%1$s` has already been defined in this `<%2$s>`",
name, parent.getTagName());
context.report(ISSUE, nameNode, location, message);
}
names.add(name);
}
}
}
}
private static boolean isFirstElementChild(Node node) {
node = node.getPreviousSibling();
while (node != null) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
return false;
}
node = node.getPreviousSibling();
}
return true;
}
/**
* Returns the resource type expected for a {@link #TYPE_MISMATCH} error reported by
* this lint detector. Intended for IDE quickfix implementations.
*
* @param message the error message created by this lint detector
* @param format the format of the error message
*/
public static String getExpectedType(@NonNull String message, @NonNull TextFormat format) {
return LintUtils.findSubstring(format.toText(message), "value of type @", "/");
}
}
@@ -1,147 +0,0 @@
/*
* 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.DefaultPosition;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Position;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Check which looks for invalid resources. Aapt already performs some validation,
* such as making sure that resource references point to resources that exist, but this
* detector looks for additional issues.
*/
public class ExtraTextDetector extends ResourceXmlDetector {
private boolean mFoundText;
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"ExtraText", //$NON-NLS-1$
"Extraneous text in resource files",
"Layout resource files should only contain elements and attributes. Any XML " +
"text content found in the file is likely accidental (and potentially " +
"dangerous if the text resembles XML and the developer believes the text " +
"to be functional)",
Category.CORRECTNESS,
3,
Severity.WARNING,
new Implementation(
ExtraTextDetector.class,
Scope.RESOURCE_FILE_SCOPE)
);
/** Constructs a new detector */
public ExtraTextDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT
|| folderType == ResourceFolderType.MENU
|| folderType == ResourceFolderType.ANIM
|| folderType == ResourceFolderType.ANIMATOR
|| folderType == ResourceFolderType.DRAWABLE
|| folderType == ResourceFolderType.COLOR;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
mFoundText = false;
visitNode(context, document);
}
private void visitNode(XmlContext context, Node node) {
short nodeType = node.getNodeType();
if (nodeType == Node.TEXT_NODE && !mFoundText) {
String text = node.getNodeValue();
for (int i = 0, n = text.length(); i < n; i++) {
char c = text.charAt(i);
if (!Character.isWhitespace(c)) {
String snippet = text.trim();
int maxLength = 100;
if (snippet.length() > maxLength) {
snippet = snippet.substring(0, maxLength) + "...";
}
Location location = context.getLocation(node);
if (i > 0) {
// Adjust the error position to point to the beginning of
// the text rather than the beginning of the text node
// (which is often the newline at the end of the previous
// line and the indentation)
Position start = location.getStart();
if (start != null) {
int line = start.getLine();
int column = start.getColumn();
int offset = start.getOffset();
for (int j = 0; j < i; j++) {
offset++;
if (text.charAt(j) == '\n') {
if (line != -1) {
line++;
}
if (column != -1) {
column = 0;
}
} else if (column != -1) {
column++;
}
}
start = new DefaultPosition(line, column, offset);
location = Location.create(context.file, start, location.getEnd());
}
}
context.report(ISSUE, node, location,
String.format("Unexpected text found in layout file: \"%1$s\"",
snippet));
mFoundText = true;
break;
}
}
}
// Visit children
NodeList childNodes = node.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
visitNode(context, child);
}
}
}
@@ -1,270 +0,0 @@
/*
* 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.google.common.collect.Maps;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Looks for getter calls within the same class that could be replaced by
* direct field references instead.
*/
public class FieldGetterDetector extends Detector implements Detector.ClassScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"FieldGetter", //$NON-NLS-1$
"Using getter instead of field",
"Accessing a field within the class that defines a getter for that field is " +
"at least 3 times faster than calling the getter. For simple getters that do " +
"nothing other than return the field, you might want to just reference the " +
"local field directly instead.\n" +
"\n" +
"*NOTE*: As of Android 2.3 (Gingerbread), this optimization is performed " +
"automatically by Dalvik, so there is no need to change your code; this is " +
"only relevant if you are targeting older versions of Android.",
Category.PERFORMANCE,
4,
Severity.WARNING,
new Implementation(
FieldGetterDetector.class,
Scope.CLASS_FILE_SCOPE)).
// This is a micro-optimization: not enabled by default
setEnabledByDefault(false).
addMoreInfo(
"http://developer.android.com/guide/practices/design/performance.html#internal_get_set"); //$NON-NLS-1$
private ArrayList<Entry> mPendingCalls;
/** Constructs a new {@link FieldGetterDetector} check */
public FieldGetterDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements ClassScanner ----
@Override
public int[] getApplicableAsmNodeTypes() {
return new int[] { AbstractInsnNode.METHOD_INSN };
}
@Override
public void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull AbstractInsnNode instruction) {
// As of Gingerbread/API 9, Dalvik performs this optimization automatically
if (context.getProject().getMinSdk() >= 9) {
return;
}
if ((method.access & Opcodes.ACC_STATIC) != 0) {
// Not an instance method
return;
}
if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) {
return;
}
MethodInsnNode node = (MethodInsnNode) instruction;
String name = node.name;
String owner = node.owner;
AbstractInsnNode prev = LintUtils.getPrevInstruction(instruction);
if (prev == null || prev.getOpcode() != Opcodes.ALOAD) {
return;
}
VarInsnNode prevVar = (VarInsnNode) prev;
if (prevVar.var != 0) { // Not on "this", variable 0 in instance methods?
return;
}
if (((name.startsWith("get") && name.length() > 3 //$NON-NLS-1$
&& Character.isUpperCase(name.charAt(3)))
|| (name.startsWith("is") && name.length() > 2 //$NON-NLS-1$
&& Character.isUpperCase(name.charAt(2))))
&& owner.equals(classNode.name)) {
// Calling a potential getter method on self. We now need to
// investigate the method body of the getter call and make sure
// it's really a plain getter, not just a method which happens
// to have a method name like a getter, or a method which not
// only returns a field but possibly computes it or performs
// other initialization or side effects. This is done in a
// second pass over the bytecode, initiated by the finish()
// method.
if (mPendingCalls == null) {
mPendingCalls = new ArrayList<Entry>();
}
mPendingCalls.add(new Entry(name, node, method));
}
super.checkInstruction(context, classNode, method, instruction);
}
@Override
public void afterCheckFile(@NonNull Context c) {
ClassContext context = (ClassContext) c;
if (mPendingCalls != null) {
Set<String> names = new HashSet<String>(mPendingCalls.size());
for (Entry entry : mPendingCalls) {
names.add(entry.name);
}
Map<String, String> getters = checkMethods(context.getClassNode(), names);
if (!getters.isEmpty()) {
for (String getter : getters.keySet()) {
for (Entry entry : mPendingCalls) {
String name = entry.name;
// There can be more than one reference to the same name:
// one for each call site
if (name.equals(getter)) {
Location location = context.getLocation(entry.call);
String fieldName = getters.get(getter);
if (fieldName == null) {
fieldName = "";
}
context.report(ISSUE, entry.method, entry.call, location,
String.format(
"Calling getter method `%1$s()` on self is " +
"slower than field access (`%2$s`)", getter, fieldName));
}
}
}
}
}
mPendingCalls = null;
}
// Holder class for getters to be checked
private static class Entry {
public final String name;
public final MethodNode method;
public final MethodInsnNode call;
public Entry(String name, MethodInsnNode call, MethodNode method) {
super();
this.name = name;
this.call = call;
this.method = method;
}
}
// Validate that these getter methods are really just simple field getters
// like these int and String getters:
// public int getFoo();
// Code:
// 0: aload_0
// 1: getfield #21; //Field mFoo:I
// 4: ireturn
//
// public java.lang.String getBar();
// Code:
// 0: aload_0
// 1: getfield #25; //Field mBar:Ljava/lang/String;
// 4: areturn
//
// Returns a map of valid getters as keys, and if the field name is found, the field name
// for each getter as its value.
private static Map<String, String> checkMethods(ClassNode classNode, Set<String> names) {
Map<String, String> validGetters = Maps.newHashMap();
@SuppressWarnings("rawtypes")
List methods = classNode.methods;
String fieldName = null;
checkMethod:
for (Object methodObject : methods) {
MethodNode method = (MethodNode) methodObject;
if (names.contains(method.name)
&& method.desc.startsWith("()")) { //$NON-NLS-1$ // (): No arguments
InsnList instructions = method.instructions;
int mState = 1;
for (AbstractInsnNode curr = instructions.getFirst();
curr != null;
curr = curr.getNext()) {
switch (curr.getOpcode()) {
case -1:
// Skip label and line number nodes
continue;
case Opcodes.ALOAD:
if (mState == 1) {
fieldName = null;
mState = 2;
} else {
continue checkMethod;
}
break;
case Opcodes.GETFIELD:
if (mState == 2) {
FieldInsnNode field = (FieldInsnNode) curr;
fieldName = field.name;
mState = 3;
} else {
continue checkMethod;
}
break;
case Opcodes.ARETURN:
case Opcodes.FRETURN:
case Opcodes.IRETURN:
case Opcodes.DRETURN:
case Opcodes.LRETURN:
case Opcodes.RETURN:
if (mState == 3) {
validGetters.put(method.name, fieldName);
}
continue checkMethod;
default:
continue checkMethod;
}
}
}
}
return validGetters;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,246 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_NS_NAME;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_COLUMN_COUNT;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
import static com.android.SdkConstants.ATTR_ORIENTATION;
import static com.android.SdkConstants.ATTR_ROW_COUNT;
import static com.android.SdkConstants.ATTR_USE_DEFAULT_MARGINS;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.FQCN_GRID_LAYOUT_V7;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.XMLNS_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.util.Arrays;
import java.util.Collection;
/**
* Check which looks for potential errors in declarations of GridLayouts, such as specifying
* row/column numbers outside the declared dimensions of the grid.
*/
public class GridLayoutDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"GridLayout", //$NON-NLS-1$
"GridLayout validation",
"Declaring a layout_row or layout_column that falls outside the declared size " +
"of a GridLayout's `rowCount` or `columnCount` is usually an unintentional error.",
Category.CORRECTNESS,
4,
Severity.FATAL,
new Implementation(
GridLayoutDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link GridLayoutDetector} check */
public GridLayoutDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
GRID_LAYOUT,
FQCN_GRID_LAYOUT_V7
);
}
private static int getInt(Element element, String attribute, int defaultValue) {
String valueString = element.getAttributeNS(ANDROID_URI, attribute);
if (valueString != null && !valueString.isEmpty()) {
try {
return Integer.decode(valueString);
} catch (NumberFormatException nufe) {
// Ignore - error in user's XML
}
}
return defaultValue;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int declaredRowCount = getInt(element, ATTR_ROW_COUNT, -1);
int declaredColumnCount = getInt(element, ATTR_COLUMN_COUNT, -1);
if (declaredColumnCount != -1 || declaredRowCount != -1) {
for (Element child : LintUtils.getChildren(element)) {
if (declaredColumnCount != -1) {
int column = getInt(child, ATTR_LAYOUT_COLUMN, -1);
if (column >= declaredColumnCount) {
Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_COLUMN);
context.report(ISSUE, node, context.getLocation(node),
String.format("Column attribute (%1$d) exceeds declared grid column count (%2$d)",
column, declaredColumnCount));
}
}
if (declaredRowCount != -1) {
int row = getInt(child, ATTR_LAYOUT_ROW, -1);
if (row > declaredRowCount) {
Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_ROW);
context.report(ISSUE, node, context.getLocation(node),
String.format("Row attribute (%1$d) exceeds declared grid row count (%2$d)",
row, declaredRowCount));
}
}
}
}
if (element.getTagName().equals(FQCN_GRID_LAYOUT_V7)) {
// Make sure that we're not using android: namespace attributes where we should
// be using app namespace attributes!
ensureAppNamespace(context, element, ATTR_COLUMN_COUNT);
ensureAppNamespace(context, element, ATTR_ORIENTATION);
ensureAppNamespace(context, element, ATTR_ROW_COUNT);
ensureAppNamespace(context, element, ATTR_USE_DEFAULT_MARGINS);
ensureAppNamespace(context, element, "alignmentMode");
ensureAppNamespace(context, element, "columnOrderPreserved");
ensureAppNamespace(context, element, "rowOrderPreserved");
for (Element child : LintUtils.getChildren(element)) {
ensureAppNamespace(context, child, ATTR_LAYOUT_COLUMN);
ensureAppNamespace(context, child, ATTR_LAYOUT_COLUMN_SPAN);
ensureAppNamespace(context, child, ATTR_LAYOUT_GRAVITY);
ensureAppNamespace(context, child, ATTR_LAYOUT_ROW);
ensureAppNamespace(context, child, ATTR_LAYOUT_ROW_SPAN);
ensureAppNamespace(context, child, "layout_rowWeight");
ensureAppNamespace(context, child, "layout_columnWeight");
}
}
}
private static void ensureAppNamespace(XmlContext context, Element element, String name) {
Attr attribute = element.getAttributeNodeNS(ANDROID_URI, name);
if (attribute != null) {
String prefix = getNamespacePrefix(element.getOwnerDocument(), AUTO_URI);
boolean haveNamespace = prefix != null;
if (!haveNamespace) {
prefix = "app";
}
StringBuilder sb = new StringBuilder();
sb.append("Wrong namespace; with v7 `GridLayout` you should use ").append(prefix)
.append(":").append(name);
if (!haveNamespace) {
sb.append(" (and add `xmlns:app=\"").append(AUTO_URI)
.append("\"` to your root element.)");
}
String message = sb.toString();
context.report(ISSUE, attribute, context.getLocation(attribute), message);
}
}
@Nullable
private static String getNamespacePrefix(Document document, String uri) {
Element root = document.getDocumentElement();
if (root == null) {
return null;
}
NamedNodeMap attributes = root.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Node attribute = attributes.item(i);
if (attribute.getNodeName().startsWith(XMLNS_PREFIX) &&
attribute.getNodeValue().equals(uri)) {
return attribute.getNodeName().substring(XMLNS_PREFIX.length());
}
}
return null;
}
/**
* Given an error message produced by this lint detector,
* returns the old value to be replaced in the source code.
* <p>
* Intended for IDE quickfix implementations.
*
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the corresponding old value, or null if not recognized
*/
@Nullable
public static String getOldValue(@NonNull String errorMessage,
@NonNull TextFormat format) {
errorMessage = format.toText(errorMessage);
String attribute = LintUtils.findSubstring(errorMessage, " should use ", " ");
if (attribute == null) {
attribute = LintUtils.findSubstring(errorMessage, " should use ", null);
}
if (attribute != null) {
int index = attribute.indexOf(':');
if (index != -1) {
return ANDROID_NS_NAME + attribute.substring(index);
}
}
return null;
}
/**
* Given an error message produced by this lint detector,
* returns the new value to be put into the source code.
* <p>
* Intended for IDE quickfix implementations.
*
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the corresponding new value, or null if not recognized
*/
@Nullable
public static String getNewValue(@NonNull String errorMessage,
@NonNull TextFormat format) {
errorMessage = format.toText(errorMessage);
String attribute = LintUtils.findSubstring(errorMessage, " should use ", " ");
if (attribute == null) {
attribute = LintUtils.findSubstring(errorMessage, " should use ", null);
}
return attribute;
}
}
@@ -33,6 +33,7 @@ import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastAndroidUtils;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.kinds.UastClassKind;
/**
* Checks that Handler implementations are top level classes or static.
@@ -84,6 +85,10 @@ public class HandlerDetector extends Detector implements UastScanner {
@Override
public void visitClass(UastAndroidContext context, UClass node) {
if (UastUtils.isTopLevel(node)) {
return;
}
if (node.hasModifier(UastModifier.STATIC)) {
return;
}
@@ -1,97 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_DEBUGGABLE;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
/**
* Checks for hardcoded debug mode in manifest files
*/
public class HardcodedDebugModeDetector extends Detector implements Detector.XmlScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"HardcodedDebugMode", //$NON-NLS-1$
"Hardcoded value of `android:debuggable` in the manifest",
"It's best to leave out the `android:debuggable` attribute from the manifest. " +
"If you do, then the tools will automatically insert `android:debuggable=true` when " +
"building an APK to debug on an emulator or device. And when you perform a " +
"release build, such as Exporting APK, it will automatically set it to `false`.\n" +
"\n" +
"If on the other hand you specify a specific value in the manifest file, then " +
"the tools will always use it. This can lead to accidentally publishing " +
"your app with debug information.",
Category.SECURITY,
5,
Severity.FATAL,
new Implementation(
HardcodedDebugModeDetector.class,
Scope.MANIFEST_SCOPE));
/** Constructs a new {@link HardcodedDebugModeDetector} check */
public HardcodedDebugModeDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return file.getName().equals(ANDROID_MANIFEST_XML);
}
// ---- Implements Detector.XmlScanner ----
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singleton(ATTR_DEBUGGABLE);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (attribute.getNamespaceURI().equals(ANDROID_URI)) {
//if (attribute.getOwnerElement().getTagName().equals(TAG_APPLICATION)) {
context.report(ISSUE, attribute, context.getLocation(attribute),
"Avoid hardcoding the debug mode; leaving it out allows debug and " +
"release builds to automatically assign one");
}
}
}
@@ -1,117 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION;
import static com.android.SdkConstants.ATTR_HINT;
import static com.android.SdkConstants.ATTR_LABEL;
import static com.android.SdkConstants.ATTR_PROMPT;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.ATTR_TITLE;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import java.util.Arrays;
import java.util.Collection;
/**
* Check which looks at the children of ScrollViews and ensures that they fill/match
* the parent width instead of setting wrap_content.
*/
public class HardcodedValuesDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"HardcodedText", //$NON-NLS-1$
"Hardcoded text",
"Hardcoding text attributes directly in layout files is bad for several reasons:\n" +
"\n" +
"* When creating configuration variations (for example for landscape or portrait)" +
"you have to repeat the actual text (and keep it up to date when making changes)\n" +
"\n" +
"* The application cannot be translated to other languages by just adding new " +
"translations for existing string resources.\n" +
"\n" +
"In Android Studio and Eclipse there are quickfixes to automatically extract this " +
"hardcoded string into a resource lookup.",
Category.I18N,
5,
Severity.WARNING,
new Implementation(
HardcodedValuesDetector.class,
Scope.RESOURCE_FILE_SCOPE));
// TODO: Add additional issues here, such as hardcoded colors, hardcoded sizes, etc
/** Constructs a new {@link HardcodedValuesDetector} */
public HardcodedValuesDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableAttributes() {
return Arrays.asList(
// Layouts
ATTR_TEXT,
ATTR_CONTENT_DESCRIPTION,
ATTR_HINT,
ATTR_LABEL,
ATTR_PROMPT,
// Menus
ATTR_TITLE
);
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.MENU;
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String value = attribute.getValue();
if (!value.isEmpty() && (value.charAt(0) != '@' && value.charAt(0) != '?')) {
// Make sure this is really one of the android: attributes
if (!ANDROID_URI.equals(attribute.getNamespaceURI())) {
return;
}
context.report(ISSUE, attribute, context.getLocation(attribute),
String.format("[I18N] Hardcoded string \"%1$s\", should use `@string` resource",
value));
}
}
}
@@ -1,148 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.VIEW_INCLUDE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import java.util.Collection;
import java.util.Collections;
/**
* Checks for problems with include tags, such as providing layout parameters
* without specifying both layout_width and layout_height
*/
public class IncludeDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"IncludeLayoutParam", //$NON-NLS-1$
"Ignored layout params on include",
"Layout parameters specified on an `<include>` tag will only be used if you " +
"also override `layout_width` and `layout_height` on the `<include>` tag; " +
"otherwise they will be ignored.",
Category.CORRECTNESS,
5,
Severity.ERROR,
new Implementation(
IncludeDetector.class,
Scope.RESOURCE_FILE_SCOPE)).addMoreInfo(
"http://stackoverflow.com/questions/2631614/does-android-xml-layouts-include-tag-really-work");
@Nullable
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(VIEW_INCLUDE);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
NamedNodeMap attributes = element.getAttributes();
int length = attributes.getLength();
boolean hasWidth = false;
boolean hasHeight = false;
boolean hasOtherLayoutParam = false;
for (int i = 0; i < length; i++) {
Attr attribute = (Attr) attributes.item(i);
String name = attribute.getLocalName();
if (name != null && name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
if (ATTR_LAYOUT_WIDTH.equals(name)) {
hasWidth = true;
} else if (ATTR_LAYOUT_HEIGHT.equals(name)) {
hasHeight = true;
} else if (ANDROID_URI.equals(attribute.getNamespaceURI())) {
hasOtherLayoutParam = true;
}
}
}
boolean flagWidth = !hasOtherLayoutParam && hasWidth && !hasHeight;
boolean flagHeight = !hasOtherLayoutParam && !hasWidth && hasHeight;
if (hasOtherLayoutParam && (!hasWidth || !hasHeight) || flagWidth || flagHeight) {
for (int i = 0; i < length; i++) {
Attr attribute = (Attr) attributes.item(i);
String name = attribute.getLocalName();
if (name != null && name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
&& (!ATTR_LAYOUT_WIDTH.equals(name) || flagWidth)
&& (!ATTR_LAYOUT_HEIGHT.equals(name) || flagHeight)
&& ANDROID_URI.equals(attribute.getNamespaceURI())) {
String condition = !hasWidth && !hasHeight ?
"both `layout_width` and `layout_height` are also specified"
: !hasWidth ? "`layout_width` is also specified"
: "`layout_height` is also specified";
String message = String.format(
"Layout parameter `%1$s` ignored unless %2$s on `<include>` tag",
name, condition);
context.report(ISSUE, element, context.getLocation(attribute),
message);
}
}
}
}
/**
* Returns true if the error message (earlier reported by this lint detector) requests
* for the layout_width to be defined.
* <p>
* Intended for IDE quickfix implementations.
*
* @param errorMessage the error message computed by lint
* @return true if the layout_width needs to be defined
*/
public static boolean requestsWidth(@NonNull String errorMessage) {
int index = errorMessage.indexOf(" unless ");
if (index != -1) {
return errorMessage.contains(ATTR_LAYOUT_WIDTH);
}
return false;
}
/**
* Returns true if the error message (earlier reported by this lint detector) requests
* for the layout_height to be defined.
* <p>
* Intended for IDE quickfix implementations.
*
* @param errorMessage the error message computed by lint
* @return true if the layout_height needs to be defined
*/
public static boolean requestsHeight(@NonNull String errorMessage) {
int index = errorMessage.indexOf(" unless ");
if (index != -1) {
return errorMessage.contains(ATTR_LAYOUT_HEIGHT);
}
return false;
}
}
@@ -1,387 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_ORIENTATION;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RADIO_GROUP;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import static com.android.SdkConstants.VALUE_VERTICAL;
import static com.android.SdkConstants.VIEW;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import static com.android.SdkConstants.VIEW_INCLUDE;
import static com.android.SdkConstants.VIEW_TAG;
import com.android.annotations.NonNull;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.tools.klint.client.api.SdkInfo;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Checks whether a layout_weight is declared inefficiently.
*/
public class InefficientWeightDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
InefficientWeightDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Can a weight be replaced with 0dp instead for better performance? */
public static final Issue INEFFICIENT_WEIGHT = Issue.create(
"InefficientWeight", //$NON-NLS-1$
"Inefficient layout weight",
"When only a single widget in a LinearLayout defines a weight, it is more " +
"efficient to assign a width/height of `0dp` to it since it will absorb all " +
"the remaining space anyway. With a declared width/height of `0dp` it " +
"does not have to measure its own size first.",
Category.PERFORMANCE,
3,
Severity.WARNING,
IMPLEMENTATION);
/** Are weights nested? */
public static final Issue NESTED_WEIGHTS = Issue.create(
"NestedWeights", //$NON-NLS-1$
"Nested layout weights",
"Layout weights require a widget to be measured twice. When a LinearLayout with " +
"non-zero weights is nested inside another LinearLayout with non-zero weights, " +
"then the number of measurements increase exponentially.",
Category.PERFORMANCE,
3,
Severity.WARNING,
IMPLEMENTATION);
/** Should a LinearLayout set android:baselineAligned? */
public static final Issue BASELINE_WEIGHTS = Issue.create(
"DisableBaselineAlignment", //$NON-NLS-1$
"Missing `baselineAligned` attribute",
"When a LinearLayout is used to distribute the space proportionally between " +
"nested layouts, the baseline alignment property should be turned off to " +
"make the layout computation faster.",
Category.PERFORMANCE,
3,
Severity.WARNING,
IMPLEMENTATION);
/** Using 0dp on the wrong dimension */
public static final Issue WRONG_0DP = Issue.create(
"Suspicious0dp", //$NON-NLS-1$
"Suspicious 0dp dimension",
"Using 0dp as the width in a horizontal LinearLayout with weights is a useful " +
"trick to ensure that only the weights (and not the intrinsic sizes) are used " +
"when sizing the children.\n" +
"\n" +
"However, if you use 0dp for the opposite dimension, the view will be invisible. " +
"This can happen if you change the orientation of a layout without also flipping " +
"the 0dp dimension in all the children.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPLEMENTATION);
/** Missing explicit orientation */
public static final Issue ORIENTATION = Issue.create(
"Orientation", //$NON-NLS-1$
"Missing explicit orientation",
"The default orientation of a LinearLayout is horizontal. It's pretty easy to "
+ "believe that the layout is vertical, add multiple children to it, and wonder "
+ "why only the first child is visible (when the subsequent children are "
+ "off screen to the right). This lint rule helps pinpoint this issue by "
+ "warning whenever a LinearLayout is used with an implicit orientation "
+ "and multiple children.\n"
+ "\n"
+ "It also checks for empty LinearLayouts without an `orientation` attribute "
+ "that also defines an `id` attribute. This catches the scenarios where "
+ "children will be added to the `LinearLayout` dynamically. ",
Category.CORRECTNESS,
2,
Severity.ERROR,
IMPLEMENTATION);
/**
* Map from element to whether that element has a non-zero linear layout
* weight or has an ancestor which does
*/
private final Map<Node, Boolean> mInsideWeight = new IdentityHashMap<Node, Boolean>();
/** Constructs a new {@link InefficientWeightDetector} */
public InefficientWeightDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(LINEAR_LAYOUT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
List<Element> children = LintUtils.getChildren(element);
// See if there is exactly one child with a weight
boolean multipleWeights = false;
Element weightChild = null;
boolean checkNesting = context.isEnabled(NESTED_WEIGHTS);
for (Element child : children) {
if (child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) {
if (weightChild != null) {
// More than one child defining a weight!
multipleWeights = true;
} else //noinspection ConstantConditions
if (!multipleWeights) {
weightChild = child;
}
if (checkNesting) {
mInsideWeight.put(child, Boolean.TRUE);
Boolean inside = mInsideWeight.get(element);
if (inside == null) {
mInsideWeight.put(element, Boolean.FALSE);
} else if (inside) {
Attr sizeNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
context.report(NESTED_WEIGHTS, sizeNode,
context.getLocation(sizeNode),
"Nested weights are bad for performance");
// Don't warn again
checkNesting = false;
}
}
}
}
String orientation = element.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
if (children.size() >= 2 && (orientation == null || orientation.isEmpty())
&& context.isEnabled(ORIENTATION)) {
// See if at least one of the children, except the last one, sets layout_width
// to match_parent (or fill_parent), in an implicitly horizontal layout, since
// that might mean the last child won't be visible. This is a source of confusion
// for new Android developers.
boolean maxWidthSet = false;
Iterator<Element> iterator = children.iterator();
while (iterator.hasNext()) {
Element child = iterator.next();
if (!iterator.hasNext()) { // Don't check the last one
break;
}
String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
if (VALUE_MATCH_PARENT.equals(width) || VALUE_FILL_PARENT.equals(width)) {
// Also check that weights are not set here; this affects the computation
// a bit and the child may not fill up the whole linear layout
if (!child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) {
maxWidthSet = true;
break;
}
}
}
if (maxWidthSet && !element.hasAttribute(ATTR_STYLE)) {
String message = "Wrong orientation? No orientation specified, and the default "
+ "is horizontal, yet this layout has multiple children where at "
+ "least one has `layout_width=\"match_parent\"`";
context.report(ORIENTATION, element, context.getLocation(element), message);
}
} else if (children.isEmpty() && (orientation == null || orientation.isEmpty())
&& context.isEnabled(ORIENTATION)
&& element.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
boolean ignore;
if (element.hasAttribute(ATTR_STYLE)) {
if (context.getClient().supportsProjectResources()) {
List<ResourceValue> values = LintUtils.getStyleAttributes(
context.getMainProject(), context.getClient(),
element.getAttribute(ATTR_STYLE), ANDROID_URI, ATTR_ORIENTATION);
ignore = values != null && !values.isEmpty();
} else {
ignore = true;
}
} else {
ignore = false;
}
if (!ignore) {
String message = "No orientation specified, and the default is horizontal. "
+ "This is a common source of bugs when children are added dynamically.";
context.report(ORIENTATION, element, context.getLocation(element), message);
}
}
if (context.isEnabled(BASELINE_WEIGHTS) && weightChild != null
&& !VALUE_VERTICAL.equals(orientation)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED)) {
// See if all the children are layouts
boolean allChildrenAreLayouts = !children.isEmpty();
SdkInfo sdkInfo = context.getClient().getSdkInfo(context.getProject());
for (Element child : children) {
String tagName = child.getTagName();
if (!(sdkInfo.isLayout(tagName)
// RadioGroup is a layout, but one which possibly should be base aligned
&& !tagName.equals(RADIO_GROUP)
// Consider <fragment> tags as layouts for the purposes of this check
|| VIEW_FRAGMENT.equals(tagName)
// Ditto for <include> tags
|| VIEW_INCLUDE.equals(tagName))) {
allChildrenAreLayouts = false;
}
}
if (allChildrenAreLayouts) {
context.report(BASELINE_WEIGHTS,
element,
context.getLocation(element),
"Set `android:baselineAligned=\"false\"` on this element for better performance");
}
}
if (context.isEnabled(INEFFICIENT_WEIGHT)
&& weightChild != null && !multipleWeights) {
String dimension;
if (VALUE_VERTICAL.equals(orientation)) {
dimension = ATTR_LAYOUT_HEIGHT;
} else {
dimension = ATTR_LAYOUT_WIDTH;
}
Attr sizeNode = weightChild.getAttributeNodeNS(ANDROID_URI, dimension);
String size = sizeNode != null ? sizeNode.getValue() : "(undefined)";
if (sizeNode == null && weightChild.hasAttribute(ATTR_STYLE)) {
String style = weightChild.getAttribute(ATTR_STYLE);
List<ResourceValue> sizes = LintUtils.getStyleAttributes(context.getMainProject(),
context.getClient(), style, ANDROID_URI, dimension);
if (sizes != null) {
for (ResourceValue value : sizes) {
String v = value.getValue();
if (v != null) {
size = v;
if (v.startsWith("0")) {
break;
}
}
}
}
}
if (!size.startsWith("0")) { //$NON-NLS-1$
String msg = String.format(
"Use a `%1$s` of `0dp` instead of `%2$s` for better performance",
dimension, size);
context.report(INEFFICIENT_WEIGHT,
weightChild,
context.getLocation(sizeNode != null ? sizeNode : weightChild), msg);
}
}
if (context.isEnabled(WRONG_0DP)) {
checkWrong0Dp(context, element, children);
}
}
private static void checkWrong0Dp(XmlContext context, Element element,
List<Element> children) {
boolean isVertical = false;
String orientation = element.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
if (VALUE_VERTICAL.equals(orientation)) {
isVertical = true;
}
for (Element child : children) {
String tagName = child.getTagName();
if (tagName.equals(VIEW)) {
// Might just used for spacing
return;
}
if (tagName.indexOf('.') != -1 || tagName.equals(VIEW_TAG)) {
// Custom views might perform their own dynamic sizing or ignore the layout
// attributes all together
return;
}
boolean hasWeight = child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
Attr widthNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
Attr heightNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
boolean noWidth = false;
boolean noHeight = false;
if (widthNode != null && widthNode.getValue().startsWith("0")) { //$NON-NLS-1$
noWidth = true;
}
if (heightNode != null && heightNode.getValue().startsWith("0")) { //$NON-NLS-1$
noHeight = true;
} else if (!noWidth) {
return;
}
// If you're specifying 0dp for both the width and height you are probably
// trying to hide it deliberately
if (noWidth && noHeight) {
return;
}
if (noWidth) {
if (!hasWeight) {
context.report(WRONG_0DP, widthNode, context.getLocation(widthNode),
"Suspicious size: this will make the view invisible, should be " +
"used with `layout_weight`");
} else if (isVertical) {
context.report(WRONG_0DP, widthNode, context.getLocation(widthNode),
"Suspicious size: this will make the view invisible, probably " +
"intended for `layout_height`");
}
} else {
if (!hasWeight) {
context.report(WRONG_0DP, widthNode, context.getLocation(heightNode),
"Suspicious size: this will make the view invisible, should be " +
"used with `layout_weight`");
} else if (!isVertical) {
context.report(WRONG_0DP, widthNode, context.getLocation(heightNode),
"Suspicious size: this will make the view invisible, probably " +
"intended for `layout_width`");
}
}
}
}
}
@@ -1,309 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.File;
import java.util.List;
import java.util.Set;
/**
* Looks for usages of Java packages that are not included in Android.
*/
public class InvalidPackageDetector extends Detector implements Detector.ClassScanner {
/** Accessing an invalid package */
public static final Issue ISSUE = Issue.create(
"InvalidPackage", //$NON-NLS-1$
"Package not included in Android",
"This check scans through libraries looking for calls to APIs that are not included " +
"in Android.\n" +
"\n" +
"When you create Android projects, the classpath is set up such that you can only " +
"access classes in the API packages that are included in Android. However, if you " +
"add other projects to your libs/ folder, there is no guarantee that those .jar " +
"files were built with an Android specific classpath, and in particular, they " +
"could be accessing unsupported APIs such as java.applet.\n" +
"\n" +
"This check scans through library jars and looks for references to API packages " +
"that are not included in Android and flags these. This is only an error if your " +
"code calls one of the library classes which wind up referencing the unsupported " +
"package.",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(
InvalidPackageDetector.class,
Scope.JAVA_LIBRARY_SCOPE));
private static final String JAVA_PKG_PREFIX = "java/"; //$NON-NLS-1$
private static final String JAVAX_PKG_PREFIX = "javax/"; //$NON-NLS-1$
private ApiLookup mApiDatabase;
/**
* List of candidates that are potential package violations. These are
* recorded as candidates rather than flagged immediately such that we can
* filter out hits for classes that are also defined as libraries (possibly
* encountered later in the library traversal).
*/
private List<Candidate> mCandidates;
/**
* Set of Java packages defined in the libraries; this means that if the
* user has added libraries in this package namespace (such as the
* null annotations jars) we don't flag these.
*/
private final Set<String> mJavaxLibraryClasses = Sets.newHashSetWithExpectedSize(64);
/** Constructs a new package check */
public InvalidPackageDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW;
}
@Override
public void beforeCheckProject(@NonNull Context context) {
mApiDatabase = ApiLookup.get(context.getClient());
}
// ---- Implements ClassScanner ----
@SuppressWarnings("rawtypes") // ASM API
@Override
public void checkClass(@NonNull final ClassContext context, @NonNull ClassNode classNode) {
if (!context.isFromClassLibrary() || shouldSkip(context.file)) {
return;
}
if (mApiDatabase == null) {
return;
}
if ((classNode.access & Opcodes.ACC_ANNOTATION) != 0
|| classNode.superName.startsWith("javax/annotation/")) {
// Don't flag references from annotations and annotation processors
return;
}
if (classNode.name.startsWith(JAVAX_PKG_PREFIX)) {
mJavaxLibraryClasses.add(classNode.name);
}
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
InsnList nodes = method.instructions;
// Check return type
// The parameter types are already handled as local variables so we can skip
// right to the return type.
// Check types in parameter list
String signature = method.desc;
if (signature != null) {
int args = signature.indexOf(')');
if (args != -1 && signature.charAt(args + 1) == 'L') {
String type = signature.substring(args + 2, signature.length() - 1);
if (isInvalidPackage(type)) {
AbstractInsnNode first = nodes.size() > 0 ? nodes.get(0) : null;
record(context, method, first, type);
}
}
}
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
int type = instruction.getType();
if (type == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode node = (MethodInsnNode) instruction;
String owner = node.owner;
// No need to check methods in this local class; we know they
// won't be an API match
if (node.getOpcode() == Opcodes.INVOKEVIRTUAL
&& owner.equals(classNode.name)) {
owner = classNode.superName;
}
while (owner != null) {
if (isInvalidPackage(owner)) {
record(context, method, instruction, owner);
}
// For virtual dispatch, walk up the inheritance chain checking
// each inherited method
if (owner.startsWith("android/") //$NON-NLS-1$
|| owner.startsWith(JAVA_PKG_PREFIX)
|| owner.startsWith(JAVAX_PKG_PREFIX)) {
owner = null;
} else if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) {
owner = context.getDriver().getSuperClass(owner);
} else if (node.getOpcode() == Opcodes.INVOKESTATIC) {
// Inherit through static classes as well
owner = context.getDriver().getSuperClass(owner);
} else {
owner = null;
}
}
} else if (type == AbstractInsnNode.FIELD_INSN) {
FieldInsnNode node = (FieldInsnNode) instruction;
String owner = node.owner;
if (isInvalidPackage(owner)) {
record(context, method, instruction, owner);
}
} else if (type == AbstractInsnNode.LDC_INSN) {
LdcInsnNode node = (LdcInsnNode) instruction;
if (node.cst instanceof Type) {
Type t = (Type) node.cst;
String className = t.getInternalName();
if (isInvalidPackage(className)) {
record(context, method, instruction, className);
}
}
}
}
}
}
private boolean isInvalidPackage(String owner) {
if (owner.startsWith(JAVA_PKG_PREFIX)) {
return !mApiDatabase.isValidJavaPackage(owner);
}
if (owner.startsWith(JAVAX_PKG_PREFIX)) {
// Annotations-related code is usually fine; these tend to be for build time
// jars, such as dagger
//noinspection SimplifiableIfStatement
if (owner.startsWith("javax/annotation/") || owner.startsWith("javax/lang/model")) {
return false;
}
return !mApiDatabase.isValidJavaPackage(owner);
}
return false;
}
private void record(ClassContext context, MethodNode method,
AbstractInsnNode instruction, String owner) {
if (owner.indexOf('$') != -1) {
// Don't report inner classes too; there will pretty much always be an outer class
// reference as well
return;
}
if (mCandidates == null) {
mCandidates = Lists.newArrayList();
}
mCandidates.add(new Candidate(owner, context.getClassNode().name, context.getJarFile()));
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mCandidates == null) {
return;
}
Set<String> seen = Sets.newHashSet();
for (Candidate candidate : mCandidates) {
String type = candidate.mClass;
if (mJavaxLibraryClasses.contains(type)) {
continue;
}
File jarFile = candidate.mJarFile;
String referencedIn = candidate.mReferencedIn;
Location location = Location.create(jarFile);
String pkg = getPackageName(type);
if (seen.contains(pkg)) {
continue;
}
seen.add(pkg);
if (pkg.equals("javax.inject")) {
String name = jarFile.getName();
//noinspection SpellCheckingInspection
if (name.startsWith("dagger-") || name.startsWith("guice-")) {
// White listed
return;
}
}
String message = String.format(
"Invalid package reference in library; not included in Android: `%1$s`. " +
"Referenced from `%2$s`.", pkg, ClassContext.getFqcn(referencedIn));
context.report(ISSUE, location, message);
}
}
private static String getPackageName(String owner) {
String pkg = owner;
int index = pkg.lastIndexOf('/');
if (index != -1) {
pkg = pkg.substring(0, index);
}
return ClassContext.getFqcn(pkg);
}
private static boolean shouldSkip(File file) {
// No need to do work on this library, which is included in pretty much all new ADT
// projects
return file.getPath().endsWith("android-support-v4.jar");
}
private static class Candidate {
private final String mReferencedIn;
private final File mJarFile;
private final String mClass;
public Candidate(String className, String referencedIn, File jarFile) {
mClass = className;
mReferencedIn = referencedIn;
mJarFile = jarFile;
}
}
}
@@ -1,171 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_HINT;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LABEL_FOR;
import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.tools.klint.detector.api.LintUtils.stripIdPrefix;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.collect.Sets;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* Detector which finds unlabeled text fields
*/
public class LabelForDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"LabelFor", //$NON-NLS-1$
"Missing `labelFor` attribute",
"Text fields should be labelled with a `labelFor` attribute, " +
"provided your `minSdkVersion` is at least 17.\n" +
"\n" +
"If your view is labeled but by a label in a different layout which " +
"includes this one, just suppress this warning from lint.",
Category.A11Y,
2,
Severity.WARNING,
new Implementation(
LabelForDetector.class,
Scope.RESOURCE_FILE_SCOPE));
private Set<String> mLabels;
private List<Element> mTextFields;
/** Constructs a new {@link LabelForDetector} */
public LabelForDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
@Nullable
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_LABEL_FOR);
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
EDIT_TEXT,
AUTO_COMPLETE_TEXT_VIEW,
MULTI_AUTO_COMPLETE_TEXT_VIEW
);
}
@Override
public void afterCheckFile(@NonNull Context context) {
if (mTextFields != null) {
if (mLabels == null) {
mLabels = Collections.emptySet();
}
for (Element element : mTextFields) {
if (element.hasAttributeNS(ANDROID_URI, ATTR_HINT)) {
continue;
}
String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
boolean missing = true;
if (mLabels.contains(id)) {
missing = false;
} else if (id.startsWith(NEW_ID_PREFIX)) {
missing = !mLabels.contains(ID_PREFIX + stripIdPrefix(id));
} else if (id.startsWith(ID_PREFIX)) {
missing = !mLabels.contains(NEW_ID_PREFIX + stripIdPrefix(id));
}
if (missing) {
XmlContext xmlContext = (XmlContext) context;
Location location = xmlContext.getLocation(element);
String message;
if (id == null || id.isEmpty()) {
message = "No label views point to this text field with a " +
"`labelFor` attribute";
} else {
message = String.format("No label views point to this text field with " +
"an `android:labelFor=\"@+id/%1$s\"` attribute", id);
}
xmlContext.report(ISSUE, element, location, message);
}
}
}
mLabels = null;
mTextFields = null;
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (mLabels == null) {
mLabels = Sets.newHashSet();
}
mLabels.add(attribute.getValue());
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
// NOTE: This should NOT be checking *minSdkVersion*, but *targetSdkVersion*
// or even buildTarget instead. However, there's a risk that this will flag
// way too much and make the rule annoying until API 17 support becomes
// more widespread, so for now limit the check to those projects *really*
// working with 17. When API 17 reaches a certain amount of adoption, change
// this to flag all apps supporting 17, including those supporting earlier
// versions as well.
if (context.getMainProject().getMinSdk() < 17) {
return;
}
if (mTextFields == null) {
mTextFields = new ArrayList<Element>();
}
mTextFields.add(element);
}
}
@@ -1,184 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.FORMAT_METHOD;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.SourceInterpreter;
import org.objectweb.asm.tree.analysis.SourceValue;
import java.util.Arrays;
import java.util.List;
/**
* Checks for errors related to locale handling
*/
public class LocaleDetector extends Detector implements ClassScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LocaleDetector.class,
Scope.CLASS_FILE_SCOPE);
/** Calling risky convenience methods */
public static final Issue STRING_LOCALE = Issue.create(
"DefaultLocale", //$NON-NLS-1$
"Implied default locale in case conversion",
"Calling `String#toLowerCase()` or `#toUpperCase()` *without specifying an " +
"explicit locale* is a common source of bugs. The reason for that is that those " +
"methods will use the current locale on the user's device, and even though the " +
"code appears to work correctly when you are developing the app, it will fail " +
"in some locales. For example, in the Turkish locale, the uppercase replacement " +
"for `i` is *not* `I`.\n" +
"\n" +
"If you want the methods to just perform ASCII replacement, for example to convert " +
"an enum name, call `String#toUpperCase(Locale.US)` instead. If you really want to " +
"use the current locale, call `String#toUpperCase(Locale.getDefault())` instead.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/reference/java/util/Locale.html#default_locale"); //$NON-NLS-1$
static final String DATE_FORMAT_OWNER = "java/text/SimpleDateFormat"; //$NON-NLS-1$
private static final String STRING_OWNER = "java/lang/String"; //$NON-NLS-1$
/** Constructs a new {@link LocaleDetector} */
public LocaleDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements ClassScanner ----
@Override
@Nullable
public List<String> getApplicableCallNames() {
return Arrays.asList(
"toLowerCase", //$NON-NLS-1$
"toUpperCase", //$NON-NLS-1$
FORMAT_METHOD
);
}
@Override
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
String owner = call.owner;
if (!owner.equals(STRING_OWNER)) {
return;
}
String desc = call.desc;
String name = call.name;
if (name.equals(FORMAT_METHOD)) {
// Only check the non-locale version of String.format
if (!desc.equals("(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;")) { //$NON-NLS-1$
return;
}
// Find the formatting string
Analyzer analyzer = new Analyzer(new SourceInterpreter() {
@Override
public SourceValue newOperation(AbstractInsnNode insn) {
if (insn.getOpcode() == Opcodes.LDC) {
Object cst = ((LdcInsnNode) insn).cst;
if (cst instanceof String) {
return new StringValue(1, (String) cst);
}
}
return super.newOperation(insn);
}
});
try {
Frame[] frames = analyzer.analyze(classNode.name, method);
InsnList instructions = method.instructions;
Frame frame = frames[instructions.indexOf(call)];
if (frame.getStackSize() == 0) {
return;
}
SourceValue stackValue = (SourceValue) frame.getStack(0);
if (stackValue instanceof StringValue) {
String format = ((StringValue) stackValue).getString();
if (format != null && StringFormatDetector.isLocaleSpecific(format)) {
Location location = context.getLocation(call);
String message =
"Implicitly using the default locale is a common source of bugs: " +
"Use `String.format(Locale, ...)` instead";
context.report(STRING_LOCALE, method, call, location, message);
}
}
} catch (AnalyzerException e) {
context.log(e, null);
}
} else {
if (desc.equals("()Ljava/lang/String;")) { //$NON-NLS-1$
Location location = context.getLocation(call);
String message = String.format(
"Implicitly using the default locale is a common source of bugs: " +
"Use `%1$s(Locale)` instead", name);
context.report(STRING_LOCALE, method, call, location, message);
}
}
}
private static class StringValue extends SourceValue {
private final String mString;
StringValue(int size, String string) {
super(size);
mString = string;
}
String getString() {
return mString;
}
@Override
public int getSize() {
return 1;
}
}
}
@@ -1,437 +0,0 @@
/*
* 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.klint.checks;
import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER;
import static com.android.ide.common.resources.configuration.LocaleQualifier.BCP_47_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.resources.LocaleManager;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.ide.common.resources.configuration.ResourceQualifier;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Checks for errors related to locale handling
*/
public class LocaleFolderDetector extends Detector implements Detector.ResourceFolderScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LocaleFolderDetector.class,
Scope.RESOURCE_FOLDER_SCOPE);
/**
* Using a locale folder that is not consulted
*/
public static final Issue DEPRECATED_CODE = Issue.create(
"LocaleFolder", //$NON-NLS-1$
"Wrong locale name",
"From the `java.util.Locale` documentation:\n" +
"\"Note that Java uses several deprecated two-letter codes. The Hebrew (\"he\") " +
"language code is rewritten as \"iw\", Indonesian (\"id\") as \"in\", and " +
"Yiddish (\"yi\") as \"ji\". This rewriting happens even if you construct your " +
"own Locale object, not just for instances returned by the various lookup methods.\n" +
"\n" +
"Because of this, if you add your localized resources in for example `values-he` " +
"they will not be used, since the system will look for `values-iw` instead.\n" +
"\n" +
"To work around this, place your resources in a `values` folder using the " +
"deprecated language code instead.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/reference/java/util/Locale.html");
/**
* Using a region that might not be a match for the given language
*/
public static final Issue WRONG_REGION = Issue.create(
"WrongRegion", //$NON-NLS-1$
"Suspicious Language/Region Combination",
"Android uses the letter codes ISO 639-1 for languages, and the letter codes " +
"ISO 3166-1 for the region codes. In many cases, the language code and the " +
"country where the language is spoken is the same, but it is also often not " +
"the case. For example, while 'se' refers to Sweden, where Swedish is spoken, " +
"the language code for Swedish is *not* `se` (which refers to the Northern " +
"Sami language), the language code is `sv`. And similarly the region code for " +
"`sv` is El Salvador.\n" +
"\n" +
"This lint check looks for suspicious language and region combinations, to help " +
"catch cases where you've accidentally used the wrong language or region code. " +
"Lint knows about the most common regions where a language is spoken, and if " +
"a folder combination is not one of these, it is flagged as suspicious.\n" +
"\n" +
"Note however that it may not be an error: you can theoretically have speakers " +
"of any language in any region and want to target that with your resources, so " +
"this check is aimed at tracking down likely mistakes, not to enforce a specific " +
"set of region and language combinations.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION);
public static final Issue USE_ALPHA_2 = Issue.create(
"UseAlpha2", //$NON-NLS-1$
"Using 3-letter Codes",
"For compatibility with earlier devices, you should only use 3-letter language " +
"and region codes when there is no corresponding 2 letter code.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo("https://tools.ietf.org/html/bcp47");
public static final Issue INVALID_FOLDER = Issue.create(
"InvalidResourceFolder", //$NON-NLS-1$
"Invalid Resource Folder",
"This lint check looks for a folder name that is not a valid resource folder " +
"name; these will be ignored and not packaged by the Android Gradle build plugin.\n" +
"\n" +
"Note that the order of resources is very important; for example, you can't specify " +
"a language before a network code.\n" +
"\n" +
"Similarly, note that to use 3 letter region codes, you have to use " +
"a special BCP 47 syntax: the prefix b+ followed by the BCP 47 language tag but " +
"with `+` as the individual separators instead of `-`. Therefore, for the BCP 47 " +
"language tag `nl-ABW` you have to use `b+nl+ABW`.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPLEMENTATION)
.addMoreInfo("http://developer.android.com/guide/topics/resources/providing-resources.html")
.addMoreInfo("https://tools.ietf.org/html/bcp47");
private Map<String,File> mBcp47Folders;
/**
* Constructs a new {@link LocaleFolderDetector}
*/
public LocaleFolderDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements ResourceFolderScanner ----
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
@Override
public void checkFolder(@NonNull ResourceContext context, @NonNull String folderName) {
LocaleQualifier locale = LintUtils.getLocale(folderName);
if (locale != null && locale.hasLanguage()) {
final String language = locale.getLanguage();
String replace = null;
if (language.equals("he")) {
replace = "iw";
} else if (language.equals("id")) {
replace = "in";
} else if (language.equals("yi")) {
replace = "ji";
}
// Note: there is also fil=>tl
if (replace != null) {
// TODO: Check for suppress somewhere other than lint.xml?
String message = String.format("The locale folder \"`%1$s`\" should be "
+ "called \"`%2$s`\" instead; see the "
+ "`java.util.Locale` documentation",
language, replace);
context.report(DEPRECATED_CODE, Location.create(context.file), message);
}
if (language.length() == 3) {
String languageAlpha2 = LocaleManager.getLanguageAlpha2(language.toLowerCase(Locale.US));
if (languageAlpha2 != null) {
String message = String.format("For compatibility, should use 2-letter "
+ "language codes when available; use `%1$s` instead of `%2$s`",
languageAlpha2, language);
context.report(USE_ALPHA_2, Location.create(context.file), message);
}
}
String region = locale.getRegion();
if (region != null && locale.hasRegion() && region.length() == 3) {
String regionAlpha2 = LocaleManager.getRegionAlpha2(region.toUpperCase(Locale.UK));
if (regionAlpha2 != null) {
String message = String.format("For compatibility, should use 2-letter "
+ "region codes when available; use `%1$s` instead of `%2$s`",
regionAlpha2 , region);
context.report(USE_ALPHA_2, Location.create(context.file), message);
}
}
if (region != null && region.length() == 2) {
List<String> relevantRegions = LocaleManager.getRelevantRegions(language);
if (!relevantRegions.isEmpty() && !relevantRegions.contains(region)) {
List<String> sortedRegions = sortRegions(language, relevantRegions);
List<String> suggestions = Lists.newArrayList();
for (String code : sortedRegions) {
suggestions.add(code + " (" + LocaleManager.getRegionName(code) + ")");
}
String message = String.format(
"Suspicious language and region combination %1$s (%2$s) "
+ "with %3$s (%4$s): language %5$s is usually "
+ "paired with: %6$s",
language, LocaleManager.getLanguageName(language), region,
LocaleManager.getRegionName(region), language,
Joiner.on(", ").join(suggestions));
context.report(WRONG_REGION, Location.create(context.file), message);
}
}
}
FolderConfiguration config = FolderConfiguration.getConfigForFolder(folderName);
if (ResourceFolderType.getFolderType(folderName) != null && config == null) {
String message = "Invalid resource folder: make sure qualifiers appear in the "
+ "correct order, are spelled correctly, etc.";
String bcpSuggestion = suggestBcp47Correction(folderName);
if (bcpSuggestion != null) {
message = String.format("Invalid resource folder; did you mean `%1$s` ?",
bcpSuggestion);
}
context.report(INVALID_FOLDER, Location.create(context.file), message);
} else if (locale != null && folderName.contains(BCP_47_PREFIX)
&& config != null && config.getLocaleQualifier() != null) {
if (mBcp47Folders == null) {
mBcp47Folders = Maps.newHashMap();
}
if (!mBcp47Folders.containsKey(folderName)) {
mBcp47Folders.put(folderName, context.file);
}
}
}
/**
* Look at the given folder name and see if it looks like an unintentional attempt to use
* 3-letter language codes or region codes, and if so, suggest a replacement.
*
* @param folderName a folder name
* @return a suggestion, or null
*/
@Nullable
@VisibleForTesting
static String suggestBcp47Correction(String folderName) {
String language = null;
String region = null;
Iterator<String> iterator = QUALIFIER_SPLITTER.split(folderName).iterator();
// Skip folder type
if (!iterator.hasNext()) {
return null;
}
iterator.next();
while (iterator.hasNext()) {
String segment = iterator.next();
String original = segment;
int length = segment.length();
if (language != null) {
// Only look for region
segment = segment.toUpperCase(Locale.US);
if (length == 3) {
if (original.charAt(0) == 'r' && Character.isUpperCase(original.charAt(1)) &&
LocaleManager.isValidRegionCode(segment.substring(1))) {
region = segment.substring(1);
break;
} else if (Character.isDigit(original.charAt(0))) {
region = segment;
} else if (LocaleManager.isValidRegionCode(segment)) {
region = segment;
}
} else if (length == 4 && original.charAt(0) == 'r'
&& Character.isUpperCase(original.charAt(1))) {
if (LocaleManager.isValidRegionCode(segment.substring(1))) {
region = segment.substring(1);
break;
}
}
} else {
segment = segment.toLowerCase(Locale.US);
if ("car".equals(segment)) { // "car" is a valid value for UI mode
return null;
}
if (LocaleManager.isValidLanguageCode(segment)) {
language = segment;
}
}
}
if (language != null) {
if (language.length() == 3) {
String better = LocaleManager.getLanguageAlpha2(language);
if (better != null) {
language = better;
}
}
if (region != null) {
if (region.length() == 3 && !Character.isDigit(region.charAt(0))) {
String better = LocaleManager.getRegionAlpha2(region);
if (better != null) {
region = better;
}
}
return BCP_47_PREFIX + language + '+' + region;
}
return BCP_47_PREFIX + language;
}
return null;
}
@Override
public void afterCheckProject(@NonNull Context context) {
// Ensure that if a language has multiple scripts, either minSdkVersion >= 21 or
// at most one folder does not have -v21 among the script options
if (mBcp47Folders != null &&
!context.getMainProject().getMinSdkVersion().isGreaterOrEqualThan(21)) {
Map<String,FolderConfiguration> folderToConfig = Maps.newHashMap();
Map<FolderConfiguration,File> configToFile = Maps.newHashMap();
Multimap<String,FolderConfiguration> languageToConfigs = ArrayListMultimap.create();
for (String folderName : mBcp47Folders.keySet()) {
FolderConfiguration config = FolderConfiguration.getConfigForFolder(folderName);
assert config != null : folderName; // we checked before adding to mBcp47Folders
LocaleQualifier locale = config.getLocaleQualifier();
assert locale != null : folderName;
folderToConfig.put(folderName, config);
configToFile.put(config, mBcp47Folders.get(folderName));
String key = locale.getLanguage();
if (locale.hasRegion()) {
key = key + '_' + locale.getRegion();
}
languageToConfigs.put(key, config);
}
for (String language : languageToConfigs.keySet()) {
Collection<FolderConfiguration> configs = languageToConfigs.get(language);
if (configs.size() <= 1) {
// No conflict
// TODO: Warn if you specify a script and don't provide a fallback?
continue;
}
// Count folders that do not specify -v21 and that don't vary in anything other
// than script
List<FolderConfiguration> candidates =
Lists.newArrayListWithExpectedSize(configs.size());
for (FolderConfiguration config : configs) {
if (config.getVersionQualifier() != null
&& config.getVersionQualifier().getVersion() >= 21) {
continue;
}
// See if sets anything *other* than the locale qualifier
boolean localeOnly = true;
for (int i = 0, n = FolderConfiguration.getQualifierCount(); i < n; i++) {
ResourceQualifier qualifier = config.getQualifier(i);
if (qualifier != null && !(qualifier instanceof LocaleQualifier)) {
localeOnly = false;
break;
}
}
if (!localeOnly) {
continue;
}
candidates.add(config);
}
if (candidates.size() > 1) {
Location location = null;
List<String> folderNames = Lists.newArrayList();
for (int i = candidates.size() - 1; i >= 0; i--) {
FolderConfiguration config = candidates.get(i);
File dir = configToFile.get(config);
assert dir != null : config;
Location secondary = location;
location = Location.create(dir);
location.setSecondary(secondary);
folderNames.add(dir.getName());
}
String message = String.format(
"Multiple locale folders for language `%1$s` map to a single folder in versions < API 21: %2$s",
language, Joiner.on(", ").join(folderNames));
context.report(INVALID_FOLDER, location, message);
}
}
}
}
/**
* Sort the "usually combined with" regions such that the preferred region
* for the language is first, followed by the default primary region (if
* not the same, followed by the same letter codes, followed by alphabetical
* order.
*/
private static List<String> sortRegions(
@NonNull final String language,
@NonNull List<String> regions) {
List<String> sortedRegions = Lists.newArrayList(regions);
final String primary = LocaleManager.getLanguageRegion(language);
final String secondary = LocaleManager.getDefaultLanguageRegion(language);
Collections.sort(sortedRegions, new Comparator<String>() {
@Override
public int compare(@NonNull String r1, @NonNull String r2) {
int rank1 = r1.equals(primary) ? 1
: r1.equals(secondary) ? 2 : r1.equalsIgnoreCase(language) ? 3 : 4;
int rank2 = r2.equals(primary) ? 1
: r2.equals(secondary) ? 2 : r2.equalsIgnoreCase(language) ? 3 : 4;
int delta = rank1 - rank2;
if (delta == 0) {
delta = r1.compareTo(r2);
}
return delta;
}
});
return sortedRegions;
}
}
@@ -1,184 +0,0 @@
/*
* Copyright (C) 2013 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.klint.checks;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
import static com.android.xml.AndroidManifest.NODE_ACTIVITY_ALIAS;
import static com.android.xml.AndroidManifest.NODE_APPLICATION;
import static com.android.xml.AndroidManifest.NODE_CATEGORY;
import static com.android.xml.AndroidManifest.NODE_COMPATIBLE_SCREENS;
import static com.android.xml.AndroidManifest.NODE_DATA;
import static com.android.xml.AndroidManifest.NODE_GRANT_URI_PERMISSION;
import static com.android.xml.AndroidManifest.NODE_INSTRUMENTATION;
import static com.android.xml.AndroidManifest.NODE_INTENT;
import static com.android.xml.AndroidManifest.NODE_MANIFEST;
import static com.android.xml.AndroidManifest.NODE_METADATA;
import static com.android.xml.AndroidManifest.NODE_PATH_PERMISSION;
import static com.android.xml.AndroidManifest.NODE_PERMISSION;
import static com.android.xml.AndroidManifest.NODE_PERMISSION_GROUP;
import static com.android.xml.AndroidManifest.NODE_PERMISSION_TREE;
import static com.android.xml.AndroidManifest.NODE_PROVIDER;
import static com.android.xml.AndroidManifest.NODE_RECEIVER;
import static com.android.xml.AndroidManifest.NODE_SERVICE;
import static com.android.xml.AndroidManifest.NODE_SUPPORTS_GL_TEXTURE;
import static com.android.xml.AndroidManifest.NODE_SUPPORTS_SCREENS;
import static com.android.xml.AndroidManifest.NODE_USES_CONFIGURATION;
import static com.android.xml.AndroidManifest.NODE_USES_FEATURE;
import static com.android.xml.AndroidManifest.NODE_USES_LIBRARY;
import static com.android.xml.AndroidManifest.NODE_USES_PERMISSION;
import static com.android.xml.AndroidManifest.NODE_USES_SDK;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.w3c.dom.Element;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* Checks for typos in manifest files
*/
public class ManifestTypoDetector extends Detector implements Detector.XmlScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"ManifestTypo", //$NON-NLS-1$
"Typos in manifest tags",
"This check looks through the manifest, and if it finds any tags " +
"that look like likely misspellings, they are flagged.",
Category.CORRECTNESS,
5,
Severity.FATAL,
new Implementation(
ManifestTypoDetector.class,
Scope.MANIFEST_SCOPE));
private static final Set<String> sValidTags;
static {
int expectedSize = 30;
sValidTags = Sets.newHashSetWithExpectedSize(expectedSize);
sValidTags.add(NODE_MANIFEST);
sValidTags.add(NODE_APPLICATION);
sValidTags.add(NODE_ACTIVITY);
sValidTags.add(NODE_SERVICE);
sValidTags.add(NODE_PROVIDER);
sValidTags.add(NODE_RECEIVER);
sValidTags.add(NODE_USES_FEATURE);
sValidTags.add(NODE_USES_LIBRARY);
sValidTags.add(NODE_USES_SDK);
sValidTags.add(NODE_INSTRUMENTATION);
sValidTags.add(NODE_USES_PERMISSION);
sValidTags.add(NODE_PERMISSION);
sValidTags.add(NODE_PERMISSION_TREE);
sValidTags.add(NODE_PERMISSION_GROUP);
sValidTags.add(NODE_USES_CONFIGURATION);
sValidTags.add(NODE_ACTIVITY_ALIAS);
sValidTags.add(NODE_INTENT);
sValidTags.add(NODE_METADATA);
sValidTags.add(NODE_ACTION);
sValidTags.add(NODE_CATEGORY);
sValidTags.add(NODE_DATA);
sValidTags.add(NODE_GRANT_URI_PERMISSION);
sValidTags.add(NODE_PATH_PERMISSION);
sValidTags.add(NODE_SUPPORTS_SCREENS);
sValidTags.add(NODE_COMPATIBLE_SCREENS);
sValidTags.add(NODE_SUPPORTS_GL_TEXTURE);
// Private tags
sValidTags.add("eat-comment"); //$NON-NLS-1$
sValidTags.add("original-package"); //$NON-NLS-1$
sValidTags.add("protected-broadcast"); //$NON-NLS-1$
sValidTags.add("adopt-permissions"); //$NON-NLS-1$
assert sValidTags.size() <= expectedSize : sValidTags.size();
}
/** Constructs a new {@link ManifestTypoDetector} check */
public ManifestTypoDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return file.getName().equals(ANDROID_MANIFEST_XML);
}
@Override
public Collection<String> getApplicableElements() {
return XmlScanner.ALL;
}
private static final int MAX_EDIT_DISTANCE = 3;
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
if (!sValidTags.contains(tag)) {
int tagLength = tag.length();
// Try to find the corresponding match
List<String> suggestions = null;
for (String suggestion : sValidTags) {
if (Math.abs(suggestion.length() - tagLength) > MAX_EDIT_DISTANCE) {
continue;
}
if (LintUtils.editDistance(suggestion, tag) <= MAX_EDIT_DISTANCE) {
if (suggestions == null) {
suggestions = Lists.newArrayList();
}
suggestions.add('<' + suggestion + '>');
}
}
if (suggestions != null) {
assert !suggestions.isEmpty();
String suggestionString;
if (suggestions.size() == 1) {
suggestionString = suggestions.get(0);
} else if (suggestions.size() == 2) {
suggestionString = String.format("%1$s or %2$s",
suggestions.get(0), suggestions.get(1));
} else {
suggestionString = LintUtils.formatList(suggestions, -1);
}
String message = String.format("Misspelled tag `<%1$s>`: Did you mean `%2$s` ?",
tag, suggestionString);
context.report(ISSUE, element, context.getLocation(element),
message);
}
}
}
}
@@ -1,100 +0,0 @@
/*
* 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.Arrays;
import java.util.List;
/**
* Looks for usages of {@link java.lang.Math} methods which can be replaced with
* {@code android.util.FloatMath} methods to avoid casting.
*/
public class MathDetector extends Detector implements Detector.ClassScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"FloatMath", //$NON-NLS-1$
"Using `FloatMath` instead of `Math`",
"In older versions of Android, using `android.util.FloatMath` was recommended " +
"for performance reasons when operating on floats. However, on modern hardware " +
"doubles are just as fast as float (though they take more memory), and in " +
"recent versions of Android, `FloatMath` is actually slower than using `java.lang.Math` " +
"due to the way the JIT optimizes `java.lang.Math`. Therefore, you should use " +
"`Math` instead of `FloatMath` if you are only targeting Froyo and above.",
Category.PERFORMANCE,
3,
Severity.WARNING,
new Implementation(
MathDetector.class,
Scope.CLASS_FILE_SCOPE))
.addMoreInfo(
"http://developer.android.com/guide/practices/design/performance.html#avoidfloat"); //$NON-NLS-1$
/** Constructs a new {@link MathDetector} check */
public MathDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements ClassScanner ----
@Override
@Nullable
public List<String> getApplicableCallNames() {
return Arrays.asList(
"sin", //$NON-NLS-1$
"cos", //$NON-NLS-1$
"ceil", //$NON-NLS-1$
"sqrt", //$NON-NLS-1$
"floor" //$NON-NLS-1$
);
}
@Override
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
String owner = call.owner;
if (owner.equals("android/util/FloatMath") //$NON-NLS-1$
&& context.getProject().getMinSdk() >= 8) {
String message = String.format(
"Use `java.lang.Math#%1$s` instead of `android.util.FloatMath#%1$s()` " +
"since it is faster as of API 8", call.name);
context.report(ISSUE, method, call, context.getLocation(call), message /*data*/);
}
}
}
@@ -1,527 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ATTR_FRAGMENT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TAG_HEADER;
import static com.android.SdkConstants.TAG_PROVIDER;
import static com.android.SdkConstants.TAG_RECEIVER;
import static com.android.SdkConstants.TAG_SERVICE;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import static com.android.SdkConstants.VIEW_TAG;
import static com.android.resources.ResourceFolderType.LAYOUT;
import static com.android.resources.ResourceFolderType.VALUES;
import static com.android.resources.ResourceFolderType.XML;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Location.Handle;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.SdkUtils;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
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.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Checks to ensure that classes referenced in the manifest actually exist and are included
*
*/
public class MissingClassDetector extends LayoutDetector implements ClassScanner {
/** Manifest-referenced classes missing from the project or libraries */
public static final Issue MISSING = Issue.create(
"MissingRegistered", //$NON-NLS-1$
"Missing registered class",
"If a class is referenced in the manifest, it must also exist in the project (or in one " +
"of the libraries included by the project. This check helps uncover typos in " +
"registration names, or attempts to rename or move classes without updating the " +
"manifest file properly.",
Category.CORRECTNESS,
8,
Severity.ERROR,
new Implementation(
MissingClassDetector.class,
EnumSet.of(Scope.MANIFEST, Scope.CLASS_FILE,
Scope.JAVA_LIBRARIES, Scope.RESOURCE_FILE)))
.addMoreInfo("http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$
/** Are activity, service, receiver etc subclasses instantiatable? */
public static final Issue INSTANTIATABLE = Issue.create(
"Instantiatable", //$NON-NLS-1$
"Registered class is not instantiatable",
"Activities, services, broadcast receivers etc. registered in the manifest file " +
"must be \"instantiatable\" by the system, which means that the class must be " +
"public, it must have an empty public constructor, and if it's an inner class, " +
"it must be a static inner class.",
Category.CORRECTNESS,
6,
Severity.FATAL,
new Implementation(
MissingClassDetector.class,
Scope.CLASS_FILE_SCOPE));
/** Is the right character used for inner class separators? */
public static final Issue INNERCLASS = Issue.create(
"InnerclassSeparator", //$NON-NLS-1$
"Inner classes should use `$` rather than `.`",
"When you reference an inner class in a manifest file, you must use '$' instead of '.' " +
"as the separator character, i.e. Outer$Inner instead of Outer.Inner.\n" +
"\n" +
"(If you get this warning for a class which is not actually an inner class, it's " +
"because you are using uppercase characters in your package name, which is not " +
"conventional.)",
Category.CORRECTNESS,
3,
Severity.WARNING,
new Implementation(
MissingClassDetector.class,
Scope.MANIFEST_SCOPE));
private Map<String, Location.Handle> mReferencedClasses;
private Set<String> mCustomViews;
private boolean mHaveClasses;
/** Constructs a new {@link MissingClassDetector} */
public MissingClassDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableElements() {
return ALL;
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == VALUES || folderType == LAYOUT || folderType == XML;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String pkg = null;
Node classNameNode;
String className;
String tag = element.getTagName();
ResourceFolderType folderType = context.getResourceFolderType();
if (folderType == VALUES) {
if (!tag.equals(TAG_STRING)) {
return;
}
Attr attr = element.getAttributeNode(ATTR_NAME);
if (attr == null) {
return;
}
className = attr.getValue();
classNameNode = attr;
} else if (folderType == LAYOUT) {
if (tag.indexOf('.') > 0) {
className = tag;
classNameNode = element;
} else if (tag.equals(VIEW_FRAGMENT) || tag.equals(VIEW_TAG)) {
Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (attr == null) {
attr = element.getAttributeNode(ATTR_CLASS);
}
if (attr == null) {
return;
}
className = attr.getValue();
classNameNode = attr;
} else {
return;
}
} else if (folderType == XML) {
if (!tag.equals(TAG_HEADER)) {
return;
}
Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_FRAGMENT);
if (attr == null) {
return;
}
className = attr.getValue();
classNameNode = attr;
} else {
// Manifest file
if (TAG_APPLICATION.equals(tag)
|| TAG_ACTIVITY.equals(tag)
|| TAG_SERVICE.equals(tag)
|| TAG_RECEIVER.equals(tag)
|| TAG_PROVIDER.equals(tag)) {
Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (attr == null) {
return;
}
className = attr.getValue();
classNameNode = attr;
pkg = context.getMainProject().getPackage();
} else {
return;
}
}
if (className.isEmpty()) {
return;
}
String fqcn;
int dotIndex = className.indexOf('.');
if (dotIndex <= 0) {
if (pkg == null) {
return; // value file
}
if (dotIndex == 0) {
fqcn = pkg + className;
} else {
// According to the <activity> manifest element documentation, this is not
// valid ( http://developer.android.com/guide/topics/manifest/activity-element.html )
// but it appears in manifest files and appears to be supported by the runtime
// so handle this in code as well:
fqcn = pkg + '.' + className;
}
} else { // else: the class name is already a fully qualified class name
fqcn = className;
// Only look for fully qualified tracker names in analytics files
if (folderType == VALUES
&& !SdkUtils.endsWith(context.file.getPath(), "analytics.xml")) { //$NON-NLS-1$
return;
}
}
String signature = ClassContext.getInternalName(fqcn);
if (signature.isEmpty() || signature.startsWith(ANDROID_PKG_PREFIX)) {
return;
}
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
Handle handle = null;
if (!context.getDriver().isSuppressed(context, MISSING, element)) {
if (mReferencedClasses == null) {
mReferencedClasses = Maps.newHashMapWithExpectedSize(16);
mCustomViews = Sets.newHashSetWithExpectedSize(8);
}
handle = context.createLocationHandle(element);
mReferencedClasses.put(signature, handle);
if (folderType == LAYOUT && !tag.equals(VIEW_FRAGMENT)) {
mCustomViews.add(ClassContext.getInternalName(className));
}
}
if (signature.indexOf('$') != -1) {
checkInnerClass(context, element, pkg, classNameNode, className);
// The internal name contains a $ which means it's an inner class.
// The conversion from fqcn to internal name is a bit ambiguous:
// "a.b.C.D" usually means "inner class D in class C in package a.b".
// However, it can (see issue 31592) also mean class D in package "a.b.C".
// To make sure we don't falsely complain that foo/Bar$Baz doesn't exist,
// in case the user has actually created a package named foo/Bar and a proper
// class named Baz, we register *both* into the reference map.
// When generating errors we'll look for these an rip them back out if
// it looks like one of the two variations have been seen.
if (handle != null) {
// Assume that each successive $ is really a capitalized package name
// instead. In other words, for A$B$C$D (assumed to be class A with
// inner classes A.B, A.B.C and A.B.C.D) generate the following possible
// referenced classes A/B$C$D (class B in package A with inner classes C and C.D),
// A/B/C$D and A/B/C/D
while (true) {
int index = signature.indexOf('$');
if (index == -1) {
break;
}
signature = signature.substring(0, index) + '/'
+ signature.substring(index + 1);
mReferencedClasses.put(signature, handle);
if (folderType == LAYOUT && !tag.equals(VIEW_FRAGMENT)) {
mCustomViews.add(signature);
}
}
}
}
}
private static void checkInnerClass(XmlContext context, Element element, String pkg,
Node classNameNode, String className) {
if (pkg != null && className.indexOf('$') == -1 && className.indexOf('.', 1) > 0) {
boolean haveUpperCase = false;
for (int i = 0, n = pkg.length(); i < n; i++) {
if (Character.isUpperCase(pkg.charAt(i))) {
haveUpperCase = true;
break;
}
}
if (!haveUpperCase) {
String fixed = className.charAt(0) + className.substring(1).replace('.','$');
String message = "Use '$' instead of '.' for inner classes " +
"(or use only lowercase letters in package names); replace \"" +
className + "\" with \"" + fixed + "\"";
Location location = context.getLocation(classNameNode);
context.report(INNERCLASS, element, location, message);
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (!context.getProject().isLibrary() && mHaveClasses
&& mReferencedClasses != null && !mReferencedClasses.isEmpty()
&& context.getDriver().getScope().contains(Scope.CLASS_FILE)) {
List<String> classes = new ArrayList<String>(mReferencedClasses.keySet());
Collections.sort(classes);
for (String owner : classes) {
Location.Handle handle = mReferencedClasses.get(owner);
String fqcn = ClassContext.getFqcn(owner);
String signature = ClassContext.getInternalName(fqcn);
if (!signature.equals(owner)) {
if (!mReferencedClasses.containsKey(signature)) {
continue;
}
} else if (signature.indexOf('$') != -1) {
signature = signature.replace('$', '/');
if (!mReferencedClasses.containsKey(signature)) {
continue;
}
}
mReferencedClasses.remove(owner);
// Ignore usages of platform libraries
if (owner.startsWith("android/")) { //$NON-NLS-1$
continue;
}
String message = String.format(
"Class referenced in the manifest, `%1$s`, was not found in the " +
"project or the libraries", fqcn);
Location location = handle.resolve();
File parentFile = location.getFile().getParentFile();
if (parentFile != null) {
String parent = parentFile.getName();
ResourceFolderType type = ResourceFolderType.getFolderType(parent);
if (type == LAYOUT) {
message = String.format(
"Class referenced in the layout file, `%1$s`, was not found in "
+ "the project or the libraries", fqcn);
} else if (type == XML) {
message = String.format(
"Class referenced in the preference header file, `%1$s`, was not "
+ "found in the project or the libraries", fqcn);
} else if (type == VALUES) {
message = String.format(
"Class referenced in the analytics file, `%1$s`, was not "
+ "found in the project or the libraries", fqcn);
}
}
context.report(MISSING, location, message);
}
}
}
// ---- Implements ClassScanner ----
@Override
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
if (!mHaveClasses && !context.isFromClassLibrary()
&& context.getProject() == context.getMainProject()) {
mHaveClasses = true;
}
String curr = classNode.name;
if (mReferencedClasses != null && mReferencedClasses.containsKey(curr)) {
boolean isCustomView = mCustomViews.contains(curr);
removeReferences(curr);
// Ensure that the class is public, non static and has a null constructor!
if ((classNode.access & Opcodes.ACC_PUBLIC) == 0) {
context.report(INSTANTIATABLE, context.getLocation(classNode), String.format(
"This class should be public (%1$s)",
ClassContext.createSignature(classNode.name, null, null)));
return;
}
if (classNode.name.indexOf('$') != -1 && !LintUtils.isStaticInnerClass(classNode)) {
context.report(INSTANTIATABLE, context.getLocation(classNode), String.format(
"This inner class should be static (%1$s)",
ClassContext.createSignature(classNode.name, null, null)));
return;
}
boolean hasDefaultConstructor = false;
@SuppressWarnings("rawtypes") // ASM API
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
if (method.name.equals(CONSTRUCTOR_NAME)) {
if (method.desc.equals("()V")) { //$NON-NLS-1$
// The constructor must be public
if ((method.access & Opcodes.ACC_PUBLIC) != 0) {
hasDefaultConstructor = true;
} else {
context.report(INSTANTIATABLE, context.getLocation(method, classNode),
"The default constructor must be public");
// Also mark that we have a constructor so we don't complain again
// below since we've already emitted a more specific error related
// to the default constructor
hasDefaultConstructor = true;
}
}
}
}
if (!hasDefaultConstructor && !isCustomView && !context.isFromClassLibrary()
&& context.getProject().getReportIssues()) {
context.report(INSTANTIATABLE, context.getLocation(classNode), String.format(
"This class should provide a default constructor (a public " +
"constructor with no arguments) (%1$s)",
ClassContext.createSignature(classNode.name, null, null)));
}
}
}
private void removeReferences(String curr) {
mReferencedClasses.remove(curr);
// Since "A.B.C" is ambiguous whether it's referencing a class in package A.B or
// an inner class C in package A, we insert multiple possible references when we
// encounter the A.B.C reference; now that we've seen the actual class we need to
// remove all the possible permutations we've added such that the permutations
// don't count as unreferenced classes.
int index = curr.lastIndexOf('/');
if (index == -1) {
return;
}
boolean hasCapitalizedPackageName = false;
for (int i = index - 1; i >= 0; i--) {
char c = curr.charAt(i);
if (Character.isUpperCase(c)) {
hasCapitalizedPackageName = true;
break;
}
}
if (!hasCapitalizedPackageName) {
// No path ambiguity
return;
}
while (true) {
index = curr.lastIndexOf('/');
if (index == -1) {
break;
}
curr = curr.substring(0, index) + '$' + curr.substring(index + 1);
mReferencedClasses.remove(curr);
}
}
/**
* Given an error message produced by this lint detector for the given issue type,
* returns the old value to be replaced in the source code.
* <p>
* Intended for IDE quickfix implementations.
*
* @param issue the corresponding issue
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the corresponding old value, or null if not recognized
*/
@Nullable
public static String getOldValue(@NonNull Issue issue, @NonNull String errorMessage,
@NonNull TextFormat format) {
if (issue == INNERCLASS) {
errorMessage = format.toText(errorMessage);
return LintUtils.findSubstring(errorMessage, " replace \"", "\"");
}
return null;
}
/**
* Given an error message produced by this lint detector for the given issue type,
* returns the new value to be put into the source code.
* <p>
* Intended for IDE quickfix implementations.
*
* @param issue the corresponding issue
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the corresponding new value, or null if not recognized
*/
@Nullable
public static String getNewValue(@NonNull Issue issue, @NonNull String errorMessage,
@NonNull TextFormat format) {
if (issue == INNERCLASS) {
errorMessage = format.toText(errorMessage);
return LintUtils.findSubstring(errorMessage, " with \"", "\"");
}
return null;
}
}
@@ -1,94 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_TAG;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import java.util.Collection;
import java.util.Collections;
/**
* Check which looks for missing id's in views where they are probably needed
*/
public class MissingIdDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"MissingId", //$NON-NLS-1$
"Fragments should specify an `id` or `tag`",
"If you do not specify an android:id or an android:tag attribute on a " +
"<fragment> element, then if the activity is restarted (for example for " +
"an orientation rotation) you may lose state. From the fragment " +
"documentation:\n" +
"\n" +
"\"Each fragment requires a unique identifier that the system can use " +
"to restore the fragment if the activity is restarted (and which you can " +
"use to capture the fragment to perform transactions, such as remove it).\n" +
"\n" +
"* Supply the android:id attribute with a unique ID.\n" +
"* Supply the android:tag attribute with a unique string.\n" +
"If you provide neither of the previous two, the system uses the ID of the " +
"container view.",
Category.CORRECTNESS,
5,
Severity.WARNING,
new Implementation(
MissingIdDetector.class,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/guide/components/fragments.html"); //$NON-NLS-1$
/** Constructs a new {@link MissingIdDetector} */
public MissingIdDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(VIEW_FRAGMENT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID) &&
!element.hasAttributeNS(ANDROID_URI, ATTR_TAG)) {
context.report(ISSUE, element, context.getLocation(element),
"This `<fragment>` tag should specify an id or a tag to preserve state " +
"across activity restarts");
}
}
}
@@ -1,281 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.URI_PREFIX;
import static com.android.SdkConstants.XMLNS_PREFIX;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.HashMap;
import java.util.Map;
/**
* Checks for various issues related to XML namespaces
*/
public class NamespaceDetector extends LayoutDetector {
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
NamespaceDetector.class,
Scope.MANIFEST_AND_RESOURCE_SCOPE,
Scope.RESOURCE_FILE_SCOPE, Scope.MANIFEST_SCOPE);
/** Typos in the namespace */
public static final Issue TYPO = Issue.create(
"NamespaceTypo", //$NON-NLS-1$
"Misspelled namespace declaration",
"Accidental misspellings in namespace declarations can lead to some very " +
"obscure error messages. This check looks for potential misspellings to " +
"help track these down.",
Category.CORRECTNESS,
8,
Severity.FATAL,
IMPLEMENTATION);
/** Unused namespace declarations */
public static final Issue UNUSED = Issue.create(
"UnusedNamespace", //$NON-NLS-1$
"Unused namespace",
"Unused namespace declarations take up space and require processing that is not " +
"necessary",
Category.PERFORMANCE,
1,
Severity.WARNING,
IMPLEMENTATION);
/** Using custom namespace attributes in a library project */
public static final Issue CUSTOM_VIEW = Issue.create(
"LibraryCustomView", //$NON-NLS-1$
"Custom views in libraries should use res-auto-namespace",
"When using a custom view with custom attributes in a library project, the layout " +
"must use the special namespace " + AUTO_URI + " instead of a URI which includes " +
"the library project's own package. This will be used to automatically adjust the " +
"namespace of the attributes when the library resources are merged into the " +
"application project.",
Category.CORRECTNESS,
6,
Severity.FATAL,
IMPLEMENTATION);
/** Unused namespace declarations */
public static final Issue RES_AUTO = Issue.create(
"ResAuto", //$NON-NLS-1$
"Hardcoded Package in Namespace",
"In Gradle projects, the actual package used in the final APK can vary; for example," +
"you can add a `.debug` package suffix in one version and not the other. " +
"Therefore, you should *not* hardcode the application package in the resource; " +
"instead, use the special namespace `http://schemas.android.com/apk/res-auto` " +
"which will cause the tools to figure out the right namespace for the resource " +
"regardless of the actual package used during the build.",
Category.CORRECTNESS,
9,
Severity.FATAL,
IMPLEMENTATION);
/** Prefix relevant for custom namespaces */
private static final String XMLNS_ANDROID = "xmlns:android"; //$NON-NLS-1$
private static final String XMLNS_A = "xmlns:a"; //$NON-NLS-1$
private Map<String, Attr> mUnusedNamespaces;
private boolean mCheckUnused;
/** Constructs a new {@link NamespaceDetector} */
public NamespaceDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
boolean haveCustomNamespace = false;
Element root = document.getDocumentElement();
NamedNodeMap attributes = root.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Node item = attributes.item(i);
if (item.getNodeName().startsWith(XMLNS_PREFIX)) {
String value = item.getNodeValue();
if (!value.equals(ANDROID_URI)) {
Attr attribute = (Attr) item;
if (value.startsWith(URI_PREFIX)) {
haveCustomNamespace = true;
if (mUnusedNamespaces == null) {
mUnusedNamespaces = new HashMap<String, Attr>();
}
mUnusedNamespaces.put(item.getNodeName().substring(XMLNS_PREFIX.length()),
attribute);
} else if (value.startsWith("urn:")) { //$NON-NLS-1$
continue;
} else if (!value.startsWith("http://")) { //$NON-NLS-1$
if (context.isEnabled(TYPO)) {
context.report(TYPO, attribute, context.getValueLocation(attribute),
"Suspicious namespace: should start with `http://`");
}
continue;
} else if (!value.equals(AUTO_URI) && value.contains("auto") && //$NON-NLS-1$
value.startsWith("http://schemas.android.com/")) { //$NON-NLS-1$
context.report(RES_AUTO, attribute, context.getValueLocation(attribute),
"Suspicious namespace: Did you mean `" + AUTO_URI + "`?");
}
if (!context.isEnabled(TYPO)) {
continue;
}
String name = attribute.getName();
if (!name.equals(XMLNS_ANDROID) && !name.equals(XMLNS_A)) {
// See if it looks like a typo
int resIndex = value.indexOf("/res/"); //$NON-NLS-1$
if (resIndex != -1 && value.length() + 5 > URI_PREFIX.length()) {
String urlPrefix = value.substring(0, resIndex + 5);
if (!urlPrefix.equals(URI_PREFIX) &&
LintUtils.editDistance(URI_PREFIX, urlPrefix) <= 3) {
String correctUri = URI_PREFIX + value.substring(resIndex + 5);
context.report(TYPO, attribute,
context.getValueLocation(attribute),
String.format(
"Possible typo in URL: was `\"%1$s\"`, should " +
"probably be `\"%2$s\"`",
value, correctUri));
}
}
continue;
}
if (name.equals(XMLNS_A)) {
// For the "android" prefix we always assume that the namespace prefix
// should be our expected prefix, but for the "a" prefix we make sure
// that it's at least "close"; if you're bound it to something completely
// different, don't complain.
if (LintUtils.editDistance(ANDROID_URI, value) > 4) {
continue;
}
}
if (value.equalsIgnoreCase(ANDROID_URI)) {
context.report(TYPO, attribute, context.getValueLocation(attribute),
String.format(
"URI is case sensitive: was `\"%1$s\"`, expected `\"%2$s\"`",
value, ANDROID_URI));
} else {
context.report(TYPO, attribute, context.getValueLocation(attribute),
String.format(
"Unexpected namespace URI bound to the `\"android\"` " +
"prefix, was `%1$s`, expected `%2$s`", value, ANDROID_URI));
}
}
}
}
if (haveCustomNamespace) {
Project project = context.getProject();
boolean checkCustomAttrs =
context.isEnabled(CUSTOM_VIEW) && project.isLibrary()
|| context.isEnabled(RES_AUTO) && project.isGradleProject();
mCheckUnused = context.isEnabled(UNUSED);
if (checkCustomAttrs) {
checkCustomNamespace(context, root);
}
checkElement(root);
if (mCheckUnused && !mUnusedNamespaces.isEmpty()) {
for (Map.Entry<String, Attr> entry : mUnusedNamespaces.entrySet()) {
String prefix = entry.getKey();
Attr attribute = entry.getValue();
context.report(UNUSED, attribute, context.getLocation(attribute),
String.format("Unused namespace `%1$s`", prefix));
}
}
}
}
private static void checkCustomNamespace(XmlContext context, Element element) {
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
if (attribute.getName().startsWith(XMLNS_PREFIX)) {
String uri = attribute.getValue();
if (uri != null && !uri.isEmpty() && uri.startsWith(URI_PREFIX)
&& !uri.equals(ANDROID_URI)) {
if (context.getProject().isGradleProject()) {
context.report(RES_AUTO, attribute, context.getValueLocation(attribute),
"In Gradle projects, always use `" + AUTO_URI + "` for custom " +
"attributes");
} else {
context.report(CUSTOM_VIEW, attribute, context.getValueLocation(attribute),
"When using a custom namespace attribute in a library project, " +
"use the namespace `\"" + AUTO_URI + "\"` instead.");
}
}
}
}
}
private void checkElement(Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
if (mCheckUnused) {
NamedNodeMap attributes = node.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
String prefix = attribute.getPrefix();
if (prefix != null) {
mUnusedNamespaces.remove(prefix);
}
}
}
NodeList childNodes = node.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
checkElement(childNodes.item(i));
}
}
}
}
@@ -1,246 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.DIMEN_PREFIX;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.TAG_DIMEN;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STYLE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
/**
* Checks for negative margins in the following scenarios:
* <ul>
* <li>In direct layout attribute usages, e.g. {@code <Button android:layoutMargin="-5dp"}</li>
* <li>In theme styles, e.g. {@code <item name="android:layoutMargin">-5dp</item>}</li>
* <li>In dimension usages, e.g. {@code <Button android:layoutMargin="@dimen/foo"} along
* with {@code <dimen name="foo">-5dp</dimen>}</li>
* </ul>
*/
public class NegativeMarginDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
NegativeMarginDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Negative margins */
public static final Issue ISSUE = Issue.create(
"NegativeMargin", //$NON-NLS-1$
"Negative Margins",
"Margin values should be positive. Negative values are generally a sign that " +
"you are making assumptions about views surrounding the current one, or may be "+
"tempted to turn off child clipping to allow a view to escape its parent. " +
"Turning off child clipping to do this not only leads to poor graphical " +
"performance, it also results in wrong touch event handling since touch events " +
"are based strictly on a chain of parent-rect hit tests. Finally, making " +
"assumptions about the size of strings can lead to localization problems.",
Category.USABILITY,
4,
Severity.WARNING,
IMPLEMENTATION).setEnabledByDefault(false);
private HashMap<String, Location.Handle> mDimenUsage;
/** Constructs a new {@link NegativeMarginDetector} */
public NegativeMarginDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
// Look in both layouts (at attribute values) and in value files (style and dimension
// definitions)
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableAttributes() {
return Arrays.asList(
ATTR_LAYOUT_MARGIN,
ATTR_LAYOUT_MARGIN_LEFT,
ATTR_LAYOUT_MARGIN_TOP,
ATTR_LAYOUT_MARGIN_RIGHT,
ATTR_LAYOUT_MARGIN_BOTTOM,
ATTR_LAYOUT_MARGIN_START,
ATTR_LAYOUT_MARGIN_END
);
}
@Override
@Nullable
public Collection<String> getApplicableElements() {
return Arrays.asList(TAG_DIMEN, TAG_STYLE);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String value = attribute.getValue();
checkMarginValue(context, value, attribute, null);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (context.getResourceFolderType() != ResourceFolderType.VALUES) {
return;
}
String tag = element.getTagName();
if (TAG_DIMEN.equals(tag)) {
NodeList itemNodes = element.getChildNodes();
String name = element.getAttribute(ATTR_NAME);
Location.Handle handle = mDimenUsage != null ? mDimenUsage.get(name) : null;
if (handle != null) {
for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
Node item = itemNodes.item(j);
if (item.getNodeType() == Node.TEXT_NODE) {
String text = item.getNodeValue().trim();
checkMarginValue(context, text, null, handle);
}
}
}
} else {
assert TAG_STYLE.equals(tag) : tag;
NodeList itemNodes = element.getChildNodes();
for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
Node item = itemNodes.item(j);
if (item.getNodeType() == Node.ELEMENT_NODE &&
TAG_ITEM.equals(item.getNodeName())) {
Element itemElement = (Element) item;
String name = itemElement.getAttribute(ATTR_NAME);
if (name.startsWith(PREFIX_ANDROID) &&
name.startsWith(ATTR_LAYOUT_MARGIN, PREFIX_ANDROID.length())) {
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() != Node.TEXT_NODE) {
return;
}
checkMarginValue(context, child.getNodeValue(), child, null);
}
}
}
}
}
}
private static boolean isNegativeDimension(@NonNull String value) {
return value.trim().startsWith("-");
}
private void checkMarginValue(
@NonNull XmlContext context,
@NonNull String value,
@Nullable Node scope,
@Nullable Location.Handle handle) {
if (isNegativeDimension(value)) {
String message = "Margin values should not be negative";
if (scope != null) {
context.report(ISSUE, scope, context.getLocation(scope), message);
} else {
assert handle != null;
context.report(ISSUE, handle.resolve(), message);
}
} else if (value.startsWith(DIMEN_PREFIX) && scope != null) {
ResourceUrl url = ResourceUrl.parse(value);
if (url == null) {
return;
}
if (context.getClient().supportsProjectResources()) {
// Typically interactive IDE usage, where we are only analyzing a single file,
// but we can use the IDE to resolve resource URLs
LintClient client = context.getClient();
Project project = context.getProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources != null) {
List<ResourceItem> items = resources.getResourceItem(url.type, url.name);
if (items != null) {
for (ResourceItem item : items) {
ResourceValue resourceValue = item.getResourceValue(false);
if (resourceValue != null) {
String dimenValue = resourceValue.getValue();
if (dimenValue != null && isNegativeDimension(dimenValue)) {
ResourceFile sourceFile = item.getSource();
assert sourceFile != null;
String message = String.format(
"Margin values should not be negative "
+ "(`%1$s` is defined as `%2$s` in `%3$s`",
value, dimenValue, sourceFile.getFile());
context.report(ISSUE, scope,
context.getLocation(scope),
message);
break;
}
}
}
}
}
} else if (!context.getDriver().isSuppressed(context, ISSUE, scope)) {
// Batch mode where we process layouts then values in order
if (mDimenUsage == null) {
mDimenUsage = new HashMap<String, Location.Handle>();
}
mDimenUsage.put(url.name, context.createLocationHandle(scope));
}
}
}
}
@@ -1,158 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.GALLERY;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.SCROLL_VIEW;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.Arrays;
import java.util.Collection;
/**
* Checks whether a scroll view contains a nested scrolling widget
*/
public class NestedScrollingWidgetDetector extends LayoutDetector {
private int mVisitingHorizontalScroll;
private int mVisitingVerticalScroll;
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"NestedScrolling", //$NON-NLS-1$
"Nested scrolling widgets",
// TODO: Better description!
"A scrolling widget such as a `ScrollView` should not contain any nested " +
"scrolling widgets since this has various usability issues",
Category.CORRECTNESS,
7,
Severity.WARNING,
new Implementation(
NestedScrollingWidgetDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link NestedScrollingWidgetDetector} */
public NestedScrollingWidgetDetector() {
}
@Override
public void beforeCheckFile(@NonNull Context context) {
mVisitingHorizontalScroll = 0;
mVisitingVerticalScroll = 0;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
@NonNull
public Collection<String> getApplicableElements() {
return Arrays.asList(
SCROLL_VIEW,
LIST_VIEW,
GRID_VIEW,
// Horizontal
GALLERY,
HORIZONTAL_SCROLL_VIEW
);
}
private Element findOuterScrollingWidget(Node node, boolean vertical) {
Collection<String> applicableElements = getApplicableElements();
while (node != null) {
if (node instanceof Element) {
Element element = (Element) node;
String tagName = element.getTagName();
if (applicableElements.contains(tagName)
&& vertical == isVerticalScroll(element)) {
return element;
}
}
node = node.getParentNode();
}
return null;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
boolean vertical = isVerticalScroll(element);
if (vertical) {
mVisitingVerticalScroll++;
} else {
mVisitingHorizontalScroll++;
}
if (mVisitingHorizontalScroll > 1 || mVisitingVerticalScroll > 1) {
Element parent = findOuterScrollingWidget(element.getParentNode(), vertical);
if (parent != null) {
String format;
if (mVisitingVerticalScroll > 1) {
format = "The vertically scrolling `%1$s` should not contain another " +
"vertically scrolling widget (`%2$s`)";
} else {
format = "The horizontally scrolling `%1$s` should not contain another " +
"horizontally scrolling widget (`%2$s`)";
}
String msg = String.format(format, parent.getTagName(), element.getTagName());
context.report(ISSUE, element, context.getLocation(element), msg);
}
}
}
@Override
public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) {
if (isVerticalScroll(element)) {
mVisitingVerticalScroll--;
assert mVisitingVerticalScroll >= 0;
} else {
mVisitingHorizontalScroll--;
assert mVisitingHorizontalScroll >= 0;
}
}
private static boolean isVerticalScroll(Element element) {
String view = element.getTagName();
if (view.equals(GALLERY) || view.equals(HORIZONTAL_SCROLL_VIEW)) {
return false;
} else {
// This method should only be called with one of the 5 widget types
// listed in getApplicableElements
assert view.equals(SCROLL_VIEW) || view.equals(LIST_VIEW) || view.equals(GRID_VIEW);
return true;
}
}
}
@@ -1,439 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_END_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_START_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_LAYOUT_X;
import static com.android.SdkConstants.ATTR_LAYOUT_Y;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.VIEW_INCLUDE;
import static com.android.SdkConstants.VIEW_MERGE;
import static com.android.SdkConstants.VIEW_TAG;
import com.android.annotations.NonNull;
import com.android.tools.klint.client.api.SdkInfo;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Location.Handle;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.Pair;
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.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;
/**
* Looks for layout params on views that are "obsolete" - may have made sense
* when the view was added but there is a different layout parent now which does
* not use the given layout params.
*/
public class ObsoleteLayoutParamsDetector extends LayoutDetector {
/** Usage of deprecated views or attributes */
public static final Issue ISSUE = Issue.create(
"ObsoleteLayoutParam", //$NON-NLS-1$
"Obsolete layout params",
"The given layout_param is not defined for the given layout, meaning it has no " +
"effect. This usually happens when you change the parent layout or move view " +
"code around without updating the layout params. This will cause useless " +
"attribute processing at runtime, and is misleading for others reading the " +
"layout so the parameter should be removed.",
Category.PERFORMANCE,
6,
Severity.WARNING,
new Implementation(
ObsoleteLayoutParamsDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/**
* Set of layout parameter names that are considered valid no matter what so
* no other checking is necessary - such as layout_width and layout_height.
*/
private static final Set<String> VALID = new HashSet<String>(10);
/**
* Mapping from a layout parameter name (local name only) to the defining
* ViewGroup. Note that it's possible for the same name to be defined by
* multiple ViewGroups - but it turns out this is extremely rare (the only
* examples are layout_column defined by both TableRow and GridLayout, and
* layout_gravity defined by many layouts) so rather than handle this with
* every single layout attribute pointing to a list, this is just special
* cased instead.
*/
private static final Map<String, String> PARAM_TO_VIEW = new HashMap<String, String>(28);
static {
// Available (mostly) everywhere: No check
VALID.add(ATTR_LAYOUT_WIDTH);
VALID.add(ATTR_LAYOUT_HEIGHT);
// The layout_gravity isn't "global" but it's defined on many of the most
// common layouts (FrameLayout, LinearLayout and GridLayout) so we don't
// currently check for it. In order to do this we'd need to make the map point
// to lists rather than individual layouts or we'd need a bunch of special cases
// like the one done for layout_column below.
VALID.add(ATTR_LAYOUT_GRAVITY);
// From ViewGroup.MarginLayoutParams
VALID.add(ATTR_LAYOUT_MARGIN_LEFT);
VALID.add(ATTR_LAYOUT_MARGIN_START);
VALID.add(ATTR_LAYOUT_MARGIN_RIGHT);
VALID.add(ATTR_LAYOUT_MARGIN_END);
VALID.add(ATTR_LAYOUT_MARGIN_TOP);
VALID.add(ATTR_LAYOUT_MARGIN_BOTTOM);
VALID.add(ATTR_LAYOUT_MARGIN);
// Absolute Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_X, ABSOLUTE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_Y, ABSOLUTE_LAYOUT);
// Linear Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_WEIGHT, LINEAR_LAYOUT);
// Grid Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN_SPAN, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW_SPAN, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW_SPAN, GRID_LAYOUT);
// Table Layout
// ATTR_LAYOUT_COLUMN is defined for both GridLayout and TableLayout,
// so we don't want to do
// PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN, TABLE_ROW);
// here since it would wipe out the above GridLayout registration.
// Since this is the only case where there is a conflict (in addition to layout_gravity
// which is defined in many places), rather than making the map point to lists
// this specific case is just special cased below, look for ATTR_LAYOUT_COLUMN.
PARAM_TO_VIEW.put(ATTR_LAYOUT_SPAN, TABLE_ROW);
// Relative Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_LEFT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_START, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_RIGHT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_END, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_TOP, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_BOTTOM, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_TOP, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_LEFT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_START, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_END, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_BASELINE, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_IN_PARENT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_VERTICAL, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_HORIZONTAL, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_RIGHT_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_END_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_LEFT_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_START_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_BELOW, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ABOVE, RELATIVE_LAYOUT);
}
/**
* Map from an included layout to all the including contexts (each including
* context is a pair of a file containing the include to the parent tag at
* the included location)
*/
private Map<String, List<Pair<File, String>>> mIncludes;
/**
* List of pending include checks. When a layout parameter attribute is
* found on a root element, or on a child of a {@code merge} root tag, then
* we want to check across layouts whether the including context (the parent
* of the include tag) is valid for this attribute. We cannot check this
* immediately because we are processing the layouts in an arbitrary order
* so the included layout may be seen before the including layout and so on.
* Therefore, we stash these attributes to be checked after we're done. Each
* pair is a pair of an attribute name to be checked, and the file that
* attribute is referenced in.
*/
private final List<Pair<String, Location.Handle>> mPending =
new ArrayList<Pair<String,Location.Handle>>();
/** Constructs a new {@link ObsoleteLayoutParamsDetector} */
public ObsoleteLayoutParamsDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(VIEW_INCLUDE);
}
@Override
public Collection<String> getApplicableAttributes() {
return ALL;
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String name = attribute.getLocalName();
if (name != null && name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
&& ANDROID_URI.equals(attribute.getNamespaceURI())) {
if (VALID.contains(name)) {
return;
}
String parent = PARAM_TO_VIEW.get(name);
if (parent != null) {
Element viewElement = attribute.getOwnerElement();
Node layoutNode = viewElement.getParentNode();
if (layoutNode == null || layoutNode.getNodeType() != Node.ELEMENT_NODE) {
// This is a layout attribute on a root element; this presumably means
// that this layout is included so check the included layouts to make
// sure at least one included context is valid for this layout_param.
// We can't do that yet since we may be processing the include tag to
// this layout after the layout itself. Instead, stash a work order...
if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
Location.Handle handle = context.createLocationHandle(attribute);
handle.setClientData(attribute);
mPending.add(Pair.of(name, handle));
}
return;
}
String parentTag = ((Element) layoutNode).getTagName();
if (parentTag.equals(VIEW_MERGE)) {
// This is a merge which means we need to check the including contexts,
// wherever they are. This has to be done after all the files have been
// scanned since we are not processing the files in any particular order.
if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
Location.Handle handle = context.createLocationHandle(attribute);
handle.setClientData(attribute);
mPending.add(Pair.of(name, handle));
}
return;
}
if (!isValidParamForParent(context, name, parent, parentTag)) {
if (name.equals(ATTR_LAYOUT_COLUMN)
&& isValidParamForParent(context, name, TABLE_ROW, parentTag)) {
return;
}
context.report(ISSUE, attribute, context.getLocation(attribute),
String.format("Invalid layout param in a `%1$s`: `%2$s`", parentTag, name));
}
} else {
// We could warn about unknown layout params but this might be brittle if
// new params are added or if people write custom ones; this is just a log
// for us to track these and update the check as necessary:
//context.client.log(null,
// String.format("Unrecognized layout param '%1$s'", name));
}
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String layout = element.getAttribute(ATTR_LAYOUT);
if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts
layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
Node parent = element.getParentNode();
if (parent.getNodeType() == Node.ELEMENT_NODE) {
String tag = parent.getNodeName();
if (tag.indexOf('.') == -1 && !tag.equals(VIEW_MERGE)) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mIncludes == null) {
mIncludes = new HashMap<String, List<Pair<File, String>>>();
}
List<Pair<File, String>> includes = mIncludes.get(layout);
if (includes == null) {
includes = new ArrayList<Pair<File, String>>();
mIncludes.put(layout, includes);
}
includes.add(Pair.of(context.file, tag));
}
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mIncludes == null) {
return;
}
for (Pair<String, Location.Handle> pending : mPending) {
Handle handle = pending.getSecond();
Location location = handle.resolve();
File file = location.getFile();
String layout = file.getName();
if (layout.endsWith(DOT_XML)) {
layout = layout.substring(0, layout.length() - DOT_XML.length());
}
List<Pair<File, String>> includes = mIncludes.get(layout);
if (includes == null) {
// Nobody included this file
continue;
}
String name = pending.getFirst();
String parent = PARAM_TO_VIEW.get(name);
if (parent == null) {
continue;
}
boolean isValid = false;
for (Pair<File, String> include : includes) {
String parentTag = include.getSecond();
if (isValidParamForParent(context, name, parent, parentTag)) {
isValid = true;
break;
} else if (!isValid && name.equals(ATTR_LAYOUT_COLUMN)
&& isValidParamForParent(context, name, TABLE_ROW, parentTag)) {
isValid = true;
break;
}
}
if (!isValid) {
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ISSUE, (Node) clientData)) {
return;
}
}
StringBuilder sb = new StringBuilder(40);
for (Pair<File, String> include : includes) {
if (sb.length() > 0) {
sb.append(", "); //$NON-NLS-1$
}
File from = include.getFirst();
String parentTag = include.getSecond();
sb.append(String.format("included from within a `%1$s` in `%2$s`",
parentTag,
from.getParentFile().getName() + File.separator + from.getName()));
}
String message = String.format("Invalid layout param '`%1$s`' (%2$s)",
name, sb.toString());
// TODO: Compute applicable scope node
context.report(ISSUE, location, message);
}
}
}
/**
* Checks whether the given layout parameter name is valid for the given
* parent tag assuming it has the given current parent tag
*/
private static boolean isValidParamForParent(Context context, String name, String parent,
String parentTag) {
if (parentTag.indexOf('.') != -1 || parentTag.equals(VIEW_TAG)) {
// Custom tag: We don't know whether it extends one of the builtin
// types where the layout param is valid, so don't complain
return true;
}
SdkInfo sdk = context.getSdkInfo();
if (!parentTag.equals(parent)) {
String tag = sdk.getParentViewName(parentTag);
while (tag != null) {
if (tag.equals(parent)) {
return true;
}
tag = sdk.getParentViewName(tag);
}
return false;
}
return true;
}
}
@@ -1,244 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ATTR_ON_CLICK;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import com.android.annotations.NonNull;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Location.Handle;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.base.Joiner;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Checks for missing onClick handlers
*/
public class OnClickDetector extends LayoutDetector implements ClassScanner {
/** Missing onClick handlers */
public static final Issue ISSUE = Issue.create(
"OnClick", //$NON-NLS-1$
"`onClick` method does not exist",
"The `onClick` attribute value should be the name of a method in this View's context " +
"to invoke when the view is clicked. This name must correspond to a public method " +
"that takes exactly one parameter of type `View`.\n" +
"\n" +
"Must be a string value, using '\\;' to escape characters such as '\\n' or " +
"'\\uxxxx' for a unicode character.",
Category.CORRECTNESS,
10,
Severity.ERROR,
new Implementation(
OnClickDetector.class,
Scope.CLASS_AND_ALL_RESOURCE_FILES));
private Map<String, Location.Handle> mNames;
private Map<String, List<String>> mSimilar;
private boolean mHaveBytecode;
/** Constructs a new {@link OnClickDetector} */
public OnClickDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mNames != null && !mNames.isEmpty() && mHaveBytecode) {
List<String> names = new ArrayList<String>(mNames.keySet());
Collections.sort(names);
LintDriver driver = context.getDriver();
for (String name : names) {
Handle handle = mNames.get(name);
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (driver.isSuppressed(null, ISSUE, (Node) clientData)) {
continue;
}
}
Location location = handle.resolve();
String message = String.format(
"Corresponding method handler '`public void %1$s(android.view.View)`' not found",
name);
List<String> similar = mSimilar != null ? mSimilar.get(name) : null;
if (similar != null) {
Collections.sort(similar);
message += String.format(" (did you mean `%1$s` ?)", Joiner.on(", ").join(similar));
}
context.report(ISSUE, location, message);
}
}
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_ON_CLICK);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String value = attribute.getValue();
if (value.isEmpty() || value.trim().isEmpty()) {
context.report(ISSUE, attribute, context.getLocation(attribute),
"`onClick` attribute value cannot be empty");
} else if (!value.equals(value.trim())) {
context.report(ISSUE, attribute, context.getLocation(attribute),
"There should be no whitespace around attribute values");
} else if (!value.startsWith(PREFIX_RESOURCE_REF)) { // Not resolved
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mNames == null) {
mNames = new HashMap<String, Location.Handle>();
}
Handle handle = context.createLocationHandle(attribute);
handle.setClientData(attribute);
// Replace unicode characters with the actual value since that's how they
// appear in the ASM signatures
if (value.contains("\\u")) { //$NON-NLS-1$
Pattern pattern = Pattern.compile("\\\\u(\\d\\d\\d\\d)"); //$NON-NLS-1$
Matcher matcher = pattern.matcher(value);
StringBuilder sb = new StringBuilder(value.length());
int remainder = 0;
while (matcher.find()) {
sb.append(value.substring(0, matcher.start()));
String unicode = matcher.group(1);
int hex = Integer.parseInt(unicode, 16);
sb.append((char) hex);
remainder = matcher.end();
}
sb.append(value.substring(remainder));
value = sb.toString();
}
mNames.put(value, handle);
}
}
// ---- Implements ClassScanner ----
@SuppressWarnings("rawtypes")
@Override
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
if (mNames == null) {
// No onClick attributes in the XML files
return;
}
mHaveBytecode = true;
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
boolean rightArguments = method.desc.equals("(Landroid/view/View;)V"); //$NON-NLS-1$
if (!mNames.containsKey(method.name)) {
if (rightArguments) {
// See if there's a possible typo instead
for (String n : mNames.keySet()) {
if (LintUtils.editDistance(n, method.name) <= 2) {
recordSimilar(n, classNode, method);
break;
}
}
}
continue;
}
// TODO: Validate class hierarchy: should extend a context method
// Longer term, also validate that it's in a layout that corresponds to
// the given activity
if (rightArguments){
// Found: remove from list to be checked
mNames.remove(method.name);
// Make sure the method is public
if ((method.access & Opcodes.ACC_PUBLIC) == 0) {
Location location = context.getLocation(method, classNode);
String message = String.format(
"On click handler `%1$s(View)` must be public",
method.name);
context.report(ISSUE, location, message);
} else if ((method.access & Opcodes.ACC_STATIC) != 0) {
Location location = context.getLocation(method, classNode);
String message = String.format(
"On click handler `%1$s(View)` should not be static",
method.name);
context.report(ISSUE, location, message);
}
if (mNames.isEmpty()) {
mNames = null;
return;
}
}
}
}
private void recordSimilar(String name, ClassNode classNode, MethodNode method) {
if (mSimilar == null) {
mSimilar = new HashMap<String, List<String>>();
}
List<String> list = mSimilar.get(name);
if (list == null) {
list = new ArrayList<String>();
mSimilar.put(name, list);
}
String signature = ClassContext.createSignature(classNode.name, method.name, method.desc);
list.add(signature);
}
}
@@ -1,280 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import com.android.annotations.NonNull;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Checks for accidental overrides
*/
public class OverrideDetector extends Detector implements ClassScanner {
/** Accidental overrides */
public static final Issue ISSUE = Issue.create(
"DalvikOverride", //$NON-NLS-1$
"Method considered overridden by Dalvik",
"The Android virtual machine will treat a package private method in one " +
"class as overriding a package private method in its super class, even if " +
"they are in separate packages. This may be surprising, but for compatibility " +
"reasons the behavior has not been changed (yet).\n" +
"\n" +
"If you really did intend for this method to override the other, make the " +
"method `protected` instead.\n" +
"\n" +
"If you did *not* intend the override, consider making the method private, or " +
"changing its name or signature.",
Category.CORRECTNESS,
7,
Severity.ERROR,
new Implementation(
OverrideDetector.class,
EnumSet.of(Scope.ALL_CLASS_FILES)));
/** map from owner class name to JVM signatures for its package private methods */
private final Map<String, Set<String>> mPackagePrivateMethods = Maps.newHashMap();
/** Map from owner to signature to super class being overridden */
private Map<String, Map<String, String>> mErrors;
/**
* Map from owner to signature to corresponding location. When there are
* errors a single error can have locations for both the overriding and
* overridden methods.
*/
private Map<String, Map<String, Location>> mLocations;
/** Constructs a new {@link OverrideDetector} */
public OverrideDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void afterCheckProject(@NonNull Context context) {
// Process the check in two passes:
//
// In the first pass, gather the full set of package private methods for
// each class.
// When all classes have been processed at the end of the first pass,
// find out whether any of the methods are potentially overriding those
// in its super classes.
//
// If so, request a second pass. In the second pass, we gather full locations
// for both the base and overridden method calls, and store these.
// If the location is found to be in a suppressed context, remove that error
// entry.
//
// At the end of the second pass, we generate the errors, combining locations
// from both the overridden and overriding methods.
if (context.getPhase() == 1) {
Set<String> classes = mPackagePrivateMethods.keySet();
LintDriver driver = context.getDriver();
for (String owner : classes) {
Set<String> methods = mPackagePrivateMethods.get(owner);
String superClass = driver.getSuperClass(owner);
int packageIndex = owner.lastIndexOf('/');
while (superClass != null) {
int superPackageIndex = superClass.lastIndexOf('/');
// Only compare methods that differ in packages
if (packageIndex == -1 || superPackageIndex != packageIndex ||
!owner.regionMatches(0, superClass, 0, packageIndex)) {
Set<String> superMethods = mPackagePrivateMethods.get(superClass);
if (superMethods != null) {
SetView<String> intersection = Sets.intersection(methods,
superMethods);
if (!intersection.isEmpty()) {
if (mLocations == null) {
mLocations = Maps.newHashMap();
}
// We need a separate data structure to keep track of which
// signatures are in error,
if (mErrors == null) {
mErrors = Maps.newHashMap();
}
for (String signature : intersection) {
Map<String, Location> locations = mLocations.get(owner);
if (locations == null) {
locations = Maps.newHashMap();
mLocations.put(owner, locations);
}
locations.put(signature, null);
locations = mLocations.get(superClass);
if (locations == null) {
locations = Maps.newHashMap();
mLocations.put(superClass, locations);
}
locations.put(signature, null);
Map<String, String> errors = mErrors.get(owner);
if (errors == null) {
errors = Maps.newHashMap();
mErrors.put(owner, errors);
}
errors.put(signature, superClass);
}
}
}
}
superClass = driver.getSuperClass(superClass);
}
}
if (mErrors != null) {
context.requestRepeat(this, ISSUE.getImplementation().getScope());
}
} else {
assert context.getPhase() == 2;
for (Entry<String, Map<String, String>> ownerEntry : mErrors.entrySet()) {
String owner = ownerEntry.getKey();
Map<String, String> methodToSuper = ownerEntry.getValue();
for (Entry<String, String> entry : methodToSuper.entrySet()) {
String signature = entry.getKey();
String superClass = entry.getValue();
Map<String, Location> ownerLocations = mLocations.get(owner);
if (ownerLocations != null) {
Location location = ownerLocations.get(signature);
if (location != null) {
Map<String, Location> superLocations = mLocations.get(superClass);
if (superLocations != null) {
Location superLocation = superLocations.get(signature);
if (superLocation != null) {
location.setSecondary(superLocation);
superLocation.setMessage(
"This method is treated as overridden");
}
}
String methodName = signature;
int index = methodName.indexOf('(');
if (index != -1) {
methodName = methodName.substring(0, index);
}
String message = String.format(
"This package private method may be unintentionally " +
"overriding `%1$s` in `%2$s`", methodName,
ClassContext.getFqcn(superClass));
context.report(ISSUE, location, message);
}
}
}
}
}
}
@SuppressWarnings("rawtypes") // ASM5 API
@Override
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
List methodList = classNode.methods;
if (context.getPhase() == 1) {
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
int access = method.access;
// Only record non-static package private methods
if ((access & (ACC_STATIC|ACC_PRIVATE|ACC_PROTECTED|ACC_PUBLIC)) != 0) {
continue;
}
// Ignore constructors too
if (CONSTRUCTOR_NAME.equals(method.name)) {
continue;
}
String owner = classNode.name;
Set<String> methods = mPackagePrivateMethods.get(owner);
if (methods == null) {
methods = Sets.newHashSetWithExpectedSize(methodList.size());
mPackagePrivateMethods.put(owner, methods);
}
methods.add(method.name + method.desc);
}
} else {
assert context.getPhase() == 2;
Map<String, Location> methods = mLocations.get(classNode.name);
if (methods == null) {
// No locations needed from this class
return;
}
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
String signature = method.name + method.desc;
if (methods.containsKey(signature)){
if (context.getDriver().isSuppressed(ISSUE, classNode,
method, null)) {
Map<String, String> errors = mErrors.get(classNode.name);
if (errors != null) {
errors.remove(signature);
}
continue;
}
Location location = context.getLocation(method, classNode);
methods.put(signature, location);
String description = ClassContext.createSignature(classNode.name,
method.name, method.desc);
location.setClientData(description);
}
}
}
}
}
@@ -1,246 +0,0 @@
/*
* Copyright (C) 2013 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.checks.PluralsDatabase.Quantity;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import static com.android.SdkConstants.*;
/**
* Checks for issues with quantity strings
* <p>
* https://code.google.com/p/android/issues/detail?id=53015
* 53015: lint could report incorrect usage of Resource.getQuantityString
*/
public class PluralsDetector extends ResourceXmlDetector {
private static final Implementation
IMPLEMENTATION = new Implementation(
PluralsDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** This locale should define a quantity string for the given quantity */
public static final Issue MISSING = Issue.create(
"MissingQuantity", //$NON-NLS-1$
"Missing quantity translation",
"Different languages have different rules for grammatical agreement with " +
"quantity. In English, for example, the quantity 1 is a special case. " +
"We write \"1 book\", but for any other quantity we'd write \"n books\". " +
"This distinction between singular and plural is very common, but other " +
"languages make finer distinctions.\n" +
"\n" +
"This lint check looks at each translation of a `<plural>` and makes sure " +
"that all the quantity strings considered by the given language are provided " +
"by this translation.\n" +
"\n" +
"For example, an English translation must provide a string for `quantity=\"one\"`. " +
"Similarly, a Czech translation must provide a string for `quantity=\"few\"`.",
Category.MESSAGES,
8,
Severity.ERROR,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/guide/topics/resources/string-resource.html#Plurals");
/** This translation is not needed in this locale */
public static final Issue EXTRA = Issue.create(
"UnusedQuantity", //$NON-NLS-1$
"Unused quantity translations",
"Android defines a number of different quantity strings, such as `zero`, `one`, " +
"`few` and `many`. However, many languages do not distinguish grammatically " +
"between all these different quantities.\n" +
"\n" +
"This lint check looks at the quantity strings defined for each translation and " +
"flags any quantity strings that are unused (because the language does not make that " +
"quantity distinction, and Android will therefore not look it up.).\n" +
"\n" +
"For example, in Chinese, only the `other` quantity is used, so even if you " +
"provide translations for `zero` and `one`, these strings will *not* be returned " +
"when `getQuantityString()` is called, even with `0` or `1`.",
Category.MESSAGES,
3,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/guide/topics/resources/string-resource.html#Plurals");
/** This plural does not use the quantity value */
public static final Issue IMPLIED_QUANTITY = Issue.create(
"ImpliedQuantity", //$NON-NLS-1$
"Implied Quantities",
"Plural strings should generally include a `%s` or `%d` formatting argument. " +
"In locales like English, the `one` quantity only applies to a single value, " +
"1, but that's not true everywhere. For example, in Slovene, the `one` quantity " +
"will apply to 1, 101, 201, 301, and so on. Similarly, there are locales where " +
"multiple values match the `zero` and `two` quantities.\n" +
"\n" +
"In these locales, it is usually an error to have a message which does not " +
"include a formatting argument (such as '%d'), since it will not be clear from " +
"the grammar what quantity the quantity string is describing.",
Category.MESSAGES,
5,
Severity.ERROR,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/guide/topics/resources/string-resource.html#Plurals");
/** Constructs a new {@link PluralsDetector} */
public PluralsDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(TAG_PLURALS);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int count = LintUtils.getChildCount(element);
if (count == 0) {
context.report(MISSING, element, context.getLocation(element),
"There should be at least one quantity string in this `<plural>` definition");
return;
}
LocaleQualifier locale = LintUtils.getLocale(context);
if (locale == null || !locale.hasLanguage()) {
return;
}
String language = locale.getLanguage();
PluralsDatabase plurals = PluralsDatabase.get();
EnumSet<Quantity> relevant = plurals.getRelevant(language);
if (relevant == null) {
return;
}
EnumSet<Quantity> defined = EnumSet.noneOf(Quantity.class);
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node noe = children.item(i);
if (noe.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element child = (Element) noe;
if (!TAG_ITEM.equals(child.getTagName())) {
continue;
}
String quantityString = child.getAttribute(ATTR_QUANTITY);
if (quantityString == null || quantityString.isEmpty()) {
continue;
}
Quantity quantity = Quantity.get(quantityString);
if (quantity == null || quantity == Quantity.other) { // Not stored in the database
continue;
}
defined.add(quantity);
if (plurals.hasMultipleValuesForQuantity(language, quantity)
&& !haveFormattingParameter(child) && context.isEnabled(IMPLIED_QUANTITY)) {
String example = plurals.findIntegerExamples(language, quantity);
String append;
if (example == null) {
append = "";
} else {
append = " (" + example + ")";
}
String message = String.format("The quantity `'%1$s'` matches more than one "
+ "specific number in this locale%2$s, but the message did "
+ "not include a formatting argument (such as `%%d`). "
+ "This is usually an internationalization error. See full issue "
+ "explanation for more.",
quantity, append);
context.report(IMPLIED_QUANTITY, child, context.getLocation(child), message);
}
}
if (relevant.equals(defined)) {
return;
}
// Look for missing
EnumSet<Quantity> missing = relevant.clone();
missing.removeAll(defined);
if (!missing.isEmpty()) {
String message = String.format(
"For locale %1$s the following quantities should also be defined: %2$s",
TranslationDetector.getLanguageDescription(language),
Quantity.formatSet(missing));
context.report(MISSING, element, context.getLocation(element), message);
}
// Look for irrelevant
EnumSet<Quantity> extra = defined.clone();
extra.removeAll(relevant);
if (!extra.isEmpty()) {
String message = String.format(
"For language %1$s the following quantities are not relevant: %2$s",
TranslationDetector.getLanguageDescription(language),
Quantity.formatSet(extra));
context.report(EXTRA, element, context.getLocation(element), message);
}
}
/**
* Returns true if the given string/plurals item element contains a formatting parameter,
* possibly within HTML markup or xliff metadata tags
*/
private static boolean haveFormattingParameter(@NonNull Element element) {
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
short nodeType = child.getNodeType();
if (nodeType == Node.ELEMENT_NODE) {
if (haveFormattingParameter((Element)child)) {
return true;
}
} else if (nodeType == Node.TEXT_NODE) {
String text = child.getNodeValue();
if (text.indexOf('%') == -1) {
continue;
}
if (StringFormatDetector.getFormatArgumentCount(text, null) >= 1) {
return true;
}
}
}
return false;
}
}
@@ -1,107 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.util.EnumSet;
/**
* Looks for packaged private key files.
*/
public class PrivateKeyDetector extends Detector implements Detector.OtherFileScanner {
/** Packaged private key files */
public static final Issue ISSUE = Issue.create(
"PackagedPrivateKey", //$NON-NLS-1$
"Packaged private key",
"In general, you should not package private key files inside your app.",
Category.SECURITY,
8,
Severity.FATAL,
new Implementation(PrivateKeyDetector.class, Scope.OTHER_SCOPE));
/** Constructs a new {@link PrivateKeyDetector} check */
public PrivateKeyDetector() {
}
private static boolean isPrivateKeyFile(File file) {
if (!file.isFile() ||
(!LintUtils.endsWith(file.getPath(), "pem") && //NON-NLS-1$
!LintUtils.endsWith(file.getPath(), "key"))) { //NON-NLS-1$
return false;
}
try {
String firstLine = Files.readFirstLine(file, Charsets.US_ASCII);
return firstLine != null &&
firstLine.startsWith("---") && //NON-NLS-1$
firstLine.contains("PRIVATE KEY"); //NON-NLS-1$
} catch (IOException ex) {
// Don't care
}
return false;
}
// ---- Implements OtherFileScanner ----
@NonNull
@Override
public EnumSet<Scope> getApplicableFiles() {
return Scope.OTHER_SCOPE;
}
@Override
public void run(@NonNull Context context) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
File file = context.file;
if (isPrivateKeyFile(file)) {
String fileName = file.getParentFile().getName() + File.separator
+ file.getName();
String message = String.format(
"The `%1$s` file seems to be a private key file. " +
"Please make sure not to embed this in your APK file.", fileName);
context.report(ISSUE, Location.create(file), message);
}
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
}
@@ -1,166 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.PROGUARD_CONFIG;
import static com.android.SdkConstants.PROJECT_PROPERTIES;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import java.io.File;
/**
* Check which looks for errors in Proguard files.
*/
public class ProguardDetector extends Detector {
private static final Implementation IMPLEMENTATION = new Implementation(ProguardDetector.class,
Scope.PROGUARD_SCOPE);
/** The main issue discovered by this detector */
public static final Issue WRONG_KEEP = Issue.create(
"Proguard", //$NON-NLS-1$
"Using obsolete ProGuard configuration",
"Using `-keepclasseswithmembernames` in a proguard config file is not " +
"correct; it can cause some symbols to be renamed which should not be.\n" +
"Earlier versions of ADT used to create proguard.cfg files with the " +
"wrong format. Instead of `-keepclasseswithmembernames` use " +
"`-keepclasseswithmembers`, since the old flags also implies " +
"\"allow shrinking\" which means symbols only referred to from XML and " +
"not Java (such as possibly CustomViews) can get deleted.",
Category.CORRECTNESS,
8,
Severity.FATAL,
IMPLEMENTATION)
.addMoreInfo(
"http://http://code.google.com/p/android/issues/detail?id=16384"); //$NON-NLS-1$
/** Finds ProGuard files that contain non-project specific configuration
* locally and suggests replacing it with an include path */
public static final Issue SPLIT_CONFIG = Issue.create(
"ProguardSplit", //$NON-NLS-1$
"Proguard.cfg file contains generic Android rules",
"Earlier versions of the Android tools bundled a single `proguard.cfg` file " +
"containing a ProGuard configuration file suitable for Android shrinking and " +
"obfuscation. However, that version was copied into new projects, which " +
"means that it does not continue to get updated as we improve the default " +
"ProGuard rules for Android.\n" +
"\n" +
"In the new version of the tools, we have split the ProGuard configuration " +
"into two halves:\n" +
"* A simple configuration file containing only project-specific flags, in " +
"your project\n" +
"* A generic configuration file containing the recommended set of ProGuard " +
"options for Android projects. This generic file lives in the SDK install " +
"directory which means that it gets updated along with the tools.\n" +
"\n" +
"In order for this to work, the proguard.config property in the " +
"`project.properties` file now refers to a path, so you can reference both " +
"the generic file as well as your own (and any additional files too).\n" +
"\n" +
"To migrate your project to the new setup, create a new `proguard-project.txt` file " +
"in your project containing any project specific ProGuard flags as well as " +
"any customizations you have made, then update your project.properties file " +
"to contain:\n" +
"`proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt`",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION);
@Override
public void run(@NonNull Context context) {
String contents = context.getContents();
if (contents != null) {
if (context.isEnabled(WRONG_KEEP)) {
int index = contents.indexOf(
// Old pattern:
"-keepclasseswithmembernames class * {\n" + //$NON-NLS-1$
" public <init>(android."); //$NON-NLS-1$
if (index != -1) {
context.report(WRONG_KEEP,
Location.create(context.file, contents, index, index),
"Obsolete ProGuard file; use `-keepclasseswithmembers` instead of " +
"`-keepclasseswithmembernames`");
}
}
if (context.isEnabled(SPLIT_CONFIG)) {
int index = contents.indexOf("-keep public class * extends android.app.Activity");
if (index != -1) {
// Only complain if project.properties actually references this file;
// no need to bother the users who got a default proguard.cfg file
// when they created their projects but haven't actually hooked it up
// to shrinking & obfuscation.
File propertyFile = new File(context.file.getParentFile(), PROJECT_PROPERTIES);
if (!propertyFile.exists()) {
return;
}
String properties = context.getClient().readFile(propertyFile);
int i = properties.indexOf(PROGUARD_CONFIG);
if (i == -1) {
return;
}
// Make sure the entry isn't just commented out, such as
// # To enable ProGuard to shrink and obfuscate your code, uncomment this:
// #proguard.config=proguard.cfg
for (; i >= 0; i--) {
char c = properties.charAt(i);
if (c == '#') {
return;
}
if (c == '\n') {
break;
}
}
if (properties.contains(PROGUARD_CONFIG)) {
context.report(SPLIT_CONFIG,
Location.create(context.file, contents, index, index),
String.format(
"Local ProGuard configuration contains general Android " +
"configuration: Inherit these settings instead? " +
"Modify `project.properties` to define " +
"`proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:%1$s`" +
" and then keep only project-specific configuration here",
context.file.getName()));
}
}
}
}
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
}
@@ -1,193 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.DOT_PROPERTIES;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.TextFormat;
import com.google.common.base.Splitter;
import java.io.File;
import java.util.Iterator;
/**
* Check for errors in .property files
* <p>
* TODO: Warn about bad paths like sdk properties with ' in the path, or suffix of " " etc
*/
public class PropertyFileDetector extends Detector {
/** Property file not escaped */
public static final Issue ESCAPE = Issue.create(
"PropertyEscape", //$NON-NLS-1$
"Incorrect property escapes",
"All backslashes and colons in .property files must be escaped with " +
"a backslash (\\). This means that when writing a Windows path, you " +
"must escape the file separators, so the path \\My\\Files should be " +
"written as `key=\\\\My\\\\Files.`",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(
PropertyFileDetector.class,
Scope.PROPERTY_SCOPE));
/** Using HTTP instead of HTTPS for the wrapper */
public static final Issue HTTP = Issue.create(
"UsingHttp", //$NON-NLS-1$
"Using HTTP instead of HTTPS",
"The Gradle Wrapper is available both via HTTP and HTTPS. HTTPS is more " +
"secure since it protects against man-in-the-middle attacks etc. Older " +
"projects created in Android Studio used HTTP but we now default to HTTPS " +
"and recommend upgrading existing projects.",
Category.SECURITY,
6,
Severity.WARNING,
new Implementation(
PropertyFileDetector.class,
Scope.PROPERTY_SCOPE));
/** Constructs a new {@link PropertyFileDetector} */
public PropertyFileDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return file.getPath().endsWith(DOT_PROPERTIES);
}
@Override
public void run(@NonNull Context context) {
String contents = context.getContents();
if (contents == null) {
return;
}
int offset = 0;
Iterator<String> iterator = Splitter.on('\n').split(contents).iterator();
String line;
for (; iterator.hasNext(); offset += line.length() + 1) {
line = iterator.next();
if (line.startsWith("#") || line.startsWith(" ")) {
continue;
}
if (line.indexOf('\\') == -1 && line.indexOf(':') == -1) {
continue;
}
int valueStart = line.indexOf('=') + 1;
if (valueStart == 0) {
continue;
}
checkLine(context, contents, line, offset, valueStart);
}
}
private static void checkLine(@NonNull Context context, @NonNull String contents,
@NonNull String line, int offset, int valueStart) {
String prefix = "distributionUrl=http\\";
if (line.startsWith(prefix)) {
String https = "https" + line.substring(prefix.length() - 1);
String message = String.format("Replace HTTP with HTTPS for better security; use %1$s",
https);
int startOffset = offset + valueStart;
int endOffset = startOffset + 4; // 4: "http".length()
Location location = Location.create(context.file, contents, startOffset, endOffset);
context.report(HTTP, location, message);
}
boolean escaped = false;
boolean hadNonPathEscape = false;
int errorStart = -1;
int errorEnd = -1;
StringBuilder path = new StringBuilder();
for (int i = valueStart; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '\\') {
escaped = !escaped;
if (escaped) {
path.append(c);
}
} else if (c == ':') {
if (!escaped) {
hadNonPathEscape = true;
if (errorStart < 0) {
errorStart = i;
}
errorEnd = i;
} else {
escaped = false;
}
path.append(c);
} else {
if (escaped) {
hadNonPathEscape = true;
if (errorStart < 0) {
errorStart = i;
}
errorEnd = i;
}
escaped = false;
path.append(c);
}
}
String pathString = path.toString();
String key = line.substring(0, valueStart);
if (hadNonPathEscape && key.endsWith(".dir=") || new File(pathString).exists()) {
String escapedPath = suggestEscapes(line.substring(valueStart, line.length()));
// NOTE: Keep in sync with {@link #getSuggestedEscape} below
String message = "Windows file separators (`\\`) and drive letter "
+ "separators (':') must be escaped (`\\\\`) in property files; use "
+ escapedPath;
int startOffset = offset + errorStart;
int endOffset = offset + errorEnd + 1;
Location location = Location.create(context.file, contents, startOffset,
endOffset);
context.report(ESCAPE, location, message);
}
}
@NonNull
static String suggestEscapes(@NonNull String value) {
value = value.replace("\\:", ":").replace("\\\\", "\\");
return LintUtils.escapePropertyValue(value);
}
/**
* Returns the escaped string value suggested by the error message which should have
* been computed by this lint detector.
*
* @param message the error message created by this lint detector
* @param format the format of the error message
* @return the suggested escaped value
*/
@Nullable
public static String getSuggestedEscape(@NonNull String message, @NonNull TextFormat format) {
return LintUtils.findSubstring(format.toText(message), "; use ", null);
}
}
@@ -1,383 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_TEXT_SIZE;
import static com.android.SdkConstants.DIMEN_PREFIX;
import static com.android.SdkConstants.TAG_DIMEN;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STYLE;
import static com.android.SdkConstants.UNIT_DIP;
import static com.android.SdkConstants.UNIT_DP;
import static com.android.SdkConstants.UNIT_IN;
import static com.android.SdkConstants.UNIT_MM;
import static com.android.SdkConstants.UNIT_PX;
import static com.android.SdkConstants.UNIT_SP;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
/**
* Check for px dimensions instead of dp dimensions.
* Also look for non-"sp" text sizes.
*/
public class PxUsageDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
PxUsageDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Using px instead of dp */
public static final Issue PX_ISSUE = Issue.create(
"PxUsage", //$NON-NLS-1$
"Using 'px' dimension",
// This description is from the below screen support document
"For performance reasons and to keep the code simpler, the Android system uses pixels " +
"as the standard unit for expressing dimension or coordinate values. That means that " +
"the dimensions of a view are always expressed in the code using pixels, but " +
"always based on the current screen density. For instance, if `myView.getWidth()` " +
"returns 10, the view is 10 pixels wide on the current screen, but on a device with " +
"a higher density screen, the value returned might be 15. If you use pixel values " +
"in your application code to work with bitmaps that are not pre-scaled for the " +
"current screen density, you might need to scale the pixel values that you use in " +
"your code to match the un-scaled bitmap source.",
Category.CORRECTNESS,
2,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/guide/practices/screens_support.html#screen-independence"); //$NON-NLS-1$
/** Using mm/in instead of dp */
public static final Issue IN_MM_ISSUE = Issue.create(
"InOrMmUsage", //$NON-NLS-1$
"Using `mm` or `in` dimensions",
"Avoid using `mm` (millimeters) or `in` (inches) as the unit for dimensions.\n" +
"\n" +
"While it should work in principle, unfortunately many devices do not report " +
"the correct true physical density, which means that the dimension calculations " +
"won't work correctly. You are better off using `dp` (and for font sizes, `sp`.)",
Category.CORRECTNESS,
4,
Severity.WARNING,
IMPLEMENTATION);
/** Using sp instead of dp */
public static final Issue DP_ISSUE = Issue.create(
"SpUsage", //$NON-NLS-1$
"Using `dp` instead of `sp` for text sizes",
"When setting text sizes, you should normally use `sp`, or \"scale-independent " +
"pixels\". This is like the `dp` unit, but it is also scaled " +
"by the user's font size preference. It is recommend you use this unit when " +
"specifying font sizes, so they will be adjusted for both the screen density " +
"and the user's preference.\n" +
"\n" +
"There *are* cases where you might need to use `dp`; typically this happens when " +
"the text is in a container with a specific dp-size. This will prevent the text " +
"from spilling outside the container. Note however that this means that the user's " +
"font size settings are not respected, so consider adjusting the layout itself " +
"to be more flexible.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/training/multiscreen/screendensities.html"); //$NON-NLS-1$
/** Using text sizes that are too small */
public static final Issue SMALL_SP_ISSUE = Issue.create(
"SmallSp", //$NON-NLS-1$
"Text size is too small",
"Avoid using sizes smaller than 12sp.",
Category.USABILITY,
4,
Severity.WARNING,
IMPLEMENTATION);
private HashMap<String, Location.Handle> mTextSizeUsage;
/** Constructs a new {@link PxUsageDetector} */
public PxUsageDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
// Look in both layouts (at attribute values) and in value files (at style definitions)
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableAttributes() {
return ALL;
}
@Override
@Nullable
public Collection<String> getApplicableElements() {
return Collections.singletonList(TAG_STYLE);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (context.getResourceFolderType() != ResourceFolderType.LAYOUT) {
assert context.getResourceFolderType() == ResourceFolderType.VALUES;
if (mTextSizeUsage != null
&& attribute.getOwnerElement().getTagName().equals(TAG_DIMEN)) {
Element element = attribute.getOwnerElement();
String name = element.getAttribute(ATTR_NAME);
if (name != null && mTextSizeUsage.containsKey(name)
&& context.isEnabled(DP_ISSUE)) {
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.TEXT_NODE &&
isDpUnit(child.getNodeValue())) {
String message = "This dimension is used as a text size: "
+ "Should use \"`sp`\" instead of \"`dp`\"";
Location location = context.getLocation(child);
Location secondary = mTextSizeUsage.get(name).resolve();
secondary.setMessage("Dimension used as a text size here");
location.setSecondary(secondary);
context.report(DP_ISSUE, attribute, location, message);
break;
}
}
}
}
return;
}
String value = attribute.getValue();
if (value.endsWith(UNIT_PX) && value.matches("\\d+px")) { //$NON-NLS-1$
if (value.charAt(0) == '0' || value.equals("1px")) { //$NON-NLS-1$
// 0px is fine. 0px is 0dp regardless of density...
// Similarly, 1px is typically used to create a single thin line (see issue 55722)
return;
}
if (context.isEnabled(PX_ISSUE)) {
context.report(PX_ISSUE, attribute, context.getLocation(attribute),
"Avoid using \"`px`\" as units; use \"`dp`\" instead");
}
} else if (value.endsWith(UNIT_MM) && value.matches("\\d+mm") //$NON-NLS-1$
|| value.endsWith(UNIT_IN) && value.matches("\\d+in")) { //$NON-NLS-1$
if (value.charAt(0) == '0') {
// 0mm == 0in == 0dp
return;
}
if (context.isEnabled(IN_MM_ISSUE)) {
String unit = value.substring(value.length() - 2);
context.report(IN_MM_ISSUE, attribute, context.getLocation(attribute),
String.format("Avoid using \"`%1$s`\" as units " +
"(it does not work accurately on all devices); use \"`dp`\" instead",
unit));
}
} else if (value.endsWith(UNIT_SP)
&& (ATTR_TEXT_SIZE.equals(attribute.getLocalName())
|| ATTR_LAYOUT_HEIGHT.equals(attribute.getLocalName()))
&& value.matches("\\d+sp")) { //$NON-NLS-1$
int size = getSize(value);
if (size > 0 && size < 12) {
context.report(SMALL_SP_ISSUE, attribute, context.getLocation(attribute),
String.format("Avoid using sizes smaller than `12sp`: `%1$s`", value));
}
} else if (ATTR_TEXT_SIZE.equals(attribute.getLocalName())) {
if (isDpUnit(value)) { //$NON-NLS-1$
if (context.isEnabled(DP_ISSUE)) {
context.report(DP_ISSUE, attribute, context.getLocation(attribute),
"Should use \"`sp`\" instead of \"`dp`\" for text sizes");
}
} else if (value.startsWith(DIMEN_PREFIX)) {
if (context.getClient().supportsProjectResources()) {
LintClient client = context.getClient();
Project project = context.getProject();
AbstractResourceRepository resources = client.getProjectResources(project,
true);
ResourceUrl url = ResourceUrl.parse(value);
if (resources != null && url != null) {
List<ResourceItem> items = resources.getResourceItem(url.type, url.name);
if (items != null) {
for (ResourceItem item : items) {
ResourceValue resourceValue = item.getResourceValue(false);
if (resourceValue != null) {
String dimenValue = resourceValue.getValue();
if (dimenValue != null && isDpUnit(dimenValue)
&& context.isEnabled(DP_ISSUE)) {
ResourceFile sourceFile = item.getSource();
assert sourceFile != null;
String message = String.format(
"Should use \"`sp`\" instead of \"`dp`\" for text sizes (`%1$s` is defined as `%2$s` in `%3$s`",
value, dimenValue, sourceFile.getFile());
context.report(DP_ISSUE, attribute,
context.getLocation(attribute),
message);
break;
}
}
}
}
}
} else {
ResourceUrl url = ResourceUrl.parse(value);
if (url != null) {
if (mTextSizeUsage == null) {
mTextSizeUsage = new HashMap<String, Location.Handle>();
}
Location.Handle handle = context.createLocationHandle(attribute);
mTextSizeUsage.put(url.name, handle);
}
}
}
}
}
private static boolean isDpUnit(String value) {
return (value.endsWith(UNIT_DP) || value.endsWith(UNIT_DIP))
&& (value.matches("\\d+di?p"));
}
private static int getSize(String text) {
assert text.matches("\\d+sp") : text; //$NON-NLS-1$
return Integer.parseInt(text.substring(0, text.length() - 2));
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (context.getResourceFolderType() != ResourceFolderType.VALUES) {
return;
}
assert element.getTagName().equals(TAG_STYLE);
NodeList itemNodes = element.getChildNodes();
for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
Node item = itemNodes.item(j);
if (item.getNodeType() == Node.ELEMENT_NODE &&
TAG_ITEM.equals(item.getNodeName())) {
Element itemElement = (Element) item;
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() != Node.TEXT_NODE) {
return;
}
checkStyleItem(context, itemElement, child);
}
}
}
}
private static void checkStyleItem(XmlContext context, Element item, Node textNode) {
String text = textNode.getNodeValue();
for (int j = text.length() - 1; j > 0; j--) {
char c = text.charAt(j);
if (!Character.isWhitespace(c)) {
if (c == 'x' && text.charAt(j - 1) == 'p') { // ends with px
text = text.trim();
if (text.matches("\\d+px") && text.charAt(0) != '0' && //$NON-NLS-1$
!text.equals("1px")) { //$NON-NLS-1$
if (context.isEnabled(PX_ISSUE)) {
context.report(PX_ISSUE, item, context.getLocation(textNode),
"Avoid using `\"px\"` as units; use `\"dp\"` instead");
}
}
} else if (c == 'm' && text.charAt(j - 1) == 'm' ||
c == 'n' && text.charAt(j - 1) == 'i') {
text = text.trim();
String unit = text.substring(text.length() - 2);
if (text.matches("\\d+" + unit) && text.charAt(0) != '0') { //$NON-NLS-1$
if (context.isEnabled(IN_MM_ISSUE)) {
context.report(IN_MM_ISSUE, item, context.getLocation(textNode),
String.format("Avoid using \"`%1$s`\" as units "
+ "(it does not work accurately on all devices); "
+ "use \"`dp`\" instead", unit));
}
}
} else if (c == 'p' && (text.charAt(j - 1) == 'd'
|| text.charAt(j - 1) == 'i')) { // ends with dp or di
text = text.trim();
String name = item.getAttribute(ATTR_NAME);
if ((name.equals(ATTR_TEXT_SIZE)
|| name.equals("android:textSize")) //$NON-NLS-1$
&& text.matches("\\d+di?p")) { //$NON-NLS-1$
if (context.isEnabled(DP_ISSUE)) {
context.report(DP_ISSUE, item, context.getLocation(textNode),
"Should use \"`sp`\" instead of \"`dp`\" for text sizes");
}
}
} else if (c == 'p' && text.charAt(j - 1) == 's') {
String name = item.getAttribute(ATTR_NAME);
if (ATTR_TEXT_SIZE.equals(name) || ATTR_LAYOUT_HEIGHT.equals(name)) {
text = text.trim();
String unit = text.substring(text.length() - 2);
if (text.matches("\\d+" + unit)) { //$NON-NLS-1$
if (context.isEnabled(SMALL_SP_ISSUE)) {
int size = getSize(text);
if (size > 0 && size < 12) {
context.report(SMALL_SP_ISSUE, item,
context.getLocation(textNode), String.format(
"Avoid using sizes smaller than `12sp`: `%1$s`",
text));
}
}
}
}
}
break;
}
}
}
}
@@ -1,412 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_END_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_START_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.ATTR_VISIBILITY;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.PREFIX_THEME_REF;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
import static com.android.SdkConstants.VIEW;
import static com.android.SdkConstants.VIEW_INCLUDE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* Check for potential item overlaps in a RelativeLayout when left- and
* right-aligned text items are used.
*/
public class RelativeOverlapDetector extends LayoutDetector {
public static final Issue ISSUE = Issue.create(
"RelativeOverlap",
"Overlapping items in RelativeLayout",
"If relative layout has text or button items aligned to left and right " +
"sides they can overlap each other due to localized text expansion " +
"unless they have mutual constraints like `toEndOf`/`toStartOf`.",
Category.I18N, 3, Severity.WARNING,
new Implementation(RelativeOverlapDetector.class, Scope.RESOURCE_FILE_SCOPE));
private static class LayoutNode {
private enum Bucket {
TOP, BOTTOM, SKIP
}
private int mIndex;
private boolean mProcessed;
private Element mNode;
private Bucket mBucket;
private LayoutNode mToLeft;
private LayoutNode mToRight;
private boolean mLastLeft;
private boolean mLastRight;
public LayoutNode(@NonNull Element node, int index) {
mNode = node;
mIndex = index;
mProcessed = false;
mLastLeft = true;
mLastRight = true;
}
@NonNull
public String getNodeId() {
String nodeid = mNode.getAttributeNS(ANDROID_URI, ATTR_ID);
if (nodeid.isEmpty()) {
return String.format("%1$s-%2$d", mNode.getTagName(), mIndex);
} else {
return uniformId(nodeid);
}
}
@NonNull
public String getNodeTextId() {
String text = mNode.getAttributeNS(ANDROID_URI, ATTR_TEXT);
if (text.isEmpty()) {
return getNodeId();
} else {
return uniformId(text);
}
}
@NonNull
@Override
public String toString() {
return getNodeTextId();
}
public boolean isInvisible() {
String visibility = mNode.getAttributeNS(ANDROID_URI,
ATTR_VISIBILITY);
return visibility.equals("gone") || visibility.equals("invisible");
}
/**
* Determine if not can grow due to localization or not.
*/
public boolean fixedWidth() {
String width = mNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
if (width.equals(VALUE_WRAP_CONTENT)) {
// First check child nodes. If at least one of them is not
// fixed-width,
// treat whole layout as non-fixed-width
NodeList childNodes = mNode.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
LayoutNode childLayout = new LayoutNode((Element) child,
i);
if (!childLayout.fixedWidth()) {
return false;
}
}
}
// If node contains text attribute, consider it fixed-width if
// text is hard-coded, otherwise it is not fixed-width.
String text = mNode.getAttributeNS(ANDROID_URI, ATTR_TEXT);
if (!text.isEmpty()) {
return !text.startsWith(PREFIX_RESOURCE_REF)
&& !text.startsWith(PREFIX_THEME_REF);
}
String nodeName = mNode.getTagName();
if (nodeName.contains("Image") || nodeName.contains("Progress")
|| nodeName.contains("Radio")) {
return true;
} else if (nodeName.contains("Button")
|| nodeName.contains("Text")) {
return false;
}
}
return true;
}
@NonNull
public Element getNode() {
return mNode;
}
/**
* Process a node of a layout. Put it into one of three processing
* units and determine its right and left neighbours.
*/
public void processNode(@NonNull Map<String, LayoutNode> nodes) {
if (mProcessed) {
return;
}
mProcessed = true;
if (isInvisible() ||
hasAttr(ATTR_LAYOUT_ALIGN_RIGHT) ||
hasAttr(ATTR_LAYOUT_ALIGN_END) ||
hasAttr(ATTR_LAYOUT_ALIGN_LEFT) ||
hasAttr(ATTR_LAYOUT_ALIGN_START)) {
mBucket = Bucket.SKIP;
} else if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_TOP)) {
mBucket = Bucket.TOP;
} else if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) {
mBucket = Bucket.BOTTOM;
} else {
if (hasAttr(ATTR_LAYOUT_ABOVE) || hasAttr(ATTR_LAYOUT_BELOW)) {
mBucket = Bucket.SKIP;
} else {
String[] checkAlignment = { ATTR_LAYOUT_ALIGN_TOP,
ATTR_LAYOUT_ALIGN_BOTTOM,
ATTR_LAYOUT_ALIGN_BASELINE };
for (String alignment : checkAlignment) {
String value = mNode.getAttributeNS(ANDROID_URI,
alignment);
if (!value.isEmpty()) {
LayoutNode otherNode = nodes.get(uniformId(value));
if (otherNode != null) {
otherNode.processNode(nodes);
mBucket = otherNode.mBucket;
}
}
}
}
}
if (mBucket == null) {
mBucket = Bucket.TOP;
}
// Check relative placement
boolean positioned = false;
mToLeft = findNodeByAttr(nodes, ATTR_LAYOUT_TO_START_OF);
if (mToLeft == null) {
mToLeft = findNodeByAttr(nodes, ATTR_LAYOUT_TO_LEFT_OF);
}
// Avoid circular dependency
for (LayoutNode n = mToLeft; n != null; n = n.mToLeft) {
if (n.equals(this)) {
mToLeft = null;
mBucket = Bucket.SKIP;
break;
}
}
if (mToLeft != null) {
mToLeft.mLastLeft = false;
mLastRight = false;
positioned = true;
}
mToRight = findNodeByAttr(nodes, ATTR_LAYOUT_TO_END_OF);
if (mToRight == null) {
mToRight = findNodeByAttr(nodes, ATTR_LAYOUT_TO_RIGHT_OF);
}
// Avoid circular dependency
for (LayoutNode n = mToRight; n != null; n = n.mToRight) {
if (n.equals(this)) {
mToRight = null;
mBucket = Bucket.SKIP;
break;
}
}
if (mToRight != null) {
mToRight.mLastRight = false;
mLastLeft = false;
positioned = true;
}
if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_END)
|| hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
mLastRight = false;
positioned = true;
}
if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_START)
|| hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
mLastLeft = false;
positioned = true;
}
// Treat any node that does not have explicit relative placement
// same as if it has layout_alignParentStart = true;
if (!positioned) {
mLastLeft = false;
}
}
@NonNull
public Set<LayoutNode> canGrowLeft() {
Set<LayoutNode> nodes;
if (mToRight != null) {
nodes = mToRight.canGrowLeft();
} else {
nodes = new LinkedHashSet<LayoutNode>();
}
if (!fixedWidth()) {
nodes.add(this);
}
return nodes;
}
@NonNull
public Set<LayoutNode> canGrowRight() {
Set<LayoutNode> nodes;
if (mToLeft != null) {
nodes = mToLeft.canGrowRight();
} else {
nodes = new LinkedHashSet<LayoutNode>();
}
if (!fixedWidth()) {
nodes.add(this);
}
return nodes;
}
/**
* Determines if not should be skipped from checking.
*/
public boolean skip() {
if (mBucket == Bucket.SKIP) {
return true;
}
// Skip all includes and Views
return mNode.getTagName().equals(VIEW_INCLUDE)
|| mNode.getTagName().equals(VIEW);
}
public boolean sameBucket(@NonNull LayoutNode node) {
return mBucket == node.mBucket;
}
@Nullable
private LayoutNode findNodeByAttr(
@NonNull Map<String, LayoutNode> nodes,
@NonNull String attrName) {
String value = mNode.getAttributeNS(ANDROID_URI, attrName);
if (!value.isEmpty()) {
return nodes.get(uniformId(value));
} else {
return null;
}
}
private boolean hasAttr(@NonNull String key) {
return mNode.hasAttributeNS(ANDROID_URI, key);
}
private boolean hasTrueAttr(@NonNull String key) {
return mNode.getAttributeNS(ANDROID_URI, key).equals(VALUE_TRUE);
}
@NonNull
private static String uniformId(@NonNull String value) {
return value.replaceFirst("@\\+", "@");
}
}
public RelativeOverlapDetector() {
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(RELATIVE_LAYOUT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
// Traverse all child elements
NodeList childNodes = element.getChildNodes();
int count = childNodes.getLength();
Map<String, LayoutNode> nodes = Maps.newHashMap();
for (int i = 0; i < count; i++) {
Node node = childNodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
LayoutNode ln = new LayoutNode((Element) node, i);
nodes.put(ln.getNodeId(), ln);
}
}
// Node map is populated, recalculate nodes sizes
for (LayoutNode ln : nodes.values()) {
ln.processNode(nodes);
}
for (LayoutNode right : nodes.values()) {
if (!right.mLastLeft || right.skip()) {
continue;
}
Set<LayoutNode> canGrowLeft = right.canGrowLeft();
for (LayoutNode left : nodes.values()) {
if (left == right || !left.mLastRight || left.skip()
|| !left.sameBucket(right)) {
continue;
}
Set<LayoutNode> canGrowRight = left.canGrowRight();
if (!canGrowLeft.isEmpty() || !canGrowRight.isEmpty()) {
canGrowRight.addAll(canGrowLeft);
LayoutNode nodeToBlame = right;
LayoutNode otherNode = left;
if (!canGrowRight.contains(right)
&& canGrowRight.contains(left)) {
nodeToBlame = left;
otherNode = right;
}
context.report(ISSUE, nodeToBlame.getNode(),
context.getLocation(nodeToBlame.getNode()),
String.format(
"`%1$s` can overlap `%2$s` if %3$s %4$s due to localized text expansion",
nodeToBlame.getNodeId(), otherNode.getNodeId(),
Joiner.on(", ").join(canGrowRight),
canGrowRight.size() > 1 ? "grow" : "grows"));
}
}
}
}
}
@@ -1,551 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_COLOR;
import static com.android.SdkConstants.ATTR_DRAWABLE;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_PARENT;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.COLOR_RESOURCE_PREFIX;
import static com.android.SdkConstants.DRAWABLE_PREFIX;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.TAG_COLOR;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STYLE;
import static com.android.SdkConstants.VIEW_INCLUDE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Checks for cycles in resource definitions
*/
public class ResourceCycleDetector extends ResourceXmlDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
ResourceCycleDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Style parent cycles, resource alias cycles, layout include cycles, etc */
public static final Issue CYCLE = Issue.create(
"ResourceCycle", //$NON-NLS-1$
"Cycle in resource definitions",
"There should be no cycles in resource definitions as this can lead to runtime " +
"exceptions.",
Category.CORRECTNESS,
8,
Severity.FATAL,
IMPLEMENTATION
);
/** Parent cycles */
public static final Issue CRASH = Issue.create(
"AaptCrash", //$NON-NLS-1$
"Potential AAPT crash",
"Defining a style which sets `android:id` to a dynamically generated id can cause " +
"many versions of `aapt`, the resource packaging tool, to crash. To work around " +
"this, declare the id explicitly with `<item type=\"id\" name=\"...\" />` instead.",
Category.CORRECTNESS,
8,
Severity.FATAL,
IMPLEMENTATION)
.addMoreInfo("https://code.google.com/p/android/issues/detail?id=20479"); //$NON-NLS-1$
/**
* For each resource type, a map from a key (style name, layout name, color name, etc) to
* a value (parent style, included layout, referenced color, etc). Note that we only initialize
* this if we are in "batch mode" (not editor incremental mode) since we allow this detector
* to also run incrementally to look for trivial chains (e.g. of length 1).
*/
private Map<ResourceType, Multimap<String, String>> mReferences;
/**
* If in batch analysis and cycles were found, in phase 2 this map should be initialized
* with locations for declaration definitions of the keys and values in {@link #mReferences}
*/
private Map<ResourceType, Multimap<String, Location>> mLocations;
/**
* If in batch analysis and cycles were found, for each resource type this is a list
* of chains (where each chain is a list of keys as described in {@link #mReferences})
*/
private Map<ResourceType, List<List<String>>> mChains;
/** Constructs a new {@link ResourceCycleDetector} */
public ResourceCycleDetector() {
}
@Override
public void beforeCheckProject(@NonNull Context context) {
// In incremental mode, or checking all files (full lint analysis) ? If the latter,
// we should store state and look for deeper cycles
if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
mReferences = Maps.newEnumMap(ResourceType.class);
}
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES
|| folderType == ResourceFolderType.COLOR
|| folderType == ResourceFolderType.DRAWABLE
|| folderType == ResourceFolderType.LAYOUT;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
VIEW_INCLUDE,
TAG_STYLE,
TAG_COLOR,
TAG_ITEM
);
}
private void recordReference(@NonNull ResourceType type, @NonNull String from,
@NonNull String to) {
if (to.isEmpty() || to.startsWith(ANDROID_PREFIX)) {
return;
}
assert mReferences != null;
Multimap<String, String> map = mReferences.get(type);
if (map == null) {
// Multimap which preserves insert order (for predictable output order)
map = Multimaps.newListMultimap(
new TreeMap<String, Collection<String>>(),
new Supplier<List<String>>() {
@Override
public List<String> get() {
return Lists.newArrayListWithExpectedSize(6);
}
});
mReferences.put(type, map);
}
if (to.charAt(0) == '@') {
int index = to.indexOf('/');
if (index != -1) {
to = to.substring(index + 1);
}
}
map.put(from, to);
}
private void recordLocation(@NonNull XmlContext context, @NonNull Node node,
@NonNull ResourceType type, @NonNull String from) {
assert mLocations != null;
// Cycles were already found; we're now in phase 2 looking up specific
// locations
Multimap<String, Location> map = mLocations.get(type);
if (map == null) {
map = ArrayListMultimap.create(30, 4);
mLocations.put(type, map);
}
Location location = context.getLocation(node);
map.put(from, location);
}
@SuppressWarnings("VariableNotUsedInsideIf")
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tagName = element.getTagName();
if (tagName.equals(TAG_ITEM)) {
if (mReferences == null) {
// Nothing to do in incremental mode
return;
}
ResourceFolderType folderType = context.getResourceFolderType();
if (folderType == ResourceFolderType.VALUES) {
// Aliases
Attr typeNode = element.getAttributeNode(ATTR_TYPE);
if (typeNode != null) {
String typeName = typeNode.getValue();
ResourceType type = ResourceType.getEnum(typeName);
Attr nameNode = element.getAttributeNode(ATTR_NAME);
if (type != null && nameNode != null) {
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
for (int k = 0, max = text.length(); k < max; k++) {
char c = text.charAt(k);
if (Character.isWhitespace(c)) {
break;
} else if (c == '@' &&
text.startsWith(type.getName(), k + 1)) {
String to = text.trim();
if (mReferences != null) {
String name = nameNode.getValue();
if (mLocations != null) {
recordLocation(context, child, type,
name);
} else {
recordReference(type, name, to);
}
}
} else {
break;
}
}
}
}
}
}
} else if (folderType == ResourceFolderType.COLOR ) {
String color = element.getAttributeNS(ANDROID_URI, ATTR_COLOR);
if (color != null && color.startsWith(COLOR_RESOURCE_PREFIX)) {
String currentColor = LintUtils.getBaseName(context.file.getName());
if (mLocations != null) {
recordLocation(context, element, ResourceType.COLOR,
currentColor);
} else {
recordReference(ResourceType.COLOR, currentColor,
color.substring(COLOR_RESOURCE_PREFIX.length()));
}
}
} else if (folderType == ResourceFolderType.DRAWABLE) {
String drawable = element.getAttributeNS(ANDROID_URI, ATTR_DRAWABLE);
if (drawable != null && drawable.startsWith(DRAWABLE_PREFIX)) {
String currentColor = LintUtils.getBaseName(context.file.getName());
if (mLocations != null) {
recordLocation(context, element, ResourceType.DRAWABLE,
currentColor);
} else {
recordReference(ResourceType.DRAWABLE, currentColor,
drawable.substring(DRAWABLE_PREFIX.length()));
}
}
}
} else if (tagName.equals(TAG_STYLE)) {
Attr nameNode = element.getAttributeNode(ATTR_NAME);
// Look for recursive style parent declarations
Attr parentNode = element.getAttributeNode(ATTR_PARENT);
if (parentNode != null && nameNode != null) {
String name = nameNode.getValue();
String parent = parentNode.getValue();
if (parent.endsWith(name) &&
parent.equals(STYLE_RESOURCE_PREFIX + name) && context.isEnabled(CYCLE)
&& context.getDriver().getPhase() == 1) {
context.report(CYCLE, parentNode, context.getLocation(parentNode),
String.format("Style `%1$s` should not extend itself", name));
} else if (parent.startsWith(STYLE_RESOURCE_PREFIX)
&& parent.startsWith(name, STYLE_RESOURCE_PREFIX.length())
&& parent.startsWith(".", STYLE_RESOURCE_PREFIX.length() + name.length())
&& context.isEnabled(CYCLE) && context.getDriver().getPhase() == 1) {
context.report(CYCLE, parentNode, context.getLocation(parentNode),
String.format("Potential cycle: `%1$s` is the implied parent of `%2$s` and " +
"this defines the opposite", name,
parent.substring(STYLE_RESOURCE_PREFIX.length())));
// Don't record this reference; we don't want to double report this
// as a chain, since this error is more helpful
return;
}
if (mReferences != null && !parent.isEmpty()) {
if (mLocations != null) {
recordLocation(context, parentNode, ResourceType.STYLE, name);
} else {
recordReference(ResourceType.STYLE, name, parent);
}
}
} else if (mReferences != null && nameNode != null) {
String name = nameNode.getValue();
int index = name.lastIndexOf('.');
if (index > 0) {
String parent = name.substring(0, index);
if (mReferences != null) {
if (mLocations != null) {
Attr node = element.getAttributeNode(ATTR_NAME);
recordLocation(context, node, ResourceType.STYLE, name);
} else {
recordReference(ResourceType.STYLE, name, parent);
}
}
}
}
if (context.isEnabled(CRASH) && context.getDriver().getPhase() == 1) {
for (Element item : LintUtils.getChildren(element)) {
if ("android:id".equals(item.getAttribute(ATTR_NAME))) {
checkCrashItem(context, item);
}
}
}
} else if (tagName.equals(VIEW_INCLUDE)) {
Attr layoutNode = element.getAttributeNode(ATTR_LAYOUT);
if (layoutNode != null) {
String layout = layoutNode.getValue();
if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) {
String currentLayout = LintUtils.getBaseName(context.file.getName());
if (mReferences != null) {
if (mLocations != null) {
recordLocation(context, layoutNode, ResourceType.LAYOUT,
currentLayout);
} else {
recordReference(ResourceType.LAYOUT, currentLayout, layout);
}
}
if (layout.startsWith(currentLayout, LAYOUT_RESOURCE_PREFIX.length()) &&
layout.length() == currentLayout.length()
+ LAYOUT_RESOURCE_PREFIX.length()
&& context.isEnabled(CYCLE)
&& context.getDriver().getPhase() == 1) {
String message = String.format("Layout `%1$s` should not include itself",
currentLayout);
context.report(CYCLE, layoutNode, context.getLocation(layoutNode),
message);
}
}
}
} else if (tagName.equals(TAG_COLOR)) {
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
for (int k = 0, max = text.length(); k < max; k++) {
char c = text.charAt(k);
if (Character.isWhitespace(c)) {
break;
} else if (text.startsWith(COLOR_RESOURCE_PREFIX, k)) {
String color = text.trim().substring(COLOR_RESOURCE_PREFIX.length());
String name = element.getAttribute(ATTR_NAME);
if (mReferences != null) {
if (mLocations != null) {
recordLocation(context, child, ResourceType.COLOR, name);
} else {
recordReference(ResourceType.COLOR, name, color);
}
}
if (color.equals(name)
&& context.isEnabled(CYCLE)
&& context.getDriver().getPhase() == 1) {
context.report(CYCLE, child, context.getLocation(child),
String.format("Color `%1$s` should not reference itself",
color));
}
} else {
break;
}
}
}
}
}
}
private static void checkCrashItem(@NonNull XmlContext context, @NonNull Element item) {
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
for (int k = 0, max = text.length(); k < max; k++) {
char c = text.charAt(k);
if (Character.isWhitespace(c)) {
return;
} else if (text.startsWith(NEW_ID_PREFIX, k)) {
String name = text.trim().substring(NEW_ID_PREFIX.length());
String message = "This construct can potentially crash `aapt` during a "
+ "build. Change `@+id/" + name + "` to `@id/" + name + "` and define "
+ "the id explicitly using "
+ "`<item type=\"id\" name=\"" + name + "\"/>` instead.";
context.report(CRASH, item, context.getLocation(item),
message);
} else {
return;
}
}
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mReferences == null) {
// Incremental analysis in a single file only; nothing to do
return;
}
int phase = context.getDriver().getPhase();
if (phase == 1) {
// Perform DFS of each resource type and look for cycles
for (Map.Entry<ResourceType, Multimap<String, String>> entry
: mReferences.entrySet()) {
ResourceType type = entry.getKey();
Multimap<String, String> map = entry.getValue();
findCycles(context, type, map);
}
} else {
assert phase == 2;
// Emit cycle report
for (Map.Entry<ResourceType, List<List<String>>> entry : mChains.entrySet()) {
ResourceType type = entry.getKey();
Multimap<String, Location> locations = mLocations.get(type);
if (locations == null) {
// No locations found. Unlikely.
locations = ArrayListMultimap.create();
}
List<List<String>> chains = entry.getValue();
for (List<String> chain : chains) {
Location location = null;
assert !chain.isEmpty();
for (int i = 0, n = chain.size(); i < n; i++) {
String item = chain.get(i);
Collection<Location> itemLocations = locations.get(item);
if (!itemLocations.isEmpty()) {
Location itemLocation = itemLocations.iterator().next();
String next = chain.get((i + 1) % chain.size());
String label = "Reference from @" + type.getName() + "/" + item
+ " to " + type.getName() + "/" + next + " here";
itemLocation.setMessage(label);
itemLocation.setSecondary(location);
location = itemLocation;
}
}
if (location == null) {
location = Location.create(context.getProject().getDir());
} else {
// Break off chain
Location curr = location.getSecondary();
while (curr != null) {
Location next = curr.getSecondary();
if (next == location) {
curr.setSecondary(null);
break;
}
curr = next;
}
}
String message = String.format("%1$s Resource definition cycle: %2$s",
type.getDisplayName(), Joiner.on(" => ").join(chain));
context.report(CYCLE, location, message);
}
}
}
}
private void findCycles(
@NonNull Context context,
@NonNull ResourceType type,
@NonNull Multimap<String, String> map) {
Set<String> visiting = Sets.newHashSetWithExpectedSize(map.size());
Set<String> seen = Sets.newHashSetWithExpectedSize(map.size());
for (String from : map.keySet()) {
if (seen.contains(from)) {
continue;
}
List<String> chain = dfs(map, from, visiting);
if (chain != null && chain.size() > 2) { // size 1 chains are handled directly
seen.addAll(chain);
Collections.reverse(chain);
if (mChains == null) {
mChains = Maps.newEnumMap(ResourceType.class);
mLocations = Maps.newEnumMap(ResourceType.class);
context.getDriver().requestRepeat(this, Scope.RESOURCE_FILE_SCOPE);
}
List<List<String>> list = mChains.get(type);
if (list == null) {
list = Lists.newArrayList();
mChains.put(type, list);
}
list.add(chain);
}
}
}
// ----- Cycle detection -----
@Nullable
private static List<String> dfs(
@NonNull Multimap<String, String> map,
@NonNull String from,
@NonNull Set<String> visiting) {
visiting.add(from);
Collection<String> targets = map.get(from);
if (targets != null && !targets.isEmpty()) {
for (String target : targets) {
if (visiting.contains(target)) {
List<String> chain = Lists.newArrayList();
chain.add(target);
chain.add(from);
return chain;
}
List<String> chain = dfs(map, target, visiting);
if (chain != null) {
chain.add(from);
return chain;
}
}
}
visiting.remove(from);
return null;
}
}
@@ -1,197 +0,0 @@
/*
* 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.klint.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.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.ResourceContext;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.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 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));
}
}
}
}
}
@@ -1,101 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Check which looks at the children of ScrollViews and ensures that they fill/match
* the parent width instead of setting wrap_content.
*/
public class ScrollViewChildDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"ScrollViewSize", //$NON-NLS-1$
"ScrollView size validation",
// TODO add a better explanation here!
"ScrollView children must set their `layout_width` or `layout_height` attributes " +
"to `wrap_content` rather than `fill_parent` or `match_parent` in the scrolling " +
"dimension",
Category.CORRECTNESS,
7,
Severity.WARNING,
new Implementation(
ScrollViewChildDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link ScrollViewChildDetector} */
public ScrollViewChildDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
SCROLL_VIEW,
HORIZONTAL_SCROLL_VIEW
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
List<Element> children = LintUtils.getChildren(element);
boolean isHorizontal = HORIZONTAL_SCROLL_VIEW.equals(element.getTagName());
String attributeName = isHorizontal ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT;
for (Element child : children) {
Attr sizeNode = child.getAttributeNodeNS(ANDROID_URI, attributeName);
if (sizeNode == null) {
return;
}
String value = sizeNode.getValue();
if (VALUE_FILL_PARENT.equals(value) || VALUE_MATCH_PARENT.equals(value)) {
String msg = String.format("This %1$s should use `android:%2$s=\"wrap_content\"`",
child.getTagName(), attributeName);
context.report(ISSUE, sizeNode, context.getLocation(sizeNode), msg);
}
}
}
}
@@ -1,161 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;
import java.util.Collections;
import java.util.List;
/**
* Checks for hardcoded seeds with random numbers.
*/
public class SecureRandomDetector extends Detector implements ClassScanner {
/** Unregistered activities and services */
public static final Issue ISSUE = Issue.create(
"SecureRandom", //$NON-NLS-1$
"Using a fixed seed with `SecureRandom`",
"Specifying a fixed seed will cause the instance to return a predictable sequence " +
"of numbers. This may be useful for testing but it is not appropriate for secure use.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
SecureRandomDetector.class,
Scope.CLASS_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/reference/java/security/SecureRandom.html");
private static final String SET_SEED = "setSeed"; //$NON-NLS-1$
static final String OWNER_SECURE_RANDOM = "java/security/SecureRandom"; //$NON-NLS-1$
private static final String OWNER_RANDOM = "java/util/Random"; //$NON-NLS-1$
private static final String VM_SECURE_RANDOM = 'L' + OWNER_SECURE_RANDOM + ';';
/** Method description for a method that takes a long argument (no return type specified */
private static final String LONG_ARG = "(J)"; //$NON-NLS-1$
/** Constructs a new {@link SecureRandomDetector} */
public SecureRandomDetector() {
}
// ---- Implements ClassScanner ----
@Override
@Nullable
public List<String> getApplicableCallNames() {
return Collections.singletonList(SET_SEED);
}
@Override
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
String owner = call.owner;
String desc = call.desc;
if (owner.equals(OWNER_SECURE_RANDOM)) {
if (desc.startsWith(LONG_ARG)) {
checkValidSetSeed(context, call);
} else if (desc.startsWith("([B)")) { //$NON-NLS-1$
// setSeed(byte[]) ...
// We could do some flow analysis here to see whether the byte array getting
// passed in appears to be fixed.
// However, people calling this constructor rather than the simpler one
// with a fixed integer are probably less likely to make that mistake... right?
}
} else if (owner.equals(OWNER_RANDOM) && desc.startsWith(LONG_ARG)) {
// Called setSeed(long) on an instanceof a Random object. Flag this if the instance
// is likely a SecureRandom.
// Track allocations such that we know whether the type of the call
// is on a SecureRandom rather than a Random
Analyzer analyzer = new Analyzer(new BasicInterpreter() {
@Override
public BasicValue newValue(Type type) {
if (type != null && type.getDescriptor().equals(VM_SECURE_RANDOM)) {
return new BasicValue(type);
}
return super.newValue(type);
}
});
try {
Frame[] frames = analyzer.analyze(classNode.name, method);
InsnList instructions = method.instructions;
Frame frame = frames[instructions.indexOf(call)];
int stackSlot = frame.getStackSize();
for (Type type : Type.getArgumentTypes(desc)) {
stackSlot -= type.getSize();
}
BasicValue stackValue = (BasicValue) frame.getStack(stackSlot);
Type type = stackValue.getType();
if (type != null && type.getDescriptor().equals(VM_SECURE_RANDOM)) {
checkValidSetSeed(context, call);
}
} catch (AnalyzerException e) {
context.log(e, null);
}
} else if (owner.equals(OWNER_RANDOM) && desc.startsWith(LONG_ARG)) {
// Called setSeed(long) on an instanceof a Random object. Flag this if the instance
// is likely a SecureRandom.
// TODO
}
}
private static void checkValidSetSeed(ClassContext context, MethodInsnNode call) {
assert call.name.equals(SET_SEED);
// Make sure the argument passed is not a literal
AbstractInsnNode prev = LintUtils.getPrevInstruction(call);
if (prev == null) {
return;
}
int opcode = prev.getOpcode();
if (opcode == Opcodes.LCONST_0 || opcode == Opcodes.LCONST_1 || opcode == Opcodes.LDC) {
context.report(ISSUE, context.getLocation(call),
"Do not call `setSeed()` on a `SecureRandom` with a fixed seed: " +
"it is not secure. Use `getSeed()`.");
} else if (opcode == Opcodes.INVOKESTATIC) {
String methodName = ((MethodInsnNode) prev).name;
if (methodName.equals("currentTimeMillis") || methodName.equals("nanoTime")) {
context.report(ISSUE, context.getLocation(call),
"It is dangerous to seed `SecureRandom` with the current time because " +
"that value is more predictable to an attacker than the default seed.");
}
}
}
}
@@ -1,239 +0,0 @@
/*
* Copyright (C) 2013 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.klint.checks;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Checks for pseudo random number generator initialization issues
*/
public class SecureRandomGeneratorDetector extends Detector implements ClassScanner {
@SuppressWarnings("SpellCheckingInspection")
private static final String BLOG_URL
= "https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html";
/** Whether the random number generator is initialized correctly */
public static final Issue ISSUE = Issue.create(
"TrulyRandom", //$NON-NLS-1$
"Weak RNG",
"Key generation, signing, encryption, and random number generation may not " +
"receive cryptographically strong values due to improper initialization of " +
"the underlying PRNG on Android 4.3 and below.\n" +
"\n" +
"If your application relies on cryptographically secure random number generation " +
"you should apply the workaround described in " + BLOG_URL + " .\n" +
"\n" +
"This lint rule is mostly informational; it does not accurately detect whether " +
"cryptographically secure RNG is required, or whether the workaround has already " +
"been applied. After reading the blog entry and updating your code if necessary, " +
"you can disable this lint issue.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
SecureRandomGeneratorDetector.class,
Scope.CLASS_FILE_SCOPE))
.addMoreInfo(BLOG_URL);
private static final String WRAP = "wrap"; //$NON-NLS-1$
private static final String UNWRAP = "unwrap"; //$NON-NLS-1$
private static final String INIT = "init"; //$NON-NLS-1$
private static final String INIT_SIGN = "initSign"; //$NON-NLS-1$
private static final String GET_INSTANCE = "getInstance"; //$NON-NLS-1$
private static final String FOR_NAME = "forName"; //$NON-NLS-1$
private static final String JAVA_LANG_CLASS = "java/lang/Class"; //$NON-NLS-1$
private static final String JAVAX_CRYPTO_KEY_GENERATOR = "javax/crypto/KeyGenerator";
private static final String JAVAX_CRYPTO_KEY_AGREEMENT = "javax/crypto/KeyAgreement";
private static final String JAVA_SECURITY_KEY_PAIR_GENERATOR =
"java/security/KeyPairGenerator";
private static final String JAVAX_CRYPTO_SIGNATURE = "javax/crypto/Signature";
private static final String JAVAX_CRYPTO_CIPHER = "javax/crypto/Cipher";
private static final String JAVAX_NET_SSL_SSLENGINE = "javax/net/ssl/SSLEngine";
/** Constructs a new {@link SecureRandomGeneratorDetector} */
public SecureRandomGeneratorDetector() {
}
// ---- Implements ClassScanner ----
@Nullable
@Override
public List<String> getApplicableCallOwners() {
return Arrays.asList(
JAVAX_CRYPTO_KEY_GENERATOR,
JAVA_SECURITY_KEY_PAIR_GENERATOR,
JAVAX_CRYPTO_KEY_AGREEMENT,
SecureRandomDetector.OWNER_SECURE_RANDOM,
JAVAX_NET_SSL_SSLENGINE,
JAVAX_CRYPTO_SIGNATURE,
JAVAX_CRYPTO_CIPHER
);
}
@Nullable
@Override
public List<String> getApplicableCallNames() {
return Collections.singletonList(FOR_NAME);
}
/** Location of first call to key generator (etc), if any */
private Location mLocation;
/** Whether the issue should be ignored (because we have a workaround, or because
* we're only targeting correct implementations, etc */
private boolean mIgnore;
@Override
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
if (mIgnore) {
return;
}
String owner = call.owner;
String name = call.name;
// Look for the workaround code: if we see a Class.forName on the harmony NativeCrypto,
// we'll consider that a sign.
if (name.equals(FOR_NAME)) {
if (call.getOpcode() != Opcodes.INVOKESTATIC ||
!owner.equals(JAVA_LANG_CLASS)) {
return;
}
AbstractInsnNode prev = LintUtils.getPrevInstruction(call);
if (prev instanceof LdcInsnNode) {
Object cst = ((LdcInsnNode)prev).cst;
//noinspection SpellCheckingInspection
if (cst instanceof String &&
"org.apache.harmony.xnet.provider.jsse.NativeCrypto".equals(cst)) {
mIgnore = true;
}
}
return;
}
// Look for calls that probably require a properly initialized random number generator.
assert owner.equals(JAVAX_CRYPTO_KEY_GENERATOR)
|| owner.equals(JAVA_SECURITY_KEY_PAIR_GENERATOR)
|| owner.equals(JAVAX_CRYPTO_KEY_AGREEMENT)
|| owner.equals(SecureRandomDetector.OWNER_SECURE_RANDOM)
|| owner.equals(JAVAX_CRYPTO_CIPHER)
|| owner.equals(JAVAX_CRYPTO_SIGNATURE)
|| owner.equals(JAVAX_NET_SSL_SSLENGINE) : owner;
boolean warn = false;
if (owner.equals(JAVAX_CRYPTO_SIGNATURE)) {
warn = name.equals(INIT_SIGN);
} else if (owner.equals(JAVAX_CRYPTO_CIPHER)) {
if (name.equals(INIT)) {
int arity = getDescArity(call.desc);
AbstractInsnNode node = call;
for (int i = 0; i < arity; i++) {
node = LintUtils.getPrevInstruction(node);
if (node == null) {
break;
}
}
if (node != null) {
int opcode = node.getOpcode();
if (opcode == Opcodes.ICONST_3 || // Cipher.WRAP_MODE
opcode == Opcodes.ICONST_1) { // Cipher.ENCRYPT_MODE
warn = true;
}
}
}
} else if (name.equals(GET_INSTANCE) || name.equals(CONSTRUCTOR_NAME)
|| name.equals(WRAP) || name.equals(UNWRAP)) { // For SSLEngine
warn = true;
}
if (warn) {
if (mLocation != null) {
return;
}
if (context.getMainProject().getMinSdk() > 18) {
// Fix no longer needed
mIgnore = true;
return;
}
if (context.getDriver().isSuppressed(ISSUE, classNode, method, call)) {
mIgnore = true;
} else {
mLocation = context.getLocation(call);
}
}
}
@VisibleForTesting
static int getDescArity(String desc) {
int arity = 0;
// For example, (ILjava/security/Key;)V => 2
for (int i = 1, max = desc.length(); i < max; i++) {
char c = desc.charAt(i);
if (c == ')') {
break;
} else if (c == 'L') {
arity++;
i = desc.indexOf(';', i);
assert i != -1 : desc;
} else {
arity++;
}
}
return arity;
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mLocation != null && !mIgnore) {
String message = "Potentially insecure random numbers on Android 4.3 and older. "
+ "Read " + BLOG_URL + " for more info.";
context.report(ISSUE, mLocation, message);
}
}
}
@@ -1,74 +0,0 @@
/*
* 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.klint.checks;
import static com.android.tools.klint.checks.PermissionRequirement.ATTR_PROTECTION_LEVEL;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import java.util.Collection;
import java.util.Collections;
/**
* Checks if signatureOrSystem level permissions are set.
*/
public class SignatureOrSystemDetector extends Detector implements Detector.XmlScanner {
public static final Issue ISSUE = Issue.create(
"SignatureOrSystemPermissions", //$NON-NLS-1$
"signatureOrSystem permissions declared",
"The `signature` protection level should probably be sufficient for most needs and "
+ "works regardless of where applications are installed. The "
+ "`signatureOrSystem` level is used for certain situations where "
+ "multiple vendors have applications built into a system image and "
+ "need to share specific features explicitly because they are being built "
+ "together.",
Category.SECURITY,
5,
Severity.WARNING,
new Implementation(
SignatureOrSystemDetector.class,
Scope.MANIFEST_SCOPE
));
private static final String SIGNATURE_OR_SYSTEM = "signatureOrSystem"; //$NON-NLS-1$
// ---- Implements Detector.XmlScanner ----
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_PROTECTION_LEVEL);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String protectionLevel = attribute.getValue();
if (protectionLevel != null
&& protectionLevel.equals(SIGNATURE_OR_SYSTEM)) {
String message = "`protectionLevel` should probably not be set to `signatureOrSystem`";
context.report(ISSUE, attribute, context.getLocation(attribute), message);
}
}
}
@@ -1,147 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Checks for unreachable states in an Android state list definition
*/
public class StateListDetector extends ResourceXmlDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"StateListReachable", //$NON-NLS-1$
"Unreachable state in a `<selector>`",
"In a selector, only the last child in the state list should omit a " +
"state qualifier. If not, all subsequent items in the list will be ignored " +
"since the given item will match all.",
Category.CORRECTNESS,
5,
Severity.WARNING,
new Implementation(
StateListDetector.class,
Scope.RESOURCE_FILE_SCOPE));
private static final String STATE_PREFIX = "state_"; //$NON-NLS-1$
/** Constructs a new {@link StateListDetector} */
public StateListDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.DRAWABLE;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
// TODO: Look for views that don't specify
// Display the error token somewhere so it can be suppressed
// Emit warning at the end "run with --help to learn how to suppress types of errors/checks";
// ("...and this message.")
Element root = document.getDocumentElement();
if (root != null && root.getTagName().equals("selector")) { //$NON-NLS-1$
List<Element> children = LintUtils.getChildren(root);
Map<Element, Set<String>> states =
new HashMap<Element, Set<String>>(children.size());
for (int i = 0; i < children.size(); i++) {
Element child = children.get(i);
NamedNodeMap attributes = child.getAttributes();
Set<String> stateNames = new HashSet<String>(attributes.getLength());
states.put(child, stateNames);
for (int j = 0; j < attributes.getLength(); j++) {
Attr attribute = (Attr) attributes.item(j);
String name = attribute.getLocalName();
if (name == null) {
continue;
}
if (name.startsWith(STATE_PREFIX)) {
stateNames.add(name + '=' + attribute.getValue());
} else {
String namespaceUri = attribute.getNamespaceURI();
if (namespaceUri != null && !namespaceUri.isEmpty() &&
!ANDROID_URI.equals(namespaceUri)) {
// There is a custom attribute on this item.
// This could be a state, see
// http://code.google.com/p/android/issues/detail?id=22339
// so don't flag this one.
stateNames.add(attribute.getName() + '=' + attribute.getValue());
}
}
}
}
// See if for each state, any subsequent state fully contains all the same
// state requirements
for (int i = 0; i < children.size() - 1; i++) {
Element prev = children.get(i);
Set<String> prevStates = states.get(prev);
assert prevStates != null : prev;
for (int j = i + 1; j < children.size(); j++) {
Element current = children.get(j);
Set<String> currentStates = states.get(current);
assert currentStates != null : current;
if (currentStates.containsAll(prevStates)) {
Location location = context.getLocation(current);
Location secondary = context.getLocation(prev);
secondary.setMessage("Earlier item which masks item");
location.setSecondary(secondary);
context.report(ISSUE, current, location, String.format(
"This item is unreachable because a previous item (item #%1$d) is a more general match than this one",
i + 1));
// Don't keep reporting errors for all the remaining cases in this file
return;
}
}
}
}
}
}
@@ -1,189 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.TAG_USES_PERMISSION;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.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.Collections;
import java.util.EnumSet;
/**
* Checks if an application wants to use permissions that can only be used by
* system applications.
*/
public class SystemPermissionsDetector extends Detector implements Detector.XmlScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"ProtectedPermissions", //$NON-NLS-1$
"Using system app permission",
"Permissions with the protection level signature or signatureOrSystem are only " +
"granted to system apps. If an app is a regular non-system app, it will never be " +
"able to use these permissions.",
Category.CORRECTNESS,
5,
Severity.ERROR,
new Implementation(
SystemPermissionsDetector.class,
EnumSet.of(Scope.MANIFEST)));
// List of permissions have the protection levels signature or systemOrSignature.
// This list must be sorted alphabetically.
private static final String[] SYSTEM_PERMISSIONS = new String[] {
"android.intent.category.MASTER_CLEAR.permission.C2D_MESSAGE",
"android.permission.ACCESS_CACHE_FILESYSTEM",
"android.permission.ACCESS_CHECKIN_PROPERTIES",
"android.permission.ACCESS_MTP",
"android.permission.ACCESS_SURFACE_FLINGER",
"android.permission.ACCOUNT_MANAGER",
"android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK",
"android.permission.ASEC_ACCESS",
"android.permission.ASEC_CREATE",
"android.permission.ASEC_DESTROY",
"android.permission.ASEC_MOUNT_UNMOUNT",
"android.permission.ASEC_RENAME",
"android.permission.BACKUP",
"android.permission.BIND_APPWIDGET",
"android.permission.BIND_DEVICE_ADMIN",
"android.permission.BIND_INPUT_METHOD",
"android.permission.BIND_PACKAGE_VERIFIER",
"android.permission.BIND_REMOTEVIEWS",
"android.permission.BIND_TEXT_SERVICE",
"android.permission.BIND_VPN_SERVICE",
"android.permission.BIND_WALLPAPER",
"android.permission.BRICK",
"android.permission.BROADCAST_PACKAGE_REMOVED",
"android.permission.BROADCAST_SMS",
"android.permission.BROADCAST_WAP_PUSH",
"android.permission.CALL_PRIVILEGED",
"android.permission.CHANGE_BACKGROUND_DATA_SETTING",
"android.permission.CHANGE_COMPONENT_ENABLED_STATE",
"android.permission.CLEAR_APP_USER_DATA",
"android.permission.CONFIRM_FULL_BACKUP",
"android.permission.CONNECTIVITY_INTERNAL",
"android.permission.CONTROL_LOCATION_UPDATES",
"android.permission.COPY_PROTECTED_DATA",
"android.permission.CRYPT_KEEPER",
"android.permission.DELETE_CACHE_FILES",
"android.permission.DELETE_PACKAGES",
"android.permission.DEVICE_POWER",
"android.permission.DIAGNOSTIC",
"android.permission.DUMP",
"android.permission.FACTORY_TEST",
"android.permission.FORCE_BACK",
"android.permission.FORCE_STOP_PACKAGES",
"android.permission.GLOBAL_SEARCH",
"android.permission.GLOBAL_SEARCH_CONTROL",
"android.permission.HARDWARE_TEST",
"android.permission.INJECT_EVENTS",
"android.permission.INSTALL_LOCATION_PROVIDER",
"android.permission.INSTALL_PACKAGES",
"android.permission.INTERNAL_SYSTEM_WINDOW",
"android.permission.MANAGE_APP_TOKENS",
"android.permission.MANAGE_NETWORK_POLICY",
"android.permission.MANAGE_USB",
"android.permission.MASTER_CLEAR",
"android.permission.MODIFY_NETWORK_ACCOUNTING",
"android.permission.MODIFY_PHONE_STATE",
"android.permission.MOVE_PACKAGE",
"android.permission.NET_ADMIN",
"android.permission.MODIFY_PHONE_STATE",
"android.permission.PACKAGE_USAGE_STATS",
"android.permission.PACKAGE_VERIFICATION_AGENT",
"android.permission.PERFORM_CDMA_PROVISIONING",
"android.permission.READ_FRAME_BUFFER",
"android.permission.READ_INPUT_STATE",
"android.permission.READ_NETWORK_USAGE_HISTORY",
"android.permission.READ_PRIVILEGED_PHONE_STATE",
"android.permission.REBOOT",
"android.permission.RECEIVE_EMERGENCY_BROADCAST",
"android.permission.REMOVE_TASKS",
"android.permission.RETRIEVE_WINDOW_CONTENT",
"android.permission.SEND_SMS_NO_CONFIRMATION",
"android.permission.SET_ACTIVITY_WATCHER",
"android.permission.SET_ORIENTATION",
"android.permission.SET_POINTER_SPEED",
"android.permission.SET_PREFERRED_APPLICATIONS",
"android.permission.SET_SCREEN_COMPATIBILITY",
"android.permission.SET_TIME",
"android.permission.SET_WALLPAPER_COMPONENT",
"android.permission.SHUTDOWN",
"android.permission.STATUS_BAR",
"android.permission.STATUS_BAR_SERVICE",
"android.permission.STOP_APP_SWITCHES",
"android.permission.UPDATE_DEVICE_STATS",
"android.permission.WRITE_APN_SETTINGS",
"android.permission.WRITE_GSERVICES",
"android.permission.WRITE_MEDIA_STORAGE",
"android.permission.WRITE_SECURE_SETTINGS"
};
/** Constructs a new {@link SystemPermissionsDetector} check */
public SystemPermissionsDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return file.getName().equals(ANDROID_MANIFEST_XML);
}
// ---- Implements Detector.XmlScanner ----
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(TAG_USES_PERMISSION);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (nameNode != null) {
String permissionName = nameNode.getValue();
if (Arrays.binarySearch(SYSTEM_PERMISSIONS, permissionName) >= 0) {
context.report(ISSUE, element, context.getLocation(nameNode),
"Permission is only granted to system apps");
}
}
}
}
@@ -1,344 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_HINT;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_INPUT_METHOD;
import static com.android.SdkConstants.ATTR_INPUT_TYPE;
import static com.android.SdkConstants.ATTR_PASSWORD;
import static com.android.SdkConstants.ATTR_PHONE_NUMBER;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Checks for usability problems in text fields: omitting inputType, or omitting a hint.
*/
public class TextFieldDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"TextFields", //$NON-NLS-1$
"Missing `inputType` or `hint`",
"Providing an `inputType` attribute on a text field improves usability " +
"because depending on the data to be input, optimized keyboards can be shown " +
"to the user (such as just digits and parentheses for a phone number). Similarly," +
"a hint attribute displays a hint to the user for what is expected in the " +
"text field.\n" +
"\n" +
"The lint detector also looks at the `id` of the view, and if the id offers a " +
"hint of the purpose of the field (for example, the `id` contains the phrase " +
"`phone` or `email`), then lint will also ensure that the `inputType` contains " +
"the corresponding type attributes.\n" +
"\n" +
"If you really want to keep the text field generic, you can suppress this warning " +
"by setting `inputType=\"text\"`.",
Category.USABILITY,
5,
Severity.WARNING,
new Implementation(
TextFieldDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link TextFieldDetector} */
public TextFieldDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(EDIT_TEXT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
Node inputTypeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_INPUT_TYPE);
String inputType = "";
if (inputTypeNode != null) {
inputType = inputTypeNode.getNodeValue();
}
boolean haveHint = false;
if (inputTypeNode == null) {
haveHint = element.hasAttributeNS(ANDROID_URI, ATTR_HINT);
String style = element.getAttribute(ATTR_STYLE);
if (style != null && !style.isEmpty()) {
LintClient client = context.getClient();
if (client.supportsProjectResources()) {
Project project = context.getMainProject();
List<ResourceValue> styles = LintUtils.getStyleAttributes(project, client,
style, ANDROID_URI, ATTR_INPUT_TYPE);
if (styles != null && !styles.isEmpty()) {
ResourceValue value = styles.get(0);
inputType = value.getValue();
inputTypeNode = element;
} else if (!haveHint) {
styles = LintUtils.getStyleAttributes(project, client, style,
ANDROID_URI, ATTR_HINT);
if (styles != null && !styles.isEmpty()) {
haveHint = true;
}
}
} else {
// The input type might be specified via a style. This will require
// us to track these (similar to what is done for the
// RequiredAttributeDetector to track layout_width and layout_height
// in style declarations). For now, simply ignore these elements
// to avoid producing false positives.
return;
}
}
}
if (inputTypeNode == null && !haveHint) {
// Also make sure the EditText does not set an inputMethod in which case
// an inputType might be provided from the input.
if (element.hasAttributeNS(ANDROID_URI, ATTR_INPUT_METHOD)) {
return;
}
context.report(ISSUE, element, context.getLocation(element),
"This text field does not specify an `inputType` or a `hint`");
}
Attr idNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_ID);
if (idNode == null) {
return;
}
String id = idNode.getValue();
if (id.isEmpty()) {
return;
}
if (id.startsWith("editText")) { //$NON-NLS-1$
// Just the default label
return;
}
// TODO: See if the name is just the default names (button1, editText1 etc)
// and if so, do nothing
// TODO: Unit test this
if (containsWord(id, "phone", true, true)) { //$NON-NLS-1$
if (!inputType.contains("phone") //$NON-NLS-1$
&& element.getAttributeNodeNS(ANDROID_URI, ATTR_PHONE_NUMBER) == null) {
String message = String.format("The view name (`%1$s`) suggests this is a phone "
+ "number, but it does not include '`phone`' in the `inputType`", id);
reportMismatch(context, idNode, inputTypeNode, message);
}
return;
}
if (containsWord(id, "width", false, true)
|| containsWord(id, "height", false, true)
|| containsWord(id, "size", false, true)
|| containsWord(id, "length", false, true)
|| containsWord(id, "weight", false, true)
|| containsWord(id, "number", false, true)) {
if (!inputType.contains("number") && !inputType.contains("phone")) { //$NON-NLS-1$
String message = String.format("The view name (`%1$s`) suggests this is a number, "
+ "but it does not include a numeric `inputType` (such as '`numberSigned`')",
id);
reportMismatch(context, idNode, inputTypeNode, message);
}
return;
}
if (containsWord(id, "password", true, true)) { //$NON-NLS-1$
if (!(inputType.contains("Password")) //$NON-NLS-1$
&& element.getAttributeNodeNS(ANDROID_URI, ATTR_PASSWORD) == null) {
String message = String.format("The view name (`%1$s`) suggests this is a password, "
+ "but it does not include '`textPassword`' in the `inputType`", id);
reportMismatch(context, idNode, inputTypeNode, message);
}
return;
}
if (containsWord(id, "email", true, true)) { //$NON-NLS-1$
if (!inputType.contains("Email")) { //$NON-NLS-1$
String message = String.format("The view name (`%1$s`) suggests this is an e-mail "
+ "address, but it does not include '`textEmail`' in the `inputType`", id);
reportMismatch(context, idNode, inputTypeNode, message);
}
return;
}
if (endsWith(id, "pin", false, true)) { //$NON-NLS-1$
if (!(inputType.contains("numberPassword")) //$NON-NLS-1$
&& element.getAttributeNodeNS(ANDROID_URI, ATTR_PASSWORD) == null) {
String message = String.format("The view name (`%1$s`) suggests this is a password, "
+ "but it does not include '`numberPassword`' in the `inputType`", id);
reportMismatch(context, idNode, inputTypeNode, message);
}
return;
}
if ((containsWord(id, "uri") || containsWord(id, "url"))
&& !inputType.contains("textUri")) {
String message = String.format("The view name (`%1$s`) suggests this is a URI, "
+ "but it does not include '`textUri`' in the `inputType`", id);
reportMismatch(context, idNode, inputTypeNode, message);
}
if ((containsWord(id, "date")) //$NON-NLS-1$
&& !inputType.contains("date")) { //$NON-NLS-1$
String message = String.format("The view name (`%1$s`) suggests this is a date, "
+ "but it does not include '`date`' or '`datetime`' in the `inputType`", id);
reportMismatch(context, idNode, inputTypeNode, message);
}
}
private static void reportMismatch(XmlContext context, Attr idNode, Node inputTypeNode,
String message) {
Location location;
if (inputTypeNode != null) {
location = context.getLocation(inputTypeNode);
Location secondary = context.getLocation(idNode);
secondary.setMessage("id defined here");
location.setSecondary(secondary);
} else {
location = context.getLocation(idNode);
}
context.report(ISSUE, idNode.getOwnerElement(), location, message);
}
/** Returns true if the given sentence contains a given word */
@VisibleForTesting
static boolean containsWord(String sentence, String word) {
return containsWord(sentence, word, false, false);
}
/**
* Returns true if the given sentence contains a given word
* @param sentence the full sentence to search within
* @param word the word to look for
* @param allowPrefix if true, allow a prefix match even if the next character
* is in the same word (same case or not an underscore)
* @param allowSuffix if true, allow a suffix match even if the preceding character
* is in the same word (same case or not an underscore)
* @return true if the word is contained in the sentence
*/
@VisibleForTesting
static boolean containsWord(String sentence, String word, boolean allowPrefix,
boolean allowSuffix) {
return indexOfWord(sentence, word, allowPrefix, allowSuffix) != -1;
}
/** Returns true if the given sentence <b>ends</b> with a given word */
private static boolean endsWith(String sentence, String word, boolean allowPrefix,
boolean allowSuffix) {
int index = indexOfWord(sentence, word, allowPrefix, allowSuffix);
if (index != -1) {
return index == sentence.length() - word.length();
}
return false;
}
/**
* Returns the index of the given word in the given sentence, if any. It will match
* across cases, and ignore words that seem to be just a substring in the middle
* of another word.
*
* @param sentence the full sentence to search within
* @param word the word to look for
* @param allowPrefix if true, allow a prefix match even if the next character
* is in the same word (same case or not an underscore)
* @param allowSuffix if true, allow a suffix match even if the preceding character
* is in the same word (same case or not an underscore)
* @return true if the word is contained in the sentence
*/
private static int indexOfWord(String sentence, String word, boolean allowPrefix,
boolean allowSuffix) {
if (sentence.isEmpty()) {
return -1;
}
int wordLength = word.length();
if (wordLength > sentence.length()) {
return -1;
}
char firstUpper = Character.toUpperCase(word.charAt(0));
char firstLower = Character.toLowerCase(firstUpper);
int start = 0;
if (sentence.startsWith(NEW_ID_PREFIX)) {
start += NEW_ID_PREFIX.length();
} else if (sentence.startsWith(ID_PREFIX)) {
start += ID_PREFIX.length();
}
for (int i = start, n = sentence.length(), m = n - (wordLength - 1); i < m; i++) {
char c = sentence.charAt(i);
if (c == firstUpper || c == firstLower) {
if (sentence.regionMatches(true, i, word, 0, wordLength)) {
if (i <= start && allowPrefix) {
return i;
}
if (i == m - 1 && allowSuffix) {
return i;
}
if (i <= start || (sentence.charAt(i - 1) == '_')
|| Character.isUpperCase(c)) {
if (i == m - 1) {
return i;
}
char after = sentence.charAt(i + wordLength);
if (after == '_' || Character.isUpperCase(after)) {
return i;
}
}
}
}
}
return -1;
}
}
@@ -1,239 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_AUTO_TEXT;
import static com.android.SdkConstants.ATTR_BUFFER_TYPE;
import static com.android.SdkConstants.ATTR_CAPITALIZE;
import static com.android.SdkConstants.ATTR_CURSOR_VISIBLE;
import static com.android.SdkConstants.ATTR_DIGITS;
import static com.android.SdkConstants.ATTR_EDITABLE;
import static com.android.SdkConstants.ATTR_EDITOR_EXTRAS;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_IME_ACTION_ID;
import static com.android.SdkConstants.ATTR_IME_ACTION_LABEL;
import static com.android.SdkConstants.ATTR_IME_OPTIONS;
import static com.android.SdkConstants.ATTR_INPUT_METHOD;
import static com.android.SdkConstants.ATTR_INPUT_TYPE;
import static com.android.SdkConstants.ATTR_NUMERIC;
import static com.android.SdkConstants.ATTR_ON_CLICK;
import static com.android.SdkConstants.ATTR_PASSWORD;
import static com.android.SdkConstants.ATTR_PHONE_NUMBER;
import static com.android.SdkConstants.ATTR_PRIVATE_IME_OPTIONS;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.ATTR_TEXT_IS_SELECTABLE;
import static com.android.SdkConstants.ATTR_VISIBILITY;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.RADIO_BUTTON;
import static com.android.SdkConstants.SWITCH;
import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.SdkConstants.TOGGLE_BUTTON;
import static com.android.SdkConstants.VALUE_EDITABLE;
import static com.android.SdkConstants.VALUE_NONE;
import static com.android.SdkConstants.VALUE_TRUE;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import java.util.Arrays;
import java.util.Collection;
/**
* Checks for cases where a TextView should probably be an EditText instead
*/
public class TextViewDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
TextViewDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"TextViewEdits", //$NON-NLS-1$
"TextView should probably be an EditText instead",
"Using a `<TextView>` to input text is generally an error, you should be " +
"using `<EditText>` instead. `EditText` is a subclass of `TextView`, and some " +
"of the editing support is provided by `TextView`, so it's possible to set " +
"some input-related properties on a `TextView`. However, using a `TextView` " +
"along with input attributes is usually a cut & paste error. To input " +
"text you should be using `<EditText>`.\n" +
"\n" +
"This check also checks subclasses of `TextView`, such as `Button` and `CheckBox`, " +
"since these have the same issue: they should not be used with editable " +
"attributes.",
Category.CORRECTNESS,
7,
Severity.WARNING,
IMPLEMENTATION);
/** Text could be selectable */
public static final Issue SELECTABLE = Issue.create(
"SelectableText", //$NON-NLS-1$
"Dynamic text should probably be selectable",
"If a `<TextView>` is used to display data, the user might want to copy that " +
"data and paste it elsewhere. To allow this, the `<TextView>` should specify " +
"`android:textIsSelectable=\"true\"`.\n" +
"\n" +
"This lint check looks for TextViews which are likely to be displaying data: " +
"views whose text is set dynamically. This value will be ignored on platforms " +
"older than API 11, so it is okay to set it regardless of your `minSdkVersion`.",
Category.USABILITY,
7,
Severity.WARNING,
IMPLEMENTATION)
// Apparently setting this can have some undesirable side effects
.setEnabledByDefault(false);
/** Constructs a new {@link TextViewDetector} */
public TextViewDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TEXT_VIEW,
BUTTON,
TOGGLE_BUTTON,
CHECK_BOX,
RADIO_BUTTON,
CHECKED_TEXT_VIEW,
SWITCH
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (element.getTagName().equals(TEXT_VIEW)) {
if (!element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)
&& element.hasAttributeNS(ANDROID_URI, ATTR_ID)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT_IS_SELECTABLE)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_VISIBILITY)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_ON_CLICK)
&& context.getMainProject().getTargetSdk() >= 11
&& context.isEnabled(SELECTABLE)) {
context.report(SELECTABLE, element, context.getLocation(element),
"Consider making the text value selectable by specifying " +
"`android:textIsSelectable=\"true\"`");
}
}
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
String name = attribute.getLocalName();
if (name == null || name.isEmpty()) {
// Attribute not in a namespace; we only care about the android: ones
continue;
}
boolean isEditAttribute = false;
switch (name.charAt(0)) {
case 'a': {
isEditAttribute = name.equals(ATTR_AUTO_TEXT);
break;
}
case 'b': {
isEditAttribute = name.equals(ATTR_BUFFER_TYPE) &&
attribute.getValue().equals(VALUE_EDITABLE);
break;
}
case 'p': {
isEditAttribute = name.equals(ATTR_PASSWORD)
|| name.equals(ATTR_PHONE_NUMBER)
|| name.equals(ATTR_PRIVATE_IME_OPTIONS);
break;
}
case 'c': {
isEditAttribute = name.equals(ATTR_CAPITALIZE)
|| name.equals(ATTR_CURSOR_VISIBLE);
break;
}
case 'd': {
isEditAttribute = name.equals(ATTR_DIGITS);
break;
}
case 'e': {
if (name.equals(ATTR_EDITABLE)) {
isEditAttribute = attribute.getValue().equals(VALUE_TRUE);
} else {
isEditAttribute = name.equals(ATTR_EDITOR_EXTRAS);
}
break;
}
case 'i': {
if (name.equals(ATTR_INPUT_TYPE)) {
String value = attribute.getValue();
isEditAttribute = !value.isEmpty() && !value.equals(VALUE_NONE);
} else {
isEditAttribute = name.equals(ATTR_INPUT_TYPE)
|| name.equals(ATTR_IME_OPTIONS)
|| name.equals(ATTR_IME_ACTION_LABEL)
|| name.equals(ATTR_IME_ACTION_ID)
|| name.equals(ATTR_INPUT_METHOD);
}
break;
}
case 'n': {
isEditAttribute = name.equals(ATTR_NUMERIC);
break;
}
}
if (isEditAttribute && ANDROID_URI.equals(attribute.getNamespaceURI()) && context.isEnabled(ISSUE)) {
Location location = context.getLocation(attribute);
String message;
String view = element.getTagName();
if (view.equals(TEXT_VIEW)) {
message = String.format(
"Attribute `%1$s` should not be used with `<TextView>`: " +
"Change element type to `<EditText>` ?", attribute.getName());
} else {
message = String.format(
"Attribute `%1$s` should not be used with `<%2$s>`: " +
"intended for editable text widgets",
attribute.getName(), view);
}
context.report(ISSUE, attribute, location, message);
}
}
}
}
@@ -1,155 +0,0 @@
/*
* 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import java.util.Collection;
/**
* Checks whether a view hierarchy has too many views or has a suspiciously deep hierarchy
*/
public class TooManyViewsDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
TooManyViewsDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Issue of having too many views in a single layout */
public static final Issue TOO_MANY = Issue.create(
"TooManyViews", //$NON-NLS-1$
"Layout has too many views",
"Using too many views in a single layout is bad for " +
"performance. Consider using compound drawables or other tricks for " +
"reducing the number of views in this layout.\n" +
"\n" +
"The maximum view count defaults to 80 but can be configured with the " +
"environment variable `ANDROID_LINT_MAX_VIEW_COUNT`.",
Category.PERFORMANCE,
1,
Severity.WARNING,
IMPLEMENTATION);
/** Issue of having too deep hierarchies in layouts */
public static final Issue TOO_DEEP = Issue.create(
"TooDeepLayout", //$NON-NLS-1$
"Layout hierarchy is too deep",
"Layouts with too much nesting is bad for performance. " +
"Consider using a flatter layout (such as `RelativeLayout` or `GridLayout`)." +
"The default maximum depth is 10 but can be configured with the environment " +
"variable `ANDROID_LINT_MAX_DEPTH`.",
Category.PERFORMANCE,
1,
Severity.WARNING,
IMPLEMENTATION);
private static final int MAX_VIEW_COUNT;
private static final int MAX_DEPTH;
static {
int maxViewCount = 0;
int maxDepth = 0;
String countValue = System.getenv("ANDROID_LINT_MAX_VIEW_COUNT"); //$NON-NLS-1$
if (countValue != null) {
try {
maxViewCount = Integer.parseInt(countValue);
} catch (NumberFormatException e) {
// pass: set to default below
}
}
String depthValue = System.getenv("ANDROID_LINT_MAX_DEPTH"); //$NON-NLS-1$
if (depthValue != null) {
try {
maxDepth = Integer.parseInt(depthValue);
} catch (NumberFormatException e) {
// pass: set to default below
}
}
if (maxViewCount == 0) {
maxViewCount = 80;
}
if (maxDepth == 0) {
maxDepth = 10;
}
MAX_VIEW_COUNT = maxViewCount;
MAX_DEPTH = maxDepth;
}
private int mViewCount;
private int mDepth;
private boolean mWarnedAboutDepth;
/** Constructs a new {@link TooManyViewsDetector} */
public TooManyViewsDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void beforeCheckFile(@NonNull Context context) {
mViewCount = mDepth = 0;
mWarnedAboutDepth = false;
}
@Override
public Collection<String> getApplicableElements() {
return ALL;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
mViewCount++;
mDepth++;
if (mDepth == MAX_DEPTH && !mWarnedAboutDepth) {
// Have to record whether or not we've warned since we could have many siblings
// at the max level and we'd warn for each one. No need to do the same thing
// for the view count error since we'll only have view count exactly equal the
// max just once.
mWarnedAboutDepth = true;
String msg = String.format("`%1$s` has more than %2$d levels, bad for performance",
context.file.getName(), MAX_DEPTH);
context.report(TOO_DEEP, element, context.getLocation(element), msg);
}
if (mViewCount == MAX_VIEW_COUNT) {
String msg = String.format("`%1$s` has more than %2$d views, bad for performance",
context.file.getName(), MAX_VIEW_COUNT);
context.report(TOO_MANY, element, context.getLocation(element), msg);
}
}
@Override
public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) {
mDepth--;
}
}
@@ -1,722 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ATTR_LOCALE;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_TRANSLATABLE;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.STRING_PREFIX;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.TAG_STRING_ARRAY;
import static com.android.SdkConstants.TOOLS_URI;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.ProductFlavor;
import com.android.builder.model.ProductFlavorContainer;
import com.android.builder.model.Variant;
import com.android.ide.common.resources.LocaleManager;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
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.Locale;
import java.util.Map;
import java.util.Set;
/**
* Checks for incomplete translations - e.g. keys that are only present in some
* locales but not all.
*/
public class TranslationDetector extends ResourceXmlDetector {
@VisibleForTesting
static boolean sCompleteRegions =
System.getenv("ANDROID_LINT_COMPLETE_REGIONS") != null; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
TranslationDetector.class,
Scope.ALL_RESOURCES_SCOPE);
/** Are all translations complete? */
public static final Issue MISSING = Issue.create(
"MissingTranslation", //$NON-NLS-1$
"Incomplete translation",
"If an application has more than one locale, then all the strings declared in " +
"one language should also be translated in all other languages.\n" +
"\n" +
"If the string should *not* be translated, you can add the attribute " +
"`translatable=\"false\"` on the `<string>` element, or you can define all " +
"your non-translatable strings in a resource file called `donottranslate.xml`. " +
"Or, you can ignore the issue with a `tools:ignore=\"MissingTranslation\"` " +
"attribute.\n" +
"\n" +
"By default this detector allows regions of a language to just provide a " +
"subset of the strings and fall back to the standard language strings. " +
"You can require all regions to provide a full translation by setting the " +
"environment variable `ANDROID_LINT_COMPLETE_REGIONS`.\n" +
"\n" +
"You can tell lint (and other tools) which language is the default language " +
"in your `res/values/` folder by specifying `tools:locale=\"languageCode\"` for " +
"the root `<resources>` element in your resource file. (The `tools` prefix refers " +
"to the namespace declaration `http://schemas.android.com/tools`.)",
Category.MESSAGES,
8,
Severity.FATAL,
IMPLEMENTATION);
/** Are there extra translations that are "unused" (appear only in specific languages) ? */
public static final Issue EXTRA = Issue.create(
"ExtraTranslation", //$NON-NLS-1$
"Extra translation",
"If a string appears in a specific language translation file, but there is " +
"no corresponding string in the default locale, then this string is probably " +
"unused. (It's technically possible that your application is only intended to " +
"run in a specific locale, but it's still a good idea to provide a fallback.).\n" +
"\n" +
"Note that these strings can lead to crashes if the string is looked up on any " +
"locale not providing a translation, so it's important to clean them up.",
Category.MESSAGES,
6,
Severity.FATAL,
IMPLEMENTATION);
private Set<String> mNames;
private Set<String> mTranslatedArrays;
private Set<String> mNonTranslatable;
private boolean mIgnoreFile;
private Map<File, Set<String>> mFileToNames;
private Map<File, String> mFileToLocale;
/** Locations for each untranslated string name. Populated during phase 2, if necessary */
private Map<String, Location> mMissingLocations;
/** Locations for each extra translated string name. Populated during phase 2, if necessary */
private Map<String, Location> mExtraLocations;
/** Error messages for each untranslated string name. Populated during phase 2, if necessary */
private Map<String, String> mDescriptions;
/** Constructs a new {@link TranslationDetector} */
public TranslationDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_STRING,
TAG_STRING_ARRAY
);
}
@Override
public void beforeCheckProject(@NonNull Context context) {
if (context.getDriver().getPhase() == 1) {
mFileToNames = new HashMap<File, Set<String>>();
}
}
@Override
public void beforeCheckFile(@NonNull Context context) {
if (context.getPhase() == 1) {
mNames = new HashSet<String>();
}
// Convention seen in various projects
mIgnoreFile = context.file.getName().startsWith("donottranslate") //$NON-NLS-1$
|| UnusedResourceDetector.isAnalyticsFile(context);
if (!context.getProject().getReportIssues()) {
mIgnoreFile = true;
}
}
@Override
public void afterCheckFile(@NonNull Context context) {
if (context.getPhase() == 1) {
// Store this layout's set of ids for full project analysis in afterCheckProject
if (context.getProject().getReportIssues() && mNames != null && !mNames.isEmpty()) {
mFileToNames.put(context.file, mNames);
Element root = ((XmlContext) context).document.getDocumentElement();
if (root != null) {
String locale = root.getAttributeNS(TOOLS_URI, ATTR_LOCALE);
if (locale != null && !locale.isEmpty()) {
if (mFileToLocale == null) {
mFileToLocale = Maps.newHashMap();
}
mFileToLocale.put(context.file, locale);
}
// Add in English here if not specified? Worry about false positives listing "en" explicitly
}
}
mNames = null;
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (context.getPhase() == 1) {
// NOTE - this will look for the presence of translation strings.
// If you create a resource folder but don't actually place a file in it
// we won't detect that, but it seems like a smaller problem.
checkTranslations(context);
mFileToNames = null;
if (mMissingLocations != null || mExtraLocations != null) {
context.getDriver().requestRepeat(this, Scope.ALL_RESOURCES_SCOPE);
}
} else {
assert context.getPhase() == 2;
reportMap(context, MISSING, mMissingLocations);
reportMap(context, EXTRA, mExtraLocations);
mMissingLocations = null;
mExtraLocations = null;
mDescriptions = null;
}
}
private void reportMap(Context context, Issue issue, Map<String, Location> map) {
if (map != null) {
for (Map.Entry<String, Location> entry : map.entrySet()) {
Location location = entry.getValue();
String name = entry.getKey();
String message = mDescriptions.get(name);
if (location == null) {
location = Location.create(context.getProject().getDir());
}
// We were prepending locations, but we want to prefer the base folders
location = Location.reverse(location);
context.report(issue, location, message);
}
}
}
private void checkTranslations(Context context) {
// Only one file defining strings? If so, no problems.
Set<File> files = mFileToNames.keySet();
Set<File> parentFolders = new HashSet<File>();
for (File file : files) {
parentFolders.add(file.getParentFile());
}
if (parentFolders.size() == 1
&& FD_RES_VALUES.equals(parentFolders.iterator().next().getName())) {
// Only one language - no problems.
return;
}
boolean reportMissing = context.isEnabled(MISSING);
boolean reportExtra = context.isEnabled(EXTRA);
// res/strings.xml etc
String defaultLanguage = "Default";
Map<File, String> parentFolderToLanguage = new HashMap<File, String>();
for (File parent : parentFolders) {
String name = parent.getName();
// Look up the language for this folder.
String language = getLanguageTag(name);
if (language == null) {
language = defaultLanguage;
}
parentFolderToLanguage.put(parent, language);
}
int languageCount = parentFolderToLanguage.values().size();
if (languageCount == 0 || languageCount == 1 && defaultLanguage.equals(
parentFolderToLanguage.values().iterator().next())) {
// At most one language -- no problems.
return;
}
// Merge together the various files building up the translations for each language
Map<String, Set<String>> languageToStrings =
new HashMap<String, Set<String>>(languageCount);
Set<String> allStrings = new HashSet<String>(200);
for (File file : files) {
String language = null;
if (mFileToLocale != null) {
String locale = mFileToLocale.get(file);
if (locale != null) {
int index = locale.indexOf('-');
if (index != -1) {
locale = locale.substring(0, index);
}
language = locale;
}
}
if (language == null) {
language = parentFolderToLanguage.get(file.getParentFile());
}
assert language != null : file.getParent();
Set<String> fileStrings = mFileToNames.get(file);
Set<String> languageStrings = languageToStrings.get(language);
if (languageStrings == null) {
// We don't need a copy; we're done with the string tables now so we
// can modify them
languageToStrings.put(language, fileStrings);
} else {
languageStrings.addAll(fileStrings);
}
allStrings.addAll(fileStrings);
}
Set<String> defaultStrings = languageToStrings.get(defaultLanguage);
if (defaultStrings == null) {
defaultStrings = new HashSet<String>();
}
// See if it looks like the user has named a specific locale as the base language
// (this impacts whether we report strings as "extra" or "missing")
if (mFileToLocale != null) {
Set<String> specifiedLocales = Sets.newHashSet();
for (Map.Entry<File, String> entry : mFileToLocale.entrySet()) {
String locale = entry.getValue();
int index = locale.indexOf('-');
if (index != -1) {
locale = locale.substring(0, index);
}
specifiedLocales.add(locale);
}
if (specifiedLocales.size() == 1) {
String first = specifiedLocales.iterator().next();
Set<String> languageStrings = languageToStrings.get(first);
assert languageStrings != null;
defaultStrings.addAll(languageStrings);
}
}
int stringCount = allStrings.size();
// Treat English is the default language if not explicitly specified
if (!sCompleteRegions && !languageToStrings.containsKey("en")
&& mFileToLocale == null) { //$NON-NLS-1$
// But only if we have an actual region
for (String l : languageToStrings.keySet()) {
if (l.startsWith("en-")) { //$NON-NLS-1$
languageToStrings.put("en", defaultStrings); //$NON-NLS-1$
break;
}
}
}
List<String> resConfigLanguages = getResConfigLanguages(context.getMainProject());
if (resConfigLanguages != null) {
List<String> keys = Lists.newArrayList(languageToStrings.keySet());
for (String locale : keys) {
if (defaultLanguage.equals(locale)) {
continue;
}
String language = locale;
int index = language.indexOf('-');
if (index != -1) {
// Strip off region
language = language.substring(0, index);
}
if (!resConfigLanguages.contains(language)) {
languageToStrings.remove(locale);
}
}
}
// Do we need to resolve fallback strings for regions that only define a subset
// of the strings in the language and fall back on the main language for the rest?
if (!sCompleteRegions) {
for (String l : languageToStrings.keySet()) {
if (l.indexOf('-') != -1) {
// Yes, we have regions. Merge all base language string names into each region.
for (Map.Entry<String, Set<String>> entry : languageToStrings.entrySet()) {
Set<String> strings = entry.getValue();
if (stringCount != strings.size()) {
String languageRegion = entry.getKey();
int regionIndex = languageRegion.indexOf('-');
if (regionIndex != -1) {
String language = languageRegion.substring(0, regionIndex);
Set<String> fallback = languageToStrings.get(language);
if (fallback != null) {
strings.addAll(fallback);
}
}
}
}
// We only need to do this once; when we see the first region we know
// we need to do it; once merged we can bail
break;
}
}
}
// Fast check to see if there's no problem: if the default locale set is the
// same as the all set (meaning there are no extra strings in the other languages)
// then we can quickly determine if everything is okay by just making sure that
// each language defines everything. If that's the case they will all have the same
// string count.
if (stringCount == defaultStrings.size()) {
boolean haveError = false;
for (Map.Entry<String, Set<String>> entry : languageToStrings.entrySet()) {
Set<String> strings = entry.getValue();
if (stringCount != strings.size()) {
haveError = true;
break;
}
}
if (!haveError) {
return;
}
}
List<String> languages = new ArrayList<String>(languageToStrings.keySet());
Collections.sort(languages);
for (String language : languages) {
Set<String> strings = languageToStrings.get(language);
if (defaultLanguage.equals(language)) {
continue;
}
// if strings.size() == stringCount, then this language is defining everything,
// both all the default language strings and the union of all extra strings
// defined in other languages, so there's no problem.
if (stringCount != strings.size()) {
if (reportMissing) {
Set<String> difference = Sets.difference(defaultStrings, strings);
if (!difference.isEmpty()) {
if (mMissingLocations == null) {
mMissingLocations = new HashMap<String, Location>();
}
if (mDescriptions == null) {
mDescriptions = new HashMap<String, String>();
}
for (String s : difference) {
mMissingLocations.put(s, null);
String message = mDescriptions.get(s);
if (message == null) {
message = String.format("\"`%1$s`\" is not translated in %2$s",
s, getLanguageDescription(language));
} else {
message = message + ", " + getLanguageDescription(language);
}
mDescriptions.put(s, message);
}
}
}
}
if (stringCount != defaultStrings.size()) {
if (reportExtra) {
Set<String> difference = Sets.difference(strings, defaultStrings);
if (!difference.isEmpty()) {
if (mExtraLocations == null) {
mExtraLocations = new HashMap<String, Location>();
}
if (mDescriptions == null) {
mDescriptions = new HashMap<String, String>();
}
for (String s : difference) {
if (mTranslatedArrays != null && mTranslatedArrays.contains(s)) {
continue;
}
if (mNonTranslatable != null && mNonTranslatable.contains(s)) {
continue;
}
mExtraLocations.put(s, null);
String message = String.format(
"\"`%1$s`\" is translated here but not found in default locale", s);
mDescriptions.put(s, message);
}
}
}
}
}
}
public static String getLanguageDescription(@NonNull String locale) {
int index = locale.indexOf('-');
String regionCode = null;
String languageCode = locale;
if (index != -1) {
regionCode = locale.substring(index + 1).toUpperCase(Locale.US);
languageCode = locale.substring(0, index).toLowerCase(Locale.US);
}
String languageName = LocaleManager.getLanguageName(languageCode);
if (languageName != null) {
if (regionCode != null) {
String regionName = LocaleManager.getRegionName(regionCode);
if (regionName != null) {
languageName = languageName + ": " + regionName;
}
}
return String.format("\"%1$s\" (%2$s)", locale, languageName);
} else {
return '"' + locale + '"';
}
}
/** Look up the language for the given folder name */
private static String getLanguageTag(String name) {
if (FD_RES_VALUES.equals(name)) {
return null;
}
FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name);
if (configuration != null) {
LocaleQualifier locale = configuration.getLocaleQualifier();
if (locale != null && !locale.hasFakeValue()) {
return locale.getTag();
}
}
return null;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (mIgnoreFile) {
return;
}
Attr attribute = element.getAttributeNode(ATTR_NAME);
if (context.getPhase() == 2) {
// Just locating names requested in the {@link #mLocations} map
if (attribute == null) {
return;
}
String name = attribute.getValue();
if (mMissingLocations != null && mMissingLocations.containsKey(name)) {
String language = getLanguageTag(context.file.getParentFile().getName());
if (language == null) {
if (context.getDriver().isSuppressed(context, MISSING, element)) {
mMissingLocations.remove(name);
return;
}
Location location = context.getLocation(attribute);
location.setClientData(element);
location.setSecondary(mMissingLocations.get(name));
mMissingLocations.put(name, location);
}
}
if (mExtraLocations != null && mExtraLocations.containsKey(name)) {
String language = getLanguageTag(context.file.getParentFile().getName());
if (language != null) {
if (context.getDriver().isSuppressed(context, EXTRA, element)) {
mExtraLocations.remove(name);
return;
}
Location location = context.getLocation(attribute);
location.setClientData(element);
location.setMessage("Also translated here");
location.setSecondary(mExtraLocations.get(name));
mExtraLocations.put(name, location);
}
}
return;
}
assert context.getPhase() == 1;
if (attribute == null || attribute.getValue().isEmpty()) {
context.report(MISSING, element, context.getLocation(element),
"Missing `name` attribute in `<string>` declaration");
} else {
String name = attribute.getValue();
Attr translatable = element.getAttributeNode(ATTR_TRANSLATABLE);
if (translatable != null && !Boolean.valueOf(translatable.getValue())) {
String l = LintUtils.getLocaleAndRegion(context.file.getParentFile().getName());
//noinspection VariableNotUsedInsideIf
if (l != null) {
context.report(EXTRA, translatable, context.getLocation(translatable),
"Non-translatable resources should only be defined in the base " +
"`values/` folder");
} else {
if (mNonTranslatable == null) {
mNonTranslatable = new HashSet<String>();
}
mNonTranslatable.add(name);
}
return;
} else if (name.equals("google_maps_key") //$NON-NLS-1$
|| name.equals("google_maps_key_instructions")) { //$NON-NLS-1$
// Older versions of the templates shipped with these not marked as
// non-translatable; don't flag them
if (mNonTranslatable == null) {
mNonTranslatable = new HashSet<String>();
}
mNonTranslatable.add(name);
return;
}
if (element.getTagName().equals(TAG_STRING_ARRAY) &&
allItemsAreReferences(element)) {
// No need to provide translations for string arrays where all
// the children items are defined as translated string resources,
// e.g.
// <string-array name="foo">
// <item>@string/item1</item>
// <item>@string/item2</item>
// </string-array>
// However, we need to remember these names such that we don't consider
// these arrays "extra" if one of the *translated* versions of the array
// perform an inline translation of an array item
if (mTranslatedArrays == null) {
mTranslatedArrays = new HashSet<String>();
}
mTranslatedArrays.add(name);
return;
}
// Check for duplicate name definitions? No, because there can be
// additional customizations like product=
//if (mNames.contains(name)) {
// context.mClient.report(ISSUE, context.getLocation(attribute),
// String.format("Duplicate name %1$s, already defined earlier in this file",
// name));
//}
mNames.add(name);
if (mNonTranslatable != null && mNonTranslatable.contains(name)) {
String message = String.format("The resource string \"`%1$s`\" has been marked as " +
"`translatable=\"false\"`", name);
context.report(EXTRA, attribute, context.getLocation(attribute), message);
}
// TBD: Also make sure that the strings are not empty or placeholders?
}
}
private static boolean allItemsAreReferences(Element element) {
assert element.getTagName().equals(TAG_STRING_ARRAY);
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node item = childNodes.item(i);
if (item.getNodeType() == Node.ELEMENT_NODE &&
TAG_ITEM.equals(item.getNodeName())) {
NodeList itemChildren = item.getChildNodes();
for (int j = 0, m = itemChildren.getLength(); j < m; j++) {
Node valueNode = itemChildren.item(j);
if (valueNode.getNodeType() == Node.TEXT_NODE) {
String value = valueNode.getNodeValue().trim();
if (!value.startsWith(ANDROID_PREFIX)
&& !value.startsWith(STRING_PREFIX)) {
return false;
}
}
}
}
}
return true;
}
@Nullable
private static List<String> getResConfigLanguages(@NonNull Project project) {
if (project.isGradleProject() && project.getGradleProjectModel() != null &&
project.getCurrentVariant() != null) {
Set<String> relevantDensities = Sets.newHashSet();
Variant variant = project.getCurrentVariant();
List<String> variantFlavors = variant.getProductFlavors();
AndroidProject gradleProjectModel = project.getGradleProjectModel();
addResConfigsFromFlavor(relevantDensities, null,
project.getGradleProjectModel().getDefaultConfig());
for (ProductFlavorContainer container : gradleProjectModel.getProductFlavors()) {
addResConfigsFromFlavor(relevantDensities, variantFlavors, container);
}
if (!relevantDensities.isEmpty()) {
ArrayList<String> strings = Lists.newArrayList(relevantDensities);
Collections.sort(strings);
return strings;
}
}
return null;
}
/**
* Adds in the resConfig values specified by the given flavor container, assuming
* it's in one of the relevant variantFlavors, into the given set
*/
private static void addResConfigsFromFlavor(@NonNull Set<String> relevantLanguages,
@Nullable List<String> variantFlavors,
@NonNull ProductFlavorContainer container) {
ProductFlavor flavor = container.getProductFlavor();
if (variantFlavors == null || variantFlavors.contains(flavor.getName())) {
if (!flavor.getResourceConfigurations().isEmpty()) {
for (String resConfig : flavor.getResourceConfigurations()) {
// Look for languages; these are of length 2. (ResConfigs
// can also refer to densities, etc.)
if (resConfig.length() == 2) {
relevantLanguages.add(resConfig);
}
}
}
}
}
}
@@ -1,496 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ATTR_LOCALE;
import static com.android.SdkConstants.ATTR_TRANSLATABLE;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.TAG_PLURALS;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.TAG_STRING_ARRAY;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.tools.klint.checks.TypoLookup.isLetter;
import static com.google.common.base.Objects.equal;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.base.Charsets;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Check which looks for likely typos in Strings.
* <p>
* TODO:
* <ul>
* <li> Add check of Java String literals too!
* <li> Add support for <b>additional</b> languages. The typo detector is now
* multilingual and looks for typos-*locale*.txt files to use. However,
* we need to seed it with additional typo databases. I did some searching
* and came up with some alternatives. Here's the strategy I used:
* Used Google Translate to translate "Wikipedia Common Misspellings", and
* then I went to google.no, google.fr etc searching with that translation, and
* came up with what looks like wikipedia language local lists of typos.
* This is how I found the Norwegian one for example:
* <br>
* http://no.wikipedia.org/wiki/Wikipedia:Liste_over_alminnelige_stavefeil/Maskinform
* <br>
* Here are some additional possibilities not yet processed:
* <ul>
* <li> French: http://fr.wikipedia.org/wiki/Wikip%C3%A9dia:Liste_de_fautes_d'orthographe_courantes
* (couldn't find a machine-readable version there?)
* <li> Swedish:
* http://sv.wikipedia.org/wiki/Wikipedia:Lista_%C3%B6ver_vanliga_spr%C3%A5kfel
* (couldn't find a machine-readable version there?)
* <li> German
* http://de.wikipedia.org/wiki/Wikipedia:Liste_von_Tippfehlern/F%C3%BCr_Maschinen
* </ul>
* <li> Consider also digesting files like
* http://sv.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/Typos
* See http://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/User_manual.
* </ul>
*/
public class TypoDetector extends ResourceXmlDetector {
@Nullable private TypoLookup mLookup;
@Nullable private String mLastLanguage;
@Nullable private String mLastRegion;
@Nullable private String mLanguage;
@Nullable private String mRegion;
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"Typos", //$NON-NLS-1$
"Spelling error",
"This check looks through the string definitions, and if it finds any words " +
"that look like likely misspellings, they are flagged.",
Category.MESSAGES,
7,
Severity.WARNING,
new Implementation(
TypoDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new detector */
public TypoDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
/** Look up the locale and region from the given parent folder name and store it
* in {@link #mLanguage} and {@link #mRegion} */
private void initLocale(@NonNull String parent) {
mLanguage = null;
mRegion = null;
if (parent.equals(FD_RES_VALUES)) {
return;
}
LocaleQualifier locale = LintUtils.getLocale(parent);
if (locale != null) {
mLanguage = locale.getLanguage();
mRegion = locale.hasRegion() ? locale.getRegion() : null;
}
}
@Override
public void beforeCheckFile(@NonNull Context context) {
initLocale(context.file.getParentFile().getName());
if (mLanguage == null) {
// Check to see if the user has specified the language for this folder
// using a tools:locale attribute
if (context instanceof XmlContext) {
Element root = ((XmlContext) context).document.getDocumentElement();
if (root != null) {
String locale = root.getAttributeNS(TOOLS_URI, ATTR_LOCALE);
if (locale != null && !locale.isEmpty()) {
initLocale(FD_RES_VALUES + '-' + locale);
}
}
}
if (mLanguage == null) {
mLanguage = "en"; //$NON-NLS-1$
}
}
if (!equal(mLastLanguage, mLanguage) || !equal(mLastRegion, mRegion)) {
mLookup = TypoLookup.get(context.getClient(), mLanguage, mRegion);
mLastLanguage = mLanguage;
mLastRegion = mRegion;
}
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_STRING,
TAG_STRING_ARRAY,
TAG_PLURALS
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (mLookup == null) {
return;
}
visit(context, element, element);
}
private void visit(XmlContext context, Element parent, Node node) {
if (node.getNodeType() == Node.TEXT_NODE) {
// TODO: Figure out how to deal with entities
check(context, parent, node, node.getNodeValue());
} else {
NodeList children = node.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
visit(context, parent, children.item(i));
}
}
}
private void check(XmlContext context, Element element, Node node, String text) {
int max = text.length();
int index = 0;
int lastWordBegin = -1;
int lastWordEnd = -1;
boolean checkedTypos = false;
for (; index < max; index++) {
char c = text.charAt(index);
if (!Character.isWhitespace(c)) {
if (c == '@' || (c == '?')) {
// Don't look for typos in resource references; they are not
// user visible anyway
return;
}
break;
}
}
while (index < max) {
for (; index < max; index++) {
char c = text.charAt(index);
if (c == '\\') {
index++;
} else if (Character.isLetter(c)) {
break;
}
}
if (index >= max) {
return;
}
int begin = index;
for (; index < max; index++) {
char c = text.charAt(index);
if (c == '\\') {
index++;
break;
} else if (!Character.isLetter(c)) {
break;
} else if (text.charAt(index) >= 0x80) {
// Switch to UTF-8 handling for this string
if (checkedTypos) {
// If we've already checked words we may have reported typos
// so create a substring from the current word and on.
byte[] utf8Text = text.substring(begin).getBytes(Charsets.UTF_8);
check(context, element, node, utf8Text, 0, utf8Text.length, text, begin);
} else {
// If all we've done so far is skip whitespace (common scenario)
// then no need to substring the text, just re-search with the
// UTF-8 routines
byte[] utf8Text = text.getBytes(Charsets.UTF_8);
check(context, element, node, utf8Text, 0, utf8Text.length, text, 0);
}
return;
}
}
int end = index;
checkedTypos = true;
assert mLookup != null;
List<String> replacements = mLookup.getTypos(text, begin, end);
if (replacements != null && isTranslatable(element)) {
reportTypo(context, node, text, begin, replacements);
}
checkRepeatedWords(context, element, node, text, lastWordBegin, lastWordEnd, begin,
end);
lastWordBegin = begin;
lastWordEnd = end;
index = end + 1;
}
}
private static void checkRepeatedWords(XmlContext context, Element element, Node node,
String text, int lastWordBegin, int lastWordEnd, int begin, int end) {
if (lastWordBegin != -1 && end - begin == lastWordEnd - lastWordBegin
&& end - begin > 1) {
// See whether we have a repeated word
boolean different = false;
for (int i = lastWordBegin, j = begin; i < lastWordEnd; i++, j++) {
if (text.charAt(i) != text.charAt(j)) {
different = true;
break;
}
}
if (!different && onlySpace(text, lastWordEnd, begin) && isTranslatable(element)) {
reportRepeatedWord(context, node, text, lastWordBegin, begin, end);
}
}
}
private static boolean onlySpace(String text, int fromInclusive, int toExclusive) {
for (int i = fromInclusive; i < toExclusive; i++) {
if (!Character.isWhitespace(text.charAt(i))) {
return false;
}
}
return true;
}
private void check(XmlContext context, Element element, Node node, byte[] utf8Text,
int byteStart, int byteEnd, String text, int charStart) {
int lastWordBegin = -1;
int lastWordEnd = -1;
int index = byteStart;
while (index < byteEnd) {
// Find beginning of word
while (index < byteEnd) {
byte b = utf8Text[index];
if (b == '\\') {
index++;
charStart++;
if (index < byteEnd) {
b = utf8Text[index];
}
} else if (isLetter(b)) {
break;
}
index++;
if ((b & 0x80) == 0 || (b & 0xC0) == 0xC0) {
// First characters in UTF-8 are always ASCII (0 high bit) or 11XXXXXX
charStart++;
}
}
if (index >= byteEnd) {
return;
}
int charEnd = charStart;
int begin = index;
// Find end of word. Unicode has the nice property that even 2nd, 3rd and 4th
// bytes won't match these ASCII characters (because the high bit must be set there)
while (index < byteEnd) {
byte b = utf8Text[index];
if (b == '\\') {
index++;
charEnd++;
if (index < byteEnd) {
b = utf8Text[index++];
if ((b & 0x80) == 0 || (b & 0xC0) == 0xC0) {
charEnd++;
}
}
break;
} else if (!isLetter(b)) {
break;
}
index++;
if ((b & 0x80) == 0 || (b & 0xC0) == 0xC0) {
// First characters in UTF-8 are always ASCII (0 high bit) or 11XXXXXX
charEnd++;
}
}
int end = index;
List<String> replacements = mLookup.getTypos(utf8Text, begin, end);
if (replacements != null && isTranslatable(element)) {
reportTypo(context, node, text, charStart, replacements);
}
checkRepeatedWords(context, element, node, text, lastWordBegin, lastWordEnd, charStart,
charEnd);
lastWordBegin = charStart;
lastWordEnd = charEnd;
charStart = charEnd;
}
}
private static boolean isTranslatable(Element element) {
Attr translatable = element.getAttributeNode(ATTR_TRANSLATABLE);
return translatable == null || Boolean.valueOf(translatable.getValue());
}
/** Report the typo found at the given offset and suggest the given replacements */
private static void reportTypo(XmlContext context, Node node, String text, int begin,
List<String> replacements) {
if (replacements.size() < 2) {
return;
}
String typo = replacements.get(0);
String word = text.substring(begin, begin + typo.length());
String first = null;
String message;
boolean isCapitalized = Character.isUpperCase(word.charAt(0));
StringBuilder sb = new StringBuilder(40);
for (int i = 1, n = replacements.size(); i < n; i++) {
String replacement = replacements.get(i);
if (first == null) {
first = replacement;
}
if (sb.length() > 0) {
sb.append(" or ");
}
sb.append('"');
if (isCapitalized) {
sb.append(Character.toUpperCase(replacement.charAt(0)));
sb.append(replacement.substring(1));
} else {
sb.append(replacement);
}
sb.append('"');
}
if (first != null && first.equalsIgnoreCase(word)) {
if (first.equals(word)) {
return;
}
message = String.format(
"\"%1$s\" is usually capitalized as \"%2$s\"",
word, first);
} else {
message = String.format(
"\"%1$s\" is a common misspelling; did you mean %2$s ?",
word, sb.toString());
}
int end = begin + word.length();
context.report(ISSUE, node, context.getLocation(node, begin, end), message);
}
/** Reports a repeated word */
private static void reportRepeatedWord(XmlContext context, Node node, String text,
int lastWordBegin,
int begin, int end) {
String message = String.format(
"Repeated word \"%1$s\" in message: possible typo",
text.substring(begin, end));
Location location = context.getLocation(node, lastWordBegin, end);
context.report(ISSUE, node, location, message);
}
/** Returns the suggested replacements, if any, for the given typo. The error
* message <b>must</b> be one supplied by lint.
*
* @param errorMessage the error message
* @param format the format of the error message
* @return a list of replacement words suggested by the error message
*/
@Nullable
public static List<String> getSuggestions(@NonNull String errorMessage,
@NonNull TextFormat format) {
errorMessage = format.toText(errorMessage);
// The words are all in quotes; the first word is the misspelling,
// the other words are the suggested replacements
List<String> words = new ArrayList<String>();
// Skip the typo
int index = errorMessage.indexOf('"');
index = errorMessage.indexOf('"', index + 1);
index++;
while (true) {
index = errorMessage.indexOf('"', index);
if (index == -1) {
break;
}
index++;
int start = index;
index = errorMessage.indexOf('"', index);
if (index == -1) {
index = errorMessage.length();
}
words.add(errorMessage.substring(start, index));
index++;
}
return words;
}
/**
* Returns the typo word in the error message from this detector
*
* @param errorMessage the error message produced earlier by this detector
* @param format the format of the error message
* @return the typo
*/
@Nullable
public static String getTypo(@NonNull String errorMessage, @NonNull TextFormat format) {
errorMessage = format.toText(errorMessage);
// The words are all in quotes
int index = errorMessage.indexOf('"');
int start = index + 1;
index = errorMessage.indexOf('"', start);
if (index != -1) {
return errorMessage.substring(start, index);
}
return null;
}
}
@@ -1,778 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.DOT_XML;
import static com.android.tools.klint.detector.api.LintUtils.assertionsEnabled;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.LintUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.WeakHashMap;
/**
* Database of common typos / misspellings.
*/
public class TypoLookup {
private static final TypoLookup NONE = new TypoLookup();
/** String separating misspellings and suggested replacements in the text file */
private static final String WORD_SEPARATOR = "->"; //$NON-NLS-1$
/** Relative path to the typos database file within the Lint installation */
private static final String XML_FILE_PATH = "tools/support/typos-%1$s.txt"; //$NON-NLS-1$
private static final String FILE_HEADER = "Typo database used by Android lint\000";
private static final int BINARY_FORMAT_VERSION = 2;
private static final boolean DEBUG_FORCE_REGENERATE_BINARY = false;
private static final boolean DEBUG_SEARCH = false;
private static final boolean WRITE_STATS = false;
/** Default size to reserve for each API entry when creating byte buffer to build up data */
private static final int BYTES_PER_ENTRY = 28;
private byte[] mData;
private int[] mIndices;
private int mWordCount;
private static final WeakHashMap<String, TypoLookup> sInstanceMap =
new WeakHashMap<String, TypoLookup>();
/**
* Returns an instance of the Typo database for the given locale
*
* @param client the client to associate with this database - used only for
* logging. The database object may be shared among repeated
* invocations, and in that case client used will be the one
* originally passed in. In other words, this parameter may be
* ignored if the client created is not new.
* @param locale the locale to look up a typo database for (should be a
* language code (ISO 639-1, two lowercase character names)
* @param region the region to look up a typo database for (should be a two
* letter ISO 3166-1 alpha-2 country code in upper case) language
* code
* @return a (possibly shared) instance of the typo database, or null if its
* data can't be found
*/
@Nullable
public static TypoLookup get(@NonNull LintClient client, @NonNull String locale,
@Nullable String region) {
synchronized (TypoLookup.class) {
String key = locale;
if (region != null && region.length() == 2) { // skip BCP-47 regions
// Allow for region-specific dictionaries. See for example
// http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences
assert region.length() == 2
&& Character.isUpperCase(region.charAt(0))
&& Character.isUpperCase(region.charAt(1)) : region;
// Look for typos-en-rUS.txt etc
key = locale + 'r' + region;
}
TypoLookup db = sInstanceMap.get(key);
if (db == null) {
String path = String.format(XML_FILE_PATH, key);
File file = client.findResource(path);
if (file == null) {
// AOSP build environment?
String build = System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$
if (build != null) {
file = new File(build, ("sdk/files/" //$NON-NLS-1$
+ path.substring(path.lastIndexOf('/') + 1))
.replace('/', File.separatorChar));
}
}
if (file == null || !file.exists()) {
//noinspection VariableNotUsedInsideIf
if (region != null) {
// Fall back to the generic locale (non-region-specific) database
return get(client, locale, null);
}
db = NONE;
} else {
db = get(client, file);
assert db != null : file;
}
sInstanceMap.put(key, db);
}
if (db == NONE) {
return null;
} else {
return db;
}
}
}
/**
* Returns an instance of the typo database
*
* @param client the client to associate with this database - used only for
* logging
* @param xmlFile the XML file containing configuration data to use for this
* database
* @return a (possibly shared) instance of the typo database, or null
* if its data can't be found
*/
@Nullable
private static TypoLookup get(LintClient client, File xmlFile) {
if (!xmlFile.exists()) {
client.log(null, "The typo database file %1$s does not exist", xmlFile);
return null;
}
String name = xmlFile.getName();
if (LintUtils.endsWith(name, DOT_XML)) {
name = name.substring(0, name.length() - DOT_XML.length());
}
File cacheDir = client.getCacheDir(true/*create*/);
if (cacheDir == null) {
cacheDir = xmlFile.getParentFile();
}
File binaryData = new File(cacheDir, name
// Incorporate version number in the filename to avoid upgrade filename
// conflicts on Windows (such as issue #26663)
+ '-' + BINARY_FORMAT_VERSION + ".bin"); //$NON-NLS-1$
if (DEBUG_FORCE_REGENERATE_BINARY) {
System.err.println("\nTemporarily regenerating binary data unconditionally \nfrom "
+ xmlFile + "\nto " + binaryData);
if (!createCache(client, xmlFile, binaryData)) {
return null;
}
} else if (!binaryData.exists() || binaryData.lastModified() < xmlFile.lastModified()) {
if (!createCache(client, xmlFile, binaryData)) {
return null;
}
}
if (!binaryData.exists()) {
client.log(null, "The typo database file %1$s does not exist", binaryData);
return null;
}
return new TypoLookup(client, xmlFile, binaryData);
}
private static boolean createCache(LintClient client, File xmlFile, File binaryData) {
long begin = 0;
if (WRITE_STATS) {
begin = System.currentTimeMillis();
}
// Read in data
List<String> lines;
try {
lines = Files.readLines(xmlFile, Charsets.UTF_8);
} catch (IOException e) {
client.log(e, "Can't read typo database file");
return false;
}
if (WRITE_STATS) {
long end = System.currentTimeMillis();
System.out.println("Reading data structures took " + (end - begin) + " ms)");
}
try {
writeDatabase(binaryData, lines);
return true;
} catch (IOException ioe) {
client.log(ioe, "Can't write typo cache file");
}
return false;
}
/** Use one of the {@link #get} factory methods instead */
private TypoLookup(
@NonNull LintClient client,
@NonNull File xmlFile,
@Nullable File binaryFile) {
if (binaryFile != null) {
readData(client, xmlFile, binaryFile);
}
}
private TypoLookup() {
}
private void readData(@NonNull LintClient client, @NonNull File xmlFile,
@NonNull File binaryFile) {
if (!binaryFile.exists()) {
client.log(null, "%1$s does not exist", binaryFile);
return;
}
long start = System.currentTimeMillis();
try {
MappedByteBuffer buffer = Files.map(binaryFile, MapMode.READ_ONLY);
assert buffer.order() == ByteOrder.BIG_ENDIAN;
// First skip the header
byte[] expectedHeader = FILE_HEADER.getBytes(Charsets.US_ASCII);
buffer.rewind();
for (int offset = 0; offset < expectedHeader.length; offset++) {
if (expectedHeader[offset] != buffer.get()) {
client.log(null, "Incorrect file header: not an typo database cache " +
"file, or a corrupt cache file");
return;
}
}
// Read in the format number
if (buffer.get() != BINARY_FORMAT_VERSION) {
// Force regeneration of new binary data with up to date format
if (createCache(client, xmlFile, binaryFile)) {
readData(client, xmlFile, binaryFile); // Recurse
}
return;
}
mWordCount = buffer.getInt();
// Read in the word table indices;
int count = mWordCount;
int[] offsets = new int[count];
// Another idea: I can just store the DELTAS in the file (and add them up
// when reading back in) such that it takes just ONE byte instead of four!
for (int i = 0; i < count; i++) {
offsets[i] = buffer.getInt();
}
// No need to read in the rest -- we'll just keep the whole byte array in memory
// TODO: Make this code smarter/more efficient.
int size = buffer.limit();
byte[] b = new byte[size];
buffer.rewind();
buffer.get(b);
mData = b;
mIndices = offsets;
// TODO: We only need to keep the data portion here since we've initialized
// the offset array separately.
// TODO: Investigate (profile) accessing the byte buffer directly instead of
// accessing a byte array.
} catch (IOException e) {
client.log(e, null);
}
if (WRITE_STATS) {
long end = System.currentTimeMillis();
System.out.println("\nRead typo database in " + (end - start)
+ " milliseconds.");
System.out.println("Size of data table: " + mData.length + " bytes ("
+ Integer.toString(mData.length/1024) + "k)\n");
}
}
/** See the {@link #readData(LintClient,File,File)} for documentation on the data format. */
private static void writeDatabase(File file, List<String> lines) throws IOException {
/*
* 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded
* as ASCII characters. The purpose of the header is to identify what the file
* is for, for anyone attempting to open the file.
* 2. A file version number. If the binary file does not match the reader's expected
* version, it can ignore it (and regenerate the cache from XML).
*/
// Drop comments etc
List<String> words = new ArrayList<String>(lines.size());
for (String line : lines) {
if (!line.isEmpty() && Character.isLetter(line.charAt(0))) {
int end = line.indexOf(WORD_SEPARATOR);
if (end == -1) {
end = line.trim().length();
}
String typo = line.substring(0, end).trim();
String replacements = line.substring(end + WORD_SEPARATOR.length()).trim();
if (replacements.isEmpty()) {
// We don't support empty replacements
continue;
}
String combined = typo + (char) 0 + replacements;
words.add(combined);
}
}
byte[][] wordArrays = new byte[words.size()][];
for (int i = 0, n = words.size(); i < n; i++) {
String word = words.get(i);
wordArrays[i] = word.getBytes(Charsets.UTF_8);
}
// Sort words, using our own comparator to ensure that it matches the
// binary search in getTypos()
Comparator<byte[]> comparator = new Comparator<byte[]>() {
@Override
public int compare(byte[] o1, byte[] o2) {
return TypoLookup.compare(o1, 0, (byte) 0, o2, 0, o2.length);
}
};
Arrays.sort(wordArrays, comparator);
byte[] headerBytes = FILE_HEADER.getBytes(Charsets.US_ASCII);
int entryCount = wordArrays.length;
int capacity = entryCount * BYTES_PER_ENTRY + headerBytes.length + 5;
ByteBuffer buffer = ByteBuffer.allocate(capacity);
buffer.order(ByteOrder.BIG_ENDIAN);
// 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded
// as ASCII characters. The purpose of the header is to identify what the file
// is for, for anyone attempting to open the file.
buffer.put(headerBytes);
// 2. A file version number. If the binary file does not match the reader's expected
// version, it can ignore it (and regenerate the cache from XML).
buffer.put((byte) BINARY_FORMAT_VERSION);
// 3. The number of words [1 int]
buffer.putInt(entryCount);
// 4. Word offset table (one integer per word, pointing to the byte offset in the
// file (relative to the beginning of the file) where each word begins.
// The words are always sorted alphabetically.
int wordOffsetTable = buffer.position();
// Reserve enough room for the offset table here: we will backfill it with pointers
// as we're writing out the data structures below
for (int i = 0, n = entryCount; i < n; i++) {
buffer.putInt(0);
}
int nextEntry = buffer.position();
int nextOffset = wordOffsetTable;
// 7. Word entry table. Each word entry consists of the word, followed by the byte 0
// as a terminator, followed by a comma separated list of suggestions (which
// may be empty), or a final 0.
for (int i = 0; i < entryCount; i++) {
byte[] word = wordArrays[i];
buffer.position(nextOffset);
buffer.putInt(nextEntry);
nextOffset = buffer.position();
buffer.position(nextEntry);
buffer.put(word); // already embeds 0 to separate typo from words
buffer.put((byte) 0);
nextEntry = buffer.position();
}
int size = buffer.position();
assert size <= buffer.limit();
buffer.mark();
if (WRITE_STATS) {
System.out.println("Wrote " + words.size() + " word entries");
System.out.print("Actual binary size: " + size + " bytes");
System.out.println(String.format(" (%.1fM)", size/(1024*1024.f)));
System.out.println("Allocated size: " + (entryCount * BYTES_PER_ENTRY) + " bytes");
System.out.println("Required bytes per entry: " + (size/ entryCount) + " bytes");
}
// Now dump this out as a file
// There's probably an API to do this more efficiently; TODO: Look into this.
byte[] b = new byte[size];
buffer.rewind();
buffer.get(b);
FileOutputStream output = Files.newOutputStreamSupplier(file).getOutput();
output.write(b);
output.close();
}
// For debugging only
private String dumpEntry(int offset) {
if (DEBUG_SEARCH) {
int end = offset;
while (mData[end] != 0) {
end++;
}
return new String(mData, offset, end - offset, Charsets.UTF_8);
} else {
return "<disabled>"; //$NON-NLS-1$
}
}
/** Comparison function: *only* used for ASCII strings */
@VisibleForTesting
static int compare(byte[] data, int offset, byte terminator, CharSequence s,
int begin, int end) {
int i = offset;
int j = begin;
for (; ; i++, j++) {
byte b = data[i];
if (b == ' ') {
// We've matched up to the space in a split-word typo, such as
// in German all zu=>allzu; here we've matched just past "all".
// Rather than terminating, attempt to continue in the buffer.
if (j == end) {
int max = s.length();
if (end < max && s.charAt(end) == ' ') {
// Find next word
for (; end < max; end++) {
char c = s.charAt(end);
if (!Character.isLetter(c)) {
if (c == ' ' && end == j) {
continue;
}
break;
}
}
}
}
}
if (j == end) {
break;
}
if (b == '*') {
// Glob match (only supported at the end)
return 0;
}
char c = s.charAt(j);
byte cb = (byte) c;
int delta = b - cb;
if (delta != 0) {
cb = (byte) Character.toLowerCase(c);
if (b != cb) {
// Ensure that it has the right sign
b = (byte) Character.toLowerCase(b);
delta = b - cb;
if (delta != 0) {
return delta;
}
}
}
}
return data[i] - terminator;
}
/** Comparison function used for general UTF-8 encoded strings */
@VisibleForTesting
static int compare(byte[] data, int offset, byte terminator, byte[] s,
int begin, int end) {
int i = offset;
int j = begin;
for (; ; i++, j++) {
byte b = data[i];
if (b == ' ') {
// We've matched up to the space in a split-word typo, such as
// in German all zu=>allzu; here we've matched just past "all".
// Rather than terminating, attempt to continue in the buffer.
// We've matched up to the space in a split-word typo, such as
// in German all zu=>allzu; here we've matched just past "all".
// Rather than terminating, attempt to continue in the buffer.
if (j == end) {
int max = s.length;
if (end < max && s[end] == ' ') {
// Find next word
for (; end < max; end++) {
byte cb = s[end];
if (!isLetter(cb)) {
if (cb == ' ' && end == j) {
continue;
}
break;
}
}
}
}
}
if (j == end) {
break;
}
if (b == '*') {
// Glob match (only supported at the end)
return 0;
}
byte cb = s[j];
int delta = b - cb;
if (delta != 0) {
cb = toLowerCase(cb);
b = toLowerCase(b);
delta = b - cb;
if (delta != 0) {
return delta;
}
}
if (b == terminator || cb == terminator) {
return delta;
}
}
return data[i] - terminator;
}
/**
* Look up whether this word is a typo, and if so, return the typo itself
* and one or more likely meanings
*
* @param text the string containing the word
* @param begin the index of the first character in the word
* @param end the index of the first character after the word. Note that the
* search may extend <b>beyond</b> this index, if for example the
* word matches a multi-word typo in the dictionary
* @return a list of the typo itself followed by the replacement strings if
* the word represents a typo, and null otherwise
*/
@Nullable
public List<String> getTypos(@NonNull CharSequence text, int begin, int end) {
assert end <= text.length();
if (assertionsEnabled()) {
for (int i = begin; i < end; i++) {
char c = text.charAt(i);
if (c >= 128) {
assert false : "Call the UTF-8 version of this method instead";
return null;
}
}
}
int low = 0;
int high = mWordCount - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
int offset = mIndices[middle];
if (DEBUG_SEARCH) {
System.out.println("Comparing string " + text +" with entry at " + offset
+ ": " + dumpEntry(offset));
}
// Compare the word at the given index.
int compare = compare(mData, offset, (byte) 0, text, begin, end);
if (compare == 0) {
offset = mIndices[middle];
// Don't allow matching uncapitalized words, such as "enlish", when
// the dictionary word is capitalized, "Enlish".
if (mData[offset] != text.charAt(begin)
&& Character.isLowerCase(text.charAt(begin))) {
return null;
}
// Make sure there is a case match; we only want to allow
// matching capitalized words to capitalized typos or uncapitalized typos
// (e.g. "Teh" and "teh" to "the"), but not uncapitalized words to capitalized
// typos (e.g. "enlish" to "Enlish").
String glob = null;
for (int i = begin; ; i++) {
byte b = mData[offset++];
if (b == 0) {
offset--;
break;
} else if (b == '*') {
int globEnd = i;
while (globEnd < text.length()
&& Character.isLetter(text.charAt(globEnd))) {
globEnd++;
}
glob = text.subSequence(i, globEnd).toString();
break;
}
char c = text.charAt(i);
byte cb = (byte) c;
if (b != cb && i > begin) {
return null;
}
}
return computeSuggestions(mIndices[middle], offset, glob);
}
if (compare < 0) {
low = middle + 1;
} else if (compare > 0) {
high = middle - 1;
} else {
assert false; // compare == 0 already handled above
return null;
}
}
return null;
}
/**
* Look up whether this word is a typo, and if so, return the typo itself
* and one or more likely meanings
*
* @param utf8Text the string containing the word, encoded as UTF-8
* @param begin the index of the first character in the word
* @param end the index of the first character after the word. Note that the
* search may extend <b>beyond</b> this index, if for example the
* word matches a multi-word typo in the dictionary
* @return a list of the typo itself followed by the replacement strings if
* the word represents a typo, and null otherwise
*/
@Nullable
public List<String> getTypos(@NonNull byte[] utf8Text, int begin, int end) {
assert end <= utf8Text.length;
int low = 0;
int high = mWordCount - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
int offset = mIndices[middle];
if (DEBUG_SEARCH) {
String s = new String(Arrays.copyOfRange(utf8Text, begin, end), Charsets.UTF_8);
System.out.println("Comparing string " + s +" with entry at " + offset
+ ": " + dumpEntry(offset));
System.out.println(" middle=" + middle + ", low=" + low + ", high=" + high);
}
// Compare the word at the given index.
int compare = compare(mData, offset, (byte) 0, utf8Text, begin, end);
if (DEBUG_SEARCH) {
System.out.println(" signum=" + (int)Math.signum(compare) + ", delta=" + compare);
}
if (compare == 0) {
offset = mIndices[middle];
// Don't allow matching uncapitalized words, such as "enlish", when
// the dictionary word is capitalized, "Enlish".
if (mData[offset] != utf8Text[begin] && isUpperCase(mData[offset])) {
return null;
}
// Make sure there is a case match; we only want to allow
// matching capitalized words to capitalized typos or uncapitalized typos
// (e.g. "Teh" and "teh" to "the"), but not uncapitalized words to capitalized
// typos (e.g. "enlish" to "Enlish").
String glob = null;
for (int i = begin; ; i++) {
byte b = mData[offset++];
if (b == 0) {
offset--;
break;
} else if (b == '*') {
int globEnd = i;
while (globEnd < utf8Text.length && isLetter(utf8Text[globEnd])) {
globEnd++;
}
glob = new String(utf8Text, i, globEnd - i, Charsets.UTF_8);
break;
}
byte cb = utf8Text[i];
if (b != cb && i > begin) {
return null;
}
}
return computeSuggestions(mIndices[middle], offset, glob);
}
if (compare < 0) {
low = middle + 1;
} else if (compare > 0) {
high = middle - 1;
} else {
assert false; // compare == 0 already handled above
return null;
}
}
return null;
}
private List<String> computeSuggestions(int begin, int offset, String glob) {
String typo = new String(mData, begin, offset - begin, Charsets.UTF_8);
if (glob != null) {
typo = typo.replaceAll("\\*", glob); //$NON-NLS-1$
}
assert mData[offset] == 0;
offset++;
int replacementEnd = offset;
while (mData[replacementEnd] != 0) {
replacementEnd++;
}
String replacements = new String(mData, offset, replacementEnd - offset, Charsets.UTF_8);
List<String> words = new ArrayList<String>();
words.add(typo);
// The first entry should be the typo itself. We need to pass this back since due
// to multi-match words and globbing it could extend beyond the initial word range
for (String s : Splitter.on(',').omitEmptyStrings().trimResults().split(replacements)) {
if (glob != null) {
// Need to append the glob string to each result
words.add(s.replaceAll("\\*", glob)); //$NON-NLS-1$
} else {
words.add(s);
}
}
return words;
}
// "Character" handling for bytes. This assumes that the bytes correspond to Unicode
// characters in the ISO 8859-1 range, which is are encoded the same way in UTF-8.
// This obviously won't work to for example uppercase to lowercase conversions for
// multi byte characters, which means we simply won't catch typos if the dictionaries
// contain these. None of the currently included dictionaries do. However, it does
// help us properly deal with punctuation and spacing characters.
static boolean isUpperCase(byte b) {
return Character.isUpperCase((char) b);
}
static byte toLowerCase(byte b) {
return (byte) Character.toLowerCase((char) b);
}
static boolean isSpace(byte b) {
return Character.isWhitespace((char) b);
}
static boolean isLetter(byte b) {
// Assume that multi byte characters represent letters in other languages.
// Obviously, it could be unusual punctuation etc but letters are more likely
// in this context.
return Character.isLetter((char) b) || (b & 0x80) != 0;
}
}
@@ -1,531 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.TAG_PLURALS;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.TAG_STRING_ARRAY;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Checks for various typographical issues in string definitions.
*/
public class TypographyDetector extends ResourceXmlDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
TypographyDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Replace hyphens with dashes? */
public static final Issue DASHES = Issue.create(
"TypographyDashes", //$NON-NLS-1$
"Hyphen can be replaced with dash",
"The \"n dash\" (\u2013, &#8211;) and the \"m dash\" (\u2014, &#8212;) " +
"characters are used for ranges (n dash) and breaks (m dash). Using these " +
"instead of plain hyphens can make text easier to read and your application " +
"will look more polished.",
Category.TYPOGRAPHY,
5,
Severity.WARNING,
IMPLEMENTATION).
addMoreInfo("http://en.wikipedia.org/wiki/Dash"); //$NON-NLS-1$
/** Replace dumb quotes with smart quotes? */
public static final Issue QUOTES = Issue.create(
"TypographyQuotes", //$NON-NLS-1$
"Straight quotes can be replaced with curvy quotes",
"Straight single quotes and double quotes, when used as a pair, can be replaced " +
"by \"curvy quotes\" (or directional quotes). This can make the text more " +
"readable.\n" +
"\n" +
"Note that you should never use grave accents and apostrophes to quote, " +
"`like this'.\n" +
"\n" +
"(Also note that you should not use curvy quotes for code fragments.)",
Category.TYPOGRAPHY,
5,
Severity.WARNING,
IMPLEMENTATION).
addMoreInfo("http://en.wikipedia.org/wiki/Quotation_mark"). //$NON-NLS-1$
// This feature is apparently controversial: recent apps have started using
// straight quotes to avoid inconsistencies. Disabled by default for now.
setEnabledByDefault(false);
/** Replace fraction strings with fraction characters? */
public static final Issue FRACTIONS = Issue.create(
"TypographyFractions", //$NON-NLS-1$
"Fraction string can be replaced with fraction character",
"You can replace certain strings, such as 1/2, and 1/4, with dedicated " +
"characters for these, such as \u00BD (&#189;) and \u00BC (&#188;). " +
"This can help make the text more readable.",
Category.TYPOGRAPHY,
5,
Severity.WARNING,
IMPLEMENTATION).
addMoreInfo("http://en.wikipedia.org/wiki/Number_Forms"); //$NON-NLS-1$
/** Replace ... with the ellipsis character? */
public static final Issue ELLIPSIS = Issue.create(
"TypographyEllipsis", //$NON-NLS-1$
"Ellipsis string can be replaced with ellipsis character",
"You can replace the string \"...\" with a dedicated ellipsis character, " +
"ellipsis character (\u2026, &#8230;). This can help make the text more readable.",
Category.TYPOGRAPHY,
5,
Severity.WARNING,
IMPLEMENTATION).
addMoreInfo("http://en.wikipedia.org/wiki/Ellipsis"); //$NON-NLS-1$
/** The main issue discovered by this detector */
public static final Issue OTHER = Issue.create(
"TypographyOther", //$NON-NLS-1$
"Other typographical problems",
"This check looks for miscellaneous typographical problems and offers replacement " +
"sequences that will make the text easier to read and your application more " +
"polished.",
Category.TYPOGRAPHY,
3,
Severity.WARNING,
IMPLEMENTATION);
private static final String GRAVE_QUOTE_MESSAGE =
"Avoid quoting with grave accents; use apostrophes or better yet directional quotes instead";
private static final String ELLIPSIS_MESSAGE =
"Replace \"...\" with ellipsis character (\u2026, &#8230;) ?";
private static final String EN_DASH_MESSAGE =
"Replace \"-\" with an \"en dash\" character (\u2013, &#8211;) ?";
private static final String EM_DASH_MESSAGE =
"Replace \"--\" with an \"em dash\" character (\u2014, &#8212;) ?";
private static final String TYPOGRAPHIC_APOSTROPHE_MESSAGE =
"Replace apostrophe (') with typographic apostrophe (\u2019, &#8217;) ?";
private static final String SINGLE_QUOTE_MESSAGE =
"Replace straight quotes ('') with directional quotes (\u2018\u2019, &#8216; and &#8217;) ?";
private static final String DBL_QUOTES_MESSAGE =
"Replace straight quotes (\") with directional quotes (\u201C\u201D, &#8220; and &#8221;) ?";
private static final String COPYRIGHT_MESSAGE =
"Replace (c) with copyright symbol \u00A9 (&#169;) ?";
/**
* Pattern used to detect scenarios which can be replaced with n dashes: a
* numeric range with a hyphen in the middle (and possibly spaces)
*/
@VisibleForTesting
static final Pattern HYPHEN_RANGE_PATTERN =
Pattern.compile(".*(\\d+\\s*)-(\\s*\\d+).*"); //$NON-NLS-1$
/**
* Pattern used to detect scenarios where a grave accent mark is used
* to do ASCII quotations of the form `this'' or ``this'', which is frowned upon.
* This pattern tries to avoid falsely complaining about strings like
* "Type Option-` then 'Escape'."
*/
@VisibleForTesting
static final Pattern GRAVE_QUOTATION =
Pattern.compile("(^[^`]*`[^'`]+'[^']*$)|(^[^`]*``[^'`]+''[^']*$)"); //$NON-NLS-1$
/**
* Pattern used to detect common fractions, e.g. 1/2, 1/3, 2/3, 1/4, 3/4 and
* variations like 2 / 3, but not 11/22 and so on.
*/
@VisibleForTesting
static final Pattern FRACTION_PATTERN =
Pattern.compile(".*\\b([13])\\s*/\\s*([234])\\b.*"); //$NON-NLS-1$
/**
* Pattern used to detect single quote strings, such as 'hello', but
* not just quoted strings like 'Double quote: "', and not sentences
* where there are multiple apostrophes but not in a quoting context such
* as "Mind Your P's and Q's".
*/
@VisibleForTesting
static final Pattern SINGLE_QUOTE =
Pattern.compile(".*\\W*'[^']+'(\\W.*)?"); //$NON-NLS-1$
private static final String FRACTION_MESSAGE =
"Use fraction character %1$c (%2$s) instead of %3$s ?";
private static final String FRACTION_MESSAGE_PATTERN =
"Use fraction character (.+) \\((.+)\\) instead of (.+) \\?";
private boolean mCheckDashes;
private boolean mCheckQuotes;
private boolean mCheckFractions;
private boolean mCheckEllipsis;
private boolean mCheckMisc;
/** Constructs a new {@link TypographyDetector} */
public TypographyDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_STRING,
TAG_STRING_ARRAY,
TAG_PLURALS
);
}
@Override
public void beforeCheckProject(@NonNull Context context) {
mCheckDashes = context.isEnabled(DASHES);
mCheckQuotes = context.isEnabled(QUOTES);
mCheckFractions = context.isEnabled(FRACTIONS);
mCheckEllipsis = context.isEnabled(ELLIPSIS);
mCheckMisc = context.isEnabled(OTHER);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
checkText(context, element, child, text);
} else if (child.getNodeType() == Node.ELEMENT_NODE &&
(child.getParentNode().getNodeName().equals(TAG_STRING_ARRAY) ||
child.getParentNode().getNodeName().equals(TAG_PLURALS))) {
// String array or plural item children
NodeList items = child.getChildNodes();
for (int j = 0, m = items.getLength(); j < m; j++) {
Node item = items.item(j);
if (item.getNodeType() == Node.TEXT_NODE) {
String text = item.getNodeValue();
checkText(context, child, item, text);
}
}
}
}
}
private void checkText(XmlContext context, Node element, Node textNode, String text) {
if (mCheckEllipsis) {
// Replace ... with ellipsis character?
int ellipsis = text.indexOf("..."); //$NON-NLS-1$
if (ellipsis != -1 && !text.startsWith(".", ellipsis + 3)) { //$NON-NLS-1$
context.report(ELLIPSIS, element, context.getLocation(textNode),
ELLIPSIS_MESSAGE);
}
}
// Dashes
if (mCheckDashes) {
int hyphen = text.indexOf('-');
if (hyphen != -1) {
// n dash
Matcher matcher = HYPHEN_RANGE_PATTERN.matcher(text);
if (matcher.matches()) {
// Make sure that if there is no space before digit there isn't
// one on the left either -- since we don't want to consider
// "1 2 -3" as a range from 2 to 3
boolean isNegativeNumber =
!Character.isWhitespace(matcher.group(2).charAt(0)) &&
Character.isWhitespace(matcher.group(1).charAt(
matcher.group(1).length() - 1));
if (!isNegativeNumber && !isAnalyticsTrackingId((Element) element)) {
context.report(DASHES, element, context.getLocation(textNode),
EN_DASH_MESSAGE);
}
}
// m dash
int emdash = text.indexOf("--"); //$NON-NLS-1$
// Don't suggest replacing -- or "--" with an m dash since these are sometimes
// used as digit marker strings
if (emdash > 1 && !text.startsWith("-", emdash + 2)) { //$NON-NLS-1$
context.report(DASHES, element, context.getLocation(textNode),
EM_DASH_MESSAGE);
}
}
}
if (mCheckQuotes) {
// Check for single quotes that can be replaced with directional quotes
int quoteStart = text.indexOf('\'');
if (quoteStart != -1) {
int quoteEnd = text.indexOf('\'', quoteStart + 1);
if (quoteEnd != -1 && quoteEnd > quoteStart + 1
&& (quoteEnd < text.length() -1 || quoteStart > 0)
&& SINGLE_QUOTE.matcher(text).matches()) {
context.report(QUOTES, element, context.getLocation(textNode),
SINGLE_QUOTE_MESSAGE);
return;
}
// Check for apostrophes that can be replaced by typographic apostrophes
if (quoteEnd == -1 && quoteStart > 0
&& Character.isLetterOrDigit(text.charAt(quoteStart - 1))) {
context.report(QUOTES, element, context.getLocation(textNode),
TYPOGRAPHIC_APOSTROPHE_MESSAGE);
return;
}
}
// Check for double quotes that can be replaced by directional double quotes
quoteStart = text.indexOf('"');
if (quoteStart != -1) {
int quoteEnd = text.indexOf('"', quoteStart + 1);
if (quoteEnd != -1 && quoteEnd > quoteStart + 1) {
if (quoteEnd < text.length() -1 || quoteStart > 0) {
context.report(QUOTES, element, context.getLocation(textNode),
DBL_QUOTES_MESSAGE);
return;
}
}
}
// Check for grave accent quotations
if (text.indexOf('`') != -1 && GRAVE_QUOTATION.matcher(text).matches()) {
// Are we indenting ``like this'' or `this' ? If so, complain
context.report(QUOTES, element, context.getLocation(textNode),
GRAVE_QUOTE_MESSAGE);
return;
}
// Consider suggesting other types of directional quotes, such as guillemets, in
// other languages?
// There are a lot of exceptions and special cases to be considered so
// this will need careful implementation and testing.
// See http://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks
}
// Fraction symbols?
if (mCheckFractions && text.indexOf('/') != -1) {
Matcher matcher = FRACTION_PATTERN.matcher(text);
if (matcher.matches()) {
String top = matcher.group(1); // Numerator
String bottom = matcher.group(2); // Denominator
if (top.equals("1") && bottom.equals("2")) { //$NON-NLS-1$ //$NON-NLS-2$
context.report(FRACTIONS, element, context.getLocation(textNode),
String.format(FRACTION_MESSAGE, '\u00BD', "&#189;", "1/2"));
} else if (top.equals("1") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$
context.report(FRACTIONS, element, context.getLocation(textNode),
String.format(FRACTION_MESSAGE, '\u00BC', "&#188;", "1/4"));
} else if (top.equals("3") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$
context.report(FRACTIONS, element, context.getLocation(textNode),
String.format(FRACTION_MESSAGE, '\u00BE', "&#190;", "3/4"));
} else if (top.equals("1") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$
context.report(FRACTIONS, element, context.getLocation(textNode),
String.format(FRACTION_MESSAGE, '\u2153', "&#8531;", "1/3"));
} else if (top.equals("2") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$
context.report(FRACTIONS, element, context.getLocation(textNode),
String.format(FRACTION_MESSAGE, '\u2154', "&#8532;", "2/3"));
}
}
}
if (mCheckMisc) {
// Fix copyright symbol?
if (text.indexOf('(') != -1
&& (text.contains("(c)") || text.contains("(C)"))) { //$NON-NLS-1$ //$NON-NLS-2$
// Suggest replacing with copyright symbol?
context.report(OTHER, element, context.getLocation(textNode), COPYRIGHT_MESSAGE);
// Replace (R) and TM as well? There are unicode characters for these but they
// are probably not very common within Android app strings.
}
}
}
private static boolean isAnalyticsTrackingId(Element element) {
String name = element.getAttribute(ATTR_NAME);
return "ga_trackingId".equals(name); //$NON-NLS-1$
}
/**
* An object describing a single edit to be made. The offset points to a
* location to start editing; the length is the number of characters to
* delete, and the replaceWith string points to a string to insert at the
* offset. Note that this can model not just replacement edits but deletions
* (empty replaceWith) and insertions (replace length = 0) too.
*/
public static class ReplaceEdit {
/** The offset of the edit */
public final int offset;
/** The number of characters to delete at the offset */
public final int length;
/** The characters to insert at the offset */
public final String replaceWith;
/**
* Creates a new replace edit
*
* @param offset the offset of the edit
* @param length the number of characters to delete at the offset
* @param replaceWith the characters to insert at the offset
*/
public ReplaceEdit(int offset, int length, String replaceWith) {
super();
this.offset = offset;
this.length = length;
this.replaceWith = replaceWith;
}
}
/**
* Returns a list of edits to be applied to fix the suggestion made by the
* given warning. The specific issue id and message should be the message
* provided by this detector in an earlier run.
* <p>
* This is intended to help tools implement automatic fixes of these
* warnings. The reason only the message and issue id can be provided
* instead of actual state passed in the data field to a reporter is that
* fix operation can be run much later than the lint is processed (for
* example, in a subsequent run of the IDE when only the warnings have been
* persisted),
*
* @param issueId the issue id, which should be the id for one of the
* typography issues
* @param message the actual error message, which should be a message
* provided by this detector
* @param textNode a text node which corresponds to the text node the
* warning operated on
* @return a list of edits, which is never null but could be empty. The
* offsets in the edit objects are relative to the text node.
*/
public static List<ReplaceEdit> getEdits(String issueId, String message, Node textNode) {
return getEdits(issueId, message, textNode.getNodeValue());
}
/**
* Returns a list of edits to be applied to fix the suggestion made by the
* given warning. The specific issue id and message should be the message
* provided by this detector in an earlier run.
* <p>
* This is intended to help tools implement automatic fixes of these
* warnings. The reason only the message and issue id can be provided
* instead of actual state passed in the data field to a reporter is that
* fix operation can be run much later than the lint is processed (for
* example, in a subsequent run of the IDE when only the warnings have been
* persisted),
*
* @param issueId the issue id, which should be the id for one of the
* typography issues
* @param message the actual error message, which should be a message
* provided by this detector
* @param text the text of the XML node where the warning appeared
* @return a list of edits, which is never null but could be empty. The
* offsets in the edit objects are relative to the text node.
*/
public static List<ReplaceEdit> getEdits(String issueId, String message, String text) {
List<ReplaceEdit> edits = new ArrayList<ReplaceEdit>();
if (message.equals(ELLIPSIS_MESSAGE)) {
int offset = text.indexOf("..."); //$NON-NLS-1$
if (offset != -1) {
edits.add(new ReplaceEdit(offset, 3, "\u2026")); //$NON-NLS-1$
}
} else if (message.equals(EN_DASH_MESSAGE)) {
int offset = text.indexOf('-');
if (offset != -1) {
edits.add(new ReplaceEdit(offset, 1, "\u2013")); //$NON-NLS-1$
}
} else if (message.equals(EM_DASH_MESSAGE)) {
int offset = text.indexOf("--"); //$NON-NLS-1$
if (offset != -1) {
edits.add(new ReplaceEdit(offset, 2, "\u2014")); //$NON-NLS-1$
}
} else if (message.equals(TYPOGRAPHIC_APOSTROPHE_MESSAGE)) {
int offset = text.indexOf('\'');
if (offset != -1) {
edits.add(new ReplaceEdit(offset, 1, "\u2019")); //$NON-NLS-1$
}
} else if (message.equals(COPYRIGHT_MESSAGE)) {
int offset = text.indexOf("(c)"); //$NON-NLS-1$
if (offset == -1) {
offset = text.indexOf("(C)"); //$NON-NLS-1$
}
if (offset != -1) {
edits.add(new ReplaceEdit(offset, 3, "\u00A9")); //$NON-NLS-1$
}
} else if (message.equals(SINGLE_QUOTE_MESSAGE)) {
int offset = text.indexOf('\'');
if (offset != -1) {
int endOffset = text.indexOf('\'', offset + 1); //$NON-NLS-1$
if (endOffset != -1) {
edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$
edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$
}
}
} else if (message.equals(DBL_QUOTES_MESSAGE)) {
int offset = text.indexOf('"');
if (offset != -1) {
int endOffset = text.indexOf('"', offset + 1);
if (endOffset != -1) {
edits.add(new ReplaceEdit(offset, 1, "\u201C")); //$NON-NLS-1$
edits.add(new ReplaceEdit(endOffset, 1, "\u201D")); //$NON-NLS-1$
}
}
} else if (message.equals(GRAVE_QUOTE_MESSAGE)) {
int offset = text.indexOf('`');
if (offset != -1) {
int endOffset = text.indexOf('\'', offset + 1);
if (endOffset != -1) {
edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$
edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$
}
}
} else {
Matcher matcher = Pattern.compile(FRACTION_MESSAGE_PATTERN).matcher(message);
if (matcher.find()) {
// "Use fraction character %1$c (%2$s) instead of %3$s ?";
String replace = matcher.group(3);
int offset = text.indexOf(replace);
if (offset != -1) {
String replaceWith = matcher.group(2);
edits.add(new ReplaceEdit(offset, replace.length(), replaceWith));
}
}
}
return edits;
}
}
@@ -1,121 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
import static com.android.SdkConstants.ATTR_SCALE_TYPE;
import static com.android.SdkConstants.IMAGE_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.TEXT_VIEW;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Checks whether the current node can be replaced by a TextView using compound
* drawables.
*/
public class UseCompoundDrawableDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"UseCompoundDrawables", //$NON-NLS-1$
"Node can be replaced by a `TextView` with compound drawables",
"A `LinearLayout` which contains an `ImageView` and a `TextView` can be more " +
"efficiently handled as a compound drawable (a single TextView, using the " +
"`drawableTop`, `drawableLeft`, `drawableRight` and/or `drawableBottom` attributes " +
"to draw one or more images adjacent to the text).\n" +
"\n" +
"If the two widgets are offset from each other with " +
"margins, this can be replaced with a `drawablePadding` attribute.\n" +
"\n" +
"There's a lint quickfix to perform this conversion in the Eclipse plugin.",
Category.PERFORMANCE,
6,
Severity.WARNING,
new Implementation(
UseCompoundDrawableDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link UseCompoundDrawableDetector} */
public UseCompoundDrawableDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(
LINEAR_LAYOUT
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int childCount = LintUtils.getChildCount(element);
if (childCount == 2) {
List<Element> children = LintUtils.getChildren(element);
Element first = children.get(0);
Element second = children.get(1);
if ((first.getTagName().equals(IMAGE_VIEW) &&
second.getTagName().equals(TEXT_VIEW) &&
!first.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) ||
((second.getTagName().equals(IMAGE_VIEW) &&
first.getTagName().equals(TEXT_VIEW) &&
!second.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)))) {
// If the layout has a background, ignore since it would disappear from
// the TextView
if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
return;
}
// Certain scale types cannot be done with compound drawables
String scaleType = first.getTagName().equals(IMAGE_VIEW)
? first.getAttributeNS(ANDROID_URI, ATTR_SCALE_TYPE)
: second.getAttributeNS(ANDROID_URI, ATTR_SCALE_TYPE);
if (scaleType != null && !scaleType.isEmpty()) {
// For now, ignore if any scale type is set
return;
}
context.report(ISSUE, element, context.getLocation(element),
"This tag and its children can be replaced by one `<TextView/>` and " +
"a compound drawable");
}
}
}
}
@@ -1,269 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_END;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_START;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RADIO_GROUP;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.VIEW_MERGE;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Checks whether the current node can be removed without affecting the layout.
*/
public class UselessViewDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
UselessViewDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Issue of including a parent that has no value on its own */
public static final Issue USELESS_PARENT = Issue.create(
"UselessParent", //$NON-NLS-1$
"Useless parent layout",
"A layout with children that has no siblings, is not a scrollview or " +
"a root layout, and does not have a background, can be removed and have " +
"its children moved directly into the parent for a flatter and more " +
"efficient layout hierarchy.",
Category.PERFORMANCE,
2,
Severity.WARNING,
IMPLEMENTATION);
/** Issue of including a leaf that isn't shown */
public static final Issue USELESS_LEAF = Issue.create(
"UselessLeaf", //$NON-NLS-1$
"Useless leaf layout",
"A layout that has no children or no background can often be removed (since it " +
"is invisible) for a flatter and more efficient layout hierarchy.",
Category.PERFORMANCE,
2,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new {@link UselessViewDetector} */
public UselessViewDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
private static final List<String> CONTAINERS = new ArrayList<String>(18);
static {
CONTAINERS.add(ABSOLUTE_LAYOUT);
CONTAINERS.add(FRAME_LAYOUT);
CONTAINERS.add(GRID_LAYOUT);
CONTAINERS.add(GRID_VIEW);
CONTAINERS.add(HORIZONTAL_SCROLL_VIEW);
CONTAINERS.add("ImageSwitcher"); //$NON-NLS-1$
CONTAINERS.add(LINEAR_LAYOUT);
CONTAINERS.add(RADIO_GROUP);
CONTAINERS.add(RELATIVE_LAYOUT);
CONTAINERS.add(SCROLL_VIEW);
CONTAINERS.add("SlidingDrawer"); //$NON-NLS-1$
CONTAINERS.add("StackView"); //$NON-NLS-1$
CONTAINERS.add(TABLE_LAYOUT);
CONTAINERS.add(TABLE_ROW);
CONTAINERS.add("TextSwitcher"); //$NON-NLS-1$
CONTAINERS.add("ViewAnimator"); //$NON-NLS-1$
CONTAINERS.add("ViewFlipper"); //$NON-NLS-1$
CONTAINERS.add("ViewSwitcher"); //$NON-NLS-1$
// Available ViewGroups that are not included by this check:
// CONTAINERS.add("android.gesture.GestureOverlayView");
// CONTAINERS.add("AdapterViewFlipper");
// CONTAINERS.add("DialerFilter");
// CONTAINERS.add("ExpandableListView");
// CONTAINERS.add("ListView");
// CONTAINERS.add("MediaController");
// CONTAINERS.add("merge");
// CONTAINERS.add("SearchView");
// CONTAINERS.add("TabWidget");
// CONTAINERS.add("TabHost");
}
@Override
public Collection<String> getApplicableElements() {
return CONTAINERS;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int childCount = LintUtils.getChildCount(element);
if (childCount == 0) {
// Check to see if this is a leaf layout that can be removed
checkUselessLeaf(context, element);
} else {
// Check to see if this is a middle-man layout which can be removed
checkUselessMiddleLayout(context, element);
}
}
// This is the old UselessLayoutCheck from layoutopt
private static void checkUselessMiddleLayout(XmlContext context, Element element) {
// Conditions:
// - The node has children
// - The node does not have siblings
// - The node's parent is not a scroll view (horizontal or vertical)
// - The node does not have a background or its parent does not have a
// background or neither the node and its parent have a background
// - The parent is not a <merge/>
Node parentNode = element.getParentNode();
if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
// Can't remove root
return;
}
Element parent = (Element) parentNode;
String parentTag = parent.getTagName();
if (parentTag.equals(SCROLL_VIEW) || parentTag.equals(HORIZONTAL_SCROLL_VIEW) ||
parentTag.equals(VIEW_MERGE)) {
// Can't remove if the parent is a scroll view or a merge
return;
}
// This method is only called when we've already ensured that it has children
assert LintUtils.getChildCount(element) > 0;
int parentChildCount = LintUtils.getChildCount(parent);
if (parentChildCount != 1) {
// Don't remove if the node has siblings
return;
}
// - A parent can be removed if it doesn't have a background
// - A parent can be removed if has a background *and* the child does not have a
// background (in which case, just move the background over to the child, remove
// the parent)
// - If both child and parent have a background, the parent cannot be removed (a
// background can be translucent, have transparent padding, etc.)
boolean nodeHasBackground = element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
boolean parentHasBackground = parent.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
if (nodeHasBackground && parentHasBackground) {
// Can't remove because both define a background, and they might both be
// visible (e.g. through transparency or padding).
return;
}
// Certain parents are special - such as the TabHost and the GestureOverlayView -
// where we want to leave things alone.
if (!CONTAINERS.contains(parentTag)) {
return;
}
// If we define a padding, and the parent provides a background, then
// this view is not *necessarily* useless.
if (parentHasBackground && element.hasAttributeNS(ANDROID_URI, ATTR_PADDING)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_START)
|| element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_END)) {
return;
}
boolean hasId = element.hasAttributeNS(ANDROID_URI, ATTR_ID);
Location location = context.getLocation(element);
String tag = element.getTagName();
String format;
if (hasId) {
format = "This `%1$s` layout or its `%2$s` parent is possibly useless";
} else {
format = "This `%1$s` layout or its `%2$s` parent is useless";
}
if (nodeHasBackground || parentHasBackground) {
format += "; transfer the `background` attribute to the other view";
}
String message = String.format(format, tag, parentTag);
context.report(USELESS_PARENT, element, location, message);
}
// This is the old UselessView check from layoutopt
private static void checkUselessLeaf(XmlContext context, Element element) {
assert LintUtils.getChildCount(element) == 0;
// Conditions:
// - The node is a container view (LinearLayout, etc.)
// - The node has no id
// - The node has no background
// - The node has no children
// - The node has no style
// - The node is not a root
if (element.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
return;
}
if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
return;
}
if (element.hasAttribute(ATTR_STYLE)) {
return;
}
if (element == context.document.getDocumentElement()) {
return;
}
Location location = context.getLocation(element);
String tag = element.getTagName();
String message = String.format(
"This `%1$s` view is useless (no children, no `background`, no `id`, no `style`)", tag);
context.report(USELESS_LEAF, element, location, message);
}
}
@@ -1,115 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Document;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Checks that the encoding used in resource files is always UTF-8
* <p>
* TODO: Add a check which looks at files which do not specify the encoding
* and check the contents to see if it contains characters where it's ambiguous.
*/
public class Utf8Detector extends ResourceXmlDetector {
/** Detects non-utf8 encodings */
public static final Issue ISSUE = Issue.create(
"EnforceUTF8", //$NON-NLS-1$
"Encoding used in resource files is not UTF-8",
"XML supports encoding in a wide variety of character sets. However, not all " +
"tools handle the XML encoding attribute correctly, and nearly all Android " +
"apps use UTF-8, so by using UTF-8 you can protect yourself against subtle " +
"bugs when using non-ASCII characters.\n" +
"\n" +
"In particular, the Android Gradle build system will merge resource XML files " +
"assuming the resource files are using UTF-8 encoding.\n",
Category.I18N,
5,
Severity.FATAL,
new Implementation(
Utf8Detector.class,
Scope.RESOURCE_FILE_SCOPE));
/** See http://www.w3.org/TR/REC-xml/#NT-EncodingDecl */
private static final Pattern ENCODING_PATTERN =
Pattern.compile("encoding=['\"](\\S*)['\"]");//$NON-NLS-1$
/** Constructs a new {@link Utf8Detector} */
public Utf8Detector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
String xml = context.getContents();
if (xml == null) {
return;
}
// AAPT: The prologue must be in the first line
int lineEnd = 0;
int max = xml.length();
for (; lineEnd < max; lineEnd++) {
char c = xml.charAt(lineEnd);
if (c == '\n' || c == '\r') {
break;
}
}
for (int i = 16; i < lineEnd - 5; i++) { // +4: Skip at least <?xml encoding="
if ((xml.charAt(i) == 'u' || xml.charAt(i) == 'U')
&& (xml.charAt(i + 1) == 't' || xml.charAt(i + 1) == 'T')
&& (xml.charAt(i + 2) == 'f' || xml.charAt(i + 2) == 'F')
&& (xml.charAt(i + 3) == '-' || xml.charAt(i + 3) == '_')
&& (xml.charAt(i + 4) == '8')) {
return;
}
}
int encodingIndex = xml.lastIndexOf("encoding", lineEnd); //$NON-NLS-1$
if (encodingIndex != -1) {
Matcher matcher = ENCODING_PATTERN.matcher(xml);
if (matcher.find(encodingIndex)) {
String encoding = matcher.group(1);
Location location = Location.create(context.file, xml,
matcher.start(1), matcher.end(1));
context.report(ISSUE, null, location, String.format(
"%1$s: Not using UTF-8 as the file encoding. This can lead to subtle " +
"bugs with non-ascii characters", encoding));
}
}
}
}
@@ -1,176 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.client.api.SdkInfo;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;
import java.util.Collections;
import java.util.List;
/**
* Checks for missing view tag detectors
*/
public class ViewTagDetector extends Detector implements ClassScanner {
/** Using setTag and leaking memory */
public static final Issue ISSUE = Issue.create(
"ViewTag", //$NON-NLS-1$
"Tagged object leaks",
"Prior to Android 4.0, the implementation of `View.setTag(int, Object)` would " +
"store the objects in a static map, where the values were strongly referenced. " +
"This means that if the object contains any references pointing back to the " +
"context, the context (which points to pretty much everything else) will leak. " +
"If you pass a view, the view provides a reference to the context " +
"that created it. Similarly, view holders typically contain a view, and cursors " +
"are sometimes also associated with views.",
Category.PERFORMANCE,
6,
Severity.WARNING,
new Implementation(
ViewTagDetector.class,
Scope.CLASS_FILE_SCOPE));
/** Constructs a new {@link ViewTagDetector} */
public ViewTagDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements ClassScanner ----
@Override
@Nullable
public List<String> getApplicableCallNames() {
return Collections.singletonList("setTag"); //$NON-NLS-1$
}
@Override
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
// The leak behavior is fixed in ICS:
// http://code.google.com/p/android/issues/detail?id=18273
if (context.getMainProject().getMinSdk() >= 14) {
return;
}
String owner = call.owner;
String desc = call.desc;
if (owner.equals("android/view/View") //$NON-NLS-1$
&& desc.equals("(ILjava/lang/Object;)V")) { //$NON-NLS-1$
Analyzer analyzer = new Analyzer(new BasicInterpreter() {
@Override
public BasicValue newValue(Type type) {
if (type == null) {
return BasicValue.UNINITIALIZED_VALUE;
} else if (type.getSort() == Type.VOID) {
return null;
} else {
return new BasicValue(type);
}
}
});
try {
Frame[] frames = analyzer.analyze(classNode.name, method);
InsnList instructions = method.instructions;
Frame frame = frames[instructions.indexOf(call)];
if (frame.getStackSize() < 3) {
return;
}
BasicValue stackValue = (BasicValue) frame.getStack(2);
Type type = stackValue.getType();
if (type == null) {
return;
}
String internalName = type.getInternalName();
String className = type.getClassName();
LintDriver driver = context.getDriver();
SdkInfo sdkInfo = context.getClient().getSdkInfo(context.getMainProject());
String objectType = null;
while (className != null) {
if (className.equals("android.view.View")) { //$NON-NLS-1$
objectType = "views";
break;
} else if (className.endsWith("ViewHolder")) { //$NON-NLS-1$
objectType = "view holders";
break;
} else if (className.endsWith("Cursor") //$NON-NLS-1$
&& className.startsWith("android.")) { //$NON-NLS-1$
objectType = "cursors";
break;
}
// TBD: Bitmaps, drawables? That's tricky, because as explained in
// http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html
// apparently these are used along with nulling out the callbacks,
// and that's harder to detect here
String parent = sdkInfo.getParentViewClass(className);
if (parent == null) {
if (internalName == null) {
internalName = className.replace('.', '/');
}
assert internalName != null;
parent = driver.getSuperClass(internalName);
}
className = parent;
internalName = null;
}
if (objectType != null) {
Location location = context.getLocation(call);
String message = String.format("Avoid setting %1$s as values for `setTag`: " +
"Can lead to memory leaks in versions older than Android 4.0",
objectType);
context.report(ISSUE, method, call, location, message);
}
} catch (AnalyzerException e) {
context.log(e, null);
}
}
}
}
@@ -1,431 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.ANDROID_APP_ACTIVITY;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.checks.ControlFlowGraph.Node;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import java.util.Arrays;
import java.util.List;
/**
* Checks for problems with wakelocks (such as failing to release them)
* which can lead to unnecessary battery usage.
*/
public class WakelockDetector extends Detector implements ClassScanner {
/** Problems using wakelocks */
public static final Issue ISSUE = Issue.create(
"Wakelock", //$NON-NLS-1$
"Incorrect `WakeLock` usage",
"Failing to release a wakelock properly can keep the Android device in " +
"a high power mode, which reduces battery life. There are several causes " +
"of this, such as releasing the wake lock in `onDestroy()` instead of in " +
"`onPause()`, failing to call `release()` in all possible code paths after " +
"an `acquire()`, and so on.\n" +
"\n" +
"NOTE: If you are using the lock just to keep the screen on, you should " +
"strongly consider using `FLAG_KEEP_SCREEN_ON` instead. This window flag " +
"will be correctly managed by the platform as the user moves between " +
"applications and doesn't require a special permission. See " +
"http://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_KEEP_SCREEN_ON.",
Category.PERFORMANCE,
9,
Severity.WARNING,
new Implementation(
WakelockDetector.class,
Scope.CLASS_FILE_SCOPE));
private static final String WAKELOCK_OWNER = "android/os/PowerManager$WakeLock"; //$NON-NLS-1$
private static final String RELEASE_METHOD = "release"; //$NON-NLS-1$
private static final String ACQUIRE_METHOD = "acquire"; //$NON-NLS-1$
private static final String IS_HELD_METHOD = "isHeld"; //$NON-NLS-1$
private static final String POWER_MANAGER = "android/os/PowerManager"; //$NON-NLS-1$
private static final String NEW_WAKE_LOCK_METHOD = "newWakeLock"; //$NON-NLS-1$
/** Print diagnostics during analysis (display flow control graph etc).
* Make sure you add the asm-debug or asm-util jars to the runtime classpath
* as well since the opcode integer to string mapping display routine looks for
* it via reflection. */
private static final boolean DEBUG = false;
/** Constructs a new {@link WakelockDetector} */
public WakelockDetector() {
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mHasAcquire && !mHasRelease && context.getDriver().getPhase() == 1) {
// Gather positions of the acquire calls
context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE);
}
}
// ---- Implements ClassScanner ----
/** Whether any {@code acquire()} calls have been encountered */
private boolean mHasAcquire;
/** Whether any {@code release()} calls have been encountered */
private boolean mHasRelease;
@Override
@Nullable
public List<String> getApplicableCallNames() {
return Arrays.asList(ACQUIRE_METHOD, RELEASE_METHOD, NEW_WAKE_LOCK_METHOD);
}
@Override
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (call.owner.equals(WAKELOCK_OWNER)) {
String name = call.name;
if (name.equals(ACQUIRE_METHOD)) {
if (call.desc.equals("(J)V")) { // acquire(long timeout) does not require a corresponding release
return;
}
mHasAcquire = true;
if (context.getDriver().getPhase() == 2) {
assert !mHasRelease;
context.report(ISSUE, method, call, context.getLocation(call),
"Found a wakelock `acquire()` but no `release()` calls anywhere");
} else {
assert context.getDriver().getPhase() == 1;
// Perform flow analysis in this method to see if we're
// performing an acquire/release block, where there are code paths
// between the acquire and release which can result in the
// release call not getting reached.
checkFlow(context, classNode, method, call);
}
} else if (name.equals(RELEASE_METHOD)) {
mHasRelease = true;
// See if the release is happening in an onDestroy method, in an
// activity.
if ("onDestroy".equals(method.name) //$NON-NLS-1$
&& context.getDriver().isSubclassOf(
classNode, ANDROID_APP_ACTIVITY)) {
context.report(ISSUE, method, call, context.getLocation(call),
"Wakelocks should be released in `onPause`, not `onDestroy`");
}
}
} else if (call.owner.equals(POWER_MANAGER)) {
if (call.name.equals(NEW_WAKE_LOCK_METHOD)) {
AbstractInsnNode prev = LintUtils.getPrevInstruction(call);
if (prev == null) {
return;
}
prev = LintUtils.getPrevInstruction(prev);
if (prev == null || prev.getOpcode() != Opcodes.LDC) {
return;
}
LdcInsnNode ldc = (LdcInsnNode) prev;
Object constant = ldc.cst;
if (constant instanceof Integer) {
int flag = ((Integer) constant).intValue();
// Constant values are copied into the bytecode so we have to compare
// values; however, that means the values are part of the API
final int PARTIAL_WAKE_LOCK = 0x00000001;
final int ACQUIRE_CAUSES_WAKEUP = 0x10000000;
final int both = PARTIAL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP;
if ((flag & both) == both) {
context.report(ISSUE, method, call, context.getLocation(call),
"Should not set both `PARTIAL_WAKE_LOCK` and `ACQUIRE_CAUSES_WAKEUP`. "
+ "If you do not want the screen to turn on, get rid of "
+ "`ACQUIRE_CAUSES_WAKEUP`");
}
}
}
}
}
private static void checkFlow(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode acquire) {
// Track allocations such that we know whether the type of the call
// is on a SecureRandom rather than a Random
final InsnList instructions = method.instructions;
MethodInsnNode release = null;
// Find release call
for (int i = 0, n = instructions.size(); i < n; i++) {
AbstractInsnNode instruction = instructions.get(i);
int type = instruction.getType();
if (type == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (call.name.equals(RELEASE_METHOD) &&
call.owner.equals(WAKELOCK_OWNER)) {
release = call;
break;
}
}
}
if (release == null) {
// Didn't find both acquire and release in this method; no point in doing
// local flow analysis
return;
}
try {
MyGraph graph = new MyGraph();
ControlFlowGraph.create(graph, classNode, method);
if (DEBUG) {
// Requires util package
//ClassNode clazz = classNode;
//clazz.accept(new TraceClassVisitor(new PrintWriter(System.out)));
System.out.println(graph.toString(graph.getNode(acquire)));
}
int status = dfs(graph.getNode(acquire));
if ((status & SEEN_RETURN) != 0) {
String message;
if ((status & SEEN_EXCEPTION) != 0) {
message = "The `release()` call is not always reached (via exceptional flow)";
} else {
message = "The `release()` call is not always reached";
}
context.report(ISSUE, method, acquire,
context.getLocation(release), message);
}
} catch (AnalyzerException e) {
context.log(e, null);
}
}
private static final int SEEN_TARGET = 1;
private static final int SEEN_BRANCH = 2;
private static final int SEEN_EXCEPTION = 4;
private static final int SEEN_RETURN = 8;
/** TODO RENAME */
private static class MyGraph extends ControlFlowGraph {
@Override
protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
if (from.getOpcode() == Opcodes.IFNULL) {
JumpInsnNode jump = (JumpInsnNode) from;
if (jump.label == to) {
// Skip jump targets on null if it's surrounding the release call
//
// if (lock != null) {
// lock.release();
// }
//
// The above shouldn't be considered a scenario where release() may not
// be called.
AbstractInsnNode next = LintUtils.getNextInstruction(from);
if (next != null && next.getType() == AbstractInsnNode.VAR_INSN) {
next = LintUtils.getNextInstruction(next);
if (next != null && next.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode method = (MethodInsnNode) next;
if (method.name.equals(RELEASE_METHOD) &&
method.owner.equals(WAKELOCK_OWNER)) {
// This isn't entirely correct; this will also trigger
// for "if (lock == null) { lock.release(); }" but that's
// not likely (and caught by other null checking in tools)
return;
}
}
}
}
} else if (from.getOpcode() == Opcodes.IFEQ) {
JumpInsnNode jump = (JumpInsnNode) from;
if (jump.label == to) {
AbstractInsnNode prev = LintUtils.getPrevInstruction(from);
if (prev != null && prev.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode method = (MethodInsnNode) prev;
if (method.name.equals(IS_HELD_METHOD) &&
method.owner.equals(WAKELOCK_OWNER)) {
AbstractInsnNode next = LintUtils.getNextInstruction(from);
if (next != null) {
super.add(from, next);
return;
}
}
}
}
}
super.add(from, to);
}
}
/** Search from the given node towards the target; return false if we reach
* an exit point such as a return or a call on the way there that is not within
* a try/catch clause.
*
* @param node the current node
* @return true if the target was reached
* XXX RETURN VALUES ARE WRONG AS OF RIGHT NOW
*/
protected static int dfs(Node node) {
AbstractInsnNode instruction = node.instruction;
if (instruction.getType() == AbstractInsnNode.JUMP_INSN) {
int opcode = instruction.getOpcode();
if (opcode == Opcodes.RETURN || opcode == Opcodes.ARETURN
|| opcode == Opcodes.LRETURN || opcode == Opcodes.IRETURN
|| opcode == Opcodes.DRETURN || opcode == Opcodes.FRETURN
|| opcode == Opcodes.ATHROW) {
if (DEBUG) {
System.out.println("Found exit via explicit return: " //$NON-NLS-1$
+ node.toString(false));
}
return SEEN_RETURN;
}
}
if (!DEBUG) {
// There are no cycles, so no *NEED* for this, though it does avoid
// researching shared labels. However, it makes debugging harder (no re-entry)
// so this is only done when debugging is off
if (node.visit != 0) {
return 0;
}
node.visit = 1;
}
// Look for the target. This is any method call node which is a release on the
// lock (later also check it's the same instance, though that's harder).
// This is because finally blocks tend to be inlined so from a single try/catch/finally
// with a release() in the finally, the bytecode can contain multiple repeated
// (inlined) release() calls.
if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode method = (MethodInsnNode) instruction;
if (method.name.equals(RELEASE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
return SEEN_TARGET;
} else if (method.name.equals(ACQUIRE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
// OK
} else if (method.name.equals(IS_HELD_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
// OK
} else {
// Some non acquire/release method call: if this is not associated with a
// try-catch block, it would mean the exception would exit the method,
// which would be an error
if (node.exceptions == null || node.exceptions.isEmpty()) {
// Look up the corresponding frame, if any
AbstractInsnNode curr = method.getPrevious();
boolean foundFrame = false;
while (curr != null) {
if (curr.getType() == AbstractInsnNode.FRAME) {
foundFrame = true;
break;
}
curr = curr.getPrevious();
}
if (!foundFrame) {
if (DEBUG) {
System.out.println("Found exit via unguarded method call: " //$NON-NLS-1$
+ node.toString(false));
}
return SEEN_RETURN;
}
}
}
}
// if (node.instruction is a call, and the call is not caught by
// a try/catch block (provided the release is not inside the try/catch block)
// then return false
int status = 0;
boolean implicitReturn = true;
List<Node> successors = node.successors;
List<Node> exceptions = node.exceptions;
if (exceptions != null) {
if (!exceptions.isEmpty()) {
implicitReturn = false;
}
for (Node successor : exceptions) {
status = dfs(successor) | status;
if ((status & SEEN_RETURN) != 0) {
if (DEBUG) {
System.out.println("Found exit via exception: " //$NON-NLS-1$
+ node.toString(false));
}
return status;
}
}
if (status != 0) {
status |= SEEN_EXCEPTION;
}
}
if (successors != null) {
if (!successors.isEmpty()) {
implicitReturn = false;
if (successors.size() > 1) {
status |= SEEN_BRANCH;
}
}
for (Node successor : successors) {
status = dfs(successor) | status;
if ((status & SEEN_RETURN) != 0) {
if (DEBUG) {
System.out.println("Found exit via branches: " //$NON-NLS-1$
+ node.toString(false));
}
return status;
}
}
}
if (implicitReturn) {
status |= SEEN_RETURN;
if (DEBUG) {
System.out.println("Found exit: via implicit return: " //$NON-NLS-1$
+ node.toString(false));
}
}
return status;
}
}
@@ -1,102 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
import static com.android.SdkConstants.WEB_VIEW;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.Collection;
import java.util.Collections;
public class WebViewDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
WebViewDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"WebViewLayout", //$NON-NLS-1$
"WebViews in wrap_content parents",
"The WebView implementation has certain performance optimizations which will not " +
"work correctly if the parent view is using `wrap_content` rather than " +
"`match_parent`. This can lead to subtle UI bugs.",
Category.CORRECTNESS,
7,
Severity.ERROR,
IMPLEMENTATION);
/** Constructs a new {@link WebViewDetector} */
public WebViewDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(WEB_VIEW);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
Node parentNode = element.getParentNode();
if (parentNode != null && parentNode.getNodeType() == Node.ELEMENT_NODE) {
Element parent = (Element)parentNode;
Attr width = parent.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
Attr height = parent.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
Attr attr = null;
if (width != null && VALUE_WRAP_CONTENT.equals(width.getValue())) {
attr = width;
}
if (height != null && VALUE_WRAP_CONTENT.equals(height.getValue())) {
attr = height;
}
if (attr != null) {
String message = String.format("Placing a `<WebView>` in a parent element that "
+ "uses a `wrap_content %1$s` can lead to subtle bugs; use `match_parent` "
+ "instead", attr.getLocalName());
Location location = context.getLocation(element);
Location secondary = context.getLocation(attr);
secondary.setMessage("`wrap_content` here may not work well with WebView below");
location.setSecondary(secondary);
context.report(ISSUE, element, location, message);
}
}
}
}
@@ -1,117 +0,0 @@
/*
* Copyright (C) 2013 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.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Element;
import java.util.Arrays;
import java.util.Collection;
/**
* Check which looks for missing wrong case usage for certain layout tags.
*
* TODO: Generalize this to handling spelling errors in general.
*/
public class WrongCaseDetector extends LayoutDetector {
/** Using the wrong case for layout tags */
public static final Issue WRONG_CASE = Issue.create(
"WrongCase", //$NON-NLS-1$
"Wrong case for view tag",
"Most layout tags, such as <Button>, refer to actual view classes and are therefore " +
"capitalized. However, there are exceptions such as <fragment> and <include>. This " +
"lint check looks for incorrect capitalizations.",
Category.CORRECTNESS,
4,
Severity.FATAL,
new Implementation(
WrongCaseDetector.class,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/guide/components/fragments.html"); //$NON-NLS-1$
/** Constructs a new {@link WrongCaseDetector} */
public WrongCaseDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
"Fragment", //$NON-NLS-1$
"RequestFocus", //$NON-NLS-1$
"Include", //$NON-NLS-1$
"Merge"
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
String correct = Character.toLowerCase(tag.charAt(0)) + tag.substring(1);
context.report(WRONG_CASE, element, context.getLocation(element),
String.format("Invalid tag `<%1$s>`; should be `<%2$s>`", tag, correct));
}
/**
* Given an error message produced by this lint detector for the given issue type,
* returns the old value to be replaced in the source code.
* <p>
* Intended for IDE quickfix implementations.
*
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the corresponding old value, or null if not recognized
*/
@Nullable
public static String getOldValue(@NonNull String errorMessage, @NonNull TextFormat format) {
return LintUtils.findSubstring(format.toText(errorMessage), " tag <", ">");
}
/**
* Given an error message produced by this lint detector for the given issue type,
* returns the new value to be put into the source code.
* <p>
* Intended for IDE quickfix implementations.
*
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the corresponding new value, or null if not recognized
*/
@Nullable
public static String getNewValue(@NonNull String errorMessage, @NonNull TextFormat format) {
return LintUtils.findSubstring(format.toText(errorMessage), " should be <", ">");
}
}
@@ -1,507 +0,0 @@
/*
* 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.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.VALUE_ID;
import static com.android.tools.klint.detector.api.LintUtils.editDistance;
import static com.android.tools.klint.detector.api.LintUtils.getChildren;
import static com.android.tools.klint.detector.api.LintUtils.isSameResourceFile;
import static com.android.tools.klint.detector.api.LintUtils.stripIdPrefix;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Location.Handle;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
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.HashSet;
import java.util.List;
import java.util.Set;
/**
* Checks for duplicate ids within a layout and within an included layout
*/
public class WrongIdDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
WrongIdDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Ids bound to widgets in any of the layout files */
private final Set<String> mGlobalIds = new HashSet<String>(100);
/** Ids bound to widgets in the current layout file */
private Set<String> mFileIds;
/** Ids declared in a value's file, e.g. {@code <item type="id" name="foo"/>} */
private Set<String> mDeclaredIds;
/**
* Location handles for the various id references that were not found as
* defined in the same layout, to be checked after the whole project has
* been scanned
*/
private List<Pair<String, Location.Handle>> mHandles;
/** List of RelativeLayout elements in the current layout */
private List<Element> mRelativeLayouts;
/** Reference to an unknown id */
@SuppressWarnings("unchecked")
public static final Issue UNKNOWN_ID = Issue.create(
"UnknownId", //$NON-NLS-1$
"Reference to an unknown id",
"The `@+id/` syntax refers to an existing id, or creates a new one if it has " +
"not already been defined elsewhere. However, this means that if you have a " +
"typo in your reference, or if the referred view no longer exists, you do not " +
"get a warning since the id will be created on demand. This check catches " +
"errors where you have renamed an id without updating all of the references to " +
"it.",
Category.CORRECTNESS,
8,
Severity.FATAL,
new Implementation(
WrongIdDetector.class,
Scope.ALL_RESOURCES_SCOPE,
Scope.RESOURCE_FILE_SCOPE));
/** Reference to an id that is not a sibling */
public static final Issue NOT_SIBLING = Issue.create(
"NotSibling", //$NON-NLS-1$
"RelativeLayout Invalid Constraints",
"Layout constraints in a given `RelativeLayout` should reference other views " +
"within the same relative layout (but not itself!)",
Category.CORRECTNESS,
6,
Severity.FATAL,
IMPLEMENTATION);
/** An ID declaration which is not valid */
public static final Issue INVALID = Issue.create(
"InvalidId", //$NON-NLS-1$
"Invalid ID declaration",
"An id definition *must* be of the form `@+id/yourname`. The tools have not " +
"rejected strings of the form `@+foo/bar` in the past, but that was an error, " +
"and could lead to tricky errors because of the way the id integers are assigned.\n" +
"\n" +
"If you really want to have different \"scopes\" for your id's, use prefixes " +
"instead, such as `login_button1` and `login_button2`.",
Category.CORRECTNESS,
6,
Severity.FATAL,
IMPLEMENTATION);
/** Reference to an id that is not in the current layout */
public static final Issue UNKNOWN_ID_LAYOUT = Issue.create(
"UnknownIdInLayout", //$NON-NLS-1$
"Reference to an id that is not in the current layout",
"The `@+id/` syntax refers to an existing id, or creates a new one if it has " +
"not already been defined elsewhere. However, this means that if you have a " +
"typo in your reference, or if the referred view no longer exists, you do not " +
"get a warning since the id will be created on demand.\n" +
"\n" +
"This is sometimes intentional, for example where you are referring to a view " +
"which is provided in a different layout via an include. However, it is usually " +
"an accident where you have a typo or you have renamed a view without updating " +
"all the references to it.",
Category.CORRECTNESS,
5,
Severity.WARNING,
new Implementation(
WrongIdDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a duplicate id check */
public WrongIdDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_ID);
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(RELATIVE_LAYOUT, TAG_ITEM);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
mFileIds = new HashSet<String>();
mRelativeLayouts = null;
}
@Override
public void afterCheckFile(@NonNull Context context) {
if (mRelativeLayouts != null) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
for (Element layout : mRelativeLayouts) {
List<Element> children = getChildren(layout);
Set<String> ids = Sets.newHashSetWithExpectedSize(children.size());
for (Element child : children) {
String id = child.getAttributeNS(ANDROID_URI, ATTR_ID);
if (id != null && !id.isEmpty()) {
ids.add(id);
}
}
for (Element element : children) {
String selfId = stripIdPrefix(element.getAttributeNS(ANDROID_URI, ATTR_ID));
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attr = (Attr) attributes.item(i);
String value = attr.getValue();
if ((value.startsWith(NEW_ID_PREFIX) ||
value.startsWith(ID_PREFIX))
&& ANDROID_URI.equals(attr.getNamespaceURI())
&& attr.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
if (!idDefined(mFileIds, value)) {
// Stash a reference to this id and location such that
// we can check after the *whole* layout has been processed,
// since it's too early to conclude here that the id does
// not exist (you are allowed to have forward references)
XmlContext xmlContext = (XmlContext) context;
Handle handle = xmlContext.createLocationHandle(attr);
handle.setClientData(attr);
if (mHandles == null) {
mHandles = new ArrayList<Pair<String,Handle>>();
}
mHandles.add(Pair.of(value, handle));
} else {
// Check siblings. TODO: Look for cycles!
if (ids.contains(value)) {
// Make sure it's not pointing to self
if (!ATTR_ID.equals(attr.getLocalName())
&& !selfId.isEmpty()
&& value.endsWith(selfId)
&& stripIdPrefix(value).equals(selfId)) {
XmlContext xmlContext = (XmlContext) context;
String message = String.format(
"Cannot be relative to self: id=%1$s, %2$s=%3$s",
selfId, attr.getLocalName(), selfId);
Location location = xmlContext.getLocation(attr);
xmlContext.report(NOT_SIBLING, attr, location, message);
}
continue;
}
if (value.startsWith(NEW_ID_PREFIX)) {
if (ids.contains(ID_PREFIX + stripIdPrefix(value))) {
continue;
}
} else {
assert value.startsWith(ID_PREFIX) : value;
if (ids.contains(NEW_ID_PREFIX + stripIdPrefix(value))) {
continue;
}
}
if (context.isEnabled(NOT_SIBLING)) {
XmlContext xmlContext = (XmlContext) context;
String message = String.format(
"`%1$s` is not a sibling in the same `RelativeLayout`",
value);
Location location = xmlContext.getLocation(attr);
xmlContext.report(NOT_SIBLING, attr, location, message);
}
}
}
}
}
}
}
mFileIds = null;
if (!context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
checkHandles(context);
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
checkHandles(context);
}
}
private void checkHandles(@NonNull Context context) {
if (mHandles != null) {
boolean checkSameLayout = context.isEnabled(UNKNOWN_ID_LAYOUT);
boolean checkExists = context.isEnabled(UNKNOWN_ID);
boolean projectScope = context.getScope().contains(Scope.ALL_RESOURCE_FILES);
for (Pair<String, Handle> pair : mHandles) {
String id = pair.getFirst();
boolean isBound = projectScope ? idDefined(mGlobalIds, id) :
idDefined(context, id, context.file);
LintClient client = context.getClient();
if (!isBound && checkExists
&& (projectScope || client.supportsProjectResources())) {
Handle handle = pair.getSecond();
boolean isDeclared = idDefined(mDeclaredIds, id);
id = stripIdPrefix(id);
String suggestionMessage;
Set<String> spellingDictionary = mGlobalIds;
if (!projectScope && client.supportsProjectResources()) {
AbstractResourceRepository resources =
client.getProjectResources(context.getProject(), true);
if (resources != null) {
spellingDictionary = Sets.newHashSet(
resources.getItemsOfType(ResourceType.ID));
spellingDictionary.remove(id);
}
}
List<String> suggestions = getSpellingSuggestions(id, spellingDictionary);
if (suggestions.size() > 1) {
suggestionMessage = String.format(" Did you mean one of {%2$s} ?",
id, Joiner.on(", ").join(suggestions));
} else if (!suggestions.isEmpty()) {
suggestionMessage = String.format(" Did you mean %2$s ?",
id, suggestions.get(0));
} else {
suggestionMessage = "";
}
String message;
if (isDeclared) {
message = String.format(
"The id \"`%1$s`\" is defined but not assigned to any views.%2$s",
id, suggestionMessage);
} else {
message = String.format(
"The id \"`%1$s`\" is not defined anywhere.%2$s",
id, suggestionMessage);
}
report(context, UNKNOWN_ID, handle, message);
} else if (checkSameLayout && (!projectScope || isBound)
&& id.startsWith(NEW_ID_PREFIX)) {
// The id was defined, but in a different layout. Usually not intentional
// (might be referring to a random other view that happens to have the same
// name.)
Handle handle = pair.getSecond();
report(context, UNKNOWN_ID_LAYOUT, handle,
String.format(
"The id \"`%1$s`\" is not referring to any views in this layout",
stripIdPrefix(id)));
}
}
}
}
private static void report(Context context, Issue issue, Handle handle, String message) {
Location location = handle.resolve();
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, issue, (Node) clientData)) {
return;
}
}
context.report(issue, location, message);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (element.getTagName().equals(RELATIVE_LAYOUT)) {
if (mRelativeLayouts == null) {
mRelativeLayouts = new ArrayList<Element>();
}
mRelativeLayouts.add(element);
} else {
assert element.getTagName().equals(TAG_ITEM);
String type = element.getAttribute(ATTR_TYPE);
if (VALUE_ID.equals(type)) {
String name = element.getAttribute(ATTR_NAME);
if (!name.isEmpty()) {
if (mDeclaredIds == null) {
mDeclaredIds = Sets.newHashSet();
}
mDeclaredIds.add(ID_PREFIX + name);
}
}
}
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
assert attribute.getName().equals(ATTR_ID) || attribute.getLocalName().equals(ATTR_ID);
String id = attribute.getValue();
mFileIds.add(id);
mGlobalIds.add(id);
if (id.equals(NEW_ID_PREFIX) || id.equals(ID_PREFIX) || "@+id".equals(ID_PREFIX)) {
String message = "Invalid id: missing value";
context.report(INVALID, attribute, context.getLocation(attribute), message);
} else if (id.startsWith("@+") && !id.startsWith(NEW_ID_PREFIX) //$NON-NLS-1$
&& !id.startsWith("@+android:id/") //$NON-NLS-1$
|| id.startsWith(NEW_ID_PREFIX)
&& id.indexOf('/', NEW_ID_PREFIX.length()) != -1) {
int nameStart = id.startsWith(NEW_ID_PREFIX) ? NEW_ID_PREFIX.length() : 2;
String suggested = NEW_ID_PREFIX + id.substring(nameStart).replace('/', '_');
String message = String.format(
"ID definitions *must* be of the form `@+id/name`; try using `%1$s`", suggested);
context.report(INVALID, attribute, context.getLocation(attribute), message);
}
}
private static boolean idDefined(Set<String> ids, String id) {
if (ids == null) {
return false;
}
boolean definedLocally = ids.contains(id);
if (!definedLocally) {
if (id.startsWith(NEW_ID_PREFIX)) {
definedLocally = ids.contains(ID_PREFIX +
id.substring(NEW_ID_PREFIX.length()));
} else if (id.startsWith(ID_PREFIX)) {
definedLocally = ids.contains(NEW_ID_PREFIX +
id.substring(ID_PREFIX.length()));
}
}
return definedLocally;
}
private boolean idDefined(@NonNull Context context, @NonNull String id,
@Nullable File notIn) {
AbstractResourceRepository resources =
context.getClient().getProjectResources(context.getProject(), true);
if (resources != null) {
List<ResourceItem> items = resources.getResourceItem(ResourceType.ID,
stripIdPrefix(id));
if (items == null || items.isEmpty()) {
return false;
}
for (ResourceItem item : items) {
ResourceFile source = item.getSource();
if (source != null) {
File file = source.getFile();
if (file.getParentFile().getName().startsWith(FD_RES_VALUES)) {
if (mDeclaredIds == null) {
mDeclaredIds = Sets.newHashSet();
}
mDeclaredIds.add(id);
continue;
}
// Ignore definitions in the given file. This is used to ignore
// matches in the same file as the reference, since the reference
// is often expressed as a definition
if (!isSameResourceFile(file, notIn)) {
return true;
}
}
}
}
return false;
}
private static List<String> getSpellingSuggestions(String id, Collection<String> ids) {
int maxDistance = id.length() >= 4 ? 2 : 1;
// Look for typos and try to match with custom views and android views
Multimap<Integer, String> matches = ArrayListMultimap.create(2, 10);
int count = 0;
if (!ids.isEmpty()) {
for (String matchWith : ids) {
matchWith = stripIdPrefix(matchWith);
if (Math.abs(id.length() - matchWith.length()) > maxDistance) {
// The string lengths differ more than the allowed edit distance;
// no point in even attempting to compute the edit distance (requires
// O(n*m) storage and O(n*m) speed, where n and m are the string lengths)
continue;
}
int distance = editDistance(id, matchWith);
if (distance <= maxDistance) {
matches.put(distance, matchWith);
}
if (count++ > 100) {
// Make sure that for huge projects we don't completely grind to a halt
break;
}
}
}
for (int i = 0; i < maxDistance; i++) {
Collection<String> strings = matches.get(i);
if (strings != null && !strings.isEmpty()) {
List<String> suggestions = new ArrayList<String>(strings);
Collections.sort(suggestions);
return suggestions;
}
}
return Collections.emptyList();
}
}
@@ -1,70 +0,0 @@
/*
* Copyright (C) 2012 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.klint.checks;
import static com.android.SdkConstants.TAG_RESOURCES;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/** Looks for problems with XML files being placed in the wrong folder */
public class WrongLocationDetector extends LayoutDetector {
/** Main issue investigated by this detector */
public static final Issue ISSUE = Issue.create(
"WrongFolder", //$NON-NLS-1$
"Resource file in the wrong `res` folder",
"Resource files are sometimes placed in the wrong folder, and it can lead to " +
"subtle bugs that are hard to understand. This check looks for problems in this " +
"area, such as attempting to place a layout \"alias\" file in a `layout/` folder " +
"rather than the `values/` folder where it belongs.",
Category.CORRECTNESS,
8,
Severity.FATAL,
new Implementation(
WrongLocationDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link WrongLocationDetector} check */
public WrongLocationDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root != null && root.getTagName().equals(TAG_RESOURCES)) {
context.report(ISSUE, root, context.getLocation(root),
"This file should be placed in a `values`/ folder, not a `layout`/ folder");
}
}
}
@@ -16,46 +16,21 @@
package org.jetbrains.android.inspections.klint;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.tools.klint.checks.*;
import com.android.tools.klint.detector.api.Issue;
import com.intellij.psi.PsiElement;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.util.AndroidBundle;
import static com.android.tools.klint.checks.FragmentDetector.ISSUE;
import static com.android.tools.klint.checks.PluralsDetector.IMPLIED_QUANTITY;
/**
* Registrations for all the various Lint rules as local IDE inspections, along with quickfixes for many of them
*/
public class AndroidLintInspectionToolProvider {
public static class AndroidKLintAaptCrashInspection extends AndroidLintInspectionBase {
public AndroidKLintAaptCrashInspection() {
super(AndroidBundle.message("android.lint.inspections.aapt.crash"), ResourceCycleDetector.CRASH);
}
}
public static class AndroidKLintInconsistentArraysInspection extends AndroidLintInspectionBase {
public AndroidKLintInconsistentArraysInspection() {
super(AndroidBundle.message("android.lint.inspections.inconsistent.arrays"), ArraySizeDetector.INCONSISTENT);
}
}
public static class AndroidKLintInconsistentLayoutInspection extends AndroidLintInspectionBase {
public AndroidKLintInconsistentLayoutInspection() {
super(AndroidBundle.message("android.lint.inspections.inconsistent.layout"), LayoutConsistencyDetector.INCONSISTENT_IDS);
}
}
public static class AndroidKLintDuplicateIncludedIdsInspection extends AndroidLintInspectionBase {
public AndroidKLintDuplicateIncludedIdsInspection() {
super(AndroidBundle.message("android.lint.inspections.duplicate.included.ids"), DuplicateIdDetector.CROSS_LAYOUT);
}
}
public static class AndroidKLintIconExpectedSizeInspection extends AndroidLintInspectionBase {
public AndroidKLintIconExpectedSizeInspection() {
super(AndroidBundle.message("android.lint.inspections.icon.expected.size"), IconDetector.ICON_EXPECTED_SIZE);
@@ -140,18 +115,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintMissingTranslationInspection extends AndroidLintInspectionBase {
public AndroidKLintMissingTranslationInspection() {
super(AndroidBundle.message("android.lint.inspections.missing.translation"), TranslationDetector.MISSING);
}
}
public static class AndroidKLintExtraTranslationInspection extends AndroidLintInspectionBase {
public AndroidKLintExtraTranslationInspection() {
super(AndroidBundle.message("android.lint.inspections.extra.translation"), TranslationDetector.EXTRA);
}
}
public static class AndroidKLintUnusedResourcesInspection extends AndroidLintInspectionBase {
public AndroidKLintUnusedResourcesInspection() {
super(AndroidBundle.message("android.lint.inspections.unused.resources"), UnusedResourceDetector.ISSUE);
@@ -188,18 +151,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintAppIndexingApiErrorInspection extends AndroidLintInspectionBase {
public AndroidKLintAppIndexingApiErrorInspection() {
super(AndroidBundle.message("android.lint.inspections.appindexing.error"), AppIndexingApiDetector.ISSUE_ERROR);
}
}
public static class AndroidKLintAppIndexingApiWarningInspection extends AndroidLintInspectionBase {
public AndroidKLintAppIndexingApiWarningInspection() {
super(AndroidBundle.message("android.lint.inspections.appindexing.warning"), AppIndexingApiDetector.ISSUE_WARNING);
}
}
public static class AndroidKLintAssertInspection extends AndroidLintInspectionBase {
public AndroidKLintAssertInspection() {
super(AndroidBundle.message("android.lint.inspections.assert"), AssertDetector.ISSUE);
@@ -224,75 +175,24 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintWrongRegionInspection extends AndroidLintInspectionBase {
public AndroidKLintWrongRegionInspection() {
super(AndroidBundle.message("android.lint.inspections.wrong.region"), LocaleFolderDetector.WRONG_REGION);
}
}
public static class AndroidKLintWrongViewCastInspection extends AndroidLintInspectionBase {
public AndroidKLintWrongViewCastInspection() {
super(AndroidBundle.message("android.lint.inspections.wrong.view.cast"), ViewTypeDetector.ISSUE);
}
}
public static class AndroidKLintUnknownIdInspection extends AndroidLintInspectionBase {
public AndroidKLintUnknownIdInspection() {
super(AndroidBundle.message("android.lint.inspections.unknown.id"), WrongIdDetector.UNKNOWN_ID);
}
}
public static class AndroidKLintCommitTransactionInspection extends AndroidLintInspectionBase {
public AndroidKLintCommitTransactionInspection() {
super(AndroidBundle.message("android.lint.inspections.commit.transaction"), CleanupDetector.COMMIT_FRAGMENT);
}
}
/**
* Local inspections processed by AndroidLintExternalAnnotator
*/
public static class AndroidKLintContentDescriptionInspection extends AndroidLintInspectionBase {
public AndroidKLintContentDescriptionInspection() {
super(AndroidBundle.message("android.lint.inspections.content.description"), AccessibilityDetector.ISSUE);
}
}
public static class AndroidKLintButtonOrderInspection extends AndroidLintInspectionBase {
public AndroidKLintButtonOrderInspection() {
super(AndroidBundle.message("android.lint.inspections.button.order"), ButtonDetector.ORDER);
}
}
public static class AndroidKLintBackButtonInspection extends AndroidLintInspectionBase {
public AndroidKLintBackButtonInspection() {
super(AndroidBundle.message("android.lint.inspections.back.button"), ButtonDetector.BACK_BUTTON);
}
}
public static class AndroidKLintButtonCaseInspection extends AndroidLintInspectionBase {
public AndroidKLintButtonCaseInspection() {
super(AndroidBundle.message("android.lint.inspections.button.case"), ButtonDetector.CASE);
}
}
public static class AndroidKLintExtraTextInspection extends AndroidLintInspectionBase {
public AndroidKLintExtraTextInspection() {
super(AndroidBundle.message("android.lint.inspections.extra.text"), ExtraTextDetector.ISSUE);
}
}
public static class AndroidKLintHandlerLeakInspection extends AndroidLintInspectionBase {
public AndroidKLintHandlerLeakInspection() {
super(AndroidBundle.message("android.lint.inspections.handler.leak"), HandlerDetector.ISSUE);
}
}
public static class AndroidKLintHardcodedDebugModeInspection extends AndroidLintInspectionBase {
public AndroidKLintHardcodedDebugModeInspection() {
super(AndroidBundle.message("android.lint.inspections.hardcoded.debug.mode"), HardcodedDebugModeDetector.ISSUE);
}
}
public static class AndroidKLintDrawAllocationInspection extends AndroidLintInspectionBase {
public AndroidKLintDrawAllocationInspection() {
super(AndroidBundle.message("android.lint.inspections.draw.allocation"), JavaPerformanceDetector.PAINT_ALLOC);
@@ -311,13 +211,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintLibraryCustomViewInspection extends AndroidLintInspectionBase {
public AndroidKLintLibraryCustomViewInspection() {
// TODO: Quickfix
super(AndroidBundle.message("android.lint.inspections.library.custom.view"), NamespaceDetector.CUSTOM_VIEW);
}
}
public static class AndroidKLintPackageManagerGetSignaturesInspection extends AndroidLintInspectionBase {
public AndroidKLintPackageManagerGetSignaturesInspection() {
super(AndroidBundle.message("android.lint.inspections.package.manager.get.signatures"), GetSignaturesDetector.ISSUE);
@@ -348,153 +241,18 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintTextViewEditsInspection extends AndroidLintInspectionBase {
public AndroidKLintTextViewEditsInspection() {
super(AndroidBundle.message("android.lint.inspections.text.view.edits"), TextViewDetector.ISSUE);
}
}
public static class AndroidKLintEnforceUTF8Inspection extends AndroidLintInspectionBase {
public AndroidKLintEnforceUTF8Inspection() {
super(AndroidBundle.message("android.lint.inspections.enforce.utf8"), Utf8Detector.ISSUE);
}
}
public static class AndroidKLintUnknownIdInLayoutInspection extends AndroidLintInspectionBase {
public AndroidKLintUnknownIdInLayoutInspection() {
super(AndroidBundle.message("android.lint.inspections.unknown.id.in.layout"), WrongIdDetector.UNKNOWN_ID_LAYOUT);
}
}
public static class AndroidKLintSuspiciousImportInspection extends AndroidLintInspectionBase {
public AndroidKLintSuspiciousImportInspection() {
super(AndroidBundle.message("android.lint.inspections.suspicious.import"), WrongImportDetector.ISSUE);
}
}
public static class AndroidKLintAccidentalOctalInspection extends AndroidLintInspectionBase {
public AndroidKLintAccidentalOctalInspection() {
super(AndroidBundle.message("android.lint.inspections.accidental.octal"), GradleDetector.ACCIDENTAL_OCTAL);
}
}
public static class AndroidKLintAdapterViewChildrenInspection extends AndroidLintInspectionBase {
public AndroidKLintAdapterViewChildrenInspection() {
super(AndroidBundle.message("android.lint.inspections.adapter.view.children"), ChildCountDetector.ADAPTER_VIEW_ISSUE);
}
}
public static class AndroidKLintSQLiteStringInspection extends AndroidLintInspectionBase {
public AndroidKLintSQLiteStringInspection() {
super(AndroidBundle.message("android.lint.inspections.sqlite.string"), SQLiteDetector.ISSUE);
}
}
public static class AndroidKLintScrollViewCountInspection extends AndroidLintInspectionBase {
public AndroidKLintScrollViewCountInspection() {
super(AndroidBundle.message("android.lint.inspections.scroll.view.count"), ChildCountDetector.SCROLLVIEW_ISSUE);
}
}
public static class AndroidKLintMissingPrefixInspection extends AndroidLintInspectionBase {
public AndroidKLintMissingPrefixInspection() {
super(AndroidBundle.message("android.lint.inspections.missing.prefix"), DetectMissingPrefix.MISSING_NAMESPACE);
}
}
public static class AndroidKLintMissingQuantityInspection extends AndroidLintInspectionBase {
public AndroidKLintMissingQuantityInspection() {
// TODO: Add fixes
super(AndroidBundle.message("android.lint.inspections.missing.quantity"), PluralsDetector.MISSING);
}
}
public static class AndroidKLintUnusedQuantityInspection extends AndroidLintInspectionBase {
public AndroidKLintUnusedQuantityInspection() {
// TODO: Add fixes
super(AndroidBundle.message("android.lint.inspections.unused.quantity"), PluralsDetector.EXTRA);
}
}
public static class AndroidKLintDuplicateIdsInspection extends AndroidLintInspectionBase {
public AndroidKLintDuplicateIdsInspection() {
super(AndroidBundle.message("android.lint.inspections.duplicate.ids"), DuplicateIdDetector.WITHIN_LAYOUT);
}
}
public static class AndroidKLintGridLayoutInspection extends AndroidLintInspectionBase {
public AndroidKLintGridLayoutInspection() {
super(AndroidBundle.message("android.lint.inspections.grid.layout"), GridLayoutDetector.ISSUE);
}
}
public static class AndroidKLintHardcodedTextInspection extends AndroidLintInspectionBase {
public AndroidKLintHardcodedTextInspection() {
super(AndroidBundle.message("android.lint.inspections.hardcoded.text"), HardcodedValuesDetector.ISSUE);
}
}
public static class AndroidKLintInefficientWeightInspection extends AndroidLintInspectionBase {
public AndroidKLintInefficientWeightInspection() {
super(AndroidBundle.message("android.lint.inspections.inefficient.weight"), InefficientWeightDetector.INEFFICIENT_WEIGHT);
}
}
public static class AndroidKLintNestedWeightsInspection extends AndroidLintInspectionBase {
public AndroidKLintNestedWeightsInspection() {
super(AndroidBundle.message("android.lint.inspections.nested.weights"), InefficientWeightDetector.NESTED_WEIGHTS);
}
}
public static class AndroidKLintDeprecatedInspection extends AndroidLintInspectionBase {
public AndroidKLintDeprecatedInspection() {
super(AndroidBundle.message("android.lint.inspections.deprecated"), DeprecationDetector.ISSUE);
}
}
public static class AndroidKLintDeviceAdminInspection extends AndroidLintInspectionBase {
public AndroidKLintDeviceAdminInspection() {
// TODO: Add quickfix
super(AndroidBundle.message("android.lint.inspections.device.admin"), ManifestDetector.DEVICE_ADMIN);
}
}
public static class AndroidKLintDisableBaselineAlignmentInspection extends AndroidLintInspectionBase {
public AndroidKLintDisableBaselineAlignmentInspection() {
super(AndroidBundle.message("android.lint.inspections.disable.baseline.alignment"), InefficientWeightDetector.BASELINE_WEIGHTS);
}
}
public static class AndroidKLintManifestOrderInspection extends AndroidLintInspectionBase {
public AndroidKLintManifestOrderInspection() {
super(AndroidBundle.message("android.lint.inspections.manifest.order"), ManifestDetector.ORDER);
}
}
public static class AndroidKLintMockLocationInspection extends AndroidLintInspectionBase {
public AndroidKLintMockLocationInspection() {
super(AndroidBundle.message("android.lint.inspections.mock.location"), ManifestDetector.MOCK_LOCATION);
}
}
public static class AndroidKLintMultipleUsesSdkInspection extends AndroidLintInspectionBase {
public AndroidKLintMultipleUsesSdkInspection() {
super(AndroidBundle.message("android.lint.inspections.multiple.uses.sdk"), ManifestDetector.MULTIPLE_USES_SDK);
}
}
public static class AndroidKLintUsesMinSdkAttributesInspection extends AndroidLintInspectionBase {
public AndroidKLintUsesMinSdkAttributesInspection() {
super(AndroidBundle.message("android.lint.inspections.uses.min.sdk.attributes"), ManifestDetector.USES_SDK);
}
}
public static class AndroidKLintUsingHttpInspection extends AndroidLintInspectionBase {
public AndroidKLintUsingHttpInspection() {
super(AndroidBundle.message("android.lint.inspections.using.http"), PropertyFileDetector.HTTP);
}
}
public static class AndroidKLintValidFragmentInspection extends AndroidLintInspectionBase {
public AndroidKLintValidFragmentInspection() {
super(AndroidBundle.message("android.lint.inspections.valid.fragment"), ISSUE);
@@ -513,139 +271,24 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintWebViewLayoutInspection extends AndroidLintInspectionBase {
public AndroidKLintWebViewLayoutInspection() {
super(AndroidBundle.message("android.lint.inspections.web.view.layout"), WebViewDetector.ISSUE);
}
}
public static class AndroidKLintMergeRootFrameInspection extends AndroidLintInspectionBase {
public AndroidKLintMergeRootFrameInspection() {
super(AndroidBundle.message("android.lint.inspections.merge.root.frame"), MergeRootFrameLayoutDetector.ISSUE);
}
}
public static class AndroidKLintNegativeMarginInspection extends AndroidLintInspectionBase {
public AndroidKLintNegativeMarginInspection() {
super(AndroidBundle.message("android.lint.inspections.negative.margin"), NegativeMarginDetector.ISSUE);
}
}
public static class AndroidKLintNestedScrollingInspection extends AndroidLintInspectionBase {
public AndroidKLintNestedScrollingInspection() {
super(AndroidBundle.message("android.lint.inspections.nested.scrolling"), NestedScrollingWidgetDetector.ISSUE);
}
}
public static class AndroidKLintNewerVersionAvailableInspection extends AndroidLintInspectionBase {
public AndroidKLintNewerVersionAvailableInspection() {
super(AndroidBundle.message("android.lint.inspections.newer.version.available"), GradleDetector.REMOTE_VERSION);
}
}
public static class AndroidKLintNfcTechWhitespaceInspection extends AndroidLintInspectionBase {
public AndroidKLintNfcTechWhitespaceInspection() {
super(AndroidBundle.message("android.lint.inspections.nfc.tech.whitespace"), NfcTechListDetector.ISSUE);
}
}
public static class AndroidKLintNotSiblingInspection extends AndroidLintInspectionBase {
public AndroidKLintNotSiblingInspection() {
super(AndroidBundle.message("android.lint.inspections.not.sibling"), WrongIdDetector.NOT_SIBLING);
}
}
public static class AndroidKLintObsoleteLayoutParamInspection extends AndroidLintInspectionBase {
public AndroidKLintObsoleteLayoutParamInspection() {
super(AndroidBundle.message("android.lint.inspections.obsolete.layout.param"), ObsoleteLayoutParamsDetector.ISSUE);
}
}
public static class AndroidKLintProguardInspection extends AndroidLintInspectionBase {
public AndroidKLintProguardInspection() {
super(AndroidBundle.message("android.lint.inspections.proguard"), ProguardDetector.WRONG_KEEP);
}
}
public static class AndroidKLintProguardSplitInspection extends AndroidLintInspectionBase {
public AndroidKLintProguardSplitInspection() {
super(AndroidBundle.message("android.lint.inspections.proguard.split"), ProguardDetector.SPLIT_CONFIG);
}
}
public static class AndroidKLintPxUsageInspection extends AndroidLintInspectionBase {
public AndroidKLintPxUsageInspection() {
super(AndroidBundle.message("android.lint.inspections.px.usage"), PxUsageDetector.PX_ISSUE);
}
}
public static class AndroidKLintScrollViewSizeInspection extends AndroidLintInspectionBase {
public AndroidKLintScrollViewSizeInspection() {
super(AndroidBundle.message("android.lint.inspections.scroll.view.size"), ScrollViewChildDetector.ISSUE);
}
}
public static class AndroidKLintExportedServiceInspection extends AndroidLintInspectionBase {
public AndroidKLintExportedServiceInspection() {
super(AndroidBundle.message("android.lint.inspections.exported.service"), SecurityDetector.EXPORTED_SERVICE);
}
}
public static class AndroidKLintGradleCompatibleInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleCompatibleInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.compatible"), GradleDetector.COMPATIBILITY);
}
}
public static class AndroidKLintGradleCompatiblePluginInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleCompatiblePluginInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.plugin.compatible"), GradleDetector.GRADLE_PLUGIN_COMPATIBILITY);
}
}
public static class AndroidKLintGradleDependencyInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleDependencyInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.dependency"), GradleDetector.DEPENDENCY);
}
}
public static class AndroidKLintGradleDeprecatedInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleDeprecatedInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.deprecated"), GradleDetector.DEPRECATED);
}
}
public static class AndroidKLintGradleDynamicVersionInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleDynamicVersionInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.dynamic.version"), GradleDetector.PLUS);
}
}
public static class AndroidKLintGradleGetterInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleGetterInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.getter"), GradleDetector.GRADLE_GETTER);
}
}
public static class AndroidKLintGradleIdeErrorInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleIdeErrorInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.ide.error"), GradleDetector.IDE_SUPPORT);
}
}
public static class AndroidKLintGradleOverridesInspection extends AndroidLintInspectionBase {
public AndroidKLintGradleOverridesInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.overrides"), ManifestDetector.GRADLE_OVERRIDES);
}
}
public static class AndroidKLintGradlePathInspection extends AndroidLintInspectionBase {
public AndroidKLintGradlePathInspection() {
super(AndroidBundle.message("android.lint.inspections.gradle.path"), GradleDetector.PATH);
}
}
public static class AndroidKLintGrantAllUrisInspection extends AndroidLintInspectionBase {
public AndroidKLintGrantAllUrisInspection() {
super(AndroidBundle.message("android.lint.inspections.grant.all.uris"), SecurityDetector.OPEN_PROVIDER);
@@ -658,94 +301,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintStateListReachableInspection extends AndroidLintInspectionBase {
public AndroidKLintStateListReachableInspection() {
super(AndroidBundle.message("android.lint.inspections.state.list.reachable"), StateListDetector.ISSUE);
}
}
public static class AndroidKLintTextFieldsInspection extends AndroidLintInspectionBase {
public AndroidKLintTextFieldsInspection() {
super(AndroidBundle.message("android.lint.inspections.text.fields"), TextFieldDetector.ISSUE);
}
}
public static class AndroidKLintTooManyViewsInspection extends AndroidLintInspectionBase {
public AndroidKLintTooManyViewsInspection() {
super(AndroidBundle.message("android.lint.inspections.too.many.views"), TooManyViewsDetector.TOO_MANY);
}
}
public static class AndroidKLintTooDeepLayoutInspection extends AndroidLintInspectionBase {
public AndroidKLintTooDeepLayoutInspection() {
super(AndroidBundle.message("android.lint.inspections.too.deep.layout"), TooManyViewsDetector.TOO_DEEP);
}
}
public static class AndroidKLintTypographyDashesInspection extends AndroidKLintTypographyInspectionBase {
public AndroidKLintTypographyDashesInspection() {
super(AndroidBundle.message("android.lint.inspections.typography.dashes"), TypographyDetector.DASHES);
}
}
public static class AndroidKLintTypographyQuotesInspection extends AndroidKLintTypographyInspectionBase {
public AndroidKLintTypographyQuotesInspection() {
super(AndroidBundle.message("android.lint.inspections.typography.quotes"), TypographyDetector.QUOTES);
}
}
public static class AndroidKLintTypographyFractionsInspection extends AndroidKLintTypographyInspectionBase {
public AndroidKLintTypographyFractionsInspection() {
super(AndroidBundle.message("android.lint.inspections.typography.fractions"), TypographyDetector.FRACTIONS);
}
}
public static class AndroidKLintTypographyEllipsisInspection extends AndroidKLintTypographyInspectionBase {
public AndroidKLintTypographyEllipsisInspection() {
super(AndroidBundle.message("android.lint.inspections.typography.ellipsis"), TypographyDetector.ELLIPSIS);
}
}
public static class AndroidKLintTypographyOtherInspection extends AndroidKLintTypographyInspectionBase {
public AndroidKLintTypographyOtherInspection() {
super(AndroidBundle.message("android.lint.inspections.typography.other"), TypographyDetector.OTHER);
}
}
public static class AndroidKLintUseAlpha2Inspection extends AndroidLintInspectionBase {
public AndroidKLintUseAlpha2Inspection() {
super(AndroidBundle.message("android.lint.inspections.use.alpha2"), LocaleFolderDetector.USE_ALPHA_2);
}
}
public static class AndroidKLintUseCompoundDrawablesInspection extends AndroidLintInspectionBase {
public AndroidKLintUseCompoundDrawablesInspection() {
super(AndroidBundle.message("android.lint.inspections.use.compound.drawables"), UseCompoundDrawableDetector.ISSUE);
}
// TODO: implement quickfix
}
public static class AndroidKLintUselessParentInspection extends AndroidLintInspectionBase {
public AndroidKLintUselessParentInspection() {
super(AndroidBundle.message("android.lint.inspections.useless.parent"), UselessViewDetector.USELESS_PARENT);
}
// TODO: implement quickfix
}
public static class AndroidKLintUselessLeafInspection extends AndroidLintInspectionBase {
public AndroidKLintUselessLeafInspection() {
super(AndroidBundle.message("android.lint.inspections.useless.leaf"), UselessViewDetector.USELESS_LEAF);
}
}
private abstract static class AndroidKLintTypographyInspectionBase extends AndroidLintInspectionBase {
public AndroidKLintTypographyInspectionBase(String displayName, Issue issue) {
super(displayName, issue);
}
}
public static class AndroidKLintNewApiInspection extends AndroidLintInspectionBase {
public AndroidKLintNewApiInspection() {
super(AndroidBundle.message("android.lint.inspections.new.api"), ApiDetector.UNSUPPORTED);
@@ -764,34 +319,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintDuplicateUsesFeatureInspection extends AndroidLintInspectionBase {
public AndroidKLintDuplicateUsesFeatureInspection() {
super(AndroidBundle.message("android.lint.inspections.duplicate.uses.feature"), ManifestDetector.DUPLICATE_USES_FEATURE);
}
}
public static class AndroidKLintMipmapIconsInspection extends AndroidLintInspectionBase {
public AndroidKLintMipmapIconsInspection() {
super(AndroidBundle.message("android.lint.inspections.mipmap.icons"), ManifestDetector.MIPMAP);
}
}
public static class AndroidKLintMissingApplicationIconInspection extends AndroidLintInspectionBase {
public AndroidKLintMissingApplicationIconInspection() {
super(AndroidBundle.message("android.lint.inspections.missing.application.icon"), ManifestDetector.APPLICATION_ICON);
}
}
public static class AndroidKLintResourceCycleInspection extends AndroidLintInspectionBase {
public AndroidKLintResourceCycleInspection() {
super(AndroidBundle.message("android.lint.inspections.resource.cycle"), ResourceCycleDetector.CYCLE);
}
}
public static class AndroidKLintResourceNameInspection extends AndroidLintInspectionBase {
public AndroidKLintResourceNameInspection() {
super(AndroidBundle.message("android.lint.inspections.resource.name"), ResourcePrefixDetector.ISSUE);
}
}
public static class AndroidKLintRtlCompatInspection extends AndroidLintInspectionBase {
public AndroidKLintRtlCompatInspection() {
super(AndroidBundle.message("android.lint.inspections.rtl.compat"), RtlDetector.COMPAT);
@@ -826,24 +353,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintAllowBackupInspection extends AndroidLintInspectionBase {
public AndroidKLintAllowBackupInspection() {
super(AndroidBundle.message("android.lint.inspections.allow.backup"), ManifestDetector.ALLOW_BACKUP);
}
}
public static class AndroidKLintButtonStyleInspection extends AndroidLintInspectionBase {
public AndroidKLintButtonStyleInspection() {
super(AndroidBundle.message("android.lint.inspections.button.style"), ButtonDetector.STYLE);
}
}
public static class AndroidKLintByteOrderMarkInspection extends AndroidLintInspectionBase {
public AndroidKLintByteOrderMarkInspection() {
super(AndroidBundle.message("android.lint.inspections.byte.order.mark"), ByteOrderMarkDetector.BOM);
}
}
public static class AndroidKLintCommitPrefEditsInspection extends AndroidLintInspectionBase {
public AndroidKLintCommitPrefEditsInspection() {
super(AndroidBundle.message("android.lint.inspections.commit.pref.edits"), SharedPrefsDetector.ISSUE);
@@ -861,16 +370,6 @@ public class AndroidLintInspectionToolProvider {
super(AndroidBundle.message("android.lint.inspections.cut.paste.id"), CutPasteDetector.ISSUE);
}
}
public static class AndroidKLintDuplicateActivityInspection extends AndroidLintInspectionBase {
public AndroidKLintDuplicateActivityInspection() {
super(AndroidBundle.message("android.lint.inspections.duplicate.activity"), ManifestDetector.DUPLICATE_ACTIVITY);
}
}
public static class AndroidKLintDuplicateDefinitionInspection extends AndroidLintInspectionBase {
public AndroidKLintDuplicateDefinitionInspection() {
super(AndroidBundle.message("android.lint.inspections.duplicate.definition"), DuplicateResourceDetector.ISSUE);
}
}
public static class AndroidKLintEasterEggInspection extends AndroidLintInspectionBase {
public AndroidKLintEasterEggInspection() {
super(AndroidBundle.message("android.lint.inspections.easter.egg"), CommentDetector.EASTER_EGG);
@@ -911,51 +410,12 @@ public class AndroidLintInspectionToolProvider {
super(AndroidBundle.message("android.lint.inspections.icon.xml.and.png"), IconDetector.ICON_XML_AND_PNG);
}
}
public static class AndroidKLintIllegalResourceRefInspection extends AndroidLintInspectionBase {
public AndroidKLintIllegalResourceRefInspection() {
super(AndroidBundle.message("android.lint.inspections.illegal.resource.ref"), ManifestDetector.ILLEGAL_REFERENCE);
}
}
public static class AndroidKLintImpliedQuantityInspection extends AndroidLintInspectionBase {
public AndroidKLintImpliedQuantityInspection() {
super(AndroidBundle.message("android.lint.inspections.implied.quantity"), IMPLIED_QUANTITY);
}
}
public static class AndroidKLintIncludeLayoutParamInspection extends AndroidLintInspectionBase {
public AndroidKLintIncludeLayoutParamInspection() {
super(AndroidBundle.message("android.lint.inspections.include.layout.param"), IncludeDetector.ISSUE);
}
}
public static class AndroidKLintInflateParamsInspection extends AndroidLintInspectionBase {
public AndroidKLintInflateParamsInspection() {
super(AndroidBundle.message("android.lint.inspections.inflate.params"), LayoutInflationDetector.ISSUE);
}
}
public static class AndroidKLintInOrMmUsageInspection extends AndroidLintInspectionBase {
public AndroidKLintInOrMmUsageInspection() {
super(AndroidBundle.message("android.lint.inspections.in.or.mm.usage"), PxUsageDetector.IN_MM_ISSUE);
}
}
public static class AndroidKLintInnerclassSeparatorInspection extends AndroidLintInspectionBase {
public AndroidKLintInnerclassSeparatorInspection() {
super(AndroidBundle.message("android.lint.inspections.innerclass.separator"), MissingClassDetector.INNERCLASS);
}
}
public static class AndroidKLintInvalidIdInspection extends AndroidLintInspectionBase {
public AndroidKLintInvalidIdInspection() {
super(AndroidBundle.message("android.lint.inspections.invalid.id"), WrongIdDetector.INVALID);
}
}
public static class AndroidKLintInvalidResourceFolderInspection extends AndroidLintInspectionBase {
public AndroidKLintInvalidResourceFolderInspection() {
super(AndroidBundle.message("android.lint.inspections.invalid.resource.folder"), LocaleFolderDetector.INVALID_FOLDER);
}
}
public static class AndroidKLintJavascriptInterfaceInspection extends AndroidLintInspectionBase {
public AndroidKLintJavascriptInterfaceInspection() {
@@ -963,16 +423,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintLabelForInspection extends AndroidLintInspectionBase {
public AndroidKLintLabelForInspection() {
super(AndroidBundle.message("android.lint.inspections.label.for"), LabelForDetector.ISSUE);
}
}
public static class AndroidKLintLocaleFolderInspection extends AndroidLintInspectionBase {
public AndroidKLintLocaleFolderInspection() {
super(AndroidBundle.message("android.lint.inspections.locale.folder"), LocaleFolderDetector.DEPRECATED_CODE);
}
}
public static class AndroidKLintLocalSuppressInspection extends AndroidLintInspectionBase {
public AndroidKLintLocalSuppressInspection() {
super(AndroidBundle.message("android.lint.inspections.local.suppress"), AnnotationDetector.ISSUE);
@@ -997,122 +447,35 @@ public class AndroidLintInspectionToolProvider {
}
}
// THIS ISSUE IS PROBABLY NOT NEEDED HERE!
public static class AndroidKLintMangledCRLFInspection extends AndroidLintInspectionBase {
public AndroidKLintMangledCRLFInspection() {
super(AndroidBundle.message("android.lint.inspections.mangled.crlf"), DosLineEndingDetector.ISSUE);
}
}
public static class AndroidKLintMenuTitleInspection extends AndroidLintInspectionBase {
public AndroidKLintMenuTitleInspection() {
super(AndroidBundle.message("android.lint.inspections.menu.title"), TitleDetector.ISSUE);
}
}
public static class AndroidKLintMissingIdInspection extends AndroidLintInspectionBase {
public AndroidKLintMissingIdInspection() {
super(AndroidBundle.message("android.lint.inspections.missing.id"), MissingIdDetector.ISSUE);
}
}
public static class AndroidKLintMissingVersionInspection extends AndroidLintInspectionBase {
public AndroidKLintMissingVersionInspection() {
super(AndroidBundle.message("android.lint.inspections.missing.version"), ManifestDetector.SET_VERSION);
}
}
public static class AndroidKLintOldTargetApiInspection extends AndroidLintInspectionBase {
public AndroidKLintOldTargetApiInspection() {
super(AndroidBundle.message("android.lint.inspections.old.target.api"), ManifestDetector.TARGET_NEWER);
}
private static int getHighestApi(PsiElement element) {
int max = SdkVersionInfo.HIGHEST_KNOWN_STABLE_API;
AndroidFacet instance = AndroidFacet.getInstance(element);
if (instance != null) {
AndroidSdkData sdkData = instance.getSdkData();
if (sdkData != null) {
for (IAndroidTarget target : sdkData.getTargets()) {
if (target.isPlatform()) {
AndroidVersion version = target.getVersion();
if (version.getApiLevel() > max && !version.isPreview()) {
max = version.getApiLevel();
}
}
}
}
}
return max;
}
}
public static class AndroidKLintOrientationInspection extends AndroidLintInspectionBase {
public AndroidKLintOrientationInspection() {
super(AndroidBundle.message("android.lint.inspections.orientation"), InefficientWeightDetector.ORIENTATION);
}
}
public static class AndroidKLintOverrideAbstractInspection extends AndroidLintInspectionBase {
public AndroidKLintOverrideAbstractInspection() {
super(AndroidBundle.message("android.lint.inspections.override.abstract"), OverrideConcreteDetector.ISSUE);
}
}
public static class AndroidKLintPackagedPrivateKeyInspection extends AndroidLintInspectionBase {
public AndroidKLintPackagedPrivateKeyInspection() {
super(AndroidBundle.message("android.lint.inspections.packaged.private.key"), PrivateKeyDetector.ISSUE);
}
}
public static class AndroidKLintPropertyEscapeInspection extends AndroidLintInspectionBase {
public AndroidKLintPropertyEscapeInspection() {
super(AndroidBundle.message("android.lint.inspections.property.escape"), PropertyFileDetector.ESCAPE);
}
}
public static class AndroidKLintProtectedPermissionsInspection extends AndroidLintInspectionBase {
public AndroidKLintProtectedPermissionsInspection() {
super(AndroidBundle.message("android.lint.inspections.protected.permissions"), SystemPermissionsDetector.ISSUE);
}
}
public static class AndroidKLintRecycleInspection extends AndroidLintInspectionBase {
public AndroidKLintRecycleInspection() {
super(AndroidBundle.message("android.lint.inspections.recycle"), CleanupDetector.RECYCLE_RESOURCE);
}
}
public static class AndroidKLintReferenceTypeInspection extends AndroidLintInspectionBase {
public AndroidKLintReferenceTypeInspection() {
super(AndroidBundle.message("android.lint.inspections.reference.type"), DuplicateResourceDetector.TYPE_MISMATCH);
}
}
public static class AndroidKLintRegisteredInspection extends AndroidLintInspectionBase {
public AndroidKLintRegisteredInspection() {
super(AndroidBundle.message("android.lint.inspections.registered"), RegistrationDetector.ISSUE);
}
}
public static class AndroidKLintRelativeOverlapInspection extends AndroidLintInspectionBase {
public AndroidKLintRelativeOverlapInspection() {
super(AndroidBundle.message("android.lint.inspections.relative.overlap"), RelativeOverlapDetector.ISSUE);
}
}
public static class AndroidKLintRequiredSizeInspection extends AndroidLintInspectionBase {
public AndroidKLintRequiredSizeInspection() {
super(AndroidBundle.message("android.lint.inspections.required.size"), RequiredAttributeDetector.ISSUE);
}
}
public static class AndroidKLintResAutoInspection extends AndroidLintInspectionBase {
public AndroidKLintResAutoInspection() {
super(AndroidBundle.message("android.lint.inspections.res.auto"), NamespaceDetector.RES_AUTO);
}
}
public static class AndroidKLintSelectableTextInspection extends AndroidLintInspectionBase {
public AndroidKLintSelectableTextInspection() {
super(AndroidBundle.message("android.lint.inspections.selectable.text"), TextViewDetector.SELECTABLE);
}
}
public static class AndroidKLintServiceCastInspection extends AndroidLintInspectionBase {
public AndroidKLintServiceCastInspection() {
@@ -1136,11 +499,6 @@ public class AndroidLintInspectionToolProvider {
super(AndroidBundle.message("android.lint.inspections.show.toast"), ToastDetector.ISSUE);
}
}
public static class AndroidKLintSignatureOrSystemPermissionsInspection extends AndroidLintInspectionBase {
public AndroidKLintSignatureOrSystemPermissionsInspection() {
super(AndroidBundle.message("android.lint.inspections.signature.or.system.permissions"), SignatureOrSystemDetector.ISSUE);
}
}
public static class AndroidKLintSimpleDateFormatInspection extends AndroidLintInspectionBase {
public AndroidKLintSimpleDateFormatInspection() {
@@ -1148,18 +506,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintSmallSpInspection extends AndroidLintInspectionBase {
public AndroidKLintSmallSpInspection() {
super(AndroidBundle.message("android.lint.inspections.small.sp"), PxUsageDetector.SMALL_SP_ISSUE);
}
}
public static class AndroidKLintSpUsageInspection extends AndroidLintInspectionBase {
public AndroidKLintSpUsageInspection() {
super(AndroidBundle.message("android.lint.inspections.sp.usage"), PxUsageDetector.DP_ISSUE);
}
}
// Maybe not relevant
public static class AndroidKLintStopShipInspection extends AndroidLintInspectionBase {
public AndroidKLintStopShipInspection() {
@@ -1167,29 +513,6 @@ public class AndroidLintInspectionToolProvider {
}
}
public static class AndroidKLintStringShouldBeIntInspection extends AndroidLintInspectionBase {
public AndroidKLintStringShouldBeIntInspection() {
super(AndroidBundle.message("android.lint.inspections.string.should.be.int"), GradleDetector.STRING_INTEGER);
}
}
public static class AndroidKLintSuspicious0dpInspection extends AndroidLintInspectionBase {
public AndroidKLintSuspicious0dpInspection() {
super(AndroidBundle.message("android.lint.inspections.suspicious0dp"), InefficientWeightDetector.WRONG_0DP);
}
}
public static class AndroidKLintTyposInspection extends AndroidLintInspectionBase {
public AndroidKLintTyposInspection() {
super(AndroidBundle.message("android.lint.inspections.typos"), TypoDetector.ISSUE);
}
}
public static class AndroidKLintUniquePermissionInspection extends AndroidLintInspectionBase {
public AndroidKLintUniquePermissionInspection() {
super(AndroidBundle.message("android.lint.inspections.unique.permission"), ManifestDetector.UNIQUE_PERMISSION);
}
}
public static class AndroidKLintUnlocalizedSmsInspection extends AndroidLintInspectionBase {
public AndroidKLintUnlocalizedSmsInspection() {
super(AndroidBundle.message("android.lint.inspections.unlocalized.sms"), NonInternationalizedSmsDetector.ISSUE);
@@ -1205,16 +528,4 @@ public class AndroidLintInspectionToolProvider {
super(AndroidBundle.message("android.lint.inspections.wrong.call"), WrongCallDetector.ISSUE);
}
}
public static class AndroidKLintWrongCaseInspection extends AndroidLintInspectionBase {
public AndroidKLintWrongCaseInspection() {
super(AndroidBundle.message("android.lint.inspections.wrong.case"), WrongCaseDetector.WRONG_CASE);
}
}
public static class AndroidKLintWrongFolderInspection extends AndroidLintInspectionBase {
public AndroidKLintWrongFolderInspection() {
super(AndroidBundle.message("android.lint.inspections.wrong.folder"), WrongLocationDetector.ISSUE);
}
}
}