Lint: copy diagnostics from IDEA (br143)

This commit is contained in:
Yan Zhulanow
2016-03-31 17:34:50 +03:00
parent 92d2e7a0a9
commit 126cc778e4
185 changed files with 66319 additions and 0 deletions
@@ -0,0 +1,202 @@
/*
* 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.lint.client.api;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.google.common.annotations.Beta;
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.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Specialized visitor for running detectors on a class object model.
* <p>
* It operates in two phases:
* <ol>
* <li> First, it computes a set of maps where it generates a map from each
* significant method name to a list of detectors to consult for that method
* name. The set of method names that a detector is interested in is provided
* by the detectors themselves.
* <li> Second, it iterates over the DOM a single time. For each method call it finds,
* it dispatches to any check that has registered interest in that method name.
* <li> Finally, it runs a full check on those class scanners that do not register
* specific method names to be checked. This is intended for those detectors
* that do custom work, not related specifically to method calls.
* </ol>
* It also notifies all the detectors before and after the document is processed
* such that they can do pre- and post-processing.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
class AsmVisitor {
/**
* Number of distinct node types specified in {@link AbstractInsnNode}. Sadly
* there isn't a max-constant there, so update this along with ASM library
* updates.
*/
private static final int TYPE_COUNT = AbstractInsnNode.LINE + 1;
private final Map<String, List<ClassScanner>> mMethodNameToChecks =
new HashMap<String, List<ClassScanner>>();
private final Map<String, List<ClassScanner>> mMethodOwnerToChecks =
new HashMap<String, List<ClassScanner>>();
private final List<Detector> mFullClassChecks = new ArrayList<Detector>();
private final List<? extends Detector> mAllDetectors;
private List<ClassScanner>[] mNodeTypeDetectors;
// Really want this:
//<T extends List<Detector> & Detector.ClassScanner> ClassVisitor(T xmlDetectors) {
// but it makes client code tricky and ugly.
@SuppressWarnings("unchecked")
AsmVisitor(@NonNull LintClient client, @NonNull List<? extends Detector> classDetectors) {
mAllDetectors = classDetectors;
// TODO: Check appliesTo() for files, and find a quick way to enable/disable
// rules when running through a full project!
for (Detector detector : classDetectors) {
Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
boolean checkFullClass = true;
Collection<String> names = scanner.getApplicableCallNames();
if (names != null) {
checkFullClass = false;
for (String element : names) {
List<Detector.ClassScanner> list = mMethodNameToChecks.get(element);
if (list == null) {
list = new ArrayList<Detector.ClassScanner>();
mMethodNameToChecks.put(element, list);
}
list.add(scanner);
}
}
Collection<String> owners = scanner.getApplicableCallOwners();
if (owners != null) {
checkFullClass = false;
for (String element : owners) {
List<Detector.ClassScanner> list = mMethodOwnerToChecks.get(element);
if (list == null) {
list = new ArrayList<Detector.ClassScanner>();
mMethodOwnerToChecks.put(element, list);
}
list.add(scanner);
}
}
int[] types = scanner.getApplicableAsmNodeTypes();
if (types != null) {
checkFullClass = false;
for (int type : types) {
if (type < 0 || type >= TYPE_COUNT) {
// Can't support this node type: looks like ASM wasn't updated correctly.
client.log(null, "Out of range node type %1$d from detector %2$s",
type, scanner);
continue;
}
if (mNodeTypeDetectors == null) {
mNodeTypeDetectors = new List[TYPE_COUNT];
}
List<ClassScanner> checks = mNodeTypeDetectors[type];
if (checks == null) {
checks = new ArrayList<ClassScanner>();
mNodeTypeDetectors[type] = checks;
}
checks.add(scanner);
}
}
if (checkFullClass) {
mFullClassChecks.add(detector);
}
}
}
@SuppressWarnings("rawtypes") // ASM API uses raw types
void runClassDetectors(ClassContext context) {
ClassNode classNode = context.getClassNode();
for (Detector detector : mAllDetectors) {
detector.beforeCheckFile(context);
}
for (Detector detector : mFullClassChecks) {
Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
scanner.checkClass(context, classNode);
detector.afterCheckFile(context);
}
if (!mMethodNameToChecks.isEmpty() || !mMethodOwnerToChecks.isEmpty() ||
mNodeTypeDetectors != null && mNodeTypeDetectors.length > 0) {
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
InsnList nodes = method.instructions;
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 call = (MethodInsnNode) instruction;
String owner = call.owner;
List<ClassScanner> scanners = mMethodOwnerToChecks.get(owner);
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkCall(context, classNode, method, call);
}
}
String name = call.name;
scanners = mMethodNameToChecks.get(name);
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkCall(context, classNode, method, call);
}
}
}
if (mNodeTypeDetectors != null && type < mNodeTypeDetectors.length) {
List<ClassScanner> scanners = mNodeTypeDetectors[type];
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkInstruction(context, classNode, method, instruction);
}
}
}
}
}
}
for (Detector detector : mAllDetectors) {
detector.afterCheckFile(context);
}
}
}
@@ -0,0 +1,81 @@
/*
* 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.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.google.common.annotations.Beta;
/**
* Exception thrown when there is a circular dependency, such as a circular dependency
* of library mProject references
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class CircularDependencyException extends RuntimeException {
@Nullable
private Project mProject;
@Nullable
private Location mLocation;
CircularDependencyException(@NonNull String message) {
super(message);
}
/**
* Returns the associated project, if any
*
* @return the associated project, if any
*/
@Nullable
public Project getProject() {
return mProject;
}
/**
* Sets the associated project, if any
*
* @param project the associated project, if any
*/
public void setProject(@Nullable Project project) {
mProject = project;
}
/**
* Returns the associated location, if any
*
* @return the associated location, if any
*/
@Nullable
public Location getLocation() {
return mLocation;
}
/**
* Sets the associated location, if any
*
* @param location the associated location, if any
*/
public void setLocation(@Nullable Location location) {
mLocation = location;
}
}
@@ -0,0 +1,335 @@
/*
* 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.lint.client.api;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static org.objectweb.asm.Opcodes.ASM5;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/** A class, present either as a .class file on disk, or inside a .jar file. */
@VisibleForTesting
class ClassEntry implements Comparable<ClassEntry> {
public final File file;
public final File jarFile;
public final File binDir;
public final byte[] bytes;
@VisibleForTesting
ClassEntry(
@NonNull File file,
@Nullable File jarFile,
@NonNull File binDir,
@NonNull byte[] bytes) {
super();
this.file = file;
this.jarFile = jarFile;
this.binDir = binDir;
this.bytes = bytes;
}
@NonNull
public String path() {
if (jarFile != null) {
return jarFile.getPath() + ':' + file.getPath();
} else {
return file.getPath();
}
}
@Override
public int compareTo(@NonNull ClassEntry other) {
String p1 = file.getPath();
String p2 = other.file.getPath();
int m1 = p1.length();
int m2 = p2.length();
if (m1 == m2 && p1.equals(p2)) {
return 0;
}
int m = Math.min(m1, m2);
for (int i = 0; i < m; i++) {
char c1 = p1.charAt(i);
char c2 = p2.charAt(i);
if (c1 != c2) {
// Sort Foo$Bar.class *after* Foo.class, even though $ < .
if (c1 == '.' && c2 == '$') {
return -1;
}
if (c1 == '$' && c2 == '.') {
return 1;
}
return c1 - c2;
}
}
return (m == m1) ? -1 : 1;
}
@Override
public String toString() {
return file.getPath();
}
/**
* Creates a list of class entries from the given class path.
*
* @param client the client to report errors to and to use to read files
* @param classPath the class path (directories and jar files) to scan
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassPath(
@NonNull LintClient client,
@NonNull List<File> classPath,
boolean sort) {
if (!classPath.isEmpty()) {
List<ClassEntry> libraryEntries = new ArrayList<ClassEntry>(64);
addEntries(client, libraryEntries, classPath);
if (sort) {
Collections.sort(libraryEntries);
}
return libraryEntries;
} else {
return Collections.emptyList();
}
}
/**
* Creates a list of class entries from the given class path and specific set of
* files within it.
*
* @param client the client to report errors to and to use to read files
* @param classFiles the specific set of class files to look for
* @param classFolders the list of class folders to look in (to determine the
* package root)
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassFiles(
@NonNull LintClient client,
@NonNull List<File> classFiles, @NonNull List<File> classFolders,
boolean sort) {
List<ClassEntry> entries = new ArrayList<ClassEntry>(classFiles.size());
if (!classFolders.isEmpty()) {
for (File file : classFiles) {
String path = file.getPath();
if (file.isFile() && path.endsWith(DOT_CLASS)) {
try {
byte[] bytes = client.readBytes(file);
for (File dir : classFolders) {
if (path.startsWith(dir.getPath())) {
entries.add(new ClassEntry(file, null /* jarFile*/, dir,
bytes));
break;
}
}
} catch (IOException e) {
client.log(e, null);
}
}
}
if (sort && !entries.isEmpty()) {
Collections.sort(entries);
}
}
return entries;
}
/**
* Given a classpath, add all the class files found within the directories and inside jar files
*/
private static void addEntries(
@NonNull LintClient client,
@NonNull List<ClassEntry> entries,
@NonNull List<File> classPath) {
for (File classPathEntry : classPath) {
if (classPathEntry.getName().endsWith(DOT_JAR)) {
//noinspection UnnecessaryLocalVariable
File jarFile = classPathEntry;
if (!jarFile.exists()) {
continue;
}
ZipInputStream zis = null;
try {
FileInputStream fis = new FileInputStream(jarFile);
try {
zis = new ZipInputStream(fis);
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
String name = entry.getName();
if (name.endsWith(DOT_CLASS)) {
try {
byte[] bytes = ByteStreams.toByteArray(zis);
if (bytes != null) {
File file = new File(entry.getName());
entries.add(new ClassEntry(file, jarFile, jarFile, bytes));
}
} catch (Exception e) {
client.log(e, null);
continue;
}
}
entry = zis.getNextEntry();
}
} finally {
Closeables.close(fis, true);
}
} catch (IOException e) {
client.log(e, "Could not read jar file contents from %1$s", jarFile);
} finally {
try {
Closeables.close(zis, true);
} catch (IOException e) {
// cannot happen
}
}
} else if (classPathEntry.isDirectory()) {
//noinspection UnnecessaryLocalVariable
File binDir = classPathEntry;
List<File> classFiles = new ArrayList<File>();
addClassFiles(binDir, classFiles);
for (File file : classFiles) {
try {
byte[] bytes = client.readBytes(file);
entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes));
} catch (IOException e) {
client.log(e, null);
}
}
} else {
client.log(null, "Ignoring class path entry %1$s", classPathEntry);
}
}
}
/** Adds in all the .class files found recursively in the given directory */
private static void addClassFiles(@NonNull File dir, @NonNull List<File> classFiles) {
// Process the resource folder
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(DOT_CLASS)) {
classFiles.add(file);
} else if (file.isDirectory()) {
// Recurse
addClassFiles(file, classFiles);
}
}
}
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param libraryEntries the set of library entries to consult
* @param classEntries the set of class entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client,
@NonNull List<ClassEntry> libraryEntries,
@NonNull List<ClassEntry> classEntries) {
int size = libraryEntries.size() + classEntries.size();
Map<String, String> map = Maps.newHashMapWithExpectedSize(size);
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, libraryEntries);
addSuperClasses(client, visitor, classEntries);
return map;
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param entries the set of library entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client,
@NonNull List<ClassEntry> entries) {
Map<String, String> map = Maps.newHashMapWithExpectedSize(entries.size());
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, entries);
return map;
}
/** Adds in all the super classes found for the given class entries into the given map */
private static void addSuperClasses(
@NonNull LintClient client,
@NonNull SuperclassVisitor visitor,
@NonNull List<ClassEntry> entries) {
for (ClassEntry entry : entries) {
try {
ClassReader reader = new ClassReader(entry.bytes);
int flags = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG
| ClassReader.SKIP_FRAMES;
reader.accept(visitor, flags);
} catch (Throwable t) {
client.log(null, "Error processing %1$s: broken class file?", entry.path());
}
}
}
/** Visitor skimming classes and initializing a map of super classes */
private static class SuperclassVisitor extends ClassVisitor {
private final Map<String, String> mMap;
public SuperclassVisitor(Map<String, String> map) {
super(ASM5);
mMap = map;
}
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
// Record super class in the map (but don't waste space on java.lang.Object)
if (superName != null && !"java/lang/Object".equals(superName)) {
mMap.put(name, superName);
}
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.lint.client.api;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Issue;
import com.google.common.collect.Lists;
import java.util.List;
/**
* Registry which merges many issue registries into one, and presents a unified list
* of issues.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
class CompositeIssueRegistry extends IssueRegistry {
private final List<IssueRegistry> myRegistries;
private List<Issue> myIssues;
public CompositeIssueRegistry(@NonNull List<IssueRegistry> registries) {
myRegistries = registries;
}
@NonNull
@Override
public List<Issue> getIssues() {
if (myIssues == null) {
List<Issue> issues = Lists.newArrayListWithExpectedSize(200);
for (IssueRegistry registry : myRegistries) {
issues.addAll(registry.getIssues());
}
myIssues = issues;
}
return myIssues;
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.annotations.Beta;
/**
* Lint configuration for an Android project such as which specific rules to include,
* which specific rules to exclude, and which specific errors to ignore.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class Configuration {
/**
* Checks whether this issue should be ignored because the user has already
* suppressed the error? Note that this refers to individual issues being
* suppressed/ignored, not a whole detector being disabled via something
* like {@link #isEnabled(Issue)}.
*
* @param context the context used by the detector when the issue was found
* @param issue the issue that was found
* @param location the location of the issue
* @param message the associated user message
* @return true if this issue should be suppressed
*/
public boolean isIgnored(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
return false;
}
/**
* Returns false if the given issue has been disabled. This is just
* a convenience method for {@code getSeverity(issue) != Severity.IGNORE}.
*
* @param issue the issue to check
* @return false if the issue has been disabled
*/
public boolean isEnabled(@NonNull Issue issue) {
return getSeverity(issue) != Severity.IGNORE;
}
/**
* Returns the severity for a given issue. This is the same as the
* {@link Issue#getDefaultSeverity()} unless the user has selected a custom
* severity (which is tool context dependent).
*
* @param issue the issue to look up the severity from
* @return the severity use for issues for the given detector
*/
public Severity getSeverity(@NonNull Issue issue) {
return issue.getDefaultSeverity();
}
// Editing configurations
/**
* Marks the given warning as "ignored".
*
* @param context The scanning context
* @param issue the issue to be ignored
* @param location The location to ignore the warning at, if any
* @param message The message for the warning
*/
public abstract void ignore(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message);
/**
* Sets the severity to be used for this issue.
*
* @param issue the issue to set the severity for
* @param severity the severity to associate with this issue, or null to
* reset the severity to the default
*/
public abstract void setSeverity(@NonNull Issue issue, @Nullable Severity severity);
// Bulk editing support
/**
* Marks the beginning of a "bulk" editing operation with repeated calls to
* {@link #setSeverity} or {@link #ignore}. After all the values have been
* set, the client <b>must</b> call {@link #finishBulkEditing()}. This
* allows configurations to avoid doing expensive I/O (such as writing out a
* config XML file) for each and every editing operation when they are
* applied in bulk, such as from a configuration dialog's "Apply" action.
*/
public void startBulkEditing() {
}
/**
* Marks the end of a "bulk" editing operation, where values should be
* committed to persistent storage. See {@link #startBulkEditing()} for
* details.
*/
public void finishBulkEditing() {
}
}
@@ -0,0 +1,609 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import static com.android.SdkConstants.CURRENT_PLATFORM;
import static com.android.SdkConstants.PLATFORM_WINDOWS;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.utils.XmlUtils;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
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 org.xml.sax.SAXParseException;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Default implementation of a {@link Configuration} which reads and writes
* configuration data into {@code lint.xml} in the project directory.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class DefaultConfiguration extends Configuration {
private final LintClient mClient;
/** Default name of the configuration file */
public static final String CONFIG_FILE_NAME = "lint.xml"; //$NON-NLS-1$
// Lint XML File
@NonNull
private static final String TAG_ISSUE = "issue"; //$NON-NLS-1$
@NonNull
private static final String ATTR_ID = "id"; //$NON-NLS-1$
@NonNull
private static final String ATTR_SEVERITY = "severity"; //$NON-NLS-1$
@NonNull
private static final String ATTR_PATH = "path"; //$NON-NLS-1$
@NonNull
private static final String ATTR_REGEXP = "regexp"; //$NON-NLS-1$
@NonNull
private static final String TAG_IGNORE = "ignore"; //$NON-NLS-1$
@NonNull
private static final String VALUE_ALL = "all"; //$NON-NLS-1$
private final Configuration mParent;
private final Project mProject;
private final File mConfigFile;
private boolean mBulkEditing;
/** Map from id to list of project-relative paths for suppressed warnings */
private Map<String, List<String>> mSuppressed;
/** Map from id to regular expressions. */
@Nullable
private Map<String, List<Pattern>> mRegexps;
/**
* Map from id to custom {@link Severity} override
*/
private Map<String, Severity> mSeverity;
protected DefaultConfiguration(
@NonNull LintClient client,
@Nullable Project project,
@Nullable Configuration parent,
@NonNull File configFile) {
mClient = client;
mProject = project;
mParent = parent;
mConfigFile = configFile;
}
protected DefaultConfiguration(
@NonNull LintClient client,
@NonNull Project project,
@Nullable Configuration parent) {
this(client, project, parent, new File(project.getDir(), CONFIG_FILE_NAME));
}
/**
* Creates a new {@link DefaultConfiguration}
*
* @param client the client to report errors to etc
* @param project the associated project
* @param parent the parent/fallback configuration or null
* @return a new configuration
*/
@NonNull
public static DefaultConfiguration create(
@NonNull LintClient client,
@NonNull Project project,
@Nullable Configuration parent) {
return new DefaultConfiguration(client, project, parent);
}
/**
* Creates a new {@link DefaultConfiguration} for the given lint config
* file, not affiliated with a project. This is used for global
* configurations.
*
* @param client the client to report errors to etc
* @param lintFile the lint file containing the configuration
* @return a new configuration
*/
@NonNull
public static DefaultConfiguration create(@NonNull LintClient client, @NonNull File lintFile) {
return new DefaultConfiguration(client, null /*project*/, null /*parent*/, lintFile);
}
@Override
public boolean isIgnored(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
ensureInitialized();
String id = issue.getId();
List<String> paths = mSuppressed.get(id);
if (paths == null) {
paths = mSuppressed.get(VALUE_ALL);
}
if (paths != null && location != null) {
File file = location.getFile();
String relativePath = context.getProject().getRelativePath(file);
for (String suppressedPath : paths) {
if (suppressedPath.equals(relativePath)) {
return true;
}
// Also allow a prefix
if (relativePath.startsWith(suppressedPath)) {
return true;
}
}
}
if (mRegexps != null) {
List<Pattern> regexps = mRegexps.get(id);
if (regexps == null) {
regexps = mRegexps.get(VALUE_ALL);
}
if (regexps != null && location != null) {
// Check message
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(message);
if (matcher.find()) {
return true;
}
}
// Check location
File file = location.getFile();
String relativePath = context.getProject().getRelativePath(file);
boolean checkUnixPath = false;
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(relativePath);
if (matcher.find()) {
return true;
} else if (regexp.pattern().indexOf('/') != -1) {
checkUnixPath = true;
}
}
if (checkUnixPath && CURRENT_PLATFORM == PLATFORM_WINDOWS) {
relativePath = relativePath.replace('\\', '/');
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(relativePath);
if (matcher.find()) {
return true;
}
}
}
}
}
return mParent != null && mParent.isIgnored(context, issue, location, message);
}
@NonNull
protected Severity getDefaultSeverity(@NonNull Issue issue) {
if (!issue.isEnabledByDefault()) {
return Severity.IGNORE;
}
return issue.getDefaultSeverity();
}
@Override
@NonNull
public Severity getSeverity(@NonNull Issue issue) {
ensureInitialized();
Severity severity = mSeverity.get(issue.getId());
if (severity == null) {
severity = mSeverity.get(VALUE_ALL);
}
if (severity != null) {
return severity;
}
if (mParent != null) {
return mParent.getSeverity(issue);
}
return getDefaultSeverity(issue);
}
private void ensureInitialized() {
if (mSuppressed == null) {
readConfig();
}
}
private void formatError(String message, Object... args) {
if (args != null && args.length > 0) {
message = String.format(message, args);
}
message = "Failed to parse `lint.xml` configuration file: " + message;
LintDriver driver = new LintDriver(new IssueRegistry() {
@Override @NonNull public List<Issue> getIssues() {
return Collections.emptyList();
}
}, mClient);
mClient.report(new Context(driver, mProject, mProject, mConfigFile),
IssueRegistry.LINT_ERROR,
mProject.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR),
Location.create(mConfigFile), message, TextFormat.RAW);
}
private void readConfig() {
mSuppressed = new HashMap<String, List<String>>();
mSeverity = new HashMap<String, Severity>();
if (!mConfigFile.exists()) {
return;
}
try {
Document document = XmlUtils.parseUtfXmlFile(mConfigFile, false);
NodeList issues = document.getElementsByTagName(TAG_ISSUE);
Splitter splitter = Splitter.on(',').trimResults().omitEmptyStrings();
for (int i = 0, count = issues.getLength(); i < count; i++) {
Node node = issues.item(i);
Element element = (Element) node;
String idList = element.getAttribute(ATTR_ID);
if (idList.isEmpty()) {
formatError("Invalid lint config file: Missing required issue id attribute");
continue;
}
Iterable<String> ids = splitter.split(idList);
NamedNodeMap attributes = node.getAttributes();
for (int j = 0, n = attributes.getLength(); j < n; j++) {
Node attribute = attributes.item(j);
String name = attribute.getNodeName();
String value = attribute.getNodeValue();
if (ATTR_ID.equals(name)) {
// already handled
} else if (ATTR_SEVERITY.equals(name)) {
for (Severity severity : Severity.values()) {
if (value.equalsIgnoreCase(severity.name())) {
for (String id : ids) {
mSeverity.put(id, severity);
}
break;
}
}
} else {
formatError("Unexpected attribute \"%1$s\"", name);
}
}
// Look up ignored errors
NodeList childNodes = element.getChildNodes();
if (childNodes.getLength() > 0) {
for (int j = 0, n = childNodes.getLength(); j < n; j++) {
Node child = childNodes.item(j);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element ignore = (Element) child;
String path = ignore.getAttribute(ATTR_PATH);
if (path.isEmpty()) {
String regexp = ignore.getAttribute(ATTR_REGEXP);
if (regexp.isEmpty()) {
formatError("Missing required attribute %1$s or %2$s under %3$s",
ATTR_PATH, ATTR_REGEXP, idList);
} else {
addRegexp(idList, ids, n, regexp, false);
}
} else {
// Normalize path format to File.separator. Also
// handle the file format containing / or \.
if (File.separatorChar == '/') {
path = path.replace('\\', '/');
} else {
path = path.replace('/', File.separatorChar);
}
if (path.indexOf('*') != -1) {
String regexp = globToRegexp(path);
addRegexp(idList, ids, n, regexp, false);
} else {
for (String id : ids) {
List<String> paths = mSuppressed.get(id);
if (paths == null) {
paths = new ArrayList<String>(n / 2 + 1);
mSuppressed.put(id, paths);
}
paths.add(path);
}
}
}
}
}
}
}
} catch (SAXParseException e) {
formatError(e.getMessage());
} catch (Exception e) {
mClient.log(e, null);
}
}
@VisibleForTesting
@NonNull
public static String globToRegexp(@NonNull String glob) {
StringBuilder sb = new StringBuilder(glob.length() * 2);
int begin = 0;
sb.append('^');
for (int i = 0, n = glob.length(); i < n; i++) {
char c = glob.charAt(i);
if (c == '*') {
begin = appendQuoted(sb, glob, begin, i) + 1;
if (i < n - 1 && glob.charAt(i + 1) == '*') {
i++;
begin++;
}
sb.append(".*?");
} else if (c == '?') {
begin = appendQuoted(sb, glob, begin, i) + 1;
sb.append(".?");
}
}
appendQuoted(sb, glob, begin, glob.length());
sb.append('$');
return sb.toString();
}
private static int appendQuoted(StringBuilder sb, String s, int from, int to) {
if (to > from) {
boolean isSimple = true;
for (int i = from; i < to; i++) {
char c = s.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '/' && c != ' ') {
isSimple = false;
break;
}
}
if (isSimple) {
for (int i = from; i < to; i++) {
sb.append(s.charAt(i));
}
return to;
}
sb.append(Pattern.quote(s.substring(from, to)));
}
return to;
}
private void addRegexp(@NonNull String idList, @NonNull Iterable<String> ids, int n,
@NonNull String regexp, boolean silent) {
try {
if (mRegexps == null) {
mRegexps = new HashMap<String, List<Pattern>>();
}
Pattern pattern = Pattern.compile(regexp);
for (String id : ids) {
List<Pattern> paths = mRegexps.get(id);
if (paths == null) {
paths = new ArrayList<Pattern>(n / 2 + 1);
mRegexps.put(id, paths);
}
paths.add(pattern);
}
} catch (PatternSyntaxException e) {
if (!silent) {
formatError("Invalid pattern %1$s under %2$s: %3$s",
regexp, idList, e.getDescription());
}
}
}
private void writeConfig() {
try {
// Write the contents to a new file first such that we don't clobber the
// existing file if some I/O error occurs.
File file = new File(mConfigFile.getParentFile(),
mConfigFile.getName() + ".new"); //$NON-NLS-1$
Writer writer = new BufferedWriter(new FileWriter(file));
writer.write(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //$NON-NLS-1$
"<lint>\n"); //$NON-NLS-1$
if (!mSuppressed.isEmpty() || !mSeverity.isEmpty()) {
// Process the maps in a stable sorted order such that if the
// files are checked into version control with the project,
// there are no random diffs just because hashing algorithms
// differ:
Set<String> idSet = new HashSet<String>();
for (String id : mSuppressed.keySet()) {
idSet.add(id);
}
for (String id : mSeverity.keySet()) {
idSet.add(id);
}
List<String> ids = new ArrayList<String>(idSet);
Collections.sort(ids);
for (String id : ids) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_ISSUE);
writeAttribute(writer, ATTR_ID, id);
Severity severity = mSeverity.get(id);
if (severity != null) {
writeAttribute(writer, ATTR_SEVERITY,
severity.name().toLowerCase(Locale.US));
}
List<Pattern> regexps = mRegexps != null ? mRegexps.get(id) : null;
List<String> paths = mSuppressed.get(id);
if (paths != null && !paths.isEmpty()
|| regexps != null && !regexps.isEmpty()) {
writer.write('>');
writer.write('\n');
// The paths are already kept in sorted order when they are modified
// by ignore(...)
if (paths != null) {
for (String path : paths) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_IGNORE);
writeAttribute(writer, ATTR_PATH, path.replace('\\', '/'));
writer.write(" />\n"); //$NON-NLS-1$
}
}
if (regexps != null) {
for (Pattern regexp : regexps) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_IGNORE);
writeAttribute(writer, ATTR_REGEXP, regexp.pattern());
writer.write(" />\n"); //$NON-NLS-1$
}
}
writer.write(" </"); //$NON-NLS-1$
writer.write(TAG_ISSUE);
writer.write('>');
writer.write('\n');
} else {
writer.write(" />\n"); //$NON-NLS-1$
}
}
}
writer.write("</lint>"); //$NON-NLS-1$
writer.close();
// Move file into place: move current version to lint.xml~ (removing the old ~ file
// if it exists), then move the new version to lint.xml.
File oldFile = new File(mConfigFile.getParentFile(),
mConfigFile.getName() + '~'); //$NON-NLS-1$
if (oldFile.exists()) {
oldFile.delete();
}
if (mConfigFile.exists()) {
mConfigFile.renameTo(oldFile);
}
boolean ok = file.renameTo(mConfigFile);
if (ok && oldFile.exists()) {
oldFile.delete();
}
} catch (Exception e) {
mClient.log(e, null);
}
}
private static void writeAttribute(
@NonNull Writer writer, @NonNull String name, @NonNull String value)
throws IOException {
writer.write(' ');
writer.write(name);
writer.write('=');
writer.write('"');
writer.write(value);
writer.write('"');
}
@Override
public void ignore(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
// This configuration only supports suppressing warnings on a per-file basis
if (location != null) {
ignore(issue, location.getFile());
}
}
/**
* Marks the given issue and file combination as being ignored.
*
* @param issue the issue to be ignored in the given file
* @param file the file to ignore the issue in
*/
public void ignore(@NonNull Issue issue, @NonNull File file) {
ensureInitialized();
String path = mProject != null ? mProject.getRelativePath(file) : file.getPath();
List<String> paths = mSuppressed.get(issue.getId());
if (paths == null) {
paths = new ArrayList<String>();
mSuppressed.put(issue.getId(), paths);
}
paths.add(path);
// Keep paths sorted alphabetically; makes XML output stable
Collections.sort(paths);
if (!mBulkEditing) {
writeConfig();
}
}
@Override
public void setSeverity(@NonNull Issue issue, @Nullable Severity severity) {
ensureInitialized();
String id = issue.getId();
if (severity == null) {
mSeverity.remove(id);
} else {
mSeverity.put(id, severity);
}
if (!mBulkEditing) {
writeConfig();
}
}
@Override
public void startBulkEditing() {
mBulkEditing = true;
}
@Override
public void finishBulkEditing() {
mBulkEditing = false;
writeConfig();
}
@VisibleForTesting
File getConfigFile() {
return mConfigFile;
}
}
@@ -0,0 +1,288 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import static com.android.SdkConstants.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ABS_LIST_VIEW;
import static com.android.SdkConstants.ABS_SEEK_BAR;
import static com.android.SdkConstants.ABS_SPINNER;
import static com.android.SdkConstants.ADAPTER_VIEW;
import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECKABLE;
import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.COMPOUND_BUTTON;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
import static com.android.SdkConstants.FRAME_LAYOUT;
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.IMAGE_BUTTON;
import static com.android.SdkConstants.IMAGE_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.PROGRESS_BAR;
import static com.android.SdkConstants.RADIO_BUTTON;
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.SEEK_BAR;
import static com.android.SdkConstants.SPINNER;
import static com.android.SdkConstants.SURFACE_VIEW;
import static com.android.SdkConstants.SWITCH;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAB_HOST;
import static com.android.SdkConstants.TAB_WIDGET;
import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.SdkConstants.TOGGLE_BUTTON;
import static com.android.SdkConstants.VIEW;
import static com.android.SdkConstants.VIEW_ANIMATOR;
import static com.android.SdkConstants.VIEW_GROUP;
import static com.android.SdkConstants.VIEW_PKG_PREFIX;
import static com.android.SdkConstants.VIEW_STUB;
import static com.android.SdkConstants.VIEW_SWITCHER;
import static com.android.SdkConstants.WEB_VIEW;
import static com.android.SdkConstants.WIDGET_PKG_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Default simple implementation of an {@link SdkInfo}
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
class DefaultSdkInfo extends SdkInfo {
@Override
@Nullable
public String getParentViewName(@NonNull String name) {
name = getRawType(name);
return PARENTS.get(name);
}
@Override
@Nullable
public String getParentViewClass(@NonNull String fqcn) {
int index = fqcn.lastIndexOf('.');
if (index != -1) {
fqcn = fqcn.substring(index + 1);
}
String parent = PARENTS.get(fqcn);
if (parent == null) {
return null;
}
// The map only stores class names internally; correct for full package paths
if (parent.equals(VIEW) || parent.equals(VIEW_GROUP) || parent.equals(SURFACE_VIEW)) {
return VIEW_PKG_PREFIX + parent;
} else {
return WIDGET_PKG_PREFIX + parent;
}
}
@Override
public boolean isSubViewOf(@NonNull String parentType, @NonNull String childType) {
String parent = getRawType(parentType);
String child = getRawType(childType);
// Do analysis just on non-fqcn paths
if (parent.indexOf('.') != -1) {
parent = parent.substring(parent.lastIndexOf('.') + 1);
}
if (child.indexOf('.') != -1) {
child = child.substring(child.lastIndexOf('.') + 1);
}
if (parent.equals(VIEW)) {
return true;
}
while (!child.equals(VIEW)) {
if (parent.equals(child)) {
return true;
}
if (implementsInterface(child, parentType)) {
return true;
}
child = PARENTS.get(child);
if (child == null) {
// Unknown view - err on the side of caution
return true;
}
}
return false;
}
private static boolean implementsInterface(String className, String interfaceName) {
return interfaceName.equals(INTERFACES.get(className));
}
// Strip off type parameters, e.g. AdapterView<?> => AdapterView
private static String getRawType(String type) {
if (type != null) {
int index = type.indexOf('<');
if (index != -1) {
type = type.substring(0, index);
}
}
return type;
}
@Override
public boolean isLayout(@NonNull String tag) {
// TODO: Read in widgets.txt from the platform install area to look up this information
// dynamically instead!
if (super.isLayout(tag)) {
return true;
}
return LAYOUTS.contains(tag);
}
private static final int CLASS_COUNT = 59;
private static final int LAYOUT_COUNT = 20;
private static final Map<String,String> PARENTS = Maps.newHashMapWithExpectedSize(CLASS_COUNT);
private static final Set<String> LAYOUTS = Sets.newHashSetWithExpectedSize(CLASS_COUNT);
static {
PARENTS.put(COMPOUND_BUTTON, BUTTON);
PARENTS.put(ABS_SPINNER, ADAPTER_VIEW);
PARENTS.put(ABS_LIST_VIEW, ADAPTER_VIEW);
PARENTS.put(ABS_SEEK_BAR, ADAPTER_VIEW);
PARENTS.put(ADAPTER_VIEW, VIEW_GROUP);
PARENTS.put(VIEW_GROUP, VIEW);
PARENTS.put(TEXT_VIEW, VIEW);
PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW);
PARENTS.put(RADIO_BUTTON, COMPOUND_BUTTON);
PARENTS.put(SPINNER, ABS_SPINNER);
PARENTS.put(IMAGE_BUTTON, IMAGE_VIEW);
PARENTS.put(IMAGE_VIEW, VIEW);
PARENTS.put(EDIT_TEXT, TEXT_VIEW);
PARENTS.put(PROGRESS_BAR, VIEW);
PARENTS.put(TOGGLE_BUTTON, COMPOUND_BUTTON);
PARENTS.put(VIEW_STUB, VIEW);
PARENTS.put(BUTTON, TEXT_VIEW);
PARENTS.put(SEEK_BAR, ABS_SEEK_BAR);
PARENTS.put(CHECK_BOX, COMPOUND_BUTTON);
PARENTS.put(SWITCH, COMPOUND_BUTTON);
PARENTS.put(GALLERY, ABS_SPINNER);
PARENTS.put(SURFACE_VIEW, VIEW);
PARENTS.put(ABSOLUTE_LAYOUT, VIEW_GROUP);
PARENTS.put(LINEAR_LAYOUT, VIEW_GROUP);
PARENTS.put(RELATIVE_LAYOUT, VIEW_GROUP);
PARENTS.put(LIST_VIEW, ABS_LIST_VIEW);
PARENTS.put(VIEW_SWITCHER, VIEW_ANIMATOR);
PARENTS.put(FRAME_LAYOUT, VIEW_GROUP);
PARENTS.put(HORIZONTAL_SCROLL_VIEW, FRAME_LAYOUT);
PARENTS.put(VIEW_ANIMATOR, FRAME_LAYOUT);
PARENTS.put(TAB_HOST, FRAME_LAYOUT);
PARENTS.put(TABLE_ROW, LINEAR_LAYOUT);
PARENTS.put(RADIO_GROUP, LINEAR_LAYOUT);
PARENTS.put(TAB_WIDGET, LINEAR_LAYOUT);
PARENTS.put(EXPANDABLE_LIST_VIEW, LIST_VIEW);
PARENTS.put(TABLE_LAYOUT, LINEAR_LAYOUT);
PARENTS.put(SCROLL_VIEW, FRAME_LAYOUT);
PARENTS.put(GRID_VIEW, ABS_LIST_VIEW);
PARENTS.put(WEB_VIEW, ABSOLUTE_LAYOUT);
PARENTS.put(AUTO_COMPLETE_TEXT_VIEW, EDIT_TEXT);
PARENTS.put(MULTI_AUTO_COMPLETE_TEXT_VIEW, AUTO_COMPLETE_TEXT_VIEW);
PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW);
PARENTS.put("MediaController", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("SlidingDrawer", VIEW_GROUP); //$NON-NLS-1$
PARENTS.put("DialerFilter", RELATIVE_LAYOUT); //$NON-NLS-1$
PARENTS.put("DigitalClock", TEXT_VIEW); //$NON-NLS-1$
PARENTS.put("Chronometer", TEXT_VIEW); //$NON-NLS-1$
PARENTS.put("ImageSwitcher", VIEW_SWITCHER); //$NON-NLS-1$
PARENTS.put("TextSwitcher", VIEW_SWITCHER); //$NON-NLS-1$
PARENTS.put("AnalogClock", VIEW); //$NON-NLS-1$
PARENTS.put("TwoLineListItem", RELATIVE_LAYOUT); //$NON-NLS-1$
PARENTS.put("ZoomControls", LINEAR_LAYOUT); //$NON-NLS-1$
PARENTS.put("DatePicker", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("TimePicker", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("VideoView", SURFACE_VIEW); //$NON-NLS-1$
PARENTS.put("ZoomButton", IMAGE_BUTTON); //$NON-NLS-1$
PARENTS.put("RatingBar", ABS_SEEK_BAR); //$NON-NLS-1$
PARENTS.put("ViewFlipper", VIEW_ANIMATOR); //$NON-NLS-1$
PARENTS.put("NumberPicker", LINEAR_LAYOUT); //$NON-NLS-1$
assert PARENTS.size() <= CLASS_COUNT : PARENTS.size();
/*
// Check that all widgets lead to the root view
if (LintUtils.assertionsEnabled()) {
for (String key : PARENTS.keySet()) {
String parent = PARENTS.get(key);
if (!parent.equals(VIEW)) {
String grandParent = PARENTS.get(parent);
assert grandParent != null : parent;
}
}
}
*/
LAYOUTS.add(TAB_HOST);
LAYOUTS.add(HORIZONTAL_SCROLL_VIEW);
LAYOUTS.add(VIEW_SWITCHER);
LAYOUTS.add(TAB_WIDGET);
LAYOUTS.add(VIEW_ANIMATOR);
LAYOUTS.add(SCROLL_VIEW);
LAYOUTS.add(GRID_VIEW);
LAYOUTS.add(TABLE_ROW);
LAYOUTS.add(RADIO_GROUP);
LAYOUTS.add(LIST_VIEW);
LAYOUTS.add(EXPANDABLE_LIST_VIEW);
LAYOUTS.add("MediaController"); //$NON-NLS-1$
LAYOUTS.add("DialerFilter"); //$NON-NLS-1$
LAYOUTS.add("ViewFlipper"); //$NON-NLS-1$
LAYOUTS.add("SlidingDrawer"); //$NON-NLS-1$
LAYOUTS.add("StackView"); //$NON-NLS-1$
LAYOUTS.add("SearchView"); //$NON-NLS-1$
LAYOUTS.add("TextSwitcher"); //$NON-NLS-1$
LAYOUTS.add("AdapterViewFlipper"); //$NON-NLS-1$
LAYOUTS.add("ImageSwitcher"); //$NON-NLS-1$
assert LAYOUTS.size() <= LAYOUT_COUNT : LAYOUTS.size();
}
// Currently using a map; this should really be a list, but using a map until we actually
// start adding more than one item
@NonNull
private static final Map<String, String> INTERFACES = new HashMap<String, String>(2);
static {
INTERFACES.put(CHECKED_TEXT_VIEW, CHECKABLE);
INTERFACES.put(COMPOUND_BUTTON, CHECKABLE);
}
}
@@ -0,0 +1,322 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.annotations.Beta;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Registry which provides a list of checks to be performed on an Android project
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class IssueRegistry {
private static List<Category> sCategories;
private static Map<String, Issue> sIdToIssue;
private static Map<EnumSet<Scope>, List<Issue>> sScopeIssues = Maps.newHashMap();
/**
* Creates a new {@linkplain IssueRegistry}
*/
protected IssueRegistry() {
}
private static final Implementation DUMMY_IMPLEMENTATION = new Implementation(Detector.class,
EnumSet.noneOf(Scope.class));
/**
* Issue reported by lint (not a specific detector) when it cannot even
* parse an XML file prior to analysis
*/
@NonNull
public static final Issue PARSER_ERROR = Issue.create(
"ParserError", //$NON-NLS-1$
"Parser Errors",
"Lint will ignore any files that contain fatal parsing errors. These may contain " +
"other errors, or contain code which affects issues in other files.",
Category.CORRECTNESS,
10,
Severity.ERROR,
DUMMY_IMPLEMENTATION);
/**
* Issue reported by lint for various other issues which prevents lint from
* running normally when it's not necessarily an error in the user's code base.
*/
@NonNull
public static final Issue LINT_ERROR = Issue.create(
"LintError", //$NON-NLS-1$
"Lint Failure",
"This issue type represents a problem running lint itself. Examples include " +
"failure to find bytecode for source files (which means certain detectors " +
"could not be run), parsing errors in lint configuration files, etc." +
"\n" +
"These errors are not errors in your own code, but they are shown to make " +
"it clear that some checks were not completed.",
Category.LINT,
10,
Severity.ERROR,
DUMMY_IMPLEMENTATION);
/**
* Issue reported when lint is canceled
*/
@NonNull
public static final Issue CANCELLED = Issue.create(
"LintCanceled", //$NON-NLS-1$
"Lint Canceled",
"Lint canceled by user; the issue report may not be complete.",
Category.LINT,
0,
Severity.INFORMATIONAL,
DUMMY_IMPLEMENTATION);
/**
* Returns the list of issues that can be found by all known detectors.
*
* @return the list of issues to be checked (including those that may be
* disabled!)
*/
@NonNull
public abstract List<Issue> getIssues();
/**
* Get an approximate issue count for a given scope. This is just an optimization,
* so the number does not have to be accurate.
*
* @param scope the scope set
* @return an approximate ceiling of the number of issues expected for a given scope set
*/
protected int getIssueCapacity(@NonNull EnumSet<Scope> scope) {
return 20;
}
/**
* Returns all available issues of a given scope (regardless of whether
* they are actually enabled for a given configuration etc)
*
* @param scope the applicable scope set
* @return a list of issues
*/
@NonNull
protected List<Issue> getIssuesForScope(@NonNull EnumSet<Scope> scope) {
List<Issue> list = sScopeIssues.get(scope);
if (list == null) {
List<Issue> issues = getIssues();
if (scope.equals(Scope.ALL)) {
list = issues;
} else {
list = new ArrayList<Issue>(getIssueCapacity(scope));
for (Issue issue : issues) {
// Determine if the scope matches
if (issue.getImplementation().isAdequate(scope)) {
list.add(issue);
}
}
}
sScopeIssues.put(scope, list);
}
return list;
}
/**
* Creates a list of detectors applicable to the given scope, and with the
* given configuration.
*
* @param client the client to report errors to
* @param configuration the configuration to look up which issues are
* enabled etc from
* @param scope the scope for the analysis, to filter out detectors that
* require wider analysis than is currently being performed
* @param scopeToDetectors an optional map which (if not null) will be
* filled by this method to contain mappings from each scope to
* the applicable detectors for that scope
* @return a list of new detector instances
*/
@NonNull
final List<? extends Detector> createDetectors(
@NonNull LintClient client,
@NonNull Configuration configuration,
@NonNull EnumSet<Scope> scope,
@Nullable Map<Scope, List<Detector>> scopeToDetectors) {
List<Issue> issues = getIssuesForScope(scope);
if (issues.isEmpty()) {
return Collections.emptyList();
}
Set<Class<? extends Detector>> detectorClasses = new HashSet<Class<? extends Detector>>();
Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope =
new HashMap<Class<? extends Detector>, EnumSet<Scope>>();
for (Issue issue : issues) {
Implementation implementation = issue.getImplementation();
Class<? extends Detector> detectorClass = implementation.getDetectorClass();
EnumSet<Scope> issueScope = implementation.getScope();
if (!detectorClasses.contains(detectorClass)) {
// Determine if the issue is enabled
if (!configuration.isEnabled(issue)) {
continue;
}
assert implementation.isAdequate(scope); // Ensured by getIssuesForScope above
detectorClass = client.replaceDetector(detectorClass);
assert detectorClass != null : issue.getId();
detectorClasses.add(detectorClass);
}
if (scopeToDetectors != null) {
EnumSet<Scope> s = detectorToScope.get(detectorClass);
if (s == null) {
detectorToScope.put(detectorClass, issueScope);
} else if (!s.containsAll(issueScope)) {
EnumSet<Scope> union = EnumSet.copyOf(s);
union.addAll(issueScope);
detectorToScope.put(detectorClass, union);
}
}
}
List<Detector> detectors = new ArrayList<Detector>(detectorClasses.size());
for (Class<? extends Detector> clz : detectorClasses) {
try {
Detector detector = clz.newInstance();
detectors.add(detector);
if (scopeToDetectors != null) {
EnumSet<Scope> union = detectorToScope.get(clz);
for (Scope s : union) {
List<Detector> list = scopeToDetectors.get(s);
if (list == null) {
list = new ArrayList<Detector>();
scopeToDetectors.put(s, list);
}
list.add(detector);
}
}
} catch (Throwable t) {
client.log(t, "Can't initialize detector %1$s", clz.getName()); //$NON-NLS-1$
}
}
return detectors;
}
/**
* Returns true if the given id represents a valid issue id
*
* @param id the id to be checked
* @return true if the given id is valid
*/
public final boolean isIssueId(@NonNull String id) {
return getIssue(id) != null;
}
/**
* Returns true if the given category is a valid category
*
* @param name the category name to be checked
* @return true if the given string is a valid category
*/
public final boolean isCategoryName(@NonNull String name) {
for (Category category : getCategories()) {
if (category.getName().equals(name) || category.getFullName().equals(name)) {
return true;
}
}
return false;
}
/**
* Returns the available categories
*
* @return an iterator for all the categories, never null
*/
@NonNull
public List<Category> getCategories() {
if (sCategories == null) {
final Set<Category> categories = new HashSet<Category>();
for (Issue issue : getIssues()) {
categories.add(issue.getCategory());
}
List<Category> sorted = new ArrayList<Category>(categories);
Collections.sort(sorted);
sCategories = Collections.unmodifiableList(sorted);
}
return sCategories;
}
/**
* Returns the issue for the given id, or null if it's not a valid id
*
* @param id the id to be checked
* @return the corresponding issue, or null
*/
@Nullable
public final Issue getIssue(@NonNull String id) {
if (sIdToIssue == null) {
List<Issue> issues = getIssues();
sIdToIssue = new HashMap<String, Issue>(issues.size());
for (Issue issue : issues) {
sIdToIssue.put(issue.getId(), issue);
}
sIdToIssue.put(PARSER_ERROR.getId(), PARSER_ERROR);
sIdToIssue.put(LINT_ERROR.getId(), LINT_ERROR);
}
return sIdToIssue.get(id);
}
/**
* Reset the registry such that it recomputes its available issues.
* <p>
* NOTE: This is only intended for testing purposes.
*/
@VisibleForTesting
protected static void reset() {
sIdToIssue = null;
sCategories = null;
sScopeIssues = Maps.newHashMap();
}
}
@@ -0,0 +1,113 @@
/*
* 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.lint.client.api;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Severity;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
* <p> An {@link IssueRegistry} for a custom lint rule jar file. The rule jar should provide a
* manifest entry with the key {@code Lint-Registry} and the value of the fully qualified name of an
* implementation of {@link IssueRegistry} (with a default constructor). </p>
*
* <p> NOTE: The custom issue registry should not extend this file; it should be a plain
* IssueRegistry! This file is used internally to wrap the given issue registry.</p>
*/
class JarFileIssueRegistry extends IssueRegistry {
/**
* Manifest constant for declaring an issue provider. Example: Lint-Registry:
* foo.bar.CustomIssueRegistry
*/
private static final String MF_LINT_REGISTRY = "Lint-Registry"; //$NON-NLS-1$
private static Map<File, SoftReference<JarFileIssueRegistry>> sCache;
private final List<Issue> myIssues;
@NonNull
static IssueRegistry get(@NonNull LintClient client, @NonNull File jarFile) throws IOException,
ClassNotFoundException, IllegalAccessException, InstantiationException {
if (sCache == null) {
sCache = new HashMap<File, SoftReference<JarFileIssueRegistry>>();
} else {
SoftReference<JarFileIssueRegistry> reference = sCache.get(jarFile);
if (reference != null) {
JarFileIssueRegistry registry = reference.get();
if (registry != null) {
return registry;
}
}
}
JarFileIssueRegistry registry = new JarFileIssueRegistry(client, jarFile);
sCache.put(jarFile, new SoftReference<JarFileIssueRegistry>(registry));
return registry;
}
private JarFileIssueRegistry(@NonNull LintClient client, @NonNull File file)
throws IOException, ClassNotFoundException, IllegalAccessException,
InstantiationException {
myIssues = Lists.newArrayList();
JarFile jarFile = null;
try {
jarFile = new JarFile(file);
Manifest manifest = jarFile.getManifest();
Attributes attrs = manifest.getMainAttributes();
Object object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY));
if (object instanceof String) {
String className = (String) object;
// Make a class loader for this jar
URL url = SdkUtils.fileToUrl(file);
URLClassLoader loader = new URLClassLoader(new URL[]{url},
JarFileIssueRegistry.class.getClassLoader());
Class<?> registryClass = Class.forName(className, true, loader);
IssueRegistry registry = (IssueRegistry) registryClass.newInstance();
myIssues.addAll(registry.getIssues());
} else {
client.log(Severity.ERROR, null,
"Custom lint rule jar %1$s does not contain a valid registry manifest key " +
"(%2$s).\n" +
"Either the custom jar is invalid, or it uses an outdated API not supported " +
"this lint client", file.getPath(), MF_LINT_REGISTRY);
}
} finally {
if (jarFile != null) {
jarFile.close();
}
}
}
@NonNull
@Override
public List<Issue> getIssues() {
return myIssues;
}
}
@@ -0,0 +1,530 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import static com.android.SdkConstants.ATTR_VALUE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import java.util.Collections;
import java.util.List;
import lombok.ast.Identifier;
import lombok.ast.Node;
import lombok.ast.StrictListAccessor;
import lombok.ast.TypeReference;
import lombok.ast.TypeReferencePart;
/**
* A wrapper for a Java parser. This allows tools integrating lint to map directly
* to builtin services, such as already-parsed data structures in Java editors.
* <p/>
* <b>NOTE: This is not public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class JavaParser {
public static final String TYPE_OBJECT = "java.lang.Object"; //$NON-NLS-1$
public static final String TYPE_STRING = "java.lang.String"; //$NON-NLS-1$
public static final String TYPE_INT = "int"; //$NON-NLS-1$
public static final String TYPE_LONG = "long"; //$NON-NLS-1$
public static final String TYPE_CHAR = "char"; //$NON-NLS-1$
public static final String TYPE_FLOAT = "float"; //$NON-NLS-1$
public static final String TYPE_DOUBLE = "double"; //$NON-NLS-1$
public static final String TYPE_BOOLEAN = "boolean"; //$NON-NLS-1$
public static final String TYPE_SHORT = "short"; //$NON-NLS-1$
public static final String TYPE_BYTE = "byte"; //$NON-NLS-1$
public static final String TYPE_NULL = "null"; //$NON-NLS-1$
/**
* Prepare to parse the given contexts. This method will be called before
* a series of {@link #parseJava(JavaContext)} calls, which allows some
* parsers to do up front global computation in case they want to more
* efficiently process multiple files at the same time. This allows a single
* type-attribution pass for example, which is a lot more efficient than
* performing global type analysis over and over again for each individual
* file
*
* @param contexts a list of contexts to be parsed
*/
public abstract void prepareJavaParse(@NonNull List<JavaContext> contexts);
/**
* Parse the file pointed to by the given context.
*
* @param context the context pointing to the file to be parsed, typically
* via {@link Context#getContents()} but the file handle (
* {@link Context#file} can also be used to map to an existing
* editor buffer in the surrounding tool, etc)
* @return the compilation unit node for the file
*/
@Nullable
public abstract Node parseJava(@NonNull JavaContext context);
/**
* Returns a {@link Location} for the given node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull JavaContext context, @NonNull Node node);
/**
* Creates a light-weight handle to a location for the given node. It can be
* turned into a full fledged location by
* {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}.
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return a location handle
*/
@NonNull
public abstract Location.Handle createLocationHandle(@NonNull JavaContext context,
@NonNull Node node);
/**
* Dispose any data structures held for the given context.
* @param context information about the file previously parsed
* @param compilationUnit the compilation unit being disposed
*/
public void dispose(@NonNull JavaContext context, @NonNull Node compilationUnit) {
}
/**
* Dispose any remaining data structures held for all contexts.
* Typically frees up any resources allocated by
* {@link #prepareJavaParse(List)}
*/
public void dispose() {
}
/**
* Resolves the given expression node: computes the declaration for the given symbol
*
* @param context information about the file being parsed
* @param node the node to resolve
* @return a node representing the resolved fully type: class/interface/annotation,
* field, method or variable
*/
@Nullable
public abstract ResolvedNode resolve(@NonNull JavaContext context, @NonNull Node node);
/**
* Finds the given type, if possible (which should be reachable from the compilation
* patch of the given node.
*
* @param context information about the file being parsed
* @param fullyQualifiedName the fully qualified name of the class to look up
* @return the class, or null if not found
*/
@Nullable
public ResolvedClass findClass(
@NonNull JavaContext context,
@NonNull String fullyQualifiedName) {
return null;
}
/**
* Gets the type of the given node
*
* @param context information about the file being parsed
* @param node the node to look up the type for
* @return the type of the node, if known
*/
@Nullable
public abstract TypeDescriptor getType(@NonNull JavaContext context, @NonNull Node node);
/** A description of a type, such as a primitive int or the android.app.Activity class */
public abstract static class TypeDescriptor {
/**
* Returns the fully qualified name of the type, such as "int" or "android.app.Activity"
* */
@NonNull public abstract String getName();
/**
* Returns the full signature of the type, which is normally the same as {@link #getName()}
* but for arrays can include []'s, for generic methods can include generics parameters
* etc
*/
@NonNull public abstract String getSignature();
public abstract boolean matchesName(@NonNull String name);
public abstract boolean matchesSignature(@NonNull String signature);
@NonNull
public TypeReference getNode() {
TypeReference typeReference = new TypeReference();
StrictListAccessor<TypeReferencePart, TypeReference> parts = typeReference.astParts();
for (String part : Splitter.on('.').split(getName())) {
Identifier identifier = Identifier.of(part);
parts.addToEnd(new TypeReferencePart().astIdentifier(identifier));
}
return typeReference;
}
/** If the type is not primitive, returns the class of the type if known */
@Nullable
public abstract ResolvedClass getTypeClass();
@Override
public abstract boolean equals(Object o);
}
/** Convenience implementation of {@link TypeDescriptor} */
public static class DefaultTypeDescriptor extends TypeDescriptor {
private String mName;
public DefaultTypeDescriptor(String name) {
mName = name;
}
@NonNull
@Override
public String getName() {
return mName;
}
@NonNull
@Override
public String getSignature() {
return getName();
}
@Override
public boolean matchesName(@NonNull String name) {
return mName.equals(name);
}
@Override
public boolean matchesSignature(@NonNull String signature) {
return matchesName(signature);
}
@Override
public String toString() {
return getSignature();
}
@Override
@Nullable
public ResolvedClass getTypeClass() {
return null;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultTypeDescriptor that = (DefaultTypeDescriptor) o;
return !(mName != null ? !mName.equals(that.mName) : that.mName != null);
}
@Override
public int hashCode() {
return mName != null ? mName.hashCode() : 0;
}
}
/** A resolved declaration from an AST Node reference */
public abstract static class ResolvedNode {
@NonNull
public abstract String getName();
/** Returns the signature of the resolved node */
public abstract String getSignature();
public abstract int getModifiers();
@Override
public String toString() {
return getSignature();
}
/** Returns any annotations defined on this node */
@NonNull
public abstract Iterable<ResolvedAnnotation> getAnnotations();
/**
* Searches for the annotation of the given type on this node
*
* @param type the fully qualified name of the annotation to check
* @return the annotation, or null if not found
*/
@Nullable
public ResolvedAnnotation getAnnotation(@NonNull String type) {
for (ResolvedAnnotation annotation : getAnnotations()) {
if (annotation.getType().matchesSignature(type)) {
return annotation;
}
}
return null;
}
}
/** A resolved class declaration (class, interface, enumeration or annotation) */
public abstract static class ResolvedClass extends ResolvedNode {
/** Returns the fully qualified name of this class */
@Override
@NonNull
public abstract String getName();
/** Returns the simple of this class */
@NonNull
public abstract String getSimpleName();
/** Returns the package name of this class */
@NonNull
public String getPackageName() {
String name = getName();
String simpleName = getSimpleName();
if (name.length() > simpleName.length() + 1) {
return name.substring(0, name.length() - simpleName.length() - 1);
}
return name;
}
/** Returns whether this class' fully qualified name matches the given name */
public abstract boolean matches(@NonNull String name);
@Nullable
public abstract ResolvedClass getSuperClass();
@Nullable
public abstract ResolvedClass getContainingClass();
public TypeDescriptor getType() {
return new DefaultTypeDescriptor(getName());
}
/**
* Determines whether this class extends the given name. If strict is true,
* it will not consider C extends C true.
*
* @param name the fully qualified class name
* @param strict if true, do not consider a class to be extending itself
* @return true if this class extends the given class
*/
public abstract boolean isSubclassOf(@NonNull String name, boolean strict);
@NonNull
public abstract Iterable<ResolvedMethod> getConstructors();
/** Returns the methods defined in this class, and optionally any methods inherited from any superclasses as well */
@NonNull
public abstract Iterable<ResolvedMethod> getMethods(boolean includeInherited);
/** Returns the methods of a given name defined in this class, and optionally any methods inherited from any superclasses as well */
@NonNull
public abstract Iterable<ResolvedMethod> getMethods(@NonNull String name, boolean includeInherited);
/** Returns the named field defined in this class, or optionally inherited from a superclass */
@Nullable
public abstract ResolvedField getField(@NonNull String name, boolean includeInherited);
}
/** A method or constructor declaration */
public abstract static class ResolvedMethod extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this method name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract ResolvedClass getContainingClass();
public abstract int getArgumentCount();
@NonNull
public abstract TypeDescriptor getArgumentType(int index);
/** Returns true if the parameter at the given index matches the given type signature */
public boolean argumentMatchesType(int index, @NonNull String signature) {
return getArgumentType(index).matchesSignature(signature);
}
@Nullable
public abstract TypeDescriptor getReturnType();
public boolean isConstructor() {
return getReturnType() == null;
}
/** Returns any annotations defined on the given parameter of this method */
@NonNull
public abstract Iterable<ResolvedAnnotation> getParameterAnnotations(int index);
/**
* Searches for the annotation of the given type on the method
*
* @param type the fully qualified name of the annotation to check
* @param parameterIndex the index of the parameter to look up
* @return the annotation, or null if not found
*/
@Nullable
public ResolvedAnnotation getParameterAnnotation(@NonNull String type,
int parameterIndex) {
for (ResolvedAnnotation annotation : getParameterAnnotations(parameterIndex)) {
if (annotation.getType().matchesSignature(type)) {
return annotation;
}
}
return null;
}
/** Returns the super implementation of the given method, if any */
@Nullable
public ResolvedMethod getSuperMethod() {
ResolvedClass cls = getContainingClass().getSuperClass();
if (cls != null) {
String methodName = getName();
int argCount = getArgumentCount();
for (ResolvedMethod method : cls.getMethods(methodName, true)) {
if (argCount != method.getArgumentCount()) {
continue;
}
boolean sameTypes = true;
for (int arg = 0; arg < argCount; arg++) {
if (!method.getArgumentType(arg).equals(getArgumentType(arg))) {
sameTypes = false;
break;
}
}
if (sameTypes) {
return method;
}
}
}
return null;
}
}
/** A field declaration */
public abstract static class ResolvedField extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this field name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract TypeDescriptor getType();
@NonNull
public abstract ResolvedClass getContainingClass();
@Nullable
public abstract Object getValue();
public String getContainingClassName() {
return getContainingClass().getName();
}
}
/**
* An annotation <b>reference</b>. Note that this refers to a usage of an annotation,
* not a declaraton of an annotation. You can call {@link #getClassType()} to
* find the declaration for the annotation.
*/
public abstract static class ResolvedAnnotation extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this field name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract TypeDescriptor getType();
/** Returns the {@link ResolvedClass} which defines the annotation */
@Nullable
public abstract ResolvedClass getClassType();
public static class Value {
@NonNull public final String name;
@Nullable public final Object value;
public Value(@NonNull String name, @Nullable Object value) {
this.name = name;
this.value = value;
}
}
@NonNull
public abstract List<Value> getValues();
@Nullable
public Object getValue(@NonNull String name) {
for (Value value : getValues()) {
if (name.equals(value.name)) {
return value.value;
}
}
return null;
}
@Nullable
public Object getValue() {
return getValue(ATTR_VALUE);
}
@NonNull
@Override
public Iterable<ResolvedAnnotation> getAnnotations() {
return Collections.emptyList();
}
}
/** A local variable or parameter declaration */
public abstract static class ResolvedVariable extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this variable name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract TypeDescriptor getType();
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.google.common.annotations.Beta;
/**
* Interface implemented by listeners to be notified of lint events
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public interface LintListener {
/** The various types of events provided to lint listeners */
enum EventType {
/** A lint check is about to begin */
STARTING,
/** Lint is about to check the given project, see {@link Context#getProject()} */
SCANNING_PROJECT,
/** Lint is about to check the given library project, see {@link Context#getProject()} */
SCANNING_LIBRARY_PROJECT,
/** Lint is about to check the given file, see {@link Context#file} */
SCANNING_FILE,
/** A new pass was initiated */
NEW_PHASE,
/** The lint check was canceled */
CANCELED,
/** The lint check is done */
COMPLETED,
}
/**
* Notifies listeners that the event of the given type has occurred.
* Additional information, such as the file being scanned, or the project
* being scanned, is available in the {@link Context} object (except for the
* {@link EventType#STARTING}, {@link EventType#CANCELED} or
* {@link EventType#COMPLETED} events which are fired outside of project
* contexts.)
*
* @param driver the driver running through the checks
* @param type the type of event that occurred
* @param context the context providing additional information
*/
void update(@NonNull LintDriver driver, @NonNull EventType type,
@Nullable Context context);
}
@@ -0,0 +1,179 @@
/*
* 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.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* Information about a request to run lint
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class LintRequest {
@NonNull
protected final LintClient mClient;
@NonNull
protected final List<File> mFiles;
@Nullable
protected EnumSet<Scope> mScope;
@Nullable
protected Boolean mReleaseMode;
@Nullable
protected Collection<Project> mProjects;
/**
* Creates a new {@linkplain LintRequest}, to be passed to a {@link LintDriver}
*
* @param client the tool wrapping the analyzer, such as an IDE or a CLI
* @param files the set of files to check with lint. This can reference Android projects,
* or directories containing Android projects, or individual XML or Java files
* (typically for incremental IDE analysis).
*/
public LintRequest(@NonNull LintClient client, @NonNull List<File> files) {
mClient = client;
mFiles = files;
}
/**
* Returns the lint client requesting the lint check
*
* @return the client, never null
*/
@NonNull
public LintClient getClient() {
return mClient;
}
/**
* Returns the set of files to check with lint. This can reference Android projects,
* or directories containing Android projects, or individual XML or Java files
* (typically for incremental IDE analysis).
*
* @return the set of files to check, should not be empty
*/
@NonNull
public List<File> getFiles() {
return mFiles;
}
/**
* Sets the scope to use; lint checks which require a wider scope set
* will be ignored
*
* @return the scope to use, or null to use the default
*/
@Nullable
public EnumSet<Scope> getScope() {
return mScope;
}
/**
* Sets the scope to use; lint checks which require a wider scope set
* will be ignored
*
* @param scope the scope
* @return this, for constructor chaining
*/
@NonNull
public LintRequest setScope(@Nullable EnumSet<Scope> scope) {
mScope = scope;
return this;
}
/**
* Returns {@code true} if lint is invoked as part of a release mode build,
* {@code false} if it is part of a debug mode build, and {@code null} if
* the release mode is not known
*
* @return true if this lint is running in release mode, null if not known
*/
@Nullable
public Boolean isReleaseMode() {
return mReleaseMode;
}
/**
* Sets the release mode. Use {@code true} if lint is invoked as part of a
* release mode build, {@code false} if it is part of a debug mode build,
* and {@code null} if the release mode is not known
*
* @param releaseMode true if this lint is running in release mode, null if not known
* @return this, for constructor chaining
*/
@NonNull
public LintRequest setReleaseMode(@Nullable Boolean releaseMode) {
mReleaseMode = releaseMode;
return this;
}
/**
* Gets the projects for the lint requests. This is optional; if not provided lint will search
* the {@link #getFiles()} directories and look for projects via {@link
* LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to
* set up all the projects ahead of time, and associate those projects with native resources
* (in an IDE for example, each lint project can be associated with the corresponding IDE
* project).
*
* @return a collection of projects, or null
*/
@Nullable
public Collection<Project> getProjects() {
return mProjects;
}
/**
* Sets the projects for the lint requests. This is optional; if not provided lint will search
* the {@link #getFiles()} directories and look for projects via {@link
* LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to
* set up all the projects ahead of time, and associate those projects with native resources
* (in an IDE for example, each lint project can be associated with the corresponding IDE
* project).
*
* @param projects a collection of projects, or null
*/
public void setProjects(@Nullable Collection<Project> projects) {
mProjects = projects;
}
/**
* Returns the project to be used as the main project during analysis. This is
* usually the project itself, but when you are for example analyzing a library project,
* it can be the app project using the library.
*
* @param project the project to look up the main project for
* @return the main project
*/
@NonNull
public Project getMainProject(@NonNull Project project) {
return project;
}
}
@@ -0,0 +1,207 @@
/*
* 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.lint.client.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FD_ASSETS;
import static com.android.tools.lint.detector.api.Detector.OtherFileScanner;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Visitor for "other" files: files that aren't java sources,
* XML sources, etc -- or which should have custom handling in some
* other way.
*/
class OtherFileVisitor {
@NonNull
private final List<Detector> mDetectors;
@NonNull
private Map<Scope, List<File>> mFiles = new EnumMap<Scope, List<File>>(Scope.class);
OtherFileVisitor(@NonNull List<Detector> detectors) {
mDetectors = detectors;
}
/** Analyze other files in the given project */
void scan(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main) {
// Collect all project files
File projectFolder = project.getDir();
EnumSet<Scope> scopes = EnumSet.noneOf(Scope.class);
for (Detector detector : mDetectors) {
OtherFileScanner fileScanner = (OtherFileScanner) detector;
EnumSet<Scope> applicable = fileScanner.getApplicableFiles();
if (applicable.contains(Scope.OTHER)) {
scopes = Scope.ALL;
break;
}
scopes.addAll(applicable);
}
List<File> subset = project.getSubset();
if (scopes.contains(Scope.RESOURCE_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (SdkUtils.endsWith(file.getPath(), DOT_XML) &&
!file.getName().equals(ANDROID_MANIFEST_XML)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.RESOURCE_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File res : project.getResourceFolders()) {
collectFiles(files, res);
}
File assets = new File(projectFolder, FD_ASSETS);
if (assets.exists()) {
collectFiles(files, assets);
}
if (!files.isEmpty()) {
mFiles.put(Scope.RESOURCE_FILE, files);
}
}
}
if (scopes.contains(Scope.JAVA_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(DOT_JAVA)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.JAVA_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File srcFolder : project.getJavaSourceFolders()) {
collectFiles(files, srcFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.JAVA_FILE, files);
}
}
}
if (scopes.contains(Scope.CLASS_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(DOT_CLASS)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.CLASS_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File classFolder : project.getJavaClassFolders()) {
collectFiles(files, classFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.CLASS_FILE, files);
}
}
}
if (scopes.contains(Scope.MANIFEST)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getName().equals(ANDROID_MANIFEST_XML)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.MANIFEST, files);
}
} else {
List<File> manifestFiles = project.getManifestFiles();
if (manifestFiles != null) {
mFiles.put(Scope.MANIFEST, manifestFiles);
}
}
}
for (Map.Entry<Scope, List<File>> entry : mFiles.entrySet()) {
Scope scope = entry.getKey();
List<File> files = entry.getValue();
List<Detector> applicable = new ArrayList<Detector>(mDetectors.size());
for (Detector detector : mDetectors) {
OtherFileScanner fileScanner = (OtherFileScanner) detector;
EnumSet<Scope> appliesTo = fileScanner.getApplicableFiles();
if (appliesTo.contains(Scope.OTHER) || appliesTo.contains(scope)) {
applicable.add(detector);
}
}
if (!applicable.isEmpty()) {
for (File file : files) {
Context context = new Context(driver, project, main, file);
for (Detector detector : applicable) {
detector.beforeCheckFile(context);
detector.run(context);
detector.afterCheckFile(context);
}
if (driver.isCanceled()) {
return;
}
}
}
}
}
private static void collectFiles(List<File> files, File file) {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
collectFiles(files, child);
}
}
} else {
files.add(file);
}
}
}
@@ -0,0 +1,245 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.ResourceContext;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
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.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
/**
* Specialized visitor for running detectors on resources: typically XML documents,
* but also binary resources.
* <p>
* It operates in two phases:
* <ol>
* <li> First, it computes a set of maps where it generates a map from each
* significant element name, and each significant attribute name, to a list
* of detectors to consult for that element or attribute name.
* The set of element names or attribute names (or both) that a detector
* is interested in is provided by the detectors themselves.
* <li> Second, it iterates over the document a single time. For each element and
* attribute it looks up the list of interested detectors, and runs them.
* </ol>
* It also notifies all the detectors before and after the document is processed
* such that they can do pre- and post-processing.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
class ResourceVisitor {
private final Map<String, List<Detector.XmlScanner>> mElementToCheck =
new HashMap<String, List<Detector.XmlScanner>>();
private final Map<String, List<Detector.XmlScanner>> mAttributeToCheck =
new HashMap<String, List<Detector.XmlScanner>>();
private final List<Detector.XmlScanner> mDocumentDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<Detector.XmlScanner> mAllElementDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<Detector.XmlScanner> mAllAttributeDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<? extends Detector> mAllDetectors;
private final List<? extends Detector> mBinaryDetectors;
private final XmlParser mParser;
// Really want this:
//<T extends List<Detector> & Detector.XmlScanner> XmlVisitor(IDomParser parser,
// T xmlDetectors) {
// but it makes client code tricky and ugly.
ResourceVisitor(
@NonNull XmlParser parser,
@NonNull List<? extends Detector> xmlDetectors,
@Nullable List<Detector> binaryDetectors) {
mParser = parser;
mAllDetectors = xmlDetectors;
mBinaryDetectors = binaryDetectors;
// TODO: Check appliesTo() for files, and find a quick way to enable/disable
// rules when running through a full project!
for (Detector detector : xmlDetectors) {
Detector.XmlScanner xmlDetector = (XmlScanner) detector;
Collection<String> attributes = xmlDetector.getApplicableAttributes();
if (attributes == XmlScanner.ALL) {
mAllAttributeDetectors.add(xmlDetector);
} else if (attributes != null) {
for (String attribute : attributes) {
List<Detector.XmlScanner> list = mAttributeToCheck.get(attribute);
if (list == null) {
list = new ArrayList<Detector.XmlScanner>();
mAttributeToCheck.put(attribute, list);
}
list.add(xmlDetector);
}
}
Collection<String> elements = xmlDetector.getApplicableElements();
if (elements == XmlScanner.ALL) {
mAllElementDetectors.add(xmlDetector);
} else if (elements != null) {
for (String element : elements) {
List<Detector.XmlScanner> list = mElementToCheck.get(element);
if (list == null) {
list = new ArrayList<Detector.XmlScanner>();
mElementToCheck.put(element, list);
}
list.add(xmlDetector);
}
}
if ((attributes == null || (attributes.isEmpty()
&& attributes != XmlScanner.ALL))
&& (elements == null || (elements.isEmpty()
&& elements != XmlScanner.ALL))) {
mDocumentDetectors.add(xmlDetector);
}
}
}
void visitFile(@NonNull XmlContext context, @NonNull File file) {
assert LintUtils.isXmlFile(file);
try {
if (context.document == null) {
context.document = mParser.parseXml(context);
if (context.document == null) {
// No need to log this; the parser should be reporting
// a full warning (such as IssueRegistry#PARSER_ERROR)
// with details, location, etc.
return;
}
if (context.document.getDocumentElement() == null) {
// Ignore empty documents
return;
}
}
for (Detector check : mAllDetectors) {
check.beforeCheckFile(context);
}
for (Detector.XmlScanner check : mDocumentDetectors) {
check.visitDocument(context, context.document);
}
if (!mElementToCheck.isEmpty() || !mAttributeToCheck.isEmpty()
|| !mAllAttributeDetectors.isEmpty() || !mAllElementDetectors.isEmpty()) {
visitElement(context, context.document.getDocumentElement());
}
for (Detector check : mAllDetectors) {
check.afterCheckFile(context);
}
} finally {
if (context.document != null) {
mParser.dispose(context, context.document);
context.document = null;
}
}
}
private void visitElement(@NonNull XmlContext context, @NonNull Element element) {
List<Detector.XmlScanner> elementChecks = mElementToCheck.get(element.getTagName());
if (elementChecks != null) {
assert elementChecks instanceof RandomAccess;
for (XmlScanner check : elementChecks) {
check.visitElement(context, element);
}
}
if (!mAllElementDetectors.isEmpty()) {
for (XmlScanner check : mAllElementDetectors) {
check.visitElement(context, element);
}
}
if (!mAttributeToCheck.isEmpty() || !mAllAttributeDetectors.isEmpty()) {
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 = attribute.getName();
}
List<Detector.XmlScanner> list = mAttributeToCheck.get(name);
if (list != null) {
for (XmlScanner check : list) {
check.visitAttribute(context, attribute);
}
}
if (!mAllAttributeDetectors.isEmpty()) {
for (XmlScanner check : mAllAttributeDetectors) {
check.visitAttribute(context, attribute);
}
}
}
}
// Visit children
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
visitElement(context, (Element) child);
}
}
// Post hooks
if (elementChecks != null) {
for (XmlScanner check : elementChecks) {
check.visitElementAfter(context, element);
}
}
if (!mAllElementDetectors.isEmpty()) {
for (XmlScanner check : mAllElementDetectors) {
check.visitElementAfter(context, element);
}
}
}
@NonNull
public XmlParser getParser() {
return mParser;
}
public void visitBinaryResource(@NonNull ResourceContext context) {
if (mBinaryDetectors == null) {
return;
}
for (Detector check : mBinaryDetectors) {
check.beforeCheckFile(context);
check.checkBinaryResource(context);
check.afterCheckFile(context);
}
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* Information about SDKs
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class SdkInfo {
/**
* Returns true if the given child view is the same class or a sub class of
* the given parent view class
*
* @param parentViewFqcn the fully qualified class name of the parent view
* @param childViewFqcn the fully qualified class name of the child view
* @return true if the child view is a sub view of (or the same class as)
* the parent view
*/
public boolean isSubViewOf(@NonNull String parentViewFqcn, @NonNull String childViewFqcn) {
while (!childViewFqcn.equals("android.view.View")) { //$NON-NLS-1$
if (parentViewFqcn.equals(childViewFqcn)) {
return true;
}
String parent = getParentViewClass(childViewFqcn);
if (parent == null) {
// Unknown view - err on the side of caution
return true;
}
childViewFqcn = parent;
}
return false;
}
/**
* Returns the fully qualified name of the parent view, or null if the view
* is the root android.view.View class.
*
* @param fqcn the fully qualified class name of the view
* @return the fully qualified class name of the parent view, or null
*/
@Nullable
public abstract String getParentViewClass(@NonNull String fqcn);
/**
* Returns the class name of the parent view, or null if the view is the
* root android.view.View class. This is the same as the
* {@link #getParentViewClass(String)} but without the package.
*
* @param name the view class name to look up the parent for (not including
* package)
* @return the view name of the parent
*/
@Nullable
public abstract String getParentViewName(@NonNull String name);
/**
* Returns true if the given widget name is a layout
*
* @param tag the XML tag for the view
* @return true if the given tag corresponds to a layout
*/
public boolean isLayout(@NonNull String tag) {
return tag.endsWith("Layout"); //$NON-NLS-1$
}
// TODO: Add access to resource resolution here.
}
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
/**
* A wrapper for an XML parser. This allows tools integrating lint to map directly
* to builtin services, such as already-parsed data structures in XML editors.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class XmlParser {
/**
* Parse the file pointed to by the given context and return as a Document
*
* @param context the context pointing to the file to be parsed, typically
* via {@link Context#getContents()} but the file handle (
* {@link Context#file} can also be used to map to an existing
* editor buffer in the surrounding tool, etc)
* @return the parsed DOM document, or null if parsing fails
*/
@Nullable
public abstract Document parseXml(@NonNull XmlContext context);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns a {@link Location} for the given DOM node. Like
* {@link #getLocation(XmlContext, Node)}, but allows a position range that
* is a subset of the node range.
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @param start the starting position within the node, inclusive
* @param end the ending position within the node, exclusive
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node,
int start, int end);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getNameLocation(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getValueLocation(@NonNull XmlContext context, @NonNull Attr node);
/**
* Creates a light-weight handle to a location for the given node. It can be
* turned into a full fledged location by
* {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}.
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return a location handle
*/
@NonNull
public abstract Location.Handle createLocationHandle(@NonNull XmlContext context,
@NonNull Node node);
/**
* Dispose any data structures held for the given context.
* @param context information about the file previously parsed
* @param document the document that was parsed and is now being disposed
*/
public void dispose(@NonNull XmlContext context, @NonNull Document document) {
}
/**
* Returns the start offset of the given node, or -1 if not known
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return the start offset, or -1 if not known
*/
public abstract int getNodeStartOffset(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns the end offset of the given node, or -1 if not known
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return the end offset, or -1 if not known
*/
public abstract int getNodeEndOffset(@NonNull XmlContext context, @NonNull Node node);
}
@@ -0,0 +1,155 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* A category is a container for related issues.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public final class Category implements Comparable<Category> {
private final String mName;
private final int mPriority;
private final Category mParent;
/**
* Creates a new {@link Category}.
*
* @param parent the name of a parent category, or null
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
*/
private Category(
@Nullable Category parent,
@NonNull String name,
int priority) {
mParent = parent;
mName = name;
mPriority = priority;
}
/**
* Creates a new top level {@link Category} with the given sorting priority.
*
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
* @return a new category
*/
@NonNull
public static Category create(@NonNull String name, int priority) {
return new Category(null, name, priority);
}
/**
* Creates a new top level {@link Category} with the given sorting priority.
*
* @param parent the name of a parent category, or null
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
* @return a new category
*/
@NonNull
public static Category create(@Nullable Category parent, @NonNull String name, int priority) {
return new Category(parent, name, priority);
}
/**
* Returns the parent category, or null if this is a top level category
*
* @return the parent category, or null if this is a top level category
*/
public Category getParent() {
return mParent;
}
/**
* Returns the name of this category
*
* @return the name of this category
*/
public String getName() {
return mName;
}
/**
* Returns a full name for this category. For a top level category, this is just
* the {@link #getName()} value, but for nested categories it will include the parent
* names as well.
*
* @return a full name for this category
*/
public String getFullName() {
if (mParent != null) {
return mParent.getFullName() + ':' + mName;
} else {
return mName;
}
}
@Override
public int compareTo(Category other) {
if (other.mPriority == mPriority) {
if (mParent == other) {
return 1;
} else if (other.mParent == this) {
return -1;
}
}
return other.mPriority - mPriority;
}
/** Issues related to running lint itself */
public static final Category LINT = create("Lint", 110);
/** Issues related to correctness */
public static final Category CORRECTNESS = create("Correctness", 100);
/** Issues related to security */
public static final Category SECURITY = create("Security", 90);
/** Issues related to performance */
public static final Category PERFORMANCE = create("Performance", 80);
/** Issues related to usability */
public static final Category USABILITY = create("Usability", 70);
/** Issues related to accessibility */
public static final Category A11Y = create("Accessibility", 60);
/** Issues related to internationalization */
public static final Category I18N = create("Internationalization", 50);
/** Issues related to right to left and bi-directional text support */
public static final Category RTL = create("Bi-directional Text", 40);
// Sub categories
/** Issues related to icons */
public static final Category ICONS = create(USABILITY, "Icons", 73);
/** Issues related to typography */
public static final Category TYPOGRAPHY = create(USABILITY, "Typography", 76);
/** Issues related to messages/strings */
public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95);
}
@@ -0,0 +1,720 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.EOL_BACKWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Location.SearchDirection;
import com.android.tools.lint.detector.api.Location.SearchHints;
import com.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.File;
import java.util.List;
/**
* A {@link Context} used when checking .class files.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class ClassContext extends Context {
private final File mBinDir;
/** The class file DOM root node */
private final ClassNode mClassNode;
/** The class file byte data */
private final byte[] mBytes;
/** The source file, if known/found */
private File mSourceFile;
/** The contents of the source file, if source file is known/found */
private String mSourceContents;
/** Whether we've searched for the source file (used to avoid repeated failed searches) */
private boolean mSearchedForSource;
/** If the file is a relative path within a jar file, this is the jar file, otherwise null */
private final File mJarFile;
/** Whether this class is part of a library (rather than corresponding to one of the
* source files in this project */
private final boolean mFromLibrary;
/**
* Construct a new {@link ClassContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is the
* root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param jarFile If the file is a relative path within a jar file, this is
* the jar file, otherwise null
* @param binDir the root binary directory containing this .class file.
* @param bytes the bytecode raw data
* @param classNode the bytecode object model
* @param fromLibrary whether this class is from a library rather than part
* of this project
* @param sourceContents initial contents of the Java source, if known, or
* null
*/
public ClassContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable File jarFile,
@NonNull File binDir,
@NonNull byte[] bytes,
@NonNull ClassNode classNode,
boolean fromLibrary,
@Nullable String sourceContents) {
super(driver, project, main, file);
mJarFile = jarFile;
mBinDir = binDir;
mBytes = bytes;
mClassNode = classNode;
mFromLibrary = fromLibrary;
mSourceContents = sourceContents;
}
/**
* Returns the raw bytecode data for this class file
*
* @return the byte array containing the bytecode data
*/
@NonNull
public byte[] getBytecode() {
return mBytes;
}
/**
* Returns the bytecode object model
*
* @return the bytecode object model, never null
*/
@NonNull
public ClassNode getClassNode() {
return mClassNode;
}
/**
* Returns the jar file, if any. If this is null, the .class file is a real file
* on disk, otherwise it represents a relative path within the jar file.
*
* @return the jar file, or null
*/
@Nullable
public File getJarFile() {
return mJarFile;
}
/**
* Returns whether this class is part of a library (not this project).
*
* @return true if this class is part of a library
*/
public boolean isFromClassLibrary() {
return mFromLibrary;
}
/**
* Returns the source file for this class file, if possible.
*
* @return the source file, or null
*/
@Nullable
public File getSourceFile() {
if (mSourceFile == null && !mSearchedForSource) {
mSearchedForSource = true;
String source = mClassNode.sourceFile;
if (source == null) {
source = file.getName();
if (source.endsWith(DOT_CLASS)) {
source = source.substring(0, source.length() - DOT_CLASS.length()) + DOT_JAVA;
}
int index = source.indexOf('$');
if (index != -1) {
source = source.substring(0, index) + DOT_JAVA;
}
}
if (source != null) {
if (mJarFile != null) {
String relative = file.getParent() + File.separator + source;
List<File> sources = getProject().getJavaSourceFolders();
for (File dir : sources) {
File sourceFile = new File(dir, relative);
if (sourceFile.exists()) {
mSourceFile = sourceFile;
break;
}
}
} else {
// Determine package
String topPath = mBinDir.getPath();
String parentPath = file.getParentFile().getPath();
if (parentPath.startsWith(topPath)) {
int start = topPath.length() + 1;
String relative = start > parentPath.length() ? // default package?
"" : parentPath.substring(start);
List<File> sources = getProject().getJavaSourceFolders();
for (File dir : sources) {
File sourceFile = new File(dir, relative + File.separator + source);
if (sourceFile.exists()) {
mSourceFile = sourceFile;
break;
}
}
}
}
}
}
return mSourceFile;
}
/**
* Returns the contents of the source file for this class file, if found.
*
* @return the source contents, or ""
*/
@NonNull
public String getSourceContents() {
if (mSourceContents == null) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
mSourceContents = getClient().readFile(mSourceFile);
}
if (mSourceContents == null) {
mSourceContents = "";
}
}
return mSourceContents;
}
/**
* Returns the contents of the source file for this class file, if found. If
* {@code read} is false, do not read the source contents if it has not
* already been read. (This is primarily intended for the lint
* infrastructure; most client code would call {@link #getSourceContents()}
* .)
*
* @param read whether to read the source contents if it has not already
* been initialized
* @return the source contents, which will never be null if {@code read} is
* true, or null if {@code read} is false and the source contents
* hasn't already been read.
*/
@Nullable
public String getSourceContents(boolean read) {
if (read) {
return getSourceContents();
} else {
return mSourceContents;
}
}
/**
* Returns a location for the given source line number in this class file's
* source file, if available.
*
* @param line the line number (1-based, which is what ASM uses)
* @param patternStart optional pattern to search for in the source for
* range start
* @param patternEnd optional pattern to search for in the source for range
* end
* @param hints additional hints about the pattern search (provided
* {@code patternStart} is non null)
* @return a location, never null
*/
@NonNull
public Location getLocationForLine(int line, @Nullable String patternStart,
@Nullable String patternEnd, @Nullable SearchHints hints) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
// ASM line numbers are 1-based, and lint line numbers are 0-based
if (line != -1) {
return Location.create(sourceFile, getSourceContents(), line - 1,
patternStart, patternEnd, hints);
} else {
return Location.create(sourceFile);
}
}
return Location.create(file);
}
/**
* Reports an issue.
* <p>
* Detectors should only call this method if an error applies to the whole class
* scope and there is no specific method or field that applies to the error.
* If so, use
* {@link #report(Issue, org.objectweb.asm.tree.MethodNode, org.objectweb.asm.tree.AbstractInsnNode, Location, String)} or
* {@link #report(Issue, org.objectweb.asm.tree.FieldNode, Location, String)}, such that
* suppress annotations are checked.
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
if (mDriver.isSuppressed(issue, mClassNode)) {
return;
}
ClassNode curr = mClassNode;
while (curr != null) {
ClassNode prev = curr;
curr = mDriver.getOuterClassNode(curr);
if (curr != null) {
if (prev.outerMethod != null) {
@SuppressWarnings("rawtypes") // ASM API
List methods = curr.methods;
for (Object m : methods) {
MethodNode method = (MethodNode) m;
if (method.name.equals(prev.outerMethod)
&& method.desc.equals(prev.outerMethodDesc)) {
// Found the outer method for this anonymous class; continue
// reporting on it (which will also work its way up the parent
// class hierarchy)
if (method != null && mDriver.isSuppressed(issue, mClassNode, method,
null)) {
return;
}
break;
}
}
}
if (mDriver.isSuppressed(issue, curr)) {
return;
}
}
}
super.report(issue, location, message);
}
// Unfortunately, ASMs nodes do not extend a common DOM node type with parent
// pointers, so we have to have multiple methods which pass in each type
// of node (class, method, field) to be checked.
/**
* Reports an issue applicable to a given method node.
*
* @param issue the issue to report
* @param method the method scope the error applies to. The lint
* infrastructure will check whether there are suppress
* annotations on this method (or its enclosing class) and if so
* suppress the warning without involving the client.
* @param instruction the instruction within the method the error applies
* to. You cannot place annotations on individual method
* instructions (for example, annotations on local variables are
* allowed, but are not kept in the .class file). However, this
* instruction is needed to handle suppressing errors on field
* initializations; in that case, the errors may be reported in
* the {@code <clinit>} method, but the annotation is found not
* on that method but for the {@link FieldNode}'s.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull String message) {
if (method != null && mDriver.isSuppressed(issue, mClassNode, method, instruction)) {
return;
}
report(issue, location, message); // also checks the class node
}
/**
* Reports an issue applicable to a given method node.
*
* @param issue the issue to report
* @param field the scope the error applies to. The lint infrastructure
* will check whether there are suppress annotations on this field (or its enclosing
* class) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull String message) {
if (field != null && mDriver.isSuppressed(issue, field)) {
return;
}
report(issue, location, message); // also checks the class node
}
/**
* Report an error.
* Like {@link #report(Issue, MethodNode, AbstractInsnNode, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, method, instruction, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, FieldNode, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, field, location, message);
}
/**
* Finds the line number closest to the given node
*
* @param node the instruction node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull AbstractInsnNode node) {
AbstractInsnNode curr = node;
// First search backwards
while (curr != null) {
if (curr.getType() == AbstractInsnNode.LINE) {
return ((LineNumberNode) curr).line;
}
curr = curr.getPrevious();
}
// Then search forwards
curr = node;
while (curr != null) {
if (curr.getType() == AbstractInsnNode.LINE) {
return ((LineNumberNode) curr).line;
}
curr = curr.getNext();
}
return -1;
}
/**
* Finds the line number closest to the given method declaration
*
* @param node the method node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull MethodNode node) {
if (node.instructions != null && node.instructions.size() > 0) {
return findLineNumber(node.instructions.get(0));
}
return -1;
}
/**
* Finds the line number closest to the given class declaration
*
* @param node the method node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull ClassNode node) {
if (node.methods != null && !node.methods.isEmpty()) {
MethodNode firstMethod = getFirstRealMethod(node);
if (firstMethod != null) {
return findLineNumber(firstMethod);
}
}
return -1;
}
/**
* Returns a location for the given {@link ClassNode}, where class node is
* either the top level class, or an inner class, in the current context.
*
* @param classNode the class in the current context
* @return a location pointing to the class declaration, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull ClassNode classNode) {
// Attempt to find a proper location for this class. This is tricky
// since classes do not have line number entries in the class file; we need
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName;
} else {
pattern = classNode.name;
}
int index = pattern.lastIndexOf('$');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
index = pattern.lastIndexOf('/');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
return getLocationForLine(findLineNumber(classNode), pattern, null,
SearchHints.create(BACKWARD).matchJavaSymbol());
}
@Nullable
private static MethodNode getFirstRealMethod(@NonNull ClassNode classNode) {
// Return the first method in the class for line number purposes. Skip <init>,
// since it's typically not located near the real source of the method.
if (classNode.methods != null) {
@SuppressWarnings("rawtypes") // ASM API
List methods = classNode.methods;
for (Object m : methods) {
MethodNode method = (MethodNode) m;
if (method.name.charAt(0) != '<') {
return method;
}
}
if (!classNode.methods.isEmpty()) {
return (MethodNode) classNode.methods.get(0);
}
}
return null;
}
/**
* Returns a location for the given {@link MethodNode}.
*
* @param methodNode the class in the current context
* @param classNode the class containing the method
* @return a location pointing to the class declaration, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull MethodNode methodNode,
@NonNull ClassNode classNode) {
// Attempt to find a proper location for this class. This is tricky
// since classes do not have line number entries in the class file; we need
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
SearchDirection searchMode;
if (methodNode.name.equals(CONSTRUCTOR_NAME)) {
searchMode = EOL_BACKWARD;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName.substring(classNode.superName.lastIndexOf('/') + 1);
} else {
pattern = classNode.name.substring(classNode.name.lastIndexOf('$') + 1);
}
} else {
searchMode = BACKWARD;
pattern = methodNode.name;
}
return getLocationForLine(findLineNumber(methodNode), pattern, null,
SearchHints.create(searchMode).matchJavaSymbol());
}
/**
* Returns a location for the given {@link AbstractInsnNode}.
*
* @param instruction the instruction to look up the location for
* @return a location pointing to the instruction, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull AbstractInsnNode instruction) {
SearchHints hints = SearchHints.create(FORWARD).matchJavaSymbol();
String pattern = null;
if (instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (call.name.equals(CONSTRUCTOR_NAME)) {
pattern = call.owner;
hints = hints.matchConstructor();
} else {
pattern = call.name;
}
int index = pattern.lastIndexOf('$');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
index = pattern.lastIndexOf('/');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
}
int line = findLineNumber(instruction);
return getLocationForLine(line, pattern, null, hints);
}
private static boolean isAnonymousClass(@NonNull String fqcn) {
int lastIndex = fqcn.lastIndexOf('$');
if (lastIndex != -1 && lastIndex < fqcn.length() - 1) {
if (Character.isDigit(fqcn.charAt(lastIndex + 1))) {
return true;
}
}
return false;
}
/**
* Converts from a VM owner name (such as foo/bar/Foo$Baz) to a
* fully qualified class name (such as foo.bar.Foo.Baz).
*
* @param owner the owner name to convert
* @return the corresponding fully qualified class name
*/
@NonNull
public static String getFqcn(@NonNull String owner) {
return owner.replace('/', '.').replace('$','.');
}
/**
* Computes a user-readable type signature from the given class owner, name
* and description. For example, for owner="foo/bar/Foo$Baz", name="foo",
* description="(I)V", it returns "void foo.bar.Foo.Bar#foo(int)".
*
* @param owner the class name
* @param name the method name
* @param desc the method description
* @return a user-readable string
*/
public static String createSignature(String owner, String name, String desc) {
StringBuilder sb = new StringBuilder(100);
if (desc != null) {
Type returnType = Type.getReturnType(desc);
sb.append(getTypeString(returnType));
sb.append(' ');
}
if (owner != null) {
sb.append(getFqcn(owner));
}
if (name != null) {
sb.append('#');
sb.append(name);
if (desc != null) {
Type[] argumentTypes = Type.getArgumentTypes(desc);
if (argumentTypes != null && argumentTypes.length > 0) {
sb.append('(');
boolean first = true;
for (Type type : argumentTypes) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(getTypeString(type));
}
sb.append(')');
}
}
}
return sb.toString();
}
private static String getTypeString(Type type) {
String s = type.getClassName();
if (s.startsWith("java.lang.")) { //$NON-NLS-1$
s = s.substring("java.lang.".length()); //$NON-NLS-1$
}
return s;
}
/**
* Computes the internal class name of the given fully qualified class name.
* For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar
*
* @param fqcn the fully qualified class name
* @return the internal class name
*/
@NonNull
public static String getInternalName(@NonNull String fqcn) {
if (fqcn.indexOf('.') == -1) {
return fqcn;
}
// If class name contains $, it's not an ambiguous inner class name.
if (fqcn.indexOf('$') != -1) {
return fqcn.replace('.', '/');
}
// Let's assume that components that start with Caps are class names.
StringBuilder sb = new StringBuilder(fqcn.length());
String prev = null;
for (String part : Splitter.on('.').split(fqcn)) {
if (prev != null && !prev.isEmpty()) {
if (Character.isUpperCase(prev.charAt(0))) {
sb.append('$');
} else {
sb.append('/');
}
}
sb.append(part);
prev = part;
}
return sb.toString();
}
}
@@ -0,0 +1,441 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.SUPPRESS_ALL;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.SdkInfo;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* Context passed to the detectors during an analysis run. It provides
* information about the file being analyzed, it allows shared properties (so
* the detectors can share results), etc.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class Context {
/**
* The file being checked. Note that this may not always be to a concrete
* file. For example, in the {@link Detector#beforeCheckProject(Context)}
* method, the context file is the directory of the project.
*/
public final File file;
/** The driver running through the checks */
protected final LintDriver mDriver;
/** The project containing the file being checked */
@NonNull
private final Project mProject;
/**
* The "main" project. For normal projects, this is the same as {@link #mProject},
* but for library projects, it's the root project that includes (possibly indirectly)
* the various library projects and their library projects.
* <p>
* Note that this is a property on the {@link Context}, not the
* {@link Project}, since a library project can be included from multiple
* different top level projects, so there isn't <b>one</b> main project,
* just one per main project being analyzed with its library projects.
*/
private final Project mMainProject;
/** The current configuration controlling which checks are enabled etc */
private final Configuration mConfiguration;
/** The contents of the file */
private String mContents;
/** Map of properties to share results between detectors */
private Map<String, Object> mProperties;
/** Whether this file contains any suppress markers (null means not yet determined) */
private Boolean mContainsCommentSuppress;
/**
* Construct a new {@link Context}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
*/
public Context(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file) {
this.file = file;
mDriver = driver;
mProject = project;
mMainProject = main;
mConfiguration = project.getConfiguration();
}
/**
* Returns the scope for the lint job
*
* @return the scope, never null
*/
@NonNull
public EnumSet<Scope> getScope() {
return mDriver.getScope();
}
/**
* Returns the configuration for this project.
*
* @return the configuration, never null
*/
@NonNull
public Configuration getConfiguration() {
return mConfiguration;
}
/**
* Returns the project containing the file being checked
*
* @return the project, never null
*/
@NonNull
public Project getProject() {
return mProject;
}
/**
* Returns the main project if this project is a library project, or self
* if this is not a library project. The main project is the root project
* of all library projects, not necessarily the directly including project.
*
* @return the main project, never null
*/
@NonNull
public Project getMainProject() {
return mMainProject != null ? mMainProject : mProject;
}
/**
* Returns the lint client requesting the lint check
*
* @return the client, never null
*/
@NonNull
public LintClient getClient() {
return mDriver.getClient();
}
/**
* Returns the driver running through the lint checks
*
* @return the driver
*/
@NonNull
public LintDriver getDriver() {
return mDriver;
}
/**
* Returns the contents of the file. This may not be the contents of the
* file on disk, since it delegates to the {@link LintClient}, which in turn
* may decide to return the current edited contents of the file open in an
* editor.
*
* @return the contents of the given file, or null if an error occurs.
*/
@Nullable
public String getContents() {
if (mContents == null) {
mContents = mDriver.getClient().readFile(file);
}
return mContents;
}
/**
* Returns the value of the given named property, or null.
*
* @param name the name of the property
* @return the corresponding value, or null
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
@Nullable
public Object getProperty(String name) {
if (mProperties == null) {
return null;
}
return mProperties.get(name);
}
/**
* Sets the value of the given named property.
*
* @param name the name of the property
* @param value the corresponding value
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
public void setProperty(@NonNull String name, @Nullable Object value) {
if (value == null) {
if (mProperties != null) {
mProperties.remove(name);
}
} else {
if (mProperties == null) {
mProperties = new HashMap<String, Object>();
}
mProperties.put(name, value);
}
}
/**
* Gets the SDK info for the current project.
*
* @return the SDK info for the current project, never null
*/
@NonNull
public SdkInfo getSdkInfo() {
return mProject.getSdkInfo();
}
// ---- Convenience wrappers ---- (makes the detector code a bit leaner)
/**
* Returns false if the given issue has been disabled. Convenience wrapper
* around {@link Configuration#getSeverity(Issue)}.
*
* @param issue the issue to check
* @return false if the issue has been disabled
*/
public boolean isEnabled(@NonNull Issue issue) {
return mConfiguration.isEnabled(issue);
}
/**
* Reports an issue. Convenience wrapper around {@link LintClient#report}
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
Configuration configuration = mConfiguration;
// If this error was computed for a context where the context corresponds to
// a project instead of a file, the actual error may be in a different project (e.g.
// a library project), so adjust the configuration as necessary.
if (location != null && location.getFile() != null) {
Project project = mDriver.findProjectFor(location.getFile());
if (project != null) {
configuration = project.getConfiguration();
}
}
// If an error occurs in a library project, but you've disabled that check in the
// main project, disable it in the library project too. (In some cases you don't
// control the lint.xml of a library project, and besides, if you're not interested in
// a check for your main project you probably don't care about it in the library either.)
if (configuration != mConfiguration
&& mConfiguration.getSeverity(issue) == Severity.IGNORE) {
return;
}
Severity severity = configuration.getSeverity(issue);
if (severity == Severity.IGNORE) {
return;
}
mDriver.getClient().report(this, issue, severity, location, message, TextFormat.RAW);
}
/**
* Report an error.
* Like {@link #report(Issue, Location, String)} but with
* a now-unused data parameter at the end
*
* @deprecated Use {@link #report(Issue, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, location, message);
}
/**
* Send an exception to the log. Convenience wrapper around {@link LintClient#log}.
*
* @param exception the exception, possibly null
* @param format the error message using {@link String#format} syntax, possibly null
* @param args any arguments for the format string
*/
public void log(
@Nullable Throwable exception,
@Nullable String format,
@Nullable Object... args) {
mDriver.getClient().log(exception, format, args);
}
/**
* Returns the current phase number. The first pass is numbered 1. Only one pass
* will be performed, unless a {@link Detector} calls {@link #requestRepeat}.
*
* @return the current phase, usually 1
*/
public int getPhase() {
return mDriver.getPhase();
}
/**
* Requests another pass through the data for the given detector. This is
* typically done when a detector needs to do more expensive computation,
* but it only wants to do this once it <b>knows</b> that an error is
* present, or once it knows more specifically what to check for.
*
* @param detector the detector that should be included in the next pass.
* Note that the lint runner may refuse to run more than a couple
* of runs.
* @param scope the scope to be revisited. This must be a subset of the
* current scope ({@link #getScope()}, and it is just a performance hint;
* in particular, the detector should be prepared to be called on other
* scopes as well (since they may have been requested by other detectors).
* You can pall null to indicate "all".
*/
public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) {
mDriver.requestRepeat(detector, scope);
}
/** Returns the comment marker used in Studio to suppress statements for language, if any */
@Nullable
protected String getSuppressCommentPrefix() {
// Java and XML files are handled in sub classes (XmlContext, JavaContext)
String path = file.getPath();
if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_GRADLE)) {
return JavaContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(DOT_XML)) {
return XmlContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(".cfg") || path.endsWith(".pro")) {
return "#suppress ";
}
return null;
}
/** Returns whether this file contains any suppress comment markers */
public boolean containsCommentSuppress() {
if (mContainsCommentSuppress == null) {
mContainsCommentSuppress = false;
String prefix = getSuppressCommentPrefix();
if (prefix != null) {
String contents = getContents();
if (contents != null) {
mContainsCommentSuppress = contents.contains(prefix);
}
}
}
return mContainsCommentSuppress;
}
/**
* Returns true if the given issue is suppressed at the given character offset
* in the file's contents
*/
public boolean isSuppressedWithComment(int startOffset, @NonNull Issue issue) {
String prefix = getSuppressCommentPrefix();
if (prefix == null) {
return false;
}
if (startOffset <= 0) {
return false;
}
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
if (startOffset >= contents.length()) {
return false;
}
// Scan backwards to the previous line and see if it contains the marker
int lineStart = contents.lastIndexOf('\n', startOffset) + 1;
if (lineStart <= 1) {
return false;
}
int index = findPrefixOnPreviousLine(contents, lineStart, prefix);
if (index != -1 &&index+prefix.length() < lineStart) {
String line = contents.substring(index + prefix.length(), lineStart);
return line.contains(issue.getId())
|| line.contains(SUPPRESS_ALL) && line.trim().startsWith(SUPPRESS_ALL);
}
return false;
}
private static int findPrefixOnPreviousLine(String contents, int lineStart, String prefix) {
// Search backwards on the previous line until you find the prefix start (also look
// back on previous lines if the previous line(s) contain just whitespace
char first = prefix.charAt(0);
int offset = lineStart - 2; // 0: first char on this line, -1: \n on previous line, -2 last
boolean seenNonWhitespace = false;
for (; offset >= 0; offset--) {
char c = contents.charAt(offset);
if (seenNonWhitespace && c == '\n') {
return -1;
}
if (!seenNonWhitespace && !Character.isWhitespace(c)) {
seenNonWhitespace = true;
}
if (c == first && contents.regionMatches(false, offset, prefix, 0,
prefix.length())) {
return offset;
}
}
return -1;
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.google.common.annotations.Beta;
/**
* A simple offset-based position *
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class DefaultPosition extends Position {
/** The line number (0-based where the first line is line 0) */
private final int mLine;
/**
* The column number (where the first character on the line is 0), or -1 if
* unknown
*/
private final int mColumn;
/** The character offset */
private final int mOffset;
/**
* Creates a new {@link DefaultPosition}
*
* @param line the 0-based line number, or -1 if unknown
* @param column the 0-based column number, or -1 if unknown
* @param offset the offset, or -1 if unknown
*/
public DefaultPosition(int line, int column, int offset) {
mLine = line;
mColumn = column;
mOffset = offset;
}
@Override
public int getLine() {
return mLine;
}
@Override
public int getOffset() {
return mOffset;
}
@Override
public int getColumn() {
return mColumn;
}
}
@@ -0,0 +1,778 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.LintDriver;
import com.google.common.annotations.Beta;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import lombok.ast.AstVisitor;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorInvocation;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
/**
* A detector is able to find a particular problem (or a set of related problems).
* Each problem type is uniquely identified as an {@link Issue}.
* <p>
* Detectors will be called in a predefined order:
* <ol>
* <li> Manifest file
* <li> Resource files, in alphabetical order by resource type
* (therefore, "layout" is checked before "values", "values-de" is checked before
* "values-en" but after "values", and so on.
* <li> Java sources
* <li> Java classes
* <li> Gradle files
* <li> Generic files
* <li> Proguard files
* <li> Property files
* </ol>
* If a detector needs information when processing a file type that comes from a type of
* file later in the order above, they can request a second phase; see
* {@link LintDriver#requestRepeat}.
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class Detector {
/** Specialized interface for detectors that scan Java source file parse trees */
public interface JavaScanner {
/**
* Create a parse tree visitor to process the parse tree. All
* {@link JavaScanner} detectors must provide a visitor, unless they
* either return true from {@link #appliesToResourceRefs()} or return
* non null from {@link #getApplicableMethodNames()}.
* <p>
* If you return specific AST node types from
* {@link #getApplicableNodeTypes()}, then the visitor will <b>only</b>
* be called for the specific requested node types. This is more
* efficient, since it allows many detectors that apply to only a small
* part of the AST (such as method call nodes) to share iteration of the
* majority of the parse tree.
* <p>
* If you return null from {@link #getApplicableNodeTypes()}, then your
* visitor will be called from the top and all node types visited.
* <p>
* Note that a new visitor is created for each separate compilation
* unit, so you can store per file state in the visitor.
*
* @param context the {@link Context} for the file being analyzed
* @return a visitor, or null.
*/
@Nullable
AstVisitor createJavaVisitor(@NonNull JavaContext context);
/**
* Return the types of AST nodes that the visitor returned from
* {@link #createJavaVisitor(JavaContext)} should visit. See the
* documentation for {@link #createJavaVisitor(JavaContext)} for details
* on how the shared visitor is used.
* <p>
* If you return null from this method, then the visitor will process
* the full tree instead.
* <p>
* Note that for the shared visitor, the return codes from the visit
* methods are ignored: returning true will <b>not</b> prune iteration
* of the subtree, since there may be other node types interested in the
* children. If you need to ensure that your visitor only processes a
* part of the tree, use a full visitor instead. See the
* OverdrawDetector implementation for an example of this.
*
* @return the list of applicable node types (AST node classes), or null
*/
@Nullable
List<Class<? extends Node>> getApplicableNodeTypes();
/**
* Return the list of method names this detector is interested in, or
* null. If this method returns non-null, then any AST nodes that match
* a method call in the list will be passed to the
* {@link #visitMethod(JavaContext, AstVisitor, MethodInvocation)}
* method for processing. The visitor created by
* {@link #createJavaVisitor(JavaContext)} is also passed to that
* method, although it can be null.
* <p>
* This makes it easy to write detectors that focus on some fixed calls.
* For example, the StringFormatDetector uses this mechanism to look for
* "format" calls, and when found it looks around (using the AST's
* {@link Node#getParent()} method) to see if it's called on
* a String class instance, and if so do its normal processing. Note
* that since it doesn't need to do any other AST processing, that
* detector does not actually supply a visitor.
*
* @return a set of applicable method names, or null.
*/
@Nullable
List<String> getApplicableMethodNames();
/**
* Method invoked for any method calls found that matches any names
* returned by {@link #getApplicableMethodNames()}. This also passes
* back the visitor that was created by
* {@link #createJavaVisitor(JavaContext)}, but a visitor is not
* required. It is intended for detectors that need to do additional AST
* processing, but also want the convenience of not having to look for
* method names on their own.
*
* @param context the context of the lint request
* @param visitor the visitor created from
* {@link #createJavaVisitor(JavaContext)}, or null
* @param node the {@link MethodInvocation} node for the invoked method
*/
void visitMethod(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull MethodInvocation node);
/**
* Return the list of constructor types this detector is interested in, or
* null. If this method returns non-null, then any AST nodes that match
* a constructor call in the list will be passed to the
* {@link #visitConstructor(JavaContext, AstVisitor, ConstructorInvocation, ResolvedMethod)}
* method for processing. The visitor created by
* {@link #createJavaVisitor(JavaContext)} is also passed to that
* method, although it can be null.
* <p>
* This makes it easy to write detectors that focus on some fixed constructors.
*
* @return a set of applicable fully qualified types, or null.
*/
@Nullable
List<String> getApplicableConstructorTypes();
/**
* Method invoked for any constructor calls found that matches any names
* returned by {@link #getApplicableConstructorTypes()}. This also passes
* back the visitor that was created by
* {@link #createJavaVisitor(JavaContext)}, but a visitor is not
* required. It is intended for detectors that need to do additional AST
* processing, but also want the convenience of not having to look for
* method names on their own.
*
* @param context the context of the lint request
* @param visitor the visitor created from
* {@link #createJavaVisitor(JavaContext)}, or null
* @param node the {@link ConstructorInvocation} node for the invoked method
* @param constructor the resolved constructor method with type information
*/
void visitConstructor(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull ConstructorInvocation node,
@NonNull ResolvedMethod constructor);
/**
* Returns whether this detector cares about Android resource references
* (such as {@code R.layout.main} or {@code R.string.app_name}). If it
* does, then the visitor will look for these patterns, and if found, it
* will invoke {@link #visitResourceReference} passing the resource type
* and resource name. It also passes the visitor, if any, that was
* created by {@link #createJavaVisitor(JavaContext)}, such that a
* detector can do more than just look for resources.
*
* @return true if this detector wants to be notified of R resource
* identifiers found in the code.
*/
boolean appliesToResourceRefs();
/**
* Called for any resource references (such as {@code R.layout.main}
* found in Java code, provided this detector returned {@code true} from
* {@link #appliesToResourceRefs()}.
*
* @param context the lint scanning context
* @param visitor the visitor created from
* {@link #createJavaVisitor(JavaContext)}, or null
* @param node the variable reference for the resource
* @param type the resource type, such as "layout" or "string"
* @param name the resource name, such as "main" from
* {@code R.layout.main}
* @param isFramework whether the resource is a framework resource
* (android.R) or a local project resource (R)
*/
void visitResourceReference(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull Node node,
@NonNull String type,
@NonNull String name,
boolean isFramework);
/**
* Returns a list of fully qualified names for super classes that this
* detector cares about. If not null, this detector will *only* be called
* if the current class is a subclass of one of the specified superclasses.
*
* @return a list of fully qualified names
*/
@Nullable
List<String> applicableSuperClasses();
/**
* Called for each class that extends one of the super classes specified with
* {@link #applicableSuperClasses()}
*
* @param context the lint scanning context
* @param declaration the class declaration node, or null for anonymous classes
* @param node the class declaration node or the anonymous class construction node
* @param resolvedClass the resolved class
*/
void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration,
@NonNull Node node, @NonNull ResolvedClass resolvedClass);
}
/** Specialized interface for detectors that scan Java class files */
public interface ClassScanner {
/**
* Checks the given class' bytecode for issues.
*
* @param context the context of the lint check, pointing to for example
* the file
* @param classNode the root class node
*/
void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode);
/**
* Returns the list of node types (corresponding to the constants in the
* {@link AbstractInsnNode} class) that this scanner applies to. The
* {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)}
* method will be called for each match.
*
* @return an array containing all the node types this detector should be
* called for, or null if none.
*/
@Nullable
int[] getApplicableAsmNodeTypes();
/**
* Process a given instruction node, and register lint issues if
* applicable.
*
* @param context the context of the lint check, pointing to for example
* the file
* @param classNode the root class node
* @param method the method node containing the call
* @param instruction the actual instruction
*/
void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull AbstractInsnNode instruction);
/**
* Return the list of method call names (in VM format, e.g. "<init>" for
* constructors, etc) for method calls this detector is interested in,
* or null. T his will be used to dispatch calls to
* {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)}
* for only the method calls in owners that the detector is interested
* in.
* <p>
* <b>NOTE</b>: If you return non null from this method, then <b>only</b>
* {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)}
* will be called if a suitable method is found;
* {@link #checkClass(ClassContext, ClassNode)} will not be called under
* any circumstances.
* <p>
* This makes it easy to write detectors that focus on some fixed calls,
* and allows lint to make a single pass over the bytecode over a class,
* and efficiently dispatch method calls to any detectors that are
* interested in it. Without this, each new lint check interested in a
* single method, would be doing a complete pass through all the
* bytecode instructions of the class via the
* {@link #checkClass(ClassContext, ClassNode)} method, which would make
* each newly added lint check make lint slower. Now a single dispatch
* map is used instead, and for each encountered call in the single
* dispatch, it looks up in the map which if any detectors are
* interested in the given call name, and dispatches to each one in
* turn.
*
* @return a list of applicable method names, or null.
*/
@Nullable
List<String> getApplicableCallNames();
/**
* Just like {@link Detector#getApplicableCallNames()}, but for the owner
* field instead. The
* {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)}
* method will be called for all {@link MethodInsnNode} instances where the
* owner field matches any of the members returned in this node.
* <p>
* Note that if your detector provides both a name and an owner, the
* method will be called for any nodes matching either the name <b>or</b>
* the owner, not only where they match <b>both</b>. Note also that it will
* be called twice - once for the name match, and (at least once) for the owner
* match.
*
* @return a list of applicable owner names, or null.
*/
@Nullable
List<String> getApplicableCallOwners();
/**
* Process a given method call node, and register lint issues if
* applicable. This is similar to the
* {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)}
* method, but has the additional advantage that it is only called for known
* method names or method owners, according to
* {@link #getApplicableCallNames()} and {@link #getApplicableCallOwners()}.
*
* @param context the context of the lint check, pointing to for example
* the file
* @param classNode the root class node
* @param method the method node containing the call
* @param call the actual method call node
*/
void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call);
}
/** Specialized interface for detectors that scan binary resource files */
public interface BinaryResourceScanner {
/**
* Called for each resource folder
*
* @param context the context for the resource file
*/
void checkBinaryResource(@NonNull ResourceContext context);
/**
* Returns whether this detector applies to the given folder type. This
* allows the detectors to be pruned from iteration, so for example when we
* are analyzing a string value file we don't need to look up detectors
* related to layout.
*
* @param folderType the folder type to be visited
* @return true if this detector can apply to resources in folders of the
* given type
*/
boolean appliesTo(@NonNull ResourceFolderType folderType);
}
/** Specialized interface for detectors that scan resource folders (the folder directory
* itself, not the individual files within it */
public interface ResourceFolderScanner {
/**
* Called for each resource folder
*
* @param context the context for the resource folder
* @param folderName the resource folder name
*/
void checkFolder(@NonNull ResourceContext context, @NonNull String folderName);
/**
* Returns whether this detector applies to the given folder type. This
* allows the detectors to be pruned from iteration, so for example when we
* are analyzing a string value file we don't need to look up detectors
* related to layout.
*
* @param folderType the folder type to be visited
* @return true if this detector can apply to resources in folders of the
* given type
*/
boolean appliesTo(@NonNull ResourceFolderType folderType);
}
/** Specialized interface for detectors that scan XML files */
public interface XmlScanner {
/**
* Visit the given document. The detector is responsible for its own iteration
* through the document.
* @param context information about the document being analyzed
* @param document the document to examine
*/
void visitDocument(@NonNull XmlContext context, @NonNull Document document);
/**
* Visit the given element.
* @param context information about the document being analyzed
* @param element the element to examine
*/
void visitElement(@NonNull XmlContext context, @NonNull Element element);
/**
* Visit the given element after its children have been analyzed.
* @param context information about the document being analyzed
* @param element the element to examine
*/
void visitElementAfter(@NonNull XmlContext context, @NonNull Element element);
/**
* Visit the given attribute.
* @param context information about the document being analyzed
* @param attribute the attribute node to examine
*/
void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute);
/**
* Returns the list of elements that this detector wants to analyze. If non
* null, this detector will be called (specifically, the
* {@link #visitElement} method) for each matching element in the document.
* <p>
* If this method returns null, and {@link #getApplicableAttributes()} also returns
* null, then the {@link #visitDocument} method will be called instead.
*
* @return a collection of elements, or null, or the special
* {@link XmlScanner#ALL} marker to indicate that every single
* element should be analyzed.
*/
@Nullable
Collection<String> getApplicableElements();
/**
* Returns the list of attributes that this detector wants to analyze. If non
* null, this detector will be called (specifically, the
* {@link #visitAttribute} method) for each matching attribute in the document.
* <p>
* If this method returns null, and {@link #getApplicableElements()} also returns
* null, then the {@link #visitDocument} method will be called instead.
*
* @return a collection of attributes, or null, or the special
* {@link XmlScanner#ALL} marker to indicate that every single
* attribute should be analyzed.
*/
@Nullable
Collection<String> getApplicableAttributes();
/**
* Special marker collection returned by {@link #getApplicableElements()} or
* {@link #getApplicableAttributes()} to indicate that the check should be
* invoked on all elements or all attributes
*/
@NonNull
List<String> ALL = new ArrayList<String>(0); // NOT Collections.EMPTY!
// We want to distinguish this from just an *empty* list returned by the caller!
}
/** Specialized interface for detectors that scan Gradle files */
public interface GradleScanner {
void visitBuildScript(@NonNull Context context, Map<String, Object> sharedData);
}
/** Specialized interface for detectors that scan other files */
public interface OtherFileScanner {
/**
* Returns the set of files this scanner wants to consider. If this includes
* {@link Scope#OTHER} then all source files will be checked. Note that the
* set of files will not just include files of the indicated type, but all files
* within the relevant source folder. For example, returning {@link Scope#JAVA_FILE}
* will not just return {@code .java} files, but also other resource files such as
* {@code .html} and other files found within the Java source folders.
* <p>
* Lint will call the {@link #run(Context)}} method when the file should be checked.
*
* @return set of scopes that define the types of source files the
* detector wants to consider
*/
@NonNull
EnumSet<Scope> getApplicableFiles();
}
/**
* Runs the detector. This method will not be called for certain specialized
* detectors, such as {@link XmlScanner} and {@link JavaScanner}, where
* there are specialized analysis methods instead such as
* {@link XmlScanner#visitElement(XmlContext, Element)}.
*
* @param context the context describing the work to be done
*/
public void run(@NonNull Context context) {
}
/**
* Returns true if this detector applies to the given file
*
* @param context the context to check
* @param file the file in the context to check
* @return true if this detector applies to the given context and file
*/
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return false;
}
/**
* Analysis is about to begin, perform any setup steps.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void beforeCheckProject(@NonNull Context context) {
}
/**
* Analysis has just been finished for the whole project, perform any
* cleanup or report issues that require project-wide analysis.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void afterCheckProject(@NonNull Context context) {
}
/**
* Analysis is about to begin for the given library project, perform any setup steps.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void beforeCheckLibraryProject(@NonNull Context context) {
}
/**
* Analysis has just been finished for the given library project, perform any
* cleanup or report issues that require library-project-wide analysis.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void afterCheckLibraryProject(@NonNull Context context) {
}
/**
* Analysis is about to be performed on a specific file, perform any setup
* steps.
* <p>
* Note: When this method is called at the beginning of checking an XML
* file, the context is guaranteed to be an instance of {@link XmlContext},
* and similarly for a Java source file, the context will be a
* {@link JavaContext} and so on.
*
* @param context the context for the check referencing the file to be
* checked, the project, etc.
*/
public void beforeCheckFile(@NonNull Context context) {
}
/**
* Analysis has just been finished for a specific file, perform any cleanup
* or report issues found
* <p>
* Note: When this method is called at the end of checking an XML
* file, the context is guaranteed to be an instance of {@link XmlContext},
* and similarly for a Java source file, the context will be a
* {@link JavaContext} and so on.
*
* @param context the context for the check referencing the file to be
* checked, the project, etc.
*/
public void afterCheckFile(@NonNull Context context) {
}
/**
* Returns the expected speed of this detector
*
* @return the expected speed of this detector
*/
@NonNull
public Speed getSpeed() {
return Speed.NORMAL;
}
/**
* Returns the expected speed of this detector.
* The issue parameter is made available for subclasses which analyze multiple issues
* and which need to distinguish implementation cost by issue. If the detector does
* not analyze multiple issues or does not vary in speed by issue type, just override
* {@link #getSpeed()} instead.
*
* @param issue the issue to look up the analysis speed for
* @return the expected speed of this detector
*/
@NonNull
public Speed getSpeed(@SuppressWarnings("UnusedParameters") @NonNull Issue issue) {
// If not overridden, this detector does not distinguish speed by issue type
return getSpeed();
}
// ---- Dummy implementations to make implementing XmlScanner easier: ----
@SuppressWarnings("javadoc")
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
// This method must be overridden if your detector does
// not return something from getApplicableElements or
// getApplicableAttributes
assert false;
}
@SuppressWarnings("javadoc")
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
// This method must be overridden if your detector returns
// tag names from getApplicableElements
assert false;
}
@SuppressWarnings("javadoc")
public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) {
}
@SuppressWarnings("javadoc")
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
// This method must be overridden if your detector returns
// attribute names from getApplicableAttributes
assert false;
}
@SuppressWarnings("javadoc")
@Nullable
public Collection<String> getApplicableElements() {
return null;
}
@Nullable
@SuppressWarnings("javadoc")
public Collection<String> getApplicableAttributes() {
return null;
}
// ---- Dummy implementations to make implementing JavaScanner easier: ----
@Nullable @SuppressWarnings("javadoc")
public List<String> getApplicableMethodNames() {
return null;
}
@Nullable @SuppressWarnings("javadoc")
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return null;
}
@Nullable @SuppressWarnings("javadoc")
public List<Class<? extends Node>> getApplicableNodeTypes() {
return null;
}
@SuppressWarnings("javadoc")
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
}
@SuppressWarnings("javadoc")
public boolean appliesToResourceRefs() {
return false;
}
@SuppressWarnings("javadoc")
public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull Node node, @NonNull String type, @NonNull String name,
boolean isFramework) {
}
@Nullable
public List<String> applicableSuperClasses() {
return null;
}
public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration,
@NonNull Node node, @NonNull ResolvedClass resolvedClass) {
}
@Nullable @SuppressWarnings("javadoc")
public List<String> getApplicableConstructorTypes() {
return null;
}
@SuppressWarnings("javadoc")
public void visitConstructor(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull ConstructorInvocation node,
@NonNull ResolvedMethod constructor) {
}
// ---- Dummy implementations to make implementing a ClassScanner easier: ----
@SuppressWarnings("javadoc")
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
}
@SuppressWarnings("javadoc")
@Nullable
public List<String> getApplicableCallNames() {
return null;
}
@SuppressWarnings("javadoc")
@Nullable
public List<String> getApplicableCallOwners() {
return null;
}
@SuppressWarnings("javadoc")
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
}
@SuppressWarnings("javadoc")
@Nullable
public int[] getApplicableAsmNodeTypes() {
return null;
}
@SuppressWarnings("javadoc")
public void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull AbstractInsnNode instruction) {
}
// ---- Dummy implementations to make implementing an OtherFileScanner easier: ----
public boolean appliesToFolder(@NonNull Scope scope, @Nullable ResourceFolderType folderType) {
return false;
}
@NonNull
public EnumSet<Scope> getApplicableFiles() {
return Scope.OTHER_SCOPE;
}
// ---- Dummy implementations to make implementing an GradleScanner easier: ----
public void visitBuildScript(@NonNull Context context, Map<String, Object> sharedData) {
}
// ---- Dummy implementations to make implementing a resource folder scanner easier: ----
public void checkFolder(@NonNull ResourceContext context, @NonNull String folderName) {
}
// ---- Dummy implementations to make implementing a binary resource scanner easier: ----
public void checkBinaryResource(@NonNull ResourceContext context) {
}
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
}
@@ -0,0 +1,189 @@
/*
* 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.lint.detector.api;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import java.util.EnumSet;
/**
* An {@linkplain Implementation} of an {@link Issue} maps to the {@link Detector}
* class responsible for analyzing the issue, as well as the {@link Scope} required
* by the detector to perform its analysis.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class Implementation {
private final Class<? extends Detector> mClass;
private final EnumSet<Scope> mScope;
private EnumSet<Scope>[] mAnalysisScopes;
@SuppressWarnings("unchecked")
private static final EnumSet<Scope>[] EMPTY = new EnumSet[0];
/**
* Creates a new implementation for analyzing one or more issues
*
* @param detectorClass the class of the detector to find this issue
* @param scope the scope of files required to analyze this issue
*/
@SuppressWarnings("unchecked")
public Implementation(
@NonNull Class<? extends Detector> detectorClass,
@NonNull EnumSet<Scope> scope) {
this(detectorClass, scope, EMPTY);
}
/**
* Creates a new implementation for analyzing one or more issues
*
* @param detectorClass the class of the detector to find this issue
* @param scope the scope of files required to analyze this issue
* @param analysisScopes optional set of extra scopes the detector is capable of working in
*/
public Implementation(
@NonNull Class<? extends Detector> detectorClass,
@NonNull EnumSet<Scope> scope,
@NonNull EnumSet<Scope>... analysisScopes) {
mClass = detectorClass;
mScope = scope;
mAnalysisScopes = analysisScopes;
}
/**
* Returns the class of the detector to use to find this issue
*
* @return the class of the detector to use to find this issue
*/
@NonNull
public Class<? extends Detector> getDetectorClass() {
return mClass;
}
@Override
public String toString() {
return mClass.toString();
}
/**
* Returns the scope required to analyze the code to detect this issue.
* This is determined by the detectors which reports the issue.
*
* @return the required scope
*/
@NonNull
public EnumSet<Scope> getScope() {
return mScope;
}
/**
* Returns the sets of scopes required to analyze this issue, or null if all
* scopes named by {@link #getScope()} are necessary. Note that only
* <b>one</b> match out of this collection is required, not all, and that
* the scope set returned by {@link #getScope()} does not have to be returned
* by this method, but is always implied to be included.
* <p>
* The scopes returned by {@link #getScope()} list all the various
* scopes that are <b>affected</b> by this issue, meaning the detector
* should consider it. Frequently, the detector must analyze all these
* scopes in order to properly decide whether an issue is found. For
* example, the unused resource detector needs to consider both the XML
* resource files and the Java source files in order to decide if a resource
* is unused. If it analyzes just the Java files for example, it might
* incorrectly conclude that a resource is unused because it did not
* discover a resource reference in an XML file.
* <p>
* However, there are other issues where the issue can occur in a variety of
* files, but the detector can consider each in isolation. For example, the
* API checker is affected by both XML files and Java class files (detecting
* both layout constructor references in XML layout files as well as code
* references in .class files). It doesn't have to analyze both; it is
* capable of incrementally analyzing just an XML file, or just a class
* file, without considering the other.
* <p>
* The required scope list provides a list of scope sets that can be used to
* analyze this issue. For each scope set, all the scopes must be matched by
* the incremental analysis, but any one of the scope sets can be analyzed
* in isolation.
* <p>
* The required scope list is not required to include the full scope set
* returned by {@link #getScope()}; that set is always assumed to be
* included.
* <p>
* NOTE: You would normally call {@link #isAdequate(EnumSet)} rather
* than calling this method directly.
*
* @return a list of required scopes, or null.
*/
@NonNull
public EnumSet<Scope>[] getAnalysisScopes() {
return mAnalysisScopes;
}
/**
* Returns true if the given scope is adequate for analyzing this issue.
* This looks through the analysis scopes (see
* {@link #getAnalysisScopes()}) and if the scope passed in fully
* covers at least one of them, or if it covers the scope of the issue
* itself (see {@link #getScope()}, which should be a superset of all the
* analysis scopes) returns true.
* <p>
* The scope set returned by {@link #getScope()} lists all the various
* scopes that are <b>affected</b> by this issue, meaning the detector
* should consider it. Frequently, the detector must analyze all these
* scopes in order to properly decide whether an issue is found. For
* example, the unused resource detector needs to consider both the XML
* resource files and the Java source files in order to decide if a resource
* is unused. If it analyzes just the Java files for example, it might
* incorrectly conclude that a resource is unused because it did not
* discover a resource reference in an XML file.
* <p>
* However, there are other issues where the issue can occur in a variety of
* files, but the detector can consider each in isolation. For example, the
* API checker is affected by both XML files and Java class files (detecting
* both layout constructor references in XML layout files as well as code
* references in .class files). It doesn't have to analyze both; it is
* capable of incrementally analyzing just an XML file, or just a class
* file, without considering the other.
* <p>
* An issue can register additional scope sets that can are adequate
* for analyzing the issue, by supplying it to
* {@link #Implementation(Class, java.util.EnumSet, java.util.EnumSet[])}.
* This method returns true if the given scope matches one or more analysis
* scope, or the overall scope.
*
* @param scope the scope available for analysis
* @return true if this issue can be analyzed with the given available scope
*/
public boolean isAdequate(@NonNull EnumSet<Scope> scope) {
if (scope.containsAll(mScope)) {
return true;
}
if (mAnalysisScopes != null) {
for (EnumSet<Scope> analysisScope : mAnalysisScopes) {
if (scope.containsAll(analysisScope)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,307 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import static com.android.tools.lint.detector.api.TextFormat.RAW;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.Configuration;
import com.google.common.annotations.Beta;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* An issue is a potential bug in an Android application. An issue is discovered
* by a {@link Detector}, and has an associated {@link Severity}.
* <p>
* Issues and detectors are separate classes because a detector can discover
* multiple different issues as it's analyzing code, and we want to be able to
* different severities for different issues, the ability to suppress one but
* not other issues from the same detector, and so on.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public final class Issue implements Comparable<Issue> {
private final String mId;
private final String mBriefDescription;
private final String mExplanation;
private final Category mCategory;
private final int mPriority;
private final Severity mSeverity;
private Object mMoreInfoUrls;
private boolean mEnabledByDefault = true;
private Implementation mImplementation;
// Use factory methods
private Issue(
@NonNull String id,
@NonNull String shortDescription,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
assert !shortDescription.isEmpty();
assert !explanation.isEmpty();
mId = id;
mBriefDescription = shortDescription;
mExplanation = explanation;
mCategory = category;
mPriority = priority;
mSeverity = severity;
mImplementation = implementation;
}
/**
* Creates a new issue. The description strings can use some simple markup;
* see the {@link TextFormat#RAW} documentation
* for details.
*
* @param id the fixed id of the issue
* @param briefDescription short summary (typically 5-6 words or less), typically
* describing the <b>problem</b> rather than the <b>fix</b>
* (e.g. "Missing minSdkVersion")
* @param explanation a full explanation of the issue, with suggestions for
* how to fix it
* @param category the associated category, if any
* @param priority the priority, a number from 1 to 10 with 10 being most
* important/severe
* @param severity the default severity of the issue
* @param implementation the default implementation for this issue
* @return a new {@link Issue}
*/
@NonNull
public static Issue create(
@NonNull String id,
@NonNull String briefDescription,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
return new Issue(id, briefDescription, explanation, category, priority,
severity, implementation);
}
/**
* For compatibility with older custom rules)
*
* @deprecated Use {@link #create(String, String, String, Category, int, Severity, Implementation)} instead
*/
@NonNull
@Deprecated
public static Issue create(
@NonNull String id,
@NonNull String briefDescription,
@SuppressWarnings("UnusedParameters") @NonNull String description,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
return new Issue(id, briefDescription, explanation, category, priority,
severity, implementation);
}
/**
* Returns the unique id of this issue. These should not change over time
* since they are used to persist the names of issues suppressed by the user
* etc. It is typically a single camel-cased word.
*
* @return the associated fixed id, never null and always unique
*/
@NonNull
public String getId() {
return mId;
}
/**
* Briefly (in a couple of words) describes these errors
*
* @return a brief summary of the issue, never null, never empty
*/
@NonNull
public String getBriefDescription(@NonNull TextFormat format) {
return RAW.convertTo(mBriefDescription, format);
}
/**
* Describes the error found by this rule, e.g.
* "Buttons must define contentDescriptions". Preferably the explanation
* should also contain a description of how the problem should be solved.
* Additional info can be provided via {@link #getMoreInfo()}.
*
* @param format the format to write the format as
* @return an explanation of the issue, never null, never empty
*/
@NonNull
public String getExplanation(@NonNull TextFormat format) {
return RAW.convertTo(mExplanation, format);
}
/**
* The primary category of the issue
*
* @return the primary category of the issue, never null
*/
@NonNull
public Category getCategory() {
return mCategory;
}
/**
* Returns a priority, in the range 1-10, with 10 being the most severe and
* 1 the least
*
* @return a priority from 1 to 10
*/
public int getPriority() {
return mPriority;
}
/**
* Returns the default severity of the issues found by this detector (some
* tools may allow the user to specify custom severities for detectors).
* <p>
* Note that even though the normal way for an issue to be disabled is for
* the {@link Configuration} to return {@link Severity#IGNORE}, there is a
* {@link #isEnabledByDefault()} method which can be used to turn off issues
* by default. This is done rather than just having the severity as the only
* attribute on the issue such that an issue can be configured with an
* appropriate severity (such as {@link Severity#ERROR}) even when issues
* are disabled by default for example because they are experimental or not
* yet stable.
*
* @return the severity of the issues found by this detector
*/
@NonNull
public Severity getDefaultSeverity() {
return mSeverity;
}
/**
* Returns a link (a URL string) to more information, or null
*
* @return a link to more information, or null
*/
@NonNull
public List<String> getMoreInfo() {
if (mMoreInfoUrls == null) {
return Collections.emptyList();
} else if (mMoreInfoUrls instanceof String) {
return Collections.singletonList((String) mMoreInfoUrls);
} else {
assert mMoreInfoUrls instanceof List;
//noinspection unchecked
return (List<String>) mMoreInfoUrls;
}
}
/**
* Adds a more info URL string
*
* @param moreInfoUrl url string
* @return this, for constructor chaining
*/
@NonNull
public Issue addMoreInfo(@NonNull String moreInfoUrl) {
// Nearly all issues supply at most a single URL, so don't bother with
// lists wrappers for most of these issues
if (mMoreInfoUrls == null) {
mMoreInfoUrls = moreInfoUrl;
} else if (mMoreInfoUrls instanceof String) {
String existing = (String) mMoreInfoUrls;
List<String> list = new ArrayList<String>(2);
list.add(existing);
list.add(moreInfoUrl);
mMoreInfoUrls = list;
} else {
assert mMoreInfoUrls instanceof List;
//noinspection unchecked
((List<String>) mMoreInfoUrls).add(moreInfoUrl);
}
return this;
}
/**
* Returns whether this issue should be enabled by default, unless the user
* has explicitly disabled it.
*
* @return true if this issue should be enabled by default
*/
public boolean isEnabledByDefault() {
return mEnabledByDefault;
}
/**
* Returns the implementation for the given issue
*
* @return the implementation for this issue
*/
@NonNull
public Implementation getImplementation() {
return mImplementation;
}
/**
* Sets the implementation for the given issue. This is typically done by
* IDEs that can offer a replacement for a given issue which performs better
* or in some other way works better within the IDE.
*
* @param implementation the new implementation to use
*/
public void setImplementation(@NonNull Implementation implementation) {
mImplementation = implementation;
}
/**
* Sorts the detectors alphabetically by id. This is intended to make it
* convenient to store settings for detectors in a fixed order. It is not
* intended as the order to be shown to the user; for that, a tool embedding
* lint might consider the priorities, categories, severities etc of the
* various detectors.
*
* @param other the {@link Issue} to compare this issue to
*/
@Override
public int compareTo(@NonNull Issue other) {
return getId().compareTo(other.getId());
}
/**
* Sets whether this issue is enabled by default.
*
* @param enabledByDefault whether the issue should be enabled by default
* @return this, for constructor chaining
*/
@NonNull
public Issue setEnabledByDefault(boolean enabledByDefault) {
mEnabledByDefault = enabledByDefault;
return this;
}
@Override
public String toString() {
return mId;
}
}
@@ -0,0 +1,394 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import static com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.LintDriver;
import java.io.File;
import java.util.Iterator;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Expression;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Position;
/**
* A {@link Context} used when checking Java files.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
public class JavaContext extends Context {
static final String SUPPRESS_COMMENT_PREFIX = "//noinspection "; //$NON-NLS-1$
/** The parse tree */
private Node mCompilationUnit;
/** The parser which produced the parse tree */
private final JavaParser mParser;
/**
* Constructs a {@link JavaContext} for running lint on the given file, with
* the given scope, in the given project reporting errors to the given
* client.
*
* @param driver the driver running through the checks
* @param project the project to run lint on which contains the given file
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file to be analyzed
* @param parser the parser to use
*/
public JavaContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@NonNull JavaParser parser) {
super(driver, project, main, file);
mParser = parser;
}
/**
* Returns a location for the given node
*
* @param node the AST node to get a location for
* @return a location for the given node
*/
@NonNull
public Location getLocation(@NonNull Node node) {
return mParser.getLocation(this, node);
}
@NonNull
public JavaParser getParser() {
return mParser;
}
@Nullable
public Node getCompilationUnit() {
return mCompilationUnit;
}
/**
* Sets the compilation result. Not intended for client usage; the lint infrastructure
* will set this when a context has been processed
*
* @param compilationUnit the parse tree
*/
public void setCompilationUnit(@Nullable Node compilationUnit) {
mCompilationUnit = compilationUnit;
}
@Override
public void report(@NonNull Issue issue, @Nullable Location location,
@NonNull String message) {
if (mDriver.isSuppressed(this, issue, mCompilationUnit)) {
return;
}
super.report(issue, location, message);
}
/**
* Reports an issue applicable to a given AST node. The AST node is used as the
* scope to check for suppress lint annotations.
*
* @param issue the issue to report
* @param scope the AST node scope the error applies to. The lint infrastructure
* will check whether there are suppress annotations on this node (or its enclosing
* nodes) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
@Nullable
public static Node findSurroundingMethod(Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {
return scope;
}
scope = scope.getParent();
}
return null;
}
@Nullable
public static ClassDeclaration findSurroundingClass(@Nullable Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == ClassDeclaration.class) {
return (ClassDeclaration) scope;
}
scope = scope.getParent();
}
return null;
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
public boolean isSuppressedWithComment(@NonNull Node scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
Position position = scope.getPosition();
if (position == null) {
return false;
}
int start = position.getStart();
return isSuppressedWithComment(start, issue);
}
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
@Nullable
public ResolvedNode resolve(@NonNull Node node) {
return mParser.resolve(this, node);
}
@Nullable
public ResolvedClass findClass(@NonNull String fullyQualifiedName) {
return mParser.findClass(this, fullyQualifiedName);
}
@Nullable
public TypeDescriptor getType(@NonNull Node node) {
return mParser.getType(this, node);
}
/**
* Returns true if the given method invocation node corresponds to a call on a
* {@code android.content.Context}
*
* @param node the method call node
* @return true iff the method call is on a class extending context
*/
public boolean isContextMethod(@NonNull MethodInvocation node) {
// Method name used in many other contexts where it doesn't have the
// same semantics; only use this one if we can resolve types
// and we're certain this is the Context method
ResolvedNode resolved = resolve(node);
if (resolved instanceof JavaParser.ResolvedMethod) {
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.isSubclassOf(CLASS_CONTEXT, false)) {
return true;
}
}
return false;
}
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
*/
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz) {
return getParentOfType(element, clz, true);
}
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
*/
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz,
boolean strict) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null) {
if (clz.isInstance(element)) {
//noinspection unchecked
return (T) element;
}
element = element.getParent();
}
return null;
}
/**
* Returns the first ancestor node of the given type, stopping at the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param terminators optional node types to terminate the search at
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
*/
@Nullable
public static <T extends Node> T getParentOfType(@Nullable Node element,
@NonNull Class<T> clz,
boolean strict,
@NonNull Class<? extends Node>... terminators) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null && !clz.isInstance(element)) {
for (Class<?> terminator : terminators) {
if (terminator.isInstance(element)) {
return null;
}
}
element = element.getParent();
}
//noinspection unchecked
return (T) element;
}
/**
* Returns the first sibling of the given node that is of the given class
*
* @param sibling the sibling to search from
* @param clz the type to look for
* @param <T> the type
* @return the first sibling of the given type, or null
*/
@Nullable
public static <T extends Node> T getNextSiblingOfType(@Nullable Node sibling,
@NonNull Class<T> clz) {
if (sibling == null) {
return null;
}
Node parent = sibling.getParent();
if (parent == null) {
return null;
}
Iterator<Node> iterator = parent.getChildren().iterator();
while (iterator.hasNext()) {
if (iterator.next() == sibling) {
break;
}
}
while (iterator.hasNext()) {
Node child = iterator.next();
if (clz.isInstance(child)) {
//noinspection unchecked
return (T) child;
}
}
return null;
}
/**
* Returns the given argument of the given call
*
* @param call the call containing arguments
* @param index the index of the target argument
* @return the argument at the given index
* @throws IllegalArgumentException if index is outside the valid range
*/
@NonNull
public static Node getArgumentNode(@NonNull MethodInvocation call, int index) {
int i = 0;
for (Expression parameter : call.astArguments()) {
if (i == index) {
return parameter;
}
i++;
}
throw new IllegalArgumentException(Integer.toString(index));
}
}
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
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.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import org.w3c.dom.Element;
/**
* Abstract class specifically intended for layout detectors which provides some
* common utility methods shared by layout detectors.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class LayoutDetector extends ResourceXmlDetector {
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
private static boolean isFillParent(@NonNull Element element, @NonNull String dimension) {
String width = element.getAttributeNS(ANDROID_URI, dimension);
return width.equals(VALUE_MATCH_PARENT) || width.equals(VALUE_FILL_PARENT);
}
protected static boolean isWidthFillParent(@NonNull Element element) {
return isFillParent(element, ATTR_LAYOUT_WIDTH);
}
protected static boolean isHeightFillParent(@NonNull Element element) {
return isFillParent(element, ATTR_LAYOUT_HEIGHT);
}
protected boolean hasPadding(@NonNull Element root) {
return root.hasAttributeNS(ANDROID_URI, ATTR_PADDING)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,777 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* Location information for a warning
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class Location {
private static final String SUPER_KEYWORD = "super"; //$NON-NLS-1$
private final File mFile;
private final Position mStart;
private final Position mEnd;
private String mMessage;
private Location mSecondary;
private Object mClientData;
/**
* (Private constructor, use one of the factory methods
* {@link Location#create(File)},
* {@link Location#create(File, Position, Position)}, or
* {@link Location#create(File, String, int, int)}.
* <p>
* Constructs a new location range for the given file, from start to end. If
* the length of the range is not known, end may be null.
*
* @param file the associated file (but see the documentation for
* {@link #getFile()} for more information on what the file
* represents)
* @param start the starting position, or null
* @param end the ending position, or null
*/
protected Location(@NonNull File file, @Nullable Position start, @Nullable Position end) {
super();
mFile = file;
mStart = start;
mEnd = end;
}
/**
* Returns the file containing the warning. Note that the file *itself* may
* not yet contain the error. When editing a file in the IDE for example,
* the tool could generate warnings in the background even before the
* document is saved. However, the file is used as a identifying token for
* the document being edited, and the IDE integration can map this back to
* error locations in the editor source code.
*
* @return the file handle for the location
*/
@NonNull
public File getFile() {
return mFile;
}
/**
* The start position of the range
*
* @return the start position of the range, or null
*/
@Nullable
public Position getStart() {
return mStart;
}
/**
* The end position of the range
*
* @return the start position of the range, may be null for an empty range
*/
@Nullable
public Position getEnd() {
return mEnd;
}
/**
* Returns a secondary location associated with this location (if
* applicable), or null.
*
* @return a secondary location or null
*/
@Nullable
public Location getSecondary() {
return mSecondary;
}
/**
* Sets a secondary location for this location.
*
* @param secondary a secondary location associated with this location
*/
public void setSecondary(@Nullable Location secondary) {
mSecondary = secondary;
}
/**
* Sets a custom message for this location. This is typically used for
* secondary locations, to describe the significance of this alternate
* location. For example, for a duplicate id warning, the primary location
* might say "This is a duplicate id", pointing to the second occurrence of
* id declaration, and then the secondary location could point to the
* original declaration with the custom message "Originally defined here".
*
* @param message the message to apply to this location
*/
public void setMessage(@NonNull String message) {
mMessage = message;
}
/**
* Returns the custom message for this location, if any. This is typically
* used for secondary locations, to describe the significance of this
* alternate location. For example, for a duplicate id warning, the primary
* location might say "This is a duplicate id", pointing to the second
* occurrence of id declaration, and then the secondary location could point
* to the original declaration with the custom message
* "Originally defined here".
*
* @return the custom message for this location, or null
*/
@Nullable
public String getMessage() {
return mMessage;
}
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
public Object getClientData() {
return mClientData;
}
@Override
public String toString() {
return "Location [file=" + mFile + ", start=" + mStart + ", end=" + mEnd + ", message="
+ mMessage + ']';
}
/**
* Creates a new location for the given file
*
* @param file the file to create a location for
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file) {
return new Location(file, null /*start*/, null /*end*/);
}
/**
* Creates a new location for the given file and SourcePosition.
*
* @param file the file containing the positions
* @param position the source position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull SourcePosition position) {
if (position.equals(SourcePosition.UNKNOWN)) {
return new Location(file, null /*start*/, null /*end*/);
}
return new Location(file,
new DefaultPosition(
position.getStartLine(),
position.getStartColumn(),
position.getStartOffset()),
new DefaultPosition(
position.getEndLine(),
position.getEndColumn(),
position.getEndOffset()));
}
/**
* Creates a new location for the given file and starting and ending
* positions.
*
* @param file the file containing the positions
* @param start the starting position
* @param end the ending position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull Position start,
@Nullable Position end) {
return new Location(file, start, end);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given offset range.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param startOffset the starting offset
* @param endOffset the ending offset
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@Nullable String contents,
int startOffset,
int endOffset) {
if (startOffset < 0 || endOffset < startOffset) {
throw new IllegalArgumentException("Invalid offsets");
}
if (contents == null) {
return new Location(file,
new DefaultPosition(-1, -1, startOffset),
new DefaultPosition(-1, -1, endOffset));
}
int size = contents.length();
endOffset = Math.min(endOffset, size);
startOffset = Math.min(startOffset, endOffset);
Position start = null;
int line = 0;
int lineOffset = 0;
char prev = 0;
for (int offset = 0; offset <= size; offset++) {
if (offset == startOffset) {
start = new DefaultPosition(line, offset - lineOffset, offset);
}
if (offset == endOffset) {
Position end = new DefaultPosition(line, offset - lineOffset, offset);
return new Location(file, start, end);
}
char c = contents.charAt(offset);
if (c == '\n') {
lineOffset = offset + 1;
if (prev != '\r') {
line++;
}
} else if (c == '\r') {
line++;
lineOffset = offset + 1;
}
prev = c;
}
return create(file);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line) {
return create(file, contents, line, null, null, null);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @param patternStart an optional pattern to search for from the line
* match; if found, adjust the column and offsets to begin at the
* pattern start
* @param patternEnd an optional pattern to search for behind the start
* pattern; if found, adjust the end offset to match the end of
* the pattern
* @param hints optional additional information regarding the pattern search
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line,
@Nullable String patternStart, @Nullable String patternEnd,
@Nullable SearchHints hints) {
int currentLine = 0;
int offset = 0;
while (currentLine < line) {
offset = contents.indexOf('\n', offset);
if (offset == -1) {
return create(file);
}
currentLine++;
offset++;
}
if (line == currentLine) {
if (patternStart != null) {
SearchDirection direction = SearchDirection.NEAREST;
if (hints != null) {
direction = hints.mDirection;
}
int index;
if (direction == SearchDirection.BACKWARD) {
index = findPreviousMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.EOL_BACKWARD) {
int lineEnd = contents.indexOf('\n', offset);
if (lineEnd == -1) {
lineEnd = contents.length();
}
index = findPreviousMatch(contents, lineEnd, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.FORWARD) {
index = findNextMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else {
assert direction == SearchDirection.NEAREST;
int before = findPreviousMatch(contents, offset, patternStart, hints);
int after = findNextMatch(contents, offset, patternStart, hints);
if (before == -1) {
index = after;
line = adjustLine(contents, line, offset, index);
} else if (after == -1) {
index = before;
line = adjustLine(contents, line, offset, index);
} else if (offset - before < after - offset) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
index = after;
line = adjustLine(contents, line, offset, index);
}
}
if (index != -1) {
int lineStart = contents.lastIndexOf('\n', index);
if (lineStart == -1) {
lineStart = 0;
} else {
lineStart++; // was pointing to the previous line's CR, not line start
}
int column = index - lineStart;
if (patternEnd != null) {
int end = contents.indexOf(patternEnd, offset + patternStart.length());
if (end != -1) {
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, -1, end + patternEnd.length()));
}
} else if (hints != null && (hints.isJavaSymbol() || hints.isWholeWord())) {
if (hints.isConstructor() && contents.startsWith(SUPER_KEYWORD, index)) {
patternStart = SUPER_KEYWORD;
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column + patternStart.length(),
index + patternStart.length()));
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column, index + patternStart.length()));
}
}
Position position = new DefaultPosition(line, -1, offset);
return new Location(file, position, position);
}
return create(file);
}
private static int findPreviousMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
while (true) {
int index = contents.lastIndexOf(pattern, offset);
if (index == -1) {
return -1;
} else {
if (isMatch(contents, index, pattern, hints)) {
return index;
} else {
offset = index - pattern.length();
}
}
}
}
private static int findNextMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
int constructorIndex = -1;
if (hints != null && hints.isConstructor()) {
// Special condition: See if the call is referenced as "super" instead.
assert hints.isWholeWord();
int index = contents.indexOf(SUPER_KEYWORD, offset);
if (index != -1 && isMatch(contents, index, SUPER_KEYWORD, hints)) {
constructorIndex = index;
}
}
while (true) {
int index = contents.indexOf(pattern, offset);
if (index == -1) {
return constructorIndex;
} else {
if (isMatch(contents, index, pattern, hints)) {
if (constructorIndex != -1) {
return Math.min(constructorIndex, index);
}
return index;
} else {
offset = index + pattern.length();
}
}
}
}
private static boolean isMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
if (!contents.startsWith(pattern, offset)) {
return false;
}
if (hints != null) {
char prevChar = offset > 0 ? contents.charAt(offset - 1) : 0;
int lastIndex = offset + pattern.length() - 1;
char nextChar = lastIndex < contents.length() - 1 ? contents.charAt(lastIndex + 1) : 0;
if (hints.isWholeWord() && (Character.isLetter(prevChar)
|| Character.isLetter(nextChar))) {
return false;
}
if (hints.isJavaSymbol()) {
if (Character.isJavaIdentifierPart(prevChar)
|| Character.isJavaIdentifierPart(nextChar)) {
return false;
}
if (prevChar == '"') {
return false;
}
// TODO: Additional validation to see if we're in a comment, string, etc.
// This will require lexing from the beginning of the buffer.
}
if (hints.isConstructor() && SUPER_KEYWORD.equals(pattern)) {
// Only looking for super(), not super.x, so assert that the next
// non-space character is (
int index = lastIndex + 1;
while (index < contents.length() - 1) {
char c = contents.charAt(index);
if (c == '(') {
break;
} else if (!Character.isWhitespace(c)) {
return false;
}
index++;
}
}
}
return true;
}
private static int adjustLine(String doc, int line, int offset, int newOffset) {
if (newOffset == -1) {
return line;
}
if (newOffset < offset) {
return line - countLines(doc, newOffset, offset);
} else {
return line + countLines(doc, offset, newOffset);
}
}
private static int countLines(String doc, int start, int end) {
int lines = 0;
for (int offset = start; offset < end; offset++) {
char c = doc.charAt(offset);
if (c == '\n') {
lines++;
}
}
return lines;
}
/**
* Reverses the secondary location list initiated by the given location
*
* @param location the first location in the list
* @return the first location in the reversed list
*/
public static Location reverse(@NonNull Location location) {
Location next = location.getSecondary();
location.setSecondary(null);
while (next != null) {
Location nextNext = next.getSecondary();
next.setSecondary(location);
location = next;
next = nextNext;
}
return location;
}
/**
* A {@link Handle} is a reference to a location. The point of a location
* handle is to be able to create them cheaply, and then resolve them into
* actual locations later (if needed). This makes it possible to for example
* delay looking up line numbers, for locations that are offset based.
*/
public interface Handle {
/**
* Compute a full location for the given handle
*
* @return create a location for this handle
*/
@NonNull
Location resolve();
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
void setClientData(@Nullable Object clientData);
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
Object getClientData();
}
/** A default {@link Handle} implementation for simple file offsets */
public static class DefaultLocationHandle implements Handle {
private final File mFile;
private final String mContents;
private final int mStartOffset;
private final int mEndOffset;
private Object mClientData;
/**
* Constructs a new {@link DefaultLocationHandle}
*
* @param context the context pointing to the file and its contents
* @param startOffset the start offset within the file
* @param endOffset the end offset within the file
*/
public DefaultLocationHandle(@NonNull Context context, int startOffset, int endOffset) {
mFile = context.file;
mContents = context.getContents();
mStartOffset = startOffset;
mEndOffset = endOffset;
}
@Override
@NonNull
public Location resolve() {
return create(mFile, mContents, mStartOffset, mEndOffset);
}
@Override
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
@Override
@Nullable
public Object getClientData() {
return mClientData;
}
}
public static class ResourceItemHandle implements Handle {
private final ResourceItem mItem;
public ResourceItemHandle(@NonNull ResourceItem item) {
mItem = item;
}
@NonNull
@Override
public Location resolve() {
// TODO: Look up the exact item location more
// closely
ResourceFile source = mItem.getSource();
assert source != null : mItem;
return create(source.getFile());
}
@Override
public void setClientData(@Nullable Object clientData) {
}
@Nullable
@Override
public Object getClientData() {
return null;
}
}
/**
* Whether to look forwards, or backwards, or in both directions, when
* searching for a pattern in the source code to determine the right
* position range for a given symbol.
* <p>
* When dealing with bytecode for example, there are only line number entries
* within method bodies, so when searching for the method declaration, we should only
* search backwards from the first line entry in the method.
*/
public enum SearchDirection {
/** Only search forwards */
FORWARD,
/** Only search backwards */
BACKWARD,
/** Search backwards from the current end of line (normally it's the beginning of
* the current line) */
EOL_BACKWARD,
/**
* Search both forwards and backwards from the given line, and prefer
* the match that is closest
*/
NEAREST,
}
/**
* Extra information pertaining to finding a symbol in a source buffer,
* used by {@link Location#create(File, String, int, String, String, SearchHints)}
*/
public static class SearchHints {
/**
* the direction to search for the nearest match in (provided
* {@code patternStart} is non null)
*/
@NonNull
private final SearchDirection mDirection;
/** Whether the matched pattern should be a whole word */
private boolean mWholeWord;
/**
* Whether the matched pattern should be a Java symbol (so for example,
* a match inside a comment or string literal should not be used)
*/
private boolean mJavaSymbol;
/**
* Whether the matched pattern corresponds to a constructor; if so, look for
* some other possible source aliases too, such as "super".
*/
private boolean mConstructor;
private SearchHints(@NonNull SearchDirection direction) {
super();
mDirection = direction;
}
/**
* Constructs a new {@link SearchHints} object
*
* @param direction the direction to search in for the pattern
* @return a new @link SearchHints} object
*/
@NonNull
public static SearchHints create(@NonNull SearchDirection direction) {
return new SearchHints(direction);
}
/**
* Indicates that pattern matches should apply to whole words only
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchWholeWord() {
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for whole words only */
public boolean isWholeWord() {
return mWholeWord;
}
/**
* Indicates that pattern matches should apply to Java symbols only
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchJavaSymbol() {
mJavaSymbol = true;
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for Java symbols only */
public boolean isJavaSymbol() {
return mJavaSymbol;
}
/**
* Indicates that pattern matches should apply to constructors. If so, look for
* some other possible source aliases too, such as "super".
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchConstructor() {
mConstructor = true;
mWholeWord = true;
mJavaSymbol = true;
return this;
}
/** @return true if the pattern match should be for a constructor */
public boolean isConstructor() {
return mConstructor;
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.google.common.annotations.Beta;
/**
* Information about a position in a file/document.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class Position {
/**
* Returns the line number (0-based where the first line is line 0)
*
* @return the 0-based line number
*/
public abstract int getLine();
/**
* The character offset
*
* @return the 0-based character offset
*/
public abstract int getOffset();
/**
* Returns the column number (where the first character on the line is 0),
* or -1 if unknown
*
* @return the 0-based column number
*/
public abstract int getColumn();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
/*
* 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.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.LintDriver;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* A {@link com.android.tools.lint.detector.api.Context} used when checking resource files
* (both bitmaps and XML files; for XML files a subclass of this context is used:
* {@link com.android.tools.lint.detector.api.XmlContext}.)
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class ResourceContext extends Context {
private final ResourceFolderType mFolderType;
/**
* Construct a new {@link com.android.tools.lint.detector.api.ResourceContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param folderType the {@link com.android.resources.ResourceFolderType} of this file, if any
*/
public ResourceContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable ResourceFolderType folderType) {
super(driver, project, main, file);
mFolderType = folderType;
}
/**
* Returns the resource folder type of this XML file, if any.
*
* @return the resource folder type or null
*/
@Nullable
public ResourceFolderType getResourceFolderType() {
return mFolderType;
}
/**
* Returns the folder version. For example, for the file values-v14/foo.xml,
* it returns 14.
*
* @return the folder version, or -1 if no specific version was specified
*/
public int getFolderVersion() {
return mDriver.getResourceFolderVersion(file);
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* Specialized detector intended for XML resources. Detectors that apply to XML
* resources should extend this detector instead since it provides special
* iteration hooks that are more efficient.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class ResourceXmlDetector extends Detector implements Detector.XmlScanner {
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return LintUtils.isXmlFile(file);
}
/**
* Returns whether this detector applies to the given folder type. This
* allows the detectors to be pruned from iteration, so for example when we
* are analyzing a string value file we don't need to look up detectors
* related to layout.
*
* @param folderType the folder type to be visited
* @return true if this detector can apply to resources in folders of the
* given type
*/
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
@Override
public void run(@NonNull Context context) {
// The infrastructure should never call this method on an xml detector since
// it will run the various visitors instead
assert false;
}
}
@@ -0,0 +1,264 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_PROPERTIES;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
import static com.android.SdkConstants.OLD_PROGUARD_FILE;
import static com.android.SdkConstants.RES_FOLDER;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* The scope of a detector is the set of files a detector must consider when
* performing its analysis. This can be used to determine when issues are
* potentially obsolete, whether a detector should re-run on a file save, etc.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public enum Scope {
/**
* The analysis only considers a single XML resource file at a time.
* <p>
* Issues which are only affected by a single resource file can be checked
* for incrementally when a file is edited.
*/
RESOURCE_FILE,
/**
* The analysis only considers a single binary (typically a bitmap) resource file at a time.
* <p>
* Issues which are only affected by a single resource file can be checked
* for incrementally when a file is edited.
*/
BINARY_RESOURCE_FILE,
/**
* The analysis considers the resource folders
*/
RESOURCE_FOLDER,
/**
* The analysis considers <b>all</b> the resource file. This scope must not
* be used in conjunction with {@link #RESOURCE_FILE}; an issue scope is
* either considering just a single resource file or all the resources, not
* both.
*/
ALL_RESOURCE_FILES,
/**
* The analysis only considers a single Java source file at a time.
* <p>
* Issues which are only affected by a single Java source file can be
* checked for incrementally when a Java source file is edited.
*/
JAVA_FILE,
/**
* The analysis considers <b>all</b> the Java source files together.
* <p>
* This flag is mutually exclusive with {@link #JAVA_FILE}.
*/
ALL_JAVA_FILES,
/**
* The analysis only considers a single Java class file at a time.
* <p>
* Issues which are only affected by a single Java class file can be checked
* for incrementally when a Java source file is edited and then recompiled.
*/
CLASS_FILE,
/**
* The analysis considers <b>all</b> the Java class files together.
* <p>
* This flag is mutually exclusive with {@link #CLASS_FILE}.
*/
ALL_CLASS_FILES,
/** The analysis considers the manifest file */
MANIFEST,
/** The analysis considers the Proguard configuration file */
PROGUARD_FILE,
/**
* The analysis considers classes in the libraries for this project. These
* will be analyzed before the classes themselves.
*/
JAVA_LIBRARIES,
/** The analysis considers a Gradle build file */
GRADLE_FILE,
/** The analysis considers Java property files */
PROPERTY_FILE,
/** The analysis considers test sources as well */
TEST_SOURCES,
/**
* Scope for other files. Issues that specify a custom scope will be called unconditionally.
* This will call {@link Detector#run(Context)}} on the detectors unconditionally.
*/
OTHER;
/**
* Returns true if the given scope set corresponds to scanning a single file
* rather than a whole project
*
* @param scopes the scope set to check
* @return true if the scope set references a single file
*/
public static boolean checkSingleFile(@NonNull EnumSet<Scope> scopes) {
int size = scopes.size();
if (size == 2) {
// When single checking a Java source file, we check both its Java source
// and the associated class files
return scopes.contains(JAVA_FILE) && scopes.contains(CLASS_FILE);
} else {
return size == 1 &&
(scopes.contains(JAVA_FILE)
|| scopes.contains(CLASS_FILE)
|| scopes.contains(RESOURCE_FILE)
|| scopes.contains(PROGUARD_FILE)
|| scopes.contains(PROPERTY_FILE)
|| scopes.contains(GRADLE_FILE)
|| scopes.contains(MANIFEST));
}
}
/**
* Returns the intersection of two scope sets
*
* @param scope1 the first set to intersect
* @param scope2 the second set to intersect
* @return the intersection of the two sets
*/
@NonNull
public static EnumSet<Scope> intersect(
@NonNull EnumSet<Scope> scope1,
@NonNull EnumSet<Scope> scope2) {
EnumSet<Scope> scope = EnumSet.copyOf(scope1);
scope.retainAll(scope2);
return scope;
}
/**
* Infers a suitable scope to use from the given projects to be analyzed
* @param projects the projects to find a suitable scope for
* @return the scope to use
*/
@NonNull
public static EnumSet<Scope> infer(@NonNull Collection<Project> projects) {
// Infer the scope
EnumSet<Scope> scope = EnumSet.noneOf(Scope.class);
for (Project project : projects) {
List<File> subset = project.getSubset();
if (subset != null) {
for (File file : subset) {
String name = file.getName();
if (name.equals(ANDROID_MANIFEST_XML)) {
scope.add(MANIFEST);
} else if (name.endsWith(DOT_XML)) {
scope.add(RESOURCE_FILE);
} else if (name.endsWith(DOT_JAVA)) {
scope.add(JAVA_FILE);
} else if (name.endsWith(DOT_CLASS)) {
scope.add(CLASS_FILE);
} else if (name.endsWith(DOT_GRADLE)) {
scope.add(GRADLE_FILE);
} else if (name.equals(OLD_PROGUARD_FILE)
|| name.equals(FN_PROJECT_PROGUARD_FILE)) {
scope.add(PROGUARD_FILE);
} else if (name.endsWith(DOT_PROPERTIES)) {
scope.add(PROPERTY_FILE);
} else if (name.endsWith(DOT_PNG)) {
scope.add(BINARY_RESOURCE_FILE);
} else if (name.equals(RES_FOLDER)
|| file.getParent().equals(RES_FOLDER)) {
scope.add(ALL_RESOURCE_FILES);
scope.add(RESOURCE_FILE);
scope.add(BINARY_RESOURCE_FILE);
scope.add(RESOURCE_FOLDER);
}
}
} else {
// Specified a full project: just use the full project scope
scope = Scope.ALL;
break;
}
}
return scope;
}
/** All scopes: running lint on a project will check these scopes */
public static final EnumSet<Scope> ALL = EnumSet.allOf(Scope.class);
/** Scope-set used for detectors which are affected by a single resource file */
public static final EnumSet<Scope> RESOURCE_FILE_SCOPE = EnumSet.of(RESOURCE_FILE);
/** Scope-set used for detectors which are affected by a single resource folder */
public static final EnumSet<Scope> RESOURCE_FOLDER_SCOPE = EnumSet.of(RESOURCE_FOLDER);
/** Scope-set used for detectors which scan all resources */
public static final EnumSet<Scope> ALL_RESOURCES_SCOPE = EnumSet.of(ALL_RESOURCE_FILES);
/** Scope-set used for detectors which are affected by a single Java source file */
public static final EnumSet<Scope> JAVA_FILE_SCOPE = EnumSet.of(JAVA_FILE);
/** Scope-set used for detectors which are affected by a single Java class file */
public static final EnumSet<Scope> CLASS_FILE_SCOPE = EnumSet.of(CLASS_FILE);
/** Scope-set used for detectors which are affected by a single Gradle build file */
public static final EnumSet<Scope> GRADLE_SCOPE = EnumSet.of(GRADLE_FILE);
/** Scope-set used for detectors which are affected by the manifest only */
public static final EnumSet<Scope> MANIFEST_SCOPE = EnumSet.of(MANIFEST);
/** Scope-set used for detectors which correspond to some other context */
public static final EnumSet<Scope> OTHER_SCOPE = EnumSet.of(OTHER);
/** Scope-set used for detectors which are affected by a single ProGuard class file */
public static final EnumSet<Scope> PROGUARD_SCOPE = EnumSet.of(PROGUARD_FILE);
/** Scope-set used for detectors which correspond to property files */
public static final EnumSet<Scope> PROPERTY_SCOPE = EnumSet.of(PROPERTY_FILE);
/** Resource XML files and manifest files */
public static final EnumSet<Scope> MANIFEST_AND_RESOURCE_SCOPE =
EnumSet.of(Scope.MANIFEST, Scope.RESOURCE_FILE);
/** Scope-set used for detectors which are affected by single XML and Java source files */
public static final EnumSet<Scope> JAVA_AND_RESOURCE_FILES =
EnumSet.of(RESOURCE_FILE, JAVA_FILE);
/** Scope-set used for analyzing individual class files and all resource files */
public static final EnumSet<Scope> CLASS_AND_ALL_RESOURCE_FILES =
EnumSet.of(ALL_RESOURCE_FILES, CLASS_FILE);
/** Scope-set used for analyzing all class files, including those in libraries */
public static final EnumSet<Scope> ALL_CLASSES_AND_LIBRARIES =
EnumSet.of(Scope.ALL_CLASS_FILES, Scope.JAVA_LIBRARIES);
/** Scope-set used for detectors which are affected by Java libraries */
public static final EnumSet<Scope> JAVA_LIBRARY_SCOPE = EnumSet.of(JAVA_LIBRARIES);
/** Scope-set used for detectors which are affected by a single binary resource file */
public static final EnumSet<Scope> BINARY_RESOURCE_FILE_SCOPE =
EnumSet.of(BINARY_RESOURCE_FILE);
}
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* Severity of an issue found by lint
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public enum Severity {
/**
* Fatal: Use sparingly because a warning marked as fatal will be
* considered critical and will abort Export APK etc in ADT
*/
@NonNull
FATAL("Fatal"),
/**
* Errors: The issue is known to be a real error that must be addressed.
*/
@NonNull
ERROR("Error"),
/**
* Warning: Probably a problem.
*/
@NonNull
WARNING("Warning"),
/**
* Information only: Might not be a problem, but the check has found
* something interesting to say about the code.
*/
@NonNull
INFORMATIONAL("Information"),
/**
* Ignore: The user doesn't want to see this issue
*/
@NonNull
IGNORE("Ignore");
@NonNull
private final String mDisplay;
Severity(@NonNull String display) {
mDisplay = display;
}
/**
* Returns a description of this severity suitable for display to the user
*
* @return a description of the severity
*/
@NonNull
public String getDescription() {
return mDisplay;
}
/** Returns the name of this severity */
@NonNull
public String getName() {
return name();
}
/**
* Looks up the severity corresponding to a given named severity. The severity
* string should be one returned by {@link #toString()}
*
* @param name the name to look up
* @return the corresponding severity, or null if it is not a valid severity name
*/
@Nullable
public static Severity fromName(@NonNull String name) {
for (Severity severity : values()) {
if (severity.name().equalsIgnoreCase(name)) {
return severity;
}
}
return null;
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
/**
* Enum which describes the different computation speeds of various detectors
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public enum Speed {
/** The detector can run very quickly */
FAST("Fast"),
/** The detector runs reasonably fast */
NORMAL("Normal"),
/** The detector might take a long time to run */
SLOW("Slow"),
/** The detector might take a huge amount of time to run */
REALLY_SLOW("Really Slow");
private final String mDisplayName;
Speed(@NonNull String displayName) {
mDisplayName = displayName;
}
/**
* Returns the user-visible description of the speed of the given
* detector
*
* @return the description of the speed to display to the user
*/
@NonNull
public String getDisplayName() {
return mDisplayName;
}
}
@@ -0,0 +1,300 @@
/*
* 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.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
/**
* Lint error message, issue explanations and location descriptions
* are described in a {@link #RAW} format which looks similar to text
* but which can contain bold, symbols and links. These issues can
* also be converted to plain text and to HTML markup, using the
* {@link #convertTo(String, TextFormat)} method.
*
* @see Issue#getDescription(TextFormat)
* @see Issue#getExplanation(TextFormat)
* @see Issue#getBriefDescription(TextFormat)
*/
public enum TextFormat {
/**
* Raw output format which is similar to text but allows some markup:
* <ul>
* <li>HTTP urls (http://...)
* <li>Sentences immediately surrounded by * will be shown as bold.
* <li>Sentences immediately surrounded by ` will be shown using monospace
* fonts
* </ul>
* Furthermore, newlines are converted to br's when converting newlines.
* Note: It does not insert {@code <html>} tags around the fragment for HTML output.
* <p>
* TODO: Consider switching to the restructured text format -
* http://docutils.sourceforge.net/docs/user/rst/quickstart.html
*/
RAW,
/**
* Plain text output
*/
TEXT,
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags)
*/
HTML;
/**
* Converts the given text to HTML
*
* @param text the text to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toHtml(@NonNull String text) {
return convertTo(text, HTML);
}
/**
* Converts the given text to plain text
*
* @param text the tetx to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toText(@NonNull String text) {
return convertTo(text, TEXT);
}
/**
* Converts the given message to the given format. Note that some
* conversions are lossy; e.g. once converting away from the raw format
* (which contains all the markup) you can't convert back to it.
* Note that you can convert to the format it's already in; that just
* returns the same string.
*
* @param message the message to convert
* @param to the format to convert to
* @return a converted message
*/
public String convertTo(@NonNull String message, @NonNull TextFormat to) {
if (this == to) {
return message;
}
switch (this) {
case RAW: {
switch (to) {
case RAW:
return message;
case TEXT:
case HTML:
return to.fromRaw(message);
}
}
case TEXT: {
switch (to) {
case TEXT:
case RAW:
return message;
case HTML:
return XmlUtils.toXmlTextValue(message);
}
}
case HTML: {
switch (to) {
case HTML:
return message;
case RAW:
case TEXT: {
return to.fromHtml(message);
}
}
}
}
return message;
}
/** Converts to this output format from the given HTML-format text */
@NonNull
private String fromHtml(@NonNull String html) {
assert this == RAW || this == TEXT : this;
// Drop all tags; replace all entities, insert newlines
// (this won't do wrapping)
StringBuilder sb = new StringBuilder(html.length());
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '<') {
// Scan forward to the end
if (html.startsWith("<br>", i) ||
html.startsWith("<br />", i) ||
html.startsWith("<BR>", i) ||
html.startsWith("<BR />", i)) {
sb.append('\n');
} else if (html.startsWith("<!--")) {
i = Math.max(i, html.indexOf("-->", i));
}
i = html.indexOf('>', i);
} else if (c == '&') {
int end = html.indexOf(';', i);
if (end > i) {
String entity = html.substring(i, end + 1);
sb.append(XmlUtils.fromXmlAttributeValue(entity));
i = end;
} else {
sb.append(c);
}
} else if (c == '\n') {
sb.append(' ');
} else {
sb.append(c);
}
}
// Collapse repeated spaces
String s = sb.toString();
sb.setLength(0);
boolean wasSpace = false;
for (int i = 0, n = s.length(); i < n; i++) {
char c = s.charAt(i);
if (c == '\t') { // we keep newlines; came from <br>'s
c = ' ';
}
boolean isSpace = c == ' ';
if (!isSpace || !wasSpace) {
wasSpace = isSpace;
sb.append(c);
}
}
s = sb.toString();
// Line-wrap
s = SdkUtils.wrap(s, 60, null);
return s;
}
private static final String HTTP_PREFIX = "http://"; //$NON-NLS-1$
/** Converts to this output format from the given raw-format text */
@NonNull
private String fromRaw(@NonNull String text) {
assert this == HTML || this == TEXT : this;
StringBuilder sb = new StringBuilder(3 * text.length() / 2);
boolean html = this == HTML;
char prev = 0;
int flushIndex = 0;
int n = text.length();
for (int i = 0; i < n; i++) {
char c = text.charAt(i);
if ((c == '*' || c == '`' && i < n - 1)) {
// Scout ahead for range end
if (!Character.isLetterOrDigit(prev)
&& !Character.isWhitespace(text.charAt(i + 1))) {
// Found * or ` immediately before a letter, and not in the middle of a word
// Find end
int end = text.indexOf(c, i + 1);
if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
}
if (html) {
String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$
sb.append('<').append(tag).append('>');
appendEscapedText(sb, text, html, i + 1, end);
sb.append('<').append('/').append(tag).append('>');
} else {
appendEscapedText(sb, text, html, i + 1, end);
}
flushIndex = end + 1;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
} else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't'
&& text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) {
// Find url end
int end = i + HTTP_PREFIX.length();
while (end < n) {
char d = text.charAt(end);
if (Character.isWhitespace(d)) {
break;
}
end++;
}
char last = text.charAt(end - 1);
if (last == '.' || last == ')' || last == '!') {
end--;
}
if (end > i + HTTP_PREFIX.length()) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
}
String url = text.substring(i, end);
sb.append("<a href=\""); //$NON-NLS-1$
sb.append(url);
sb.append('"').append('>');
sb.append(url);
sb.append("</a>"); //$NON-NLS-1$
flushIndex = end;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
prev = c;
}
if (flushIndex < n) {
appendEscapedText(sb, text, html, flushIndex, n);
}
return sb.toString();
}
private static void appendEscapedText(@NonNull StringBuilder sb, @NonNull String text,
boolean html, int start, int end) {
if (html) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (c == '<') {
sb.append("&lt;"); //$NON-NLS-1$
} else if (c == '&') {
sb.append("&amp;"); //$NON-NLS-1$
} else if (c == '\n') {
sb.append("<br/>\n");
} else {
if (c > 255) {
sb.append("&#"); //$NON-NLS-1$
sb.append(Integer.toString(c));
sb.append(';');
} else if (c == '\u00a0') {
sb.append("&nbsp;"); //$NON-NLS-1$
} else {
sb.append(c);
}
}
}
} else {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
sb.append(c);
}
}
}
}
@@ -0,0 +1,205 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.XmlParser;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.io.File;
/**
* A {@link Context} used when checking XML files.
* <p/>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class XmlContext extends ResourceContext {
static final String SUPPRESS_COMMENT_PREFIX = "<!--suppress "; //$NON-NLS-1$
/** The XML parser */
private final XmlParser mParser;
/** The XML document */
public Document document;
/**
* Construct a new {@link XmlContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param folderType the {@link ResourceFolderType} of this file, if any
*/
public XmlContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable ResourceFolderType folderType,
@NonNull XmlParser parser) {
super(driver, project, main, file, folderType);
mParser = parser;
}
/**
* Returns the location for the given node, which may be an element or an attribute.
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getLocation(@NonNull Node node) {
return mParser.getLocation(this, node);
}
/**
* Returns the location for name-portion of the given element or attribute.
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getNameLocation(@NonNull Node node) {
return mParser.getNameLocation(this, node);
}
/**
* Returns the location for value-portion of the given attribute
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getValueLocation(@NonNull Attr node) {
return mParser.getValueLocation(this, node);
}
/**
* Creates a new location within an XML text node
*
* @param textNode the text node
* @param begin the start offset within the text node (inclusive)
* @param end the end offset within the text node (exclusive)
* @return a new location
*/
@NonNull
public Location getLocation(@NonNull Node textNode, int begin, int end) {
assert textNode.getNodeType() == Node.TEXT_NODE;
return mParser.getLocation(this, textNode, begin, end);
}
@NonNull
public XmlParser getParser() {
return mParser;
}
/**
* Reports an issue applicable to a given DOM node. The DOM node is used as the
* scope to check for suppress lint annotations.
*
* @param issue the issue to report
* @param scope the DOM node scope the error applies to. The lint infrastructure
* will check whether there are suppress directives on this node (or its enclosing
* nodes) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, org.w3c.dom.Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, org.w3c.dom.Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
// Warn if clients use the non-scoped form? No, there are cases where an
// XML detector's error isn't applicable to one particular location (or it's
// not feasible to compute it cheaply)
//mDriver.getClient().log(null, "Warning: Issue " + issue
// + " was reported without a scope node: Can't be suppressed.");
// For now just check the document root itself
if (document != null && mDriver.isSuppressed(this, issue, document)) {
return;
}
super.report(issue, location, message);
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
public boolean isSuppressedWithComment(@NonNull Node node, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
int start = mParser.getNodeStartOffset(this, node);
if (start != -1) {
return isSuppressedWithComment(start, issue);
}
return false;
}
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
}
@@ -0,0 +1,132 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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");
}
}
}
}
@@ -0,0 +1,106 @@
/*
* 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.lint.checks;
import static com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import static com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import static com.android.tools.lint.client.api.JavaParser.TYPE_OBJECT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.MethodInvocation;
/**
* Ensures that addJavascriptInterface is not called for API levels below 17.
*/
public class AddJavascriptInterfaceDetector extends Detector implements Detector.JavaScanner {
public static final Issue ISSUE = Issue.create(
"AddJavascriptInterface", //$NON-NLS-1$
"addJavascriptInterface Called",
"For applications built for API levels below 17, `WebView#addJavascriptInterface` "
+ "presents a security hazard as JavaScript on the target web page has the "
+ "ability to use reflection to access the injected object's public fields and "
+ "thus manipulate the host application in unintended ways.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
AddJavascriptInterfaceDetector.class,
Scope.JAVA_FILE_SCOPE)).
addMoreInfo(
"https://labs.mwrinfosecurity.com/blog/2013/09/24/webview-addjavascriptinterface-remote-code-execution/");
private static final String WEB_VIEW = "android.webkit.WebView"; //$NON-NLS-1$
private static final String ADD_JAVASCRIPT_INTERFACE = "addJavascriptInterface"; //$NON-NLS-1$
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
// Ignore the issue if we never build for any API less than 17.
if (context.getMainProject().getMinSdk() >= 17) {
return;
}
// Ignore if the method doesn't fit our description.
ResolvedNode resolved = context.resolve(node);
if (!(resolved instanceof ResolvedMethod)) {
return;
}
ResolvedMethod method = (ResolvedMethod) resolved;
if (!method.getContainingClass().isSubclassOf(WEB_VIEW, false)) {
return;
}
if (method.getArgumentCount() != 2
|| !method.getArgumentType(0).matchesName(TYPE_OBJECT)
|| !method.getArgumentType(1).matchesName(TYPE_STRING)) {
return;
}
String message = "`WebView.addJavascriptInterface` should not be called with minSdkVersion < 17 for security reasons: " +
"JavaScript can use reflection to manipulate application";
context.report(ISSUE, node, context.getLocation(node.astName()), message);
}
}
@@ -0,0 +1,136 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.Expression;
import lombok.ast.MethodInvocation;
/**
* Makes sure that alarms are handled correctly
*/
public class AlarmDetector extends Detector implements Detector.JavaScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
AlarmDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Alarm set too soon/frequently */
public static final Issue ISSUE = Issue.create(
"ShortAlarm", //$NON-NLS-1$
"Short or Frequent Alarm",
"Frequent alarms are bad for battery life. As of API 22, the `AlarmManager` " +
"will override near-future and high-frequency alarm requests, delaying the alarm " +
"at least 5 seconds into the future and ensuring that the repeat interval is at " +
"least 60 seconds.\n" +
"\n" +
"If you really need to do work sooner than 5 seconds, post a delayed message " +
"or runnable to a Handler.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new {@link AlarmDetector} check */
public AlarmDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("setRepeating");
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
ResolvedNode resolved = context.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
if (method.getContainingClass().matches("android.app.AlarmManager")
&& method.getArgumentCount() == 4) {
ensureAtLeast(context, node, 1, 5000L);
ensureAtLeast(context, node, 2, 60000L);
}
}
}
private static void ensureAtLeast(@NonNull JavaContext context,
@NonNull MethodInvocation node, int parameter, long min) {
Iterator<Expression> iterator = node.astArguments().iterator();
Expression argument = null;
for (int i = 0; i <= parameter; i++) {
if (!iterator.hasNext()) {
return;
}
argument = iterator.next();
}
if (argument == null) {
return;
}
long value = getLongValue(context, argument);
if (value < min) {
String message = String.format("Value will be forced up to %d as of Android 5.1; "
+ "don't rely on this to be exact", min);
context.report(ISSUE, argument, context.getLocation(argument), message);
}
}
private static long getLongValue(@NonNull JavaContext context, @NonNull Expression argument) {
Object value = ConstantEvaluator.evaluate(context, argument);
if (value instanceof Number) {
return ((Number)value).longValue();
}
return Long.MAX_VALUE;
}
}
@@ -0,0 +1,239 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.ATTR_SHOW_AS_ACTION;
import static com.android.SdkConstants.VALUE_ALWAYS;
import static com.android.SdkConstants.VALUE_IF_ROOM;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Node;
import lombok.ast.Select;
/**
* Check which looks for usage of showAsAction="always" in menus (or
* MenuItem.SHOW_AS_ACTION_ALWAYS in code), which is usually a style guide violation.
* (Use ifRoom instead).
*/
public class AlwaysShowActionDetector extends ResourceXmlDetector implements JavaScanner {
/** The main issue discovered by this detector */
@SuppressWarnings("unchecked")
public static final Issue ISSUE = Issue.create(
"AlwaysShowAction", //$NON-NLS-1$
"Usage of `showAsAction=always`",
"Using `showAsAction=\"always\"` in menu XML, or `MenuItem.SHOW_AS_ACTION_ALWAYS` in " +
"Java code is usually a deviation from the user interface style guide." +
"Use `ifRoom` or the corresponding `MenuItem.SHOW_AS_ACTION_IF_ROOM` instead.\n" +
"\n" +
"If `always` is used sparingly there are usually no problems and behavior is " +
"roughly equivalent to `ifRoom` but with preference over other `ifRoom` " +
"items. Using it more than twice in the same menu is a bad idea.\n" +
"\n" +
"This check looks for menu XML files that contain more than two `always` " +
"actions, or some `always` actions and no `ifRoom` actions. In Java code, " +
"it looks for projects that contain references to `MenuItem.SHOW_AS_ACTION_ALWAYS` " +
"and no references to `MenuItem.SHOW_AS_ACTION_IF_ROOM`.",
Category.USABILITY,
3,
Severity.WARNING,
new Implementation(
AlwaysShowActionDetector.class,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/design/patterns/actionbar.html"); //$NON-NLS-1$
/** List of showAsAction attributes appearing in the current menu XML file */
private List<Attr> mFileAttributes;
/** If at least one location has been marked ignore in this file, ignore all */
private boolean mIgnoreFile;
/** List of locations of MenuItem.SHOW_AS_ACTION_ALWAYS references in Java code */
private List<Location> mAlwaysFields;
/** True if references to MenuItem.SHOW_AS_ACTION_IF_ROOM were found */
private boolean mHasIfRoomRefs;
/** Constructs a new {@link AlwaysShowActionDetector} */
public AlwaysShowActionDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.MENU;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_SHOW_AS_ACTION);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
mFileAttributes = null;
}
@Override
public void afterCheckFile(@NonNull Context context) {
if (mIgnoreFile) {
mFileAttributes = null;
return;
}
if (mFileAttributes != null) {
assert context instanceof XmlContext; // mFileAttributes is only set in XML files
List<Attr> always = new ArrayList<Attr>();
List<Attr> ifRoom = new ArrayList<Attr>();
for (Attr attribute : mFileAttributes) {
String value = attribute.getValue();
if (value.equals(VALUE_ALWAYS)) {
always.add(attribute);
} else if (value.equals(VALUE_IF_ROOM)) {
ifRoom.add(attribute);
} else if (value.indexOf('|') != -1) {
String[] flags = value.split("\\|"); //$NON-NLS-1$
for (String flag : flags) {
if (flag.equals(VALUE_ALWAYS)) {
always.add(attribute);
break;
} else if (flag.equals(VALUE_IF_ROOM)) {
ifRoom.add(attribute);
break;
}
}
}
}
if (!always.isEmpty() && mFileAttributes.size() > 1) {
// Complain if you're using more than one "always", or if you're
// using "always" and aren't using "ifRoom" at all (and provided you
// have more than a single item)
if (always.size() > 2 || ifRoom.isEmpty()) {
XmlContext xmlContext = (XmlContext) context;
Location location = null;
for (int i = always.size() - 1; i >= 0; i--) {
Location next = location;
location = xmlContext.getLocation(always.get(i));
if (next != null) {
location.setSecondary(next);
}
}
context.report(ISSUE, location,
"Prefer \"`ifRoom`\" instead of \"`always`\"");
}
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mAlwaysFields != null && !mHasIfRoomRefs) {
for (Location location : mAlwaysFields) {
context.report(ISSUE, location,
"Prefer \"`SHOW_AS_ACTION_IF_ROOM`\" instead of \"`SHOW_AS_ACTION_ALWAYS`\"");
}
}
}
// ---- Implements XmlScanner ----
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (context.getDriver().isSuppressed(context, ISSUE, attribute)) {
mIgnoreFile = true;
return;
}
if (mFileAttributes == null) {
mFileAttributes = new ArrayList<Attr>();
}
mFileAttributes.add(attribute);
}
// ---- Implements JavaScanner ----
@Override
public
List<Class<? extends Node>> getApplicableNodeTypes() {
return Collections.<Class<? extends Node>>singletonList(Select.class);
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return new FieldAccessChecker(context);
}
private class FieldAccessChecker extends ForwardingAstVisitor {
private final JavaContext mContext;
public FieldAccessChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitSelect(Select node) {
String description = node.astIdentifier().astValue();
boolean isIfRoom = description.equals("SHOW_AS_ACTION_IF_ROOM"); //$NON-NLS-1$
boolean isAlways = description.equals("SHOW_AS_ACTION_ALWAYS"); //$NON-NLS-1$
if ((isIfRoom || isAlways)
&& node.astOperand().toString().equals("MenuItem")) { //$NON-NLS-1$
if (isAlways) {
if (mContext.getDriver().isSuppressed(mContext, ISSUE, node)) {
return super.visitSelect(node);
}
if (mAlwaysFields == null) {
mAlwaysFields = new ArrayList<Location>();
}
mAlwaysFields.add(mContext.getLocation(node));
} else {
mHasIfRoomRefs = true;
}
}
return super.visitSelect(node);
}
}
}
@@ -0,0 +1,206 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.FQCN_SUPPRESS_LINT;
import static com.android.SdkConstants.SUPPRESS_LINT;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import lombok.ast.Annotation;
import lombok.ast.AnnotationElement;
import lombok.ast.AnnotationValue;
import lombok.ast.ArrayInitializer;
import lombok.ast.AstVisitor;
import lombok.ast.Block;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodDeclaration;
import lombok.ast.Modifiers;
import lombok.ast.Node;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
import lombok.ast.StringLiteral;
import lombok.ast.TypeBody;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
/**
* Checks annotations to make sure they are valid
*/
public class AnnotationDetector extends Detector implements Detector.JavaScanner {
/** Placing SuppressLint on a local variable doesn't work for class-file based checks */
public static final Issue ISSUE = Issue.create(
"LocalSuppress", //$NON-NLS-1$
"@SuppressLint on invalid element",
"The `@SuppressAnnotation` is used to suppress Lint warnings in Java files. However, " +
"while many lint checks analyzes the Java source code, where they can find " +
"annotations on (for example) local variables, some checks are analyzing the " +
"`.class` files. And in class files, annotations only appear on classes, fields " +
"and methods. Annotations placed on local variables disappear. If you attempt " +
"to suppress a lint error for a class-file based lint check, the suppress " +
"annotation not work. You must move the annotation out to the surrounding method.",
Category.CORRECTNESS,
3,
Severity.ERROR,
new Implementation(
AnnotationDetector.class,
Scope.JAVA_FILE_SCOPE));
/** Constructs a new {@link AnnotationDetector} check */
public AnnotationDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
return Collections.<Class<? extends Node>>singletonList(Annotation.class);
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return new AnnotationChecker(context);
}
private static class AnnotationChecker extends ForwardingAstVisitor {
private final JavaContext mContext;
public AnnotationChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitAnnotation(Annotation node) {
String type = node.astAnnotationTypeReference().getTypeName();
if (SUPPRESS_LINT.equals(type) || FQCN_SUPPRESS_LINT.equals(type)) {
Node parent = node.getParent();
if (parent instanceof Modifiers) {
parent = parent.getParent();
if (parent instanceof VariableDefinition) {
for (AnnotationElement element : node.astElements()) {
AnnotationValue valueNode = element.astValue();
if (valueNode == null) {
continue;
}
if (valueNode instanceof StringLiteral) {
StringLiteral literal = (StringLiteral) valueNode;
String id = literal.astValue();
if (!checkId(node, id)) {
return super.visitAnnotation(node);
}
} else if (valueNode instanceof ArrayInitializer) {
ArrayInitializer array = (ArrayInitializer) valueNode;
StrictListAccessor<Expression, ArrayInitializer> expressions =
array.astExpressions();
if (expressions == null) {
continue;
}
Iterator<Expression> arrayIterator = expressions.iterator();
while (arrayIterator.hasNext()) {
Expression arrayElement = arrayIterator.next();
if (arrayElement instanceof StringLiteral) {
String id = ((StringLiteral) arrayElement).astValue();
if (!checkId(node, id)) {
return super.visitAnnotation(node);
}
}
}
}
}
}
}
}
return super.visitAnnotation(node);
}
private boolean checkId(Annotation node, String id) {
IssueRegistry registry = mContext.getDriver().getRegistry();
Issue issue = registry.getIssue(id);
// Special-case the ApiDetector issue, since it does both source file analysis
// only on field references, and class file analysis on the rest, so we allow
// annotations outside of methods only on fields
if (issue != null && !issue.getImplementation().getScope().contains(Scope.JAVA_FILE)
|| issue == ApiDetector.UNSUPPORTED) {
// Ensure that this isn't a field
Node parent = node.getParent();
while (parent != null) {
if (parent instanceof MethodDeclaration
|| parent instanceof ConstructorDeclaration
|| parent instanceof Block) {
break;
} else if (parent instanceof TypeBody) { // It's a field
return true;
} else if (issue == ApiDetector.UNSUPPORTED
&& parent instanceof VariableDefinition) {
VariableDefinition definition = (VariableDefinition) parent;
for (VariableDefinitionEntry entry : definition.astVariables()) {
Expression initializer = entry.astInitializer();
if (initializer instanceof Select) {
return true;
}
}
}
parent = parent.getParent();
if (parent == null) {
return true;
}
}
// This issue doesn't have AST access: annotations are not
// available for local variables or parameters
mContext.report(ISSUE, node, mContext.getLocation(node), String.format(
"The `@SuppressLint` annotation cannot be used on a local " +
"variable with the lint check '%1$s': move out to the " +
"surrounding method", id));
return false;
}
return true;
}
}
}
@@ -0,0 +1,87 @@
/*
* 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.lint.checks;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
* Main entry point for API description.
*
* To create the {@link Api}, use {@link #parseApi(File)}
*
*/
public class Api {
/**
* Parses simplified API file.
* @param apiFile the file to read
* @return a new ApiInfo
*/
public static Api parseApi(File apiFile) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(apiFile);
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
SAXParser parser = parserFactory.newSAXParser();
ApiParser apiParser = new ApiParser();
parser.parse(fileInputStream, apiParser);
return new Api(apiParser.getClasses());
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
// ignore
}
}
}
return null;
}
private final Map<String, ApiClass> mClasses;
private Api(Map<String, ApiClass> classes) {
mClasses = new HashMap<String, ApiClass>(classes);
}
ApiClass getClass(String fqcn) {
return mClasses.get(fqcn);
}
Map<String, ApiClass> getClasses() {
return Collections.unmodifiableMap(mClasses);
}
}
@@ -0,0 +1,424 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import com.android.annotations.Nullable;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Represents a class and its methods/fields.
*
* {@link #getSince()} gives the API level it was introduced.
*
* {@link #getMethod} returns when the method was introduced.
* {@link #getField} returns when the field was introduced.
*/
public class ApiClass {
private final String mName;
private final int mSince;
private final List<Pair<String, Integer>> mSuperClasses = Lists.newArrayList();
private final List<Pair<String, Integer>> mInterfaces = Lists.newArrayList();
private final Map<String, Integer> mFields = new HashMap<String, Integer>();
private final Map<String, Integer> mMethods = new HashMap<String, Integer>();
ApiClass(String name, int since) {
mName = name;
mSince = since;
}
/**
* Returns the name of the class.
* @return the name of the class
*/
String getName() {
return mName;
}
/**
* Returns when the class was introduced.
* @return the api level the class was introduced.
*/
int getSince() {
return mSince;
}
/**
* Returns when a field was added, or null if it doesn't exist.
* @param name the name of the field.
* @param info the corresponding info
*/
Integer getField(String name, Api info) {
// The field can come from this class or from a super class or an interface
// The value can never be lower than this introduction of this class.
// When looking at super classes and interfaces, it can never be lower than when the
// super class or interface was added as a super class or interface to this class.
// Look at all the values and take the lowest.
// For instance:
// This class A is introduced in 5 with super class B.
// In 10, the interface C was added.
// Looking for SOME_FIELD we get the following:
// Present in A in API 15
// Present in B in API 11
// Present in C in API 7.
// The answer is 10, which is when C became an interface
int min = Integer.MAX_VALUE;
Integer i = mFields.get(name);
if (i != null) {
min = i;
}
// now look at the super classes
for (Pair<String, Integer> superClassPair : mSuperClasses) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getField(name, info);
if (i != null) {
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
}
// now look at the interfaces
for (Pair<String, Integer> superClassPair : mInterfaces) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getField(name, info);
if (i != null) {
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
}
return min;
}
/**
* Returns when a method was added, or null if it doesn't exist. This goes through the super
* class to find method only present there.
* @param methodSignature the method signature
*/
int getMethod(String methodSignature, Api info) {
// The method can come from this class or from a super class.
// The value can never be lower than this introduction of this class.
// When looking at super classes, it can never be lower than when the super class became
// a super class of this class.
// Look at all the values and take the lowest.
// For instance:
// This class A is introduced in 5 with super class B.
// In 10, the super class changes to C.
// Looking for foo() we get the following:
// Present in A in API 15
// Present in B in API 11
// Present in C in API 7.
// The answer is 10, which is when C became the super class.
int min = Integer.MAX_VALUE;
Integer i = mMethods.get(methodSignature);
if (i != null) {
min = i;
// Constructors aren't inherited
if (methodSignature.startsWith(CONSTRUCTOR_NAME)) {
return i;
}
}
// now look at the super classes
for (Pair<String, Integer> superClassPair : mSuperClasses) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getMethod(methodSignature, info);
if (i != null) {
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
}
// now look at the interfaces classes
for (Pair<String, Integer> interfacePair : mInterfaces) {
ApiClass superClass = info.getClass(interfacePair.getFirst());
if (superClass != null) {
i = superClass.getMethod(methodSignature, info);
if (i != null) {
int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
}
return min;
}
void addField(String name, int since) {
Integer i = mFields.get(name);
if (i == null || i.intValue() > since) {
mFields.put(name, Integer.valueOf(since));
}
}
void addMethod(String name, int since) {
// Strip off the method type at the end to ensure that the code which
// produces inherited methods doesn't get confused and end up multiple entries.
// For example, java/nio/Buffer has the method "array()Ljava/lang/Object;",
// and the subclass java/nio/ByteBuffer has the method "array()[B". We want
// the lookup on mMethods to associate the ByteBuffer array method to be
// considered overriding the Buffer method.
int index = name.indexOf(')');
if (index != -1) {
name = name.substring(0, index + 1);
}
Integer i = mMethods.get(name);
if (i == null || i.intValue() > since) {
mMethods.put(name, Integer.valueOf(since));
}
}
void addSuperClass(String superClass, int since) {
addToArray(mSuperClasses, superClass, since);
}
void addInterface(String interfaceClass, int since) {
addToArray(mInterfaces, interfaceClass, since);
}
static void addToArray(List<Pair<String, Integer>> list, String name, int value) {
// check if we already have that name (at a lower level)
for (Pair<String, Integer> pair : list) {
if (name.equals(pair.getFirst())) {
return;
}
}
list.add(Pair.of(name, Integer.valueOf(value)));
}
@Nullable
public String getPackage() {
int index = mName.lastIndexOf('/');
if (index != -1) {
return mName.substring(0, index);
}
return null;
}
@Override
public String toString() {
return mName;
}
/**
* Returns the set of all methods, including inherited
* ones.
*
* @param info the api to look up super classes from
* @return a set containing all the members fields
*/
Set<String> getAllMethods(Api info) {
Set<String> members = new HashSet<String>(100);
addAllMethods(info, members, true /*includeConstructors*/);
return members;
}
private void addAllMethods(Api info, Set<String> set, boolean includeConstructors) {
if (!includeConstructors) {
for (String method : mMethods.keySet()) {
if (!method.startsWith(CONSTRUCTOR_NAME)) {
set.add(method);
}
}
} else {
for (String method : mMethods.keySet()) {
set.add(method);
}
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllMethods(info, set, false);
}
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllMethods(info, set, false);
}
}
}
/**
* Returns the set of all fields, including inherited
* ones.
*
* @param info the api to look up super classes from
* @return a set containing all the fields
*/
Set<String> getAllFields(Api info) {
Set<String> members = new HashSet<String>(100);
addAllFields(info, members);
return members;
}
private void addAllFields(Api info, Set<String> set) {
for (String field : mFields.keySet()) {
set.add(field);
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllFields(info, set);
}
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllFields(info, set);
}
}
}
/* This code can be used to scan through all the fields and look for fields
that have moved to a higher class:
Field android/view/MotionEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9
Field android/provider/ContactsContract$CommonDataKinds$Organization#PHONETIC_NAME has api=5 but parent android/provider/ContactsContract$ContactNameColumns provides it as 11
Field android/widget/ListView#CHOICE_MODE_MULTIPLE has api=1 but parent android/widget/AbsListView provides it as 11
Field android/widget/ListView#CHOICE_MODE_NONE has api=1 but parent android/widget/AbsListView provides it as 11
Field android/widget/ListView#CHOICE_MODE_SINGLE has api=1 but parent android/widget/AbsListView provides it as 11
Field android/view/KeyEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9
This is used for example in the ApiDetector to filter out warnings which result
when people follow Eclipse's advice to replace
ListView.CHOICE_MODE_MULTIPLE
references with
AbsListView.CHOICE_MODE_MULTIPLE
since the latter has API=11 and the former has API=1; since the constant is unchanged
between the two, and the literal is copied into the class, using the AbsListView
reference works.
public void checkFields(Api info) {
fieldLoop:
for (String field : mFields.keySet()) {
Integer since = getField(field, info);
if (since == null || since == Integer.MAX_VALUE) {
continue;
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
Integer superSince = clz.getField(field, info);
if (superSince == Integer.MAX_VALUE) {
continue;
}
if (superSince != null && superSince > since) {
String declaredIn = clz.findFieldDeclaration(info, field);
System.out.println("Field " + getName() + "#" + field + " has api="
+ since + " but parent " + declaredIn + " provides it as "
+ superSince);
continue fieldLoop;
}
}
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
Integer superSince = clz.getField(field, info);
if (superSince == Integer.MAX_VALUE) {
continue;
}
if (superSince != null && superSince > since) {
String declaredIn = clz.findFieldDeclaration(info, field);
System.out.println("Field " + getName() + "#" + field + " has api="
+ since + " but parent " + declaredIn + " provides it as "
+ superSince);
continue fieldLoop;
}
}
}
}
}
private String findFieldDeclaration(Api info, String name) {
if (mFields.containsKey(name)) {
return getName();
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
String declaredIn = clz.findFieldDeclaration(info, name);
if (declaredIn != null) {
return declaredIn;
}
}
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
String declaredIn = clz.findFieldDeclaration(info, name);
if (declaredIn != null) {
return declaredIn;
}
}
}
return null;
}
*/
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,996 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.ANDROID_PKG;
import static com.android.SdkConstants.DOT_XML;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.sdklib.repository.FullRevision;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.sdklib.repository.local.LocalPkgInfo;
import com.android.sdklib.repository.local.LocalSdk;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.LintUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.common.primitives.UnsignedBytes;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
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.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Database for API checking: Allows quick lookup of a given class, method or field
* to see which API level it was introduced in.
* <p>
* This class is optimized for quick bytecode lookup used in conjunction with the
* ASM library: It has lookup methods that take internal JVM signatures, and for a method
* call for example it processes the owner, name and description parameters separately
* the way they are provided from ASM.
* <p>
* The {@link Api} class provides access to the full Android API along with version
* information, initialized from an XML file. This lookup class adds a binary cache around
* the API to make initialization faster and to require fewer objects. It creates
* a binary cache data structure, which fits in a single byte array, which means that
* to open the database you can just read in the byte array and go. On one particular
* machine, this takes about 30-50 ms versus 600-800ms for the full parse. It also
* helps memory by placing everything in a compact byte array instead of needing separate
* strings (2 bytes per character in a char[] for the 25k method entries, 11k field entries
* and 6k class entries) - and it also avoids the same number of Map.Entry objects.
* When creating the memory data structure it performs a few other steps to help memory:
* <ul>
* <li> It stores the strings as single bytes, since all the JVM signatures are in ASCII
* <li> It strips out the method return types (which takes the binary size down from
* about 4.7M to 4.0M)
* <li> It strips out all APIs that have since=1, since the lookup only needs to find
* classes, methods and fields that have an API level *higher* than 1. This drops
* the memory use down from 4.0M to 1.7M.
* </ul>
*/
public class ApiLookup {
/** Relative path to the api-versions.xml database file within the Lint installation */
private static final String XML_FILE_PATH = "platform-tools/api/api-versions.xml"; //$NON-NLS-1$
private static final String FILE_HEADER = "API database used by Android lint\000";
private static final int BINARY_FORMAT_VERSION = 6;
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 = 36;
private final Api mInfo;
private byte[] mData;
private int[] mIndices;
private int mClassCount;
private String[] mJavaPackages;
private static WeakReference<ApiLookup> sInstance =
new WeakReference<ApiLookup>(null);
/**
* Returns an instance of the API database
*
* @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.
* @return a (possibly shared) instance of the API database, or null
* if its data can't be found
*/
@Nullable
public static ApiLookup get(@NonNull LintClient client) {
synchronized (ApiLookup.class) {
ApiLookup db = sInstance.get();
if (db == null) {
File file = client.findResource(XML_FILE_PATH);
if (file == null) {
// AOSP build environment?
String build = System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$
if (build != null) {
file = new File(build, "development/sdk/api-versions.xml" //$NON-NLS-1$
.replace('/', File.separatorChar));
}
}
if (file == null || !file.exists()) {
return null;
} else {
db = get(client, file);
}
sInstance = new WeakReference<ApiLookup>(db);
}
return db;
}
}
@VisibleForTesting
@Nullable
static String getPlatformVersion(@NonNull LintClient client) {
LocalSdk sdk = client.getSdk();
if (sdk != null) {
LocalPkgInfo pkgInfo = sdk.getPkgInfo(PkgType.PKG_PLATFORM_TOOLS);
if (pkgInfo != null) {
FullRevision version = pkgInfo.getDesc().getFullRevision();
if (version != null) {
return version.toShortString();
}
}
}
return null;
}
@VisibleForTesting
@NonNull
static String getCacheFileName(@NonNull String xmlFileName, @Nullable String platformVersion) {
if (LintUtils.endsWith(xmlFileName, DOT_XML)) {
xmlFileName = xmlFileName.substring(0, xmlFileName.length() - DOT_XML.length());
}
StringBuilder sb = new StringBuilder(100);
sb.append(xmlFileName);
// Incorporate version number in the filename to avoid upgrade filename
// conflicts on Windows (such as issue #26663)
sb.append('-').append(BINARY_FORMAT_VERSION);
if (platformVersion != null) {
sb.append('-').append(platformVersion);
}
sb.append(".bin"); //$NON-NLS-1$
return sb.toString();
}
/**
* Returns an instance of the API 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 API database, or null
* if its data can't be found
*/
private static ApiLookup get(LintClient client, File xmlFile) {
if (!xmlFile.exists()) {
client.log(null, "The API database file %1$s does not exist", xmlFile);
return null;
}
File cacheDir = client.getCacheDir(true/*create*/);
if (cacheDir == null) {
cacheDir = xmlFile.getParentFile();
}
String platformVersion = getPlatformVersion(client);
File binaryData = new File(cacheDir, getCacheFileName(xmlFile.getName(), platformVersion));
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()
|| binaryData.length() == 0) {
if (!createCache(client, xmlFile, binaryData)) {
return null;
}
}
if (!binaryData.exists()) {
client.log(null, "The API database file %1$s does not exist", binaryData);
return null;
}
return new ApiLookup(client, xmlFile, binaryData, null);
}
private static boolean createCache(LintClient client, File xmlFile, File binaryData) {
long begin = 0;
if (WRITE_STATS) {
begin = System.currentTimeMillis();
}
Api info = Api.parseApi(xmlFile);
if (WRITE_STATS) {
long end = System.currentTimeMillis();
System.out.println("Reading XML data structures took " + (end - begin) + " ms)");
}
if (info != null) {
try {
writeDatabase(binaryData, info);
return true;
} catch (IOException ioe) {
client.log(ioe, "Can't write API cache file");
}
}
return false;
}
/** Use one of the {@link #get} factory methods instead */
private ApiLookup(
@NonNull LintClient client,
@NonNull File xmlFile,
@Nullable File binaryFile,
@Nullable Api info) {
mInfo = info;
if (binaryFile != null) {
readData(client, xmlFile, binaryFile);
}
}
/**
* Database format:
* <pre>
* 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).
* 3. The number of classes [1 int]
* 4. The number of members (across all classes) [1 int].
* 5. The number of java/javax packages [1 int]
* 6. The java/javax package name table. Each item consists of a byte count for
* the package string (as 1 byte) followed by the UTF-8 encoded bytes for each package.
* These are in sorted order.
* 7. Class offset table (one integer per class, pointing to the byte offset in the
* file (relative to the beginning of the file) where each class begins.
* The classes are always sorted alphabetically by fully qualified name.
* 8. Member offset table (one integer per member, pointing to the byte offset in the
* file (relative to the beginning of the file) where each member entry begins.
* The members are always sorted alphabetically.
* 9. Class entry table. Each class entry consists of the fully qualified class name,
* in JVM format (using / instead of . in package names and $ for inner classes),
* followed by the byte 0 as a terminator, followed by the API version as a byte.
* 10. Member entry table. Each member entry consists of the class number (as a short),
* followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte
* signature terminator, followed by the API level as a byte.
* <p>
* TODO: Pack the offsets: They increase by a small amount for each entry, so no need
* to spend 4 bytes on each. These will need to be processed when read back in anyway,
* so consider storing the offset -deltas- as single bytes and adding them up cumulatively
* in readData().
* </pre>
*/
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 API 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;
}
mClassCount = buffer.getInt();
int methodCount = buffer.getInt();
int javaPackageCount = buffer.getInt();
// Read in the Java packages
mJavaPackages = new String[javaPackageCount];
for (int i = 0; i < javaPackageCount; i++) {
int count = UnsignedBytes.toInt(buffer.get());
byte[] bytes = new byte[count];
buffer.get(bytes, 0, count);
mJavaPackages[i] = new String(bytes, Charsets.UTF_8);
}
// Read in the class table indices;
int count = mClassCount + methodCount;
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 (Throwable e) {
client.log(null, "Failure reading binary cache file %1$s", binaryFile.getPath());
client.log(null, "Please delete the file and restart the IDE/lint: %1$s",
binaryFile.getPath());
client.log(e, null);
}
if (WRITE_STATS) {
long end = System.currentTimeMillis();
System.out.println("\nRead API 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, Api info) 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).
*/
Map<String, ApiClass> classMap = info.getClasses();
// Write the class table
List<String> classes = new ArrayList<String>(classMap.size());
Map<ApiClass, List<String>> memberMap =
Maps.newHashMapWithExpectedSize(classMap.size());
int memberCount = 0;
Set<String> javaPackageSet = Sets.newHashSetWithExpectedSize(70);
for (Map.Entry<String, ApiClass> entry : classMap.entrySet()) {
String className = entry.getKey();
ApiClass apiClass = entry.getValue();
if (className.startsWith("java/") //$NON-NLS-1$
|| className.startsWith("javax/")) { //$NON-NLS-1$
String pkg = apiClass.getPackage();
javaPackageSet.add(pkg);
}
if (!isRelevantOwner(className)) {
System.out.println("Warning: The isRelevantOwner method does not pass "
+ className);
}
Set<String> allMethods = apiClass.getAllMethods(info);
Set<String> allFields = apiClass.getAllFields(info);
// Strip out all members that have been supported since version 1.
// This makes the database *much* leaner (down from about 4M to about
// 1.7M), and this just fills the table with entries that ultimately
// don't help the API checker since it just needs to know if something
// requires a version *higher* than the minimum. If in the future the
// database needs to answer queries about whether a method is public
// or not, then we'd need to put this data back in.
List<String> members = new ArrayList<String>(allMethods.size() + allFields.size());
for (String member : allMethods) {
Integer since = apiClass.getMethod(member, info);
if (since == null) {
assert false : className + ':' + member;
since = 1;
}
if (since != 1) {
members.add(member);
}
}
// Strip out all members that have been supported since version 1.
// This makes the database *much* leaner (down from about 4M to about
// 1.7M), and this just fills the table with entries that ultimately
// don't help the API checker since it just needs to know if something
// requires a version *higher* than the minimum. If in the future the
// database needs to answer queries about whether a method is public
// or not, then we'd need to put this data back in.
for (String member : allFields) {
Integer since = apiClass.getField(member, info);
if (since == null) {
assert false : className + ':' + member;
since = 1;
}
if (since != 1) {
members.add(member);
}
}
// Only include classes that have one or more members requiring version 2 or higher:
if (!members.isEmpty()) {
classes.add(className);
memberMap.put(apiClass, members);
memberCount += members.size();
}
}
Collections.sort(classes);
List<String> javaPackages = Lists.newArrayList(javaPackageSet);
Collections.sort(javaPackages);
int javaPackageCount = javaPackages.size();
int entryCount = classMap.size() + memberCount;
int capacity = entryCount * BYTES_PER_ENTRY;
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(FILE_HEADER.getBytes(Charsets.US_ASCII));
// 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 classes [1 int]
buffer.putInt(classes.size());
// 4. The number of members (across all classes) [1 int].
buffer.putInt(memberCount);
// 5. The number of Java packages [1 int].
buffer.putInt(javaPackageCount);
// 6. The Java package table. There are javaPackage.size() entries, where each entry
// consists of a string length, as a byte, followed by the bytes in the package.
// There is no terminating 0.
for (String pkg : javaPackages) {
byte[] bytes = pkg.getBytes(Charsets.UTF_8);
assert bytes.length < 255 : pkg;
buffer.put((byte) bytes.length);
buffer.put(bytes);
}
// 7. Class offset table (one integer per class, pointing to the byte offset in the
// file (relative to the beginning of the file) where each class begins.
// The classes are always sorted alphabetically by fully qualified name.
int classOffsetTable = 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 = classes.size(); i < n; i++) {
buffer.putInt(0);
}
// 8. Member offset table (one integer per member, pointing to the byte offset in the
// file (relative to the beginning of the file) where each member entry begins.
// The members are always sorted alphabetically.
int methodOffsetTable = buffer.position();
for (int i = 0, n = memberCount; i < n; i++) {
buffer.putInt(0);
}
int nextEntry = buffer.position();
int nextOffset = classOffsetTable;
// 9. Class entry table. Each class entry consists of the fully qualified class name,
// in JVM format (using / instead of . in package names and $ for inner classes),
// followed by the byte 0 as a terminator, followed by the API version as a byte.
for (String clz : classes) {
buffer.position(nextOffset);
buffer.putInt(nextEntry);
nextOffset = buffer.position();
buffer.position(nextEntry);
buffer.put(clz.getBytes(Charsets.UTF_8));
buffer.put((byte) 0);
ApiClass apiClass = classMap.get(clz);
assert apiClass != null : clz;
int since = apiClass.getSince();
assert since == UnsignedBytes.toInt((byte) since) : since; // make sure it fits
buffer.put((byte) since);
nextEntry = buffer.position();
}
// 10. Member entry table. Each member entry consists of the class number (as a short),
// followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte
// signature terminator, followed by the API level as a byte.
assert nextOffset == methodOffsetTable;
for (int classNumber = 0, n = classes.size(); classNumber < n; classNumber++) {
String clz = classes.get(classNumber);
ApiClass apiClass = classMap.get(clz);
assert apiClass != null : clz;
List<String> members = memberMap.get(apiClass);
Collections.sort(members);
for (String member : members) {
buffer.position(nextOffset);
buffer.putInt(nextEntry);
nextOffset = buffer.position();
buffer.position(nextEntry);
Integer since;
if (member.indexOf('(') != -1) {
since = apiClass.getMethod(member, info);
} else {
since = apiClass.getField(member, info);
}
if (since == null) {
assert false : clz + ':' + member;
since = 1;
}
assert classNumber == (short) classNumber;
buffer.putShort((short) classNumber);
byte[] signature = member.getBytes(Charsets.UTF_8);
for (int i = 0; i < signature.length; i++) {
// Make sure all signatures are really just simple ASCII
byte b = signature[i];
assert b == (b & 0x7f) : member;
buffer.put(b);
// Skip types on methods
if (b == (byte) ')') {
break;
}
}
buffer.put((byte) 0);
int api = since;
assert api == UnsignedBytes.toInt((byte) api);
//assert api >= 1 && api < 0xFF; // max that fits in a byte
buffer.put((byte) api);
nextEntry = buffer.position();
}
}
int size = buffer.position();
assert size <= buffer.limit();
buffer.mark();
if (WRITE_STATS) {
System.out.println("Wrote " + classes.size() + " classes and "
+ memberCount + " member 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);
if (file.exists()) {
file.delete();
}
FileOutputStream output = Files.newOutputStreamSupplier(file).getOutput();
output.write(b);
output.close();
}
// For debugging only
private String dumpEntry(int offset) {
if (DEBUG_SEARCH) {
StringBuilder sb = new StringBuilder(200);
for (int i = offset; i < mData.length; i++) {
if (mData[i] == 0) {
break;
}
char c = (char) UnsignedBytes.toInt(mData[i]);
sb.append(c);
}
return sb.toString();
} else {
return "<disabled>"; //$NON-NLS-1$
}
}
private static int compare(byte[] data, int offset, byte terminator, String s, int max) {
int i = offset;
int j = 0;
for (; j < max; i++, j++) {
byte b = data[i];
char c = s.charAt(j);
// TODO: Check somewhere that the strings are purely in the ASCII range; if not
// they're not a match in the database
byte cb = (byte) c;
int delta = b - cb;
if (delta != 0) {
return delta;
}
}
return data[i] - terminator;
}
/**
* Quick determination whether a given class name is possibly interesting; this
* is a quick package prefix check to determine whether we need to consider
* the class at all. This let's us do less actual searching for the vast majority
* of APIs (in libraries, application code etc) that have nothing to do with the
* APIs in our packages.
* @param name the class name in VM format (e.g. using / instead of .)
* @return true if the owner is <b>possibly</b> relevant
*/
public static boolean isRelevantClass(String name) {
// TODO: Add quick switching here. This is tied to the database file so if
// we end up with unexpected prefixes there, this could break. For that reason,
// for now we consider everything relevant.
return true;
}
/**
* Returns the API version required by the given class reference,
* or -1 if this is not a known API class. Note that it may return -1
* for classes introduced in version 1; internally the database only
* stores version data for version 2 and up.
*
* @param className the internal name of the class, e.g. its
* fully qualified name (as returned by Class.getName(), but with
* '.' replaced by '/'.
* @return the minimum API version the method is supported for, or -1 if
* it's unknown <b>or version 1</b>.
*/
public int getClassVersion(@NonNull String className) {
if (!isRelevantClass(className)) {
return -1;
}
if (mData != null) {
int classNumber = findClass(className);
if (classNumber != -1) {
int offset = mIndices[classNumber];
while (mData[offset] != 0) {
offset++;
}
offset++;
return UnsignedBytes.toInt(mData[offset]);
}
} else {
ApiClass clz = mInfo.getClass(className);
if (clz != null) {
int since = clz.getSince();
if (since == Integer.MAX_VALUE) {
since = -1;
}
return since;
}
}
return -1;
}
/**
* Returns the API version required by the given method call. The method is
* referred to by its {@code owner}, {@code name} and {@code desc} fields.
* If the method is unknown it returns -1. Note that it may return -1 for
* classes introduced in version 1; internally the database only stores
* version data for version 2 and up.
*
* @param owner the internal name of the method's owner class, e.g. its
* fully qualified name (as returned by Class.getName(), but with
* '.' replaced by '/'.
* @param name the method's name
* @param desc the method's descriptor - see {@link org.objectweb.asm.Type}
* @return the minimum API version the method is supported for, or -1 if
* it's unknown <b>or version 1</b>.
*/
public int getCallVersion(
@NonNull String owner,
@NonNull String name,
@NonNull String desc) {
if (!isRelevantClass(owner)) {
return -1;
}
if (mData != null) {
int classNumber = findClass(owner);
if (classNumber != -1) {
return findMember(classNumber, name, desc);
}
} else {
ApiClass clz = mInfo.getClass(owner);
if (clz != null) {
String signature = name + desc;
int since = clz.getMethod(signature, mInfo);
if (since == Integer.MAX_VALUE) {
since = -1;
}
return since;
}
}
return -1;
}
/**
* Returns the API version required to access the given field, or -1 if this
* is not a known API method. Note that it may return -1 for classes
* introduced in version 1; internally the database only stores version data
* for version 2 and up.
*
* @param owner the internal name of the method's owner class, e.g. its
* fully qualified name (as returned by Class.getName(), but with
* '.' replaced by '/'.
* @param name the method's name
* @return the minimum API version the method is supported for, or -1 if
* it's unknown <b>or version 1</b>
*/
public int getFieldVersion(
@NonNull String owner,
@NonNull String name) {
if (!isRelevantClass(owner)) {
return -1;
}
if (mData != null) {
int classNumber = findClass(owner);
if (classNumber != -1) {
return findMember(classNumber, name, null);
}
} else {
ApiClass clz = mInfo.getClass(owner);
if (clz != null) {
int since = clz.getField(name, mInfo);
if (since == Integer.MAX_VALUE) {
since = -1;
}
return since;
}
}
return -1;
}
/**
* Returns true if the given owner (in VM format) is relevant to the database.
* This allows quick filtering out of owners that won't return any data
* for the various {@code #getFieldVersion} etc methods.
*
* @param owner the owner to look up
* @return true if the owner might be relevant to the API database
*/
public static boolean isRelevantOwner(@NonNull String owner) {
if (owner.startsWith("java")) { //$NON-NLS-1$ // includes javax/
return true;
}
if (owner.startsWith(ANDROID_PKG)) {
return !owner.startsWith("/support/", 7);
} else if (owner.startsWith("org/")) { //$NON-NLS-1$
if (owner.startsWith("xml", 4) //$NON-NLS-1$
|| owner.startsWith("w3c/", 4) //$NON-NLS-1$
|| owner.startsWith("json/", 4) //$NON-NLS-1$
|| owner.startsWith("apache/", 4)) { //$NON-NLS-1$
return true;
}
} else if (owner.startsWith("com/")) { //$NON-NLS-1$
if (owner.startsWith("google/", 4) //$NON-NLS-1$
|| owner.startsWith("android/", 4)) { //$NON-NLS-1$
return true;
}
} else if (owner.startsWith("junit") //$NON-NLS-1$
|| owner.startsWith("dalvik")) { //$NON-NLS-1$
return true;
}
return false;
}
/**
* Returns true if the given owner (in VM format) is a valid Java package supported
* in any version of Android.
*
* @param owner the package, in VM format
* @return true if the package is included in one or more versions of Android
*/
public boolean isValidJavaPackage(@NonNull String owner) {
int packageLength = owner.lastIndexOf('/');
if (packageLength == -1) {
return false;
}
// The index array contains class indexes from 0 to classCount and
// member indices from classCount to mIndices.length.
int low = 0;
int high = mJavaPackages.length - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
int offset = middle;
if (DEBUG_SEARCH) {
System.out.println("Comparing string " + owner + " with entry at " + offset
+ ": " + mJavaPackages[offset]);
}
// Compare the api info at the given index.
int compare = comparePackage(mJavaPackages[offset], owner, packageLength);
if (compare == 0) {
return true;
}
if (compare < 0) {
low = middle + 1;
} else if (compare > 0) {
high = middle - 1;
} else {
assert false; // compare == 0 already handled above
return false;
}
}
return false;
}
private static int comparePackage(String s1, String s2, int max) {
for (int i = 0; i < max; i++) {
if (i == s1.length()) {
return -1;
}
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
return c1 - c2;
}
}
if (s1.length() > max) {
return 1;
}
return 0;
}
/** Returns the class number of the given class, or -1 if it is unknown */
private int findClass(@NonNull String owner) {
assert owner.indexOf('.') == -1 : "Should use / instead of . in owner: " + owner;
// The index array contains class indexes from 0 to classCount and
// member indices from classCount to mIndices.length.
int low = 0;
int high = mClassCount - 1;
// Compare the api info at the given index.
int classNameLength = owner.length();
while (low <= high) {
int middle = (low + high) >>> 1;
int offset = mIndices[middle];
if (DEBUG_SEARCH) {
System.out.println("Comparing string " + owner + " with entry at " + offset
+ ": " + dumpEntry(offset));
}
int compare = compare(mData, offset, (byte) 0, owner, classNameLength);
if (compare == 0) {
return middle;
}
if (compare < 0) {
low = middle + 1;
} else if (compare > 0) {
high = middle - 1;
} else {
assert false; // compare == 0 already handled above
return -1;
}
}
return -1;
}
private int findMember(int classNumber, @NonNull String name, @Nullable String desc) {
// The index array contains class indexes from 0 to classCount and
// member indices from classCount to mIndices.length.
int low = mClassCount;
int high = mIndices.length - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
int offset = mIndices[middle];
if (DEBUG_SEARCH) {
System.out.println("Comparing string " + (name + ';' + desc) +
" with entry at " + offset + ": " + dumpEntry(offset));
}
// Check class number: read short. The byte data is always big endian.
int entryClass = (mData[offset++] & 0xFF) << 8 | (mData[offset++] & 0xFF);
int compare = entryClass - classNumber;
if (compare == 0) {
if (desc != null) {
// Method
int nameLength = name.length();
compare = compare(mData, offset, (byte) '(', name, nameLength);
if (compare == 0) {
offset += nameLength;
int argsEnd = desc.indexOf(')');
// Only compare up to the ) -- after that we have a return value in the
// input description, which isn't there in the database
compare = compare(mData, offset, (byte) ')', desc, argsEnd);
if (compare == 0) {
offset += argsEnd + 1;
if (mData[offset++] == 0) {
// Yes, terminated argument list: get the API level
return UnsignedBytes.toInt(mData[offset]);
}
}
}
} else {
// Field
int nameLength = name.length();
compare = compare(mData, offset, (byte) 0, name, nameLength);
if (compare == 0) {
offset += nameLength;
if (mData[offset++] == 0) {
// Yes, terminated argument list: get the API level
return UnsignedBytes.toInt(mData[offset]);
}
}
}
}
if (compare < 0) {
low = middle + 1;
} else if (compare > 0) {
high = middle - 1;
} else {
assert false; // compare == 0 already handled above
return -1;
}
}
return -1;
}
/** Clears out any existing lookup instances */
@VisibleForTesting
static void dispose() {
sInstance.clear();
}
}
@@ -0,0 +1,122 @@
/*
* 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.lint.checks;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.HashMap;
import java.util.Map;
/**
* Parser for the simplified XML API format version 1.
*/
public class ApiParser extends DefaultHandler {
private static final String NODE_API = "api";
private static final String NODE_CLASS = "class";
private static final String NODE_FIELD = "field";
private static final String NODE_METHOD = "method";
private static final String NODE_EXTENDS = "extends";
private static final String NODE_IMPLEMENTS = "implements";
private static final String ATTR_NAME = "name";
private static final String ATTR_SINCE = "since";
private final Map<String, ApiClass> mClasses = new HashMap<String, ApiClass>();
private ApiClass mCurrentClass;
ApiParser() {
}
Map<String, ApiClass> getClasses() {
return mClasses;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
if (localName == null || localName.isEmpty()) {
localName = qName;
}
try {
if (NODE_API.equals(localName)) {
// do nothing.
} else if (NODE_CLASS.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = Integer.parseInt(attributes.getValue(ATTR_SINCE));
mCurrentClass = addClass(name, since);
} else if (NODE_EXTENDS.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
mCurrentClass.addSuperClass(name, since);
} else if (NODE_IMPLEMENTS.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
mCurrentClass.addInterface(name, since);
} else if (NODE_METHOD.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
mCurrentClass.addMethod(name, since);
} else if (NODE_FIELD.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
mCurrentClass.addField(name, since);
}
} finally {
super.startElement(uri, localName, qName, attributes);
}
}
private ApiClass addClass(String name, int apiLevel) {
ApiClass theClass = mClasses.get(name);
if (theClass == null) {
theClass = new ApiClass(name, apiLevel);
mClasses.put(name, theClass);
}
return theClass;
}
private int getSince(Attributes attributes) {
int since = mCurrentClass.getSince();
String sinceAttr = attributes.getValue(ATTR_SINCE);
if (sinceAttr != null) {
since = Integer.parseInt(sinceAttr);
}
return since;
}
}
@@ -0,0 +1,187 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.APPCOMPAT_LIB_ARTIFACT;
import static com.android.SdkConstants.CLASS_ACTIVITY;
import static com.android.tools.lint.detector.api.TextFormat.RAW;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import java.util.Arrays;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.ClassDeclaration;
import lombok.ast.MethodInvocation;
public class AppCompatCallDetector extends Detector implements Detector.JavaScanner {
public static final Issue ISSUE = Issue.create(
"AppCompatMethod",
"Using Wrong AppCompat Method",
"When using the appcompat library, there are some methods you should be calling " +
"instead of the normal ones; for example, `getSupportActionBar()` instead of " +
"`getActionBar()`. This lint check looks for calls to the wrong method.",
Category.CORRECTNESS, 6, Severity.WARNING,
new Implementation(
AppCompatCallDetector.class,
Scope.JAVA_FILE_SCOPE)).
addMoreInfo("http://developer.android.com/tools/support-library/index.html");
private static final String GET_ACTION_BAR = "getActionBar";
private static final String START_ACTION_MODE = "startActionMode";
private static final String SET_PROGRESS_BAR_VIS = "setProgressBarVisibility";
private static final String SET_PROGRESS_BAR_IN_VIS = "setProgressBarIndeterminateVisibility";
private static final String SET_PROGRESS_BAR_INDETERMINATE = "setProgressBarIndeterminate";
private static final String REQUEST_WINDOW_FEATURE = "requestWindowFeature";
/** If you change number of parameters or order, update {@link #getMessagePart(String, int,TextFormat)} */
private static final String ERROR_MESSAGE_FORMAT = "Should use `%1$s` instead of `%2$s` name";
private boolean mDependsOnAppCompat;
public AppCompatCallDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void beforeCheckProject(@NonNull Context context) {
Boolean dependsOnAppCompat = context.getProject().dependsOn(APPCOMPAT_LIB_ARTIFACT);
mDependsOnAppCompat = dependsOnAppCompat != null && dependsOnAppCompat;
}
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
GET_ACTION_BAR,
START_ACTION_MODE,
SET_PROGRESS_BAR_VIS,
SET_PROGRESS_BAR_IN_VIS,
SET_PROGRESS_BAR_INDETERMINATE,
REQUEST_WINDOW_FEATURE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
if (mDependsOnAppCompat && isAppBarActivityCall(context, node)) {
String name = node.astName().astValue();
String replace = null;
if (GET_ACTION_BAR.equals(name)) {
replace = "getSupportActionBar";
} else if (START_ACTION_MODE.equals(name)) {
replace = "startSupportActionMode";
} else if (SET_PROGRESS_BAR_VIS.equals(name)) {
replace = "setSupportProgressBarVisibility";
} else if (SET_PROGRESS_BAR_IN_VIS.equals(name)) {
replace = "setSupportProgressBarIndeterminateVisibility";
} else if (SET_PROGRESS_BAR_INDETERMINATE.equals(name)) {
replace = "setSupportProgressBarIndeterminate";
} else if (REQUEST_WINDOW_FEATURE.equals(name)) {
replace = "supportRequestWindowFeature";
}
if (replace != null) {
String message = String.format(ERROR_MESSAGE_FORMAT, replace, name);
context.report(ISSUE, node, context.getLocation(node), message);
}
}
}
private static boolean isAppBarActivityCall(@NonNull JavaContext context,
@NonNull MethodInvocation node) {
ResolvedNode resolved = context.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.isSubclassOf(CLASS_ACTIVITY, false)) {
// Make sure that the calling context is a subclass of ActionBarActivity;
// we don't want to flag these calls if they are in non-appcompat activities
// such as PreferenceActivity (see b.android.com/58512)
ClassDeclaration surroundingClass = JavaContext.findSurroundingClass(node);
if (surroundingClass != null) {
ResolvedNode clz = context.resolve(surroundingClass);
return clz instanceof ResolvedClass &&
((ResolvedClass)clz).isSubclassOf(
"android.support.v7.app.ActionBarActivity",
false);
}
}
}
return false;
}
/**
* Given an error message created by this lint check, return the corresponding old method name
* that it suggests should be deleted. (Intended to support quickfix implementations
* for this lint check.)
*
* @param errorMessage the error message originally produced by this detector
* @param format the format of the error message
* @return the corresponding old method name, or null if not recognized
*/
@Nullable
public static String getOldCall(@NonNull String errorMessage, @NonNull TextFormat format) {
return getMessagePart(errorMessage, 2, format);
}
/**
* Given an error message created by this lint check, return the corresponding new method name
* that it suggests replace the old method name. (Intended to support quickfix implementations
* for this lint check.)
*
* @param errorMessage the error message originally produced by this detector
* @param format the format of the error message
* @return the corresponding new method name, or null if not recognized
*/
@Nullable
public static String getNewCall(@NonNull String errorMessage, @NonNull TextFormat format) {
return getMessagePart(errorMessage, 1, format);
}
@Nullable
private static String getMessagePart(@NonNull String errorMessage, int group,
@NonNull TextFormat format) {
List<String> parameters = LintUtils.getFormattedParameters(
RAW.convertTo(ERROR_MESSAGE_FORMAT, format),
errorMessage);
if (parameters.size() == 2 && group <= 2) {
return parameters.get(group - 1);
}
return null;
}
}
@@ -0,0 +1,109 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_SHOW_AS_ACTION;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import java.util.Collection;
import java.util.Collections;
/**
* Check that the right namespace is used for app compat menu items
*
* Using app:showAsAction instead of android:showAsAction leads to problems, but
* isn't caught by the API Detector since it's not in the Android namespace.
*/
public class AppCompatResourceDetector extends ResourceXmlDetector implements JavaScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"AppCompatResource", //$NON-NLS-1$
"Menu namespace",
"When using the appcompat library, menu resources should refer to the " +
"`showAsAction` in the `app:` namespace, not the `android:` namespace.\n" +
"\n" +
"Similarly, when *not* using the appcompat library, you should be using " +
"the `android:showAsAction` attribute.",
Category.USABILITY,
5,
Severity.ERROR,
new Implementation(
AppCompatResourceDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/** Constructs a new {@link com.android.tools.lint.checks.AppCompatResourceDetector} */
public AppCompatResourceDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.MENU;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_SHOW_AS_ACTION);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
Project mainProject = context.getMainProject();
if (mainProject.isGradleProject()) {
Boolean appCompat = mainProject.dependsOn("com.android.support:appcompat-v7");
if (ANDROID_URI.equals(attribute.getNamespaceURI())) {
if (context.getFolderVersion() >= 14) {
return;
}
if (appCompat == Boolean.TRUE) {
context.report(ISSUE, attribute,
context.getLocation(attribute),
"Should use `app:showAsAction` with the appcompat library with "
+ "`xmlns:app=\"http://schemas.android.com/apk/res-auto\"`");
}
} else {
if (appCompat == Boolean.FALSE) {
context.report(ISSUE, attribute,
context.getLocation(attribute),
"Should use `android:showAsAction` when not using the appcompat library");
}
}
}
}
}
@@ -0,0 +1,287 @@
/*
* 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.lint.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.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import 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();
}
}
@@ -0,0 +1,324 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.TAG_ARRAY;
import static com.android.SdkConstants.TAG_INTEGER_ARRAY;
import static com.android.SdkConstants.TAG_STRING_ARRAY;
import com.android.annotations.NonNull;
import com.android.ide.common.rendering.api.ArrayResourceValue;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Checks for arrays with inconsistent item counts
*/
public class ArraySizeDetector extends ResourceXmlDetector {
/** Are there differences in how many array elements are declared? */
public static final Issue INCONSISTENT = Issue.create(
"InconsistentArrays", //$NON-NLS-1$
"Inconsistencies in array element counts",
"When an array is translated in a different locale, it should normally have " +
"the same number of elements as the original array. When adding or removing " +
"elements to an array, it is easy to forget to update all the locales, and this " +
"lint warning finds inconsistencies like these.\n" +
"\n" +
"Note however that there may be cases where you really want to declare a " +
"different number of array items in each configuration (for example where " +
"the array represents available options, and those options differ for " +
"different layout orientations and so on), so use your own judgement to " +
"decide if this is really an error.\n" +
"\n" +
"You can suppress this error type if it finds false errors in your project.",
Category.CORRECTNESS,
7,
Severity.WARNING,
new Implementation(
ArraySizeDetector.class,
Scope.RESOURCE_FILE_SCOPE));
private Multimap<File, Pair<String, Integer>> mFileToArrayCount;
/** Locations for each array name. Populated during phase 2, if necessary */
private Map<String, Location> mLocations;
/** Error messages for each array name. Populated during phase 2, if necessary */
private Map<String, String> mDescriptions;
/** Constructs a new {@link ArraySizeDetector} */
public ArraySizeDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_ARRAY,
TAG_STRING_ARRAY,
TAG_INTEGER_ARRAY
);
}
@Override
public void beforeCheckProject(@NonNull Context context) {
if (context.getPhase() == 1) {
mFileToArrayCount = ArrayListMultimap.create(30, 5);
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (context.getPhase() == 1) {
boolean haveAllResources = context.getScope().contains(Scope.ALL_RESOURCE_FILES);
if (!haveAllResources) {
return;
}
// Check that all arrays for the same name have the same number of translations
Set<String> alreadyReported = new HashSet<String>();
Map<String, Integer> countMap = new HashMap<String, Integer>();
Map<String, File> fileMap = new HashMap<String, File>();
// Process the file in sorted file order to ensure stable output
List<File> keys = new ArrayList<File>(mFileToArrayCount.keySet());
Collections.sort(keys);
for (File file : keys) {
Collection<Pair<String, Integer>> pairs = mFileToArrayCount.get(file);
for (Pair<String, Integer> pair : pairs) {
String name = pair.getFirst();
if (alreadyReported.contains(name)) {
continue;
}
Integer count = pair.getSecond();
Integer current = countMap.get(name);
if (current == null) {
countMap.put(name, count);
fileMap.put(name, file);
} else if (!count.equals(current)) {
alreadyReported.add(name);
if (mLocations == null) {
mLocations = new HashMap<String, Location>();
mDescriptions = new HashMap<String, String>();
}
mLocations.put(name, null);
String thisName = file.getParentFile().getName() + File.separator
+ file.getName();
File otherFile = fileMap.get(name);
String otherName = otherFile.getParentFile().getName() + File.separator
+ otherFile.getName();
String message = String.format(
"Array `%1$s` has an inconsistent number of items (%2$d in `%3$s`, %4$d in `%5$s`)",
name, count, thisName, current, otherName);
mDescriptions.put(name, message);
}
}
}
//noinspection VariableNotUsedInsideIf
if (mLocations != null) {
// Request another scan through the resources such that we can
// gather the actual locations
context.getDriver().requestRepeat(this, Scope.ALL_RESOURCES_SCOPE);
}
mFileToArrayCount = null;
} else {
if (mLocations != null) {
List<String> names = new ArrayList<String>(mLocations.keySet());
Collections.sort(names);
for (String name : names) {
Location location = mLocations.get(name);
if (location == null) {
// Suppressed; see visitElement
continue;
}
// We were prepending locations, but we want to prefer the base folders
location = Location.reverse(location);
// Make sure we still have a conflict, in case one or more of the
// elements were marked with tools:ignore
int count = -1;
LintDriver driver = context.getDriver();
boolean foundConflict = false;
Location curr;
for (curr = location; curr != null; curr = curr.getSecondary()) {
Object clientData = curr.getClientData();
if (clientData instanceof Node) {
Node node = (Node) clientData;
if (driver.isSuppressed(null, INCONSISTENT, node)) {
continue;
}
int newCount = LintUtils.getChildCount(node);
if (newCount != count) {
if (count == -1) {
count = newCount; // first number encountered
} else {
foundConflict = true;
break;
}
}
} else {
foundConflict = true;
break;
}
}
// Through one or more tools:ignore, there is no more conflict so
// ignore this element
if (!foundConflict) {
continue;
}
String message = mDescriptions.get(name);
context.report(INCONSISTENT, location, message);
}
}
mLocations = null;
mDescriptions = null;
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
int phase = context.getPhase();
Attr attribute = element.getAttributeNode(ATTR_NAME);
if (attribute == null || attribute.getValue().isEmpty()) {
if (phase != 1) {
return;
}
context.report(INCONSISTENT, element, context.getLocation(element),
String.format("Missing name attribute in `%1$s` declaration",
element.getTagName()));
} else {
String name = attribute.getValue();
if (phase == 1) {
if (context.getProject().getReportIssues()) {
int childCount = LintUtils.getChildCount(element);
if (!context.getScope().contains(Scope.ALL_RESOURCE_FILES) &&
context.getClient().supportsProjectResources()) {
incrementalCheckCount(context, element, name, childCount);
return;
}
mFileToArrayCount.put(context.file, Pair.of(name, childCount));
}
} else {
assert phase == 2;
if (mLocations.containsKey(name)) {
if (context.getDriver().isSuppressed(context, INCONSISTENT, element)) {
return;
}
Location location = context.getLocation(element);
location.setClientData(element);
location.setMessage(String.format("Declaration with array size (%1$d)",
LintUtils.getChildCount(element)));
location.setSecondary(mLocations.get(name));
mLocations.put(name, location);
}
}
}
}
private static void incrementalCheckCount(@NonNull XmlContext context, @NonNull Element element,
@NonNull String name, int childCount) {
LintClient client = context.getClient();
Project project = context.getMainProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return;
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.ARRAY, name);
if (items != null) {
for (ResourceItem item : items) {
ResourceFile source = item.getSource();
if (source != null && LintUtils.isSameResourceFile(context.file,
source.getFile())) {
continue;
}
ResourceValue rv = item.getResourceValue(false);
if (rv instanceof ArrayResourceValue) {
ArrayResourceValue arv = (ArrayResourceValue) rv;
if (childCount != arv.getElementCount()) {
String thisName = context.file.getParentFile().getName() + File.separator
+ context.file.getName();
assert source != null;
File otherFile = source.getFile();
String otherName = otherFile.getParentFile().getName() + File.separator
+ otherFile.getName();
String message = String.format(
"Array `%1$s` has an inconsistent number of items (%2$d in `%3$s`, %4$d in `%5$s`)",
name, childCount, thisName, arv.getElementCount(), otherName);
context.report(INCONSISTENT, element, context.getLocation(element),
message);
}
}
}
}
}
}
@@ -0,0 +1,142 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import java.io.File;
import java.util.Collections;
import java.util.List;
import lombok.ast.Assert;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BooleanLiteral;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Node;
import lombok.ast.NullLiteral;
/**
* Looks for assertion usages.
*/
public class AssertDetector extends Detector implements Detector.JavaScanner {
/** Using assertions */
public static final Issue ISSUE = Issue.create(
"Assert", //$NON-NLS-1$
"Assertions",
"Assertions are not checked at runtime. There are ways to request that they be used " +
"by Dalvik (`adb shell setprop debug.assert 1`), but the property is ignored in " +
"many places and can not be relied upon. Instead, perform conditional checking " +
"inside `if (BuildConfig.DEBUG) { }` blocks. That constant is a static final boolean " +
"which is true in debug builds and false in release builds, and the Java compiler " +
"completely removes all code inside the if-body from the app.\n" +
"\n" +
"For example, you can replace `assert speed > 0` with " +
"`if (BuildConfig.DEBUG && !(speed > 0)) { throw new AssertionError() }`.\n" +
"\n" +
"(Note: This lint check does not flag assertions purely asserting nullness or " +
"non-nullness; these are typically more intended for tools usage than runtime " +
"checks.)",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
AssertDetector.class,
Scope.JAVA_FILE_SCOPE))
.addMoreInfo(
"https://code.google.com/p/android/issues/detail?id=65183"); //$NON-NLS-1$
/** Constructs a new {@link com.android.tools.lint.checks.AssertDetector} check */
public AssertDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
// ---- Implements JavaScanner ----
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
return Collections.<Class<? extends Node>>singletonList(Assert.class);
}
@Override
public AstVisitor createJavaVisitor(@NonNull final JavaContext context) {
return new ForwardingAstVisitor() {
@Override
public boolean visitAssert(Assert node) {
if (!context.getMainProject().isAndroidProject()) {
return true;
}
Expression assertion = node.astAssertion();
// Allow "assert true"; it's basically a no-op
if (assertion instanceof BooleanLiteral) {
Boolean b = ((BooleanLiteral) assertion).astValue();
if (b != null && b) {
return false;
}
} else {
// Allow assertions of the form "assert foo != null" because they are often used
// to make statements to tools about known nullness properties. For example,
// findViewById() may technically return null in some cases, but a developer
// may know that it won't be when it's called correctly, so the assertion helps
// to clear nullness warnings.
if (isNullCheck(assertion)) {
return false;
}
}
String message
= "Assertions are unreliable. Use `BuildConfig.DEBUG` conditional checks instead.";
context.report(ISSUE, node, context.getLocation(node), message);
return false;
}
};
}
/**
* Checks whether the given expression is purely a non-null check, e.g. it will return
* true for expressions like "a != null" and "a != null && b != null" and
* "b == null || c != null".
*/
private static boolean isNullCheck(Expression expression) {
if (expression instanceof BinaryExpression) {
BinaryExpression binExp = (BinaryExpression) expression;
if (binExp.astLeft() instanceof NullLiteral ||
binExp.astRight() instanceof NullLiteral) {
return true;
} else {
return isNullCheck(binExp.astLeft()) && isNullCheck(binExp.astRight());
}
} else {
return false;
}
}
}
@@ -0,0 +1,308 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
/** Registry which provides a list of checks to be performed on an Android project */
public class BuiltinIssueRegistry extends IssueRegistry {
private static final List<Issue> sIssues;
static final int INITIAL_CAPACITY = 220;
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);
issues.add(AnnotationDetector.ISSUE);
issues.add(ApiDetector.INLINED);
issues.add(ApiDetector.OVERRIDE);
issues.add(ApiDetector.UNSUPPORTED);
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);
issues.add(IconDetector.ICON_COLORS);
issues.add(IconDetector.ICON_DENSITIES);
issues.add(IconDetector.ICON_DIP_SIZE);
issues.add(IconDetector.ICON_EXPECTED_SIZE);
issues.add(IconDetector.ICON_EXTENSION);
issues.add(IconDetector.ICON_LAUNCHER_SHAPE);
issues.add(IconDetector.ICON_LOCATION);
issues.add(IconDetector.ICON_MISSING_FOLDER);
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);
issues.add(SecurityDetector.OPEN_PROVIDER);
issues.add(SecurityDetector.WORLD_READABLE);
issues.add(SecurityDetector.WORLD_WRITEABLE);
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);
issues.add(StringFormatDetector.POTENTIAL_PLURAL);
issues.add(SupportAnnotationDetector.CHECK_PERMISSION);
issues.add(SupportAnnotationDetector.CHECK_RESULT);
issues.add(SupportAnnotationDetector.COLOR_USAGE);
issues.add(SupportAnnotationDetector.MISSING_PERMISSION);
issues.add(SupportAnnotationDetector.RANGE);
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);
}
/**
* Constructs a new {@link BuiltinIssueRegistry}
*/
public BuiltinIssueRegistry() {
}
@NonNull
@Override
public List<Issue> getIssues() {
return sIssues;
}
@Override
protected int getIssueCapacity(@NonNull EnumSet<Scope> scope) {
if (scope.equals(Scope.ALL)) {
return getIssues().size();
} else {
int initialSize = 12;
if (scope.contains(Scope.RESOURCE_FILE)) {
initialSize += 75;
} else if (scope.contains(Scope.ALL_RESOURCE_FILES)) {
initialSize += 10;
}
if (scope.contains(Scope.JAVA_FILE)) {
initialSize += 55;
} else if (scope.contains(Scope.CLASS_FILE)) {
initialSize += 15;
} else if (scope.contains(Scope.MANIFEST)) {
initialSize += 30;
} else if (scope.contains(Scope.GRADLE_FILE)) {
initialSize += 5;
}
return initialSize;
}
}
/**
* Reset the registry such that it recomputes its available issues.
* <p>
* NOTE: This is only intended for testing purposes.
*/
@VisibleForTesting
public static void reset() {
IssueRegistry.reset();
}
}
@@ -0,0 +1,784 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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;
}
}
@@ -0,0 +1,90 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.ATTR_NAME;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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;
}
}
}
}
@@ -0,0 +1,223 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.io.File;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Super;
/**
* Makes sure that methods call super when overriding methods.
*/
public class CallSuperDetector extends Detector implements Detector.JavaScanner {
private static final String CALL_SUPER_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "CallSuper"; //$NON-NLS-1$
private static final String ON_DETACHED_FROM_WINDOW = "onDetachedFromWindow"; //$NON-NLS-1$
private static final String ON_VISIBILITY_CHANGED = "onVisibilityChanged"; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
CallSuperDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Missing call to super */
public static final Issue ISSUE = Issue.create(
"MissingSuperCall", //$NON-NLS-1$
"Missing Super Call",
"Some methods, such as `View#onDetachedFromWindow`, require that you also " +
"call the super implementation as part of your method.",
Category.CORRECTNESS,
9,
Severity.ERROR,
IMPLEMENTATION);
/** Constructs a new {@link CallSuperDetector} check */
public CallSuperDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
return Collections.<Class<? extends Node>>singletonList(MethodDeclaration.class);
}
@Override
public AstVisitor createJavaVisitor(@NonNull final JavaContext context) {
return new ForwardingAstVisitor() {
@Override
public boolean visitMethodDeclaration(MethodDeclaration node) {
ResolvedNode resolved = context.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
checkCallSuper(context, node, method);
}
return false;
}
};
}
private static void checkCallSuper(@NonNull JavaContext context,
@NonNull MethodDeclaration declaration,
@NonNull ResolvedMethod method) {
ResolvedMethod superMethod = getRequiredSuperMethod(method);
if (superMethod != null) {
if (!SuperCallVisitor.callsSuper(context, declaration, superMethod)) {
String methodName = method.getName();
String message = "Overriding method should call `super."
+ methodName + "`";
Location location = context.getLocation(declaration.astMethodName());
context.report(ISSUE, declaration, location, message);
}
}
}
/**
* Checks whether the given method overrides a method which requires the super method
* to be invoked, and if so, returns it (otherwise returns null)
*/
@Nullable
private static ResolvedMethod getRequiredSuperMethod(
@NonNull ResolvedMethod method) {
String name = method.getName();
if (ON_DETACHED_FROM_WINDOW.equals(name)) {
// No longer annotated on the framework method since it's
// now handled via onDetachedFromWindowInternal, but overriding
// is still dangerous if supporting older versions so flag
// this for now (should make annotation carry metadata like
// compileSdkVersion >= N).
if (!method.getContainingClass().isSubclassOf(CLASS_VIEW, false)) {
return null;
}
return method.getSuperMethod();
} else if (ON_VISIBILITY_CHANGED.equals(name)) {
// From Android Wear API; doesn't yet have an annotation
// but we want to enforce this right away until the AAR
// is updated to supply it once @CallSuper is available in
// the support library
if (!method.getContainingClass().isSubclassOf(
"android.support.wearable.watchface.WatchFaceService.Engine", false)) {
return null;
}
return method.getSuperMethod();
}
// Look up annotations metadata
ResolvedMethod directSuper = method.getSuperMethod();
ResolvedMethod superMethod = directSuper;
while (superMethod != null) {
Iterable<JavaParser.ResolvedAnnotation> annotations = superMethod.getAnnotations();
for (JavaParser.ResolvedAnnotation annotation : annotations) {
annotation = SupportAnnotationDetector.getRelevantAnnotation(annotation);
if (annotation != null) {
String signature = annotation.getSignature();
if (CALL_SUPER_ANNOTATION.equals(signature)) {
return directSuper;
} else if (signature.endsWith(".OverrideMustInvoke")) {
// Handle findbugs annotation on the fly too
return directSuper;
}
}
}
superMethod = superMethod.getSuperMethod();
}
return null;
}
/** Visits a method and determines whether the method calls its super method */
private static class SuperCallVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
private final ResolvedMethod mMethod;
private boolean mCallsSuper;
public static boolean callsSuper(
@NonNull JavaContext context,
@NonNull MethodDeclaration methodDeclaration,
@NonNull ResolvedMethod method) {
SuperCallVisitor visitor = new SuperCallVisitor(context, method);
methodDeclaration.accept(visitor);
return visitor.mCallsSuper;
}
private SuperCallVisitor(@NonNull JavaContext context, @NonNull ResolvedMethod method) {
mContext = context;
mMethod = method;
}
@Override
public boolean visitSuper(Super node) {
ResolvedNode resolved = null;
if (node.getParent() instanceof MethodInvocation) {
resolved = mContext.resolve(node.getParent());
}
if (resolved == null) {
resolved = mContext.resolve(node);
}
if (mMethod.equals(resolved)) {
mCallsSuper = true;
return true;
}
return false;
}
@Override
public boolean visitNode(Node node) {
return mCallsSuper || super.visitNode(node);
}
}
}
@@ -0,0 +1,131 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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;
}
}
@@ -0,0 +1,127 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import lombok.ast.AstVisitor;
import lombok.ast.Expression;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.StrictListAccessor;
import lombok.ast.StringLiteral;
/**
* Ensures that Cipher.getInstance is not called with AES as the parameter.
*/
public class CipherGetInstanceDetector extends Detector implements Detector.JavaScanner {
public static final Issue ISSUE = Issue.create(
"GetInstance", //$NON-NLS-1$
"Cipher.getInstance with ECB",
"`Cipher#getInstance` should not be called with ECB as the cipher mode or without "
+ "setting the cipher mode because the default mode on android is ECB, which "
+ "is insecure.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
CipherGetInstanceDetector.class,
Scope.JAVA_FILE_SCOPE));
private static final String CIPHER = "javax.crypto.Cipher"; //$NON-NLS-1$
private static final String GET_INSTANCE = "getInstance"; //$NON-NLS-1$
private static final Set<String> ALGORITHM_ONLY =
Sets.newHashSet("AES", "DES", "DESede"); //$NON-NLS-1$
private static final String ECB = "ECB"; //$NON-NLS-1$
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(GET_INSTANCE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
// Ignore if the method doesn't fit our description.
JavaParser.ResolvedNode resolved = context.resolve(node);
if (!(resolved instanceof JavaParser.ResolvedMethod)) {
return;
}
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved;
if (!method.getContainingClass().isSubclassOf(CIPHER, false)) {
return;
}
StrictListAccessor<Expression, MethodInvocation> argumentList = node.astArguments();
if (argumentList != null && argumentList.size() == 1) {
Expression expression = argumentList.first();
if (expression instanceof StringLiteral) {
StringLiteral argument = (StringLiteral)expression;
String parameter = argument.astValue();
checkParameter(context, node, argument, parameter, false);
} else {
JavaParser.ResolvedNode resolve = context.resolve(expression);
if (resolve instanceof JavaParser.ResolvedField) {
JavaParser.ResolvedField field = (JavaParser.ResolvedField) resolve;
Object value = field.getValue();
if (value instanceof String) {
checkParameter(context, node, expression, (String)value, true);
}
}
}
}
}
private static void checkParameter(@NonNull JavaContext context,
@NonNull MethodInvocation call, @NonNull Node node, @NonNull String value,
boolean includeValue) {
if (ALGORITHM_ONLY.contains(value)) {
String message = "`Cipher.getInstance` should not be called without setting the"
+ " encryption mode and padding";
context.report(ISSUE, call, context.getLocation(node), message);
} else if (value.contains(ECB)) {
String message = "ECB encryption mode should not be used";
if (includeValue) {
message += " (was \"" + value + "\")";
}
context.report(ISSUE, call, context.getLocation(node), message);
}
}
}
@@ -0,0 +1,601 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.CLASS_CONTEXT;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedField;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.client.api.JavaParser.ResolvedVariable;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.collect.Lists;
import java.util.Arrays;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Return;
import lombok.ast.StrictListAccessor;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Checks for missing {@code recycle} calls on resources that encourage it, and
* for missing {@code commit} calls on FragmentTransactions, etc.
*/
public class CleanupDetector extends Detector implements JavaScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
CleanupDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Problems with missing recycle calls */
public static final Issue RECYCLE_RESOURCE = Issue.create(
"Recycle", //$NON-NLS-1$
"Missing `recycle()` calls",
"Many resources, such as TypedArrays, VelocityTrackers, etc., " +
"should be recycled (with a `recycle()` call) after use. This lint check looks " +
"for missing `recycle()` calls.",
Category.PERFORMANCE,
7,
Severity.WARNING,
IMPLEMENTATION);
/** Problems with missing commit calls. */
public static final Issue COMMIT_FRAGMENT = Issue.create(
"CommitTransaction", //$NON-NLS-1$
"Missing `commit()` calls",
"After creating a `FragmentTransaction`, you typically need to commit it as well",
Category.CORRECTNESS,
7,
Severity.WARNING,
IMPLEMENTATION);
// Target method names
private static final String RECYCLE = "recycle"; //$NON-NLS-1$
private static final String RELEASE = "release"; //$NON-NLS-1$
private static final String OBTAIN = "obtain"; //$NON-NLS-1$
private static final String SHOW = "show"; //$NON-NLS-1$
private static final String ACQUIRE_CPC = "acquireContentProviderClient"; //$NON-NLS-1$
private static final String OBTAIN_NO_HISTORY = "obtainNoHistory"; //$NON-NLS-1$
private static final String OBTAIN_ATTRIBUTES = "obtainAttributes"; //$NON-NLS-1$
private static final String OBTAIN_TYPED_ARRAY = "obtainTypedArray"; //$NON-NLS-1$
private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$
private static final String BEGIN_TRANSACTION = "beginTransaction"; //$NON-NLS-1$
private static final String COMMIT = "commit"; //$NON-NLS-1$
private static final String COMMIT_ALLOWING_LOSS = "commitAllowingStateLoss"; //$NON-NLS-1$
private static final String QUERY = "query"; //$NON-NLS-1$
private static final String RAW_QUERY = "rawQuery"; //$NON-NLS-1$
private static final String QUERY_WITH_FACTORY = "queryWithFactory"; //$NON-NLS-1$
private static final String RAW_QUERY_WITH_FACTORY = "rawQueryWithFactory"; //$NON-NLS-1$
private static final String CLOSE = "close"; //$NON-NLS-1$
private static final String MOTION_EVENT_CLS = "android.view.MotionEvent"; //$NON-NLS-1$
private static final String RESOURCES_CLS = "android.content.res.Resources"; //$NON-NLS-1$
private static final String PARCEL_CLS = "android.os.Parcel"; //$NON-NLS-1$
private static final String TYPED_ARRAY_CLS = "android.content.res.TypedArray"; //$NON-NLS-1$
private static final String VELOCITY_TRACKER_CLS = "android.view.VelocityTracker";//$NON-NLS-1$
private static final String DIALOG_FRAGMENT = "android.app.DialogFragment"; //$NON-NLS-1$
private static final String DIALOG_V4_FRAGMENT =
"android.support.v4.app.DialogFragment"; //$NON-NLS-1$
private static final String FRAGMENT_MANAGER_CLS = "android.app.FragmentManager"; //$NON-NLS-1$
private static final String FRAGMENT_MANAGER_V4_CLS =
"android.support.v4.app.FragmentManager"; //$NON-NLS-1$
private static final String FRAGMENT_TRANSACTION_CLS =
"android.app.FragmentTransaction"; //$NON-NLS-1$
private static final String FRAGMENT_TRANSACTION_V4_CLS =
"android.support.v4.app.FragmentTransaction"; //$NON-NLS-1$
public static final String SURFACE_CLS = "android.view.Surface";
public static final String SURFACE_TEXTURE_CLS = "android.graphics.SurfaceTexture";
public static final String CONTENT_PROVIDER_CLIENT_CLS
= "android.content.ContentProviderClient";
public static final String CONTENT_RESOLVER_CLS = "android.content.ContentResolver";
public static final String CONTENT_PROVIDER_CLS = "android.content.ContentProvider";
@SuppressWarnings("SpellCheckingInspection")
public static final String SQLITE_DATABASE_CLS = "android.database.sqlite.SQLiteDatabase";
public static final String CURSOR_CLS = "android.database.Cursor";
/** Constructs a new {@link CleanupDetector} */
public CleanupDetector() {
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
// FragmentManager commit check
BEGIN_TRANSACTION,
// Recycle check
OBTAIN, OBTAIN_NO_HISTORY,
OBTAIN_STYLED_ATTRIBUTES,
OBTAIN_ATTRIBUTES,
OBTAIN_TYPED_ARRAY,
// Release check
ACQUIRE_CPC,
// Cursor close check
QUERY, RAW_QUERY, QUERY_WITH_FACTORY, RAW_QUERY_WITH_FACTORY
);
}
@Nullable
@Override
public List<String> getApplicableConstructorTypes() {
return Arrays.asList(SURFACE_TEXTURE_CLS, SURFACE_CLS);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
String name = node.astName().astValue();
if (BEGIN_TRANSACTION.equals(name)) {
checkTransactionCommits(context, node);
} else {
checkResourceRecycled(context, node, name);
}
}
@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull ConstructorInvocation node, @NonNull ResolvedMethod constructor) {
checkRecycled(context, node, constructor.getContainingClass().getSignature(), RELEASE);
}
private static void checkResourceRecycled(@NonNull JavaContext context,
@NonNull MethodInvocation node, @NonNull String name) {
// Recycle detector
ResolvedNode resolved = context.resolve(node);
if (!(resolved instanceof ResolvedMethod)) {
return;
}
ResolvedMethod method = (ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if ((OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) &&
containingClass.isSubclassOf(MOTION_EVENT_CLS, false)) {
checkRecycled(context, node, MOTION_EVENT_CLS, RECYCLE);
} else if (OBTAIN.equals(name) && containingClass.isSubclassOf(PARCEL_CLS, false)) {
checkRecycled(context, node, PARCEL_CLS, RECYCLE);
} else if (OBTAIN.equals(name) &&
containingClass.isSubclassOf(VELOCITY_TRACKER_CLS, false)) {
checkRecycled(context, node, VELOCITY_TRACKER_CLS, RECYCLE);
} else if ((OBTAIN_STYLED_ATTRIBUTES.equals(name)
|| OBTAIN_ATTRIBUTES.equals(name)
|| OBTAIN_TYPED_ARRAY.equals(name)) &&
(containingClass.isSubclassOf(CLASS_CONTEXT, false) ||
containingClass.isSubclassOf(RESOURCES_CLS, false))) {
TypeDescriptor returnType = method.getReturnType();
if (returnType != null && returnType.matchesSignature(TYPED_ARRAY_CLS)) {
checkRecycled(context, node, TYPED_ARRAY_CLS, RECYCLE);
}
} else if (ACQUIRE_CPC.equals(name) && containingClass.isSubclassOf(
CONTENT_RESOLVER_CLS, false)) {
checkRecycled(context, node, CONTENT_PROVIDER_CLIENT_CLS, RELEASE);
} else if ((QUERY.equals(name)
|| RAW_QUERY.equals(name)
|| QUERY_WITH_FACTORY.equals(name)
|| RAW_QUERY_WITH_FACTORY.equals(name))
&& (containingClass.isSubclassOf(SQLITE_DATABASE_CLS, false) ||
containingClass.isSubclassOf(CONTENT_RESOLVER_CLS, false) ||
containingClass.isSubclassOf(CONTENT_PROVIDER_CLS, false) ||
containingClass.isSubclassOf(CONTENT_PROVIDER_CLIENT_CLS, false))) {
// Other potential cursors-returning methods that should be tracked:
// android.app.DownloadManager#query
// android.content.ContentProviderClient#query
// android.content.ContentResolver#query
// android.database.sqlite.SQLiteQueryBuilder#query
// android.provider.Browser#getAllBookmarks
// android.provider.Browser#getAllVisitedUrls
// android.provider.DocumentsProvider#queryChildDocuments
// android.provider.DocumentsProvider#qqueryDocument
// android.provider.DocumentsProvider#queryRecentDocuments
// android.provider.DocumentsProvider#queryRoots
// android.provider.DocumentsProvider#querySearchDocuments
// android.provider.MediaStore$Images$Media#query
// android.widget.FilterQueryProvider#runQuery
checkRecycled(context, node, CURSOR_CLS, CLOSE);
}
}
private static void checkRecycled(@NonNull final JavaContext context, @NonNull Node node,
@NonNull final String recycleType, @NonNull final String recycleName) {
ResolvedVariable boundVariable = getVariable(context, node);
if (boundVariable == null) {
return;
}
Node method = JavaContext.findSurroundingMethod(node);
if (method == null) {
return;
}
FinishVisitor visitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull MethodInvocation call) {
String methodName = call.astName().astValue();
if (!recycleName.equals(methodName)) {
return false;
}
ResolvedNode resolved = mContext.resolve(call);
if (resolved instanceof ResolvedMethod) {
ResolvedClass containingClass = ((ResolvedMethod) resolved).getContainingClass();
if (containingClass.isSubclassOf(recycleType, false)) {
// Yes, called the right recycle() method; now make sure
// we're calling it on the right variable
Expression operand = call.astOperand();
if (operand != null) {
resolved = mContext.resolve(operand);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
}
return false;
}
};
method.accept(visitor);
if (visitor.isCleanedUp() || visitor.variableEscapes()) {
return;
}
String className = recycleType.substring(recycleType.lastIndexOf('.') + 1);
String message;
if (RECYCLE.equals(recycleName)) {
message = String.format(
"This `%1$s` should be recycled after use with `#recycle()`", className);
} else {
message = String.format(
"This `%1$s` should be freed up after use with `#%2$s()`", className,
recycleName);
}
Node locationNode = node instanceof MethodInvocation ?
((MethodInvocation) node).astName() : node;
Location location = context.getLocation(locationNode);
context.report(RECYCLE_RESOURCE, node, location, message);
}
private static boolean checkTransactionCommits(@NonNull JavaContext context,
@NonNull MethodInvocation node) {
if (isBeginTransaction(context, node)) {
ResolvedVariable boundVariable = getVariable(context, node);
if (boundVariable == null && isCommittedInChainedCalls(context, node)) {
return true;
}
if (boundVariable != null) {
Node method = JavaContext.findSurroundingMethod(node);
if (method == null) {
return true;
}
FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull MethodInvocation call) {
if (isTransactionCommitMethodCall(mContext, call)) {
Expression operand = call.astOperand();
if (operand != null) {
ResolvedNode resolved = mContext.resolve(operand);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
} else if (resolved instanceof ResolvedMethod
&& operand instanceof MethodInvocation
&& isCommittedInChainedCalls(mContext,(MethodInvocation) operand)) {
// Check that the target of the committed chains is the
// right variable!
while (operand instanceof MethodInvocation) {
operand = ((MethodInvocation)operand).astOperand();
}
if (operand instanceof VariableReference) {
resolved = mContext.resolve(operand);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
}
} else if (isShowFragmentMethodCall(mContext, call)) {
StrictListAccessor<Expression, MethodInvocation> arguments =
call.astArguments();
if (arguments.size() == 2) {
Expression first = arguments.first();
ResolvedNode resolved = mContext.resolve(first);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
return false;
}
};
method.accept(commitVisitor);
if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) {
return true;
}
}
String message = "This transaction should be completed with a `commit()` call";
context.report(COMMIT_FRAGMENT, node, context.getLocation(node.astName()),
message);
}
return false;
}
private static boolean isCommittedInChainedCalls(@NonNull JavaContext context,
@NonNull MethodInvocation node) {
// Look for chained calls since the FragmentManager methods all return "this"
// to allow constructor chaining, e.g.
// getFragmentManager().beginTransaction().addToBackStack("test")
// .disallowAddToBackStack().hide(mFragment2).setBreadCrumbShortTitle("test")
// .show(mFragment2).setCustomAnimations(0, 0).commit();
Node parent = node.getParent();
while (parent instanceof MethodInvocation) {
MethodInvocation methodInvocation = (MethodInvocation) parent;
if (isTransactionCommitMethodCall(context, methodInvocation)
|| isShowFragmentMethodCall(context, methodInvocation)) {
return true;
}
parent = parent.getParent();
}
return false;
}
private static boolean isTransactionCommitMethodCall(@NonNull JavaContext context,
@NonNull MethodInvocation call) {
String methodName = call.astName().astValue();
return (COMMIT.equals(methodName) || COMMIT_ALLOWING_LOSS.equals(methodName)) &&
isMethodOnFragmentClass(context, call,
FRAGMENT_TRANSACTION_CLS,
FRAGMENT_TRANSACTION_V4_CLS);
}
private static boolean isShowFragmentMethodCall(@NonNull JavaContext context,
@NonNull MethodInvocation call) {
String methodName = call.astName().astValue();
return SHOW.equals(methodName)
&& isMethodOnFragmentClass(context, call,
DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT);
}
private static boolean isMethodOnFragmentClass(
@NonNull JavaContext context,
@NonNull MethodInvocation call,
@NonNull String fragmentClass,
@NonNull String v4FragmentClass) {
ResolvedNode resolved = context.resolve(call);
if (resolved instanceof ResolvedMethod) {
ResolvedClass containingClass = ((ResolvedMethod) resolved).getContainingClass();
return containingClass.isSubclassOf(fragmentClass, false) ||
containingClass.isSubclassOf(v4FragmentClass, false);
}
return false;
}
@Nullable
public static ResolvedVariable getVariable(@NonNull JavaContext context,
@NonNull Node expression) {
Node parent = expression.getParent();
if (parent instanceof BinaryExpression) {
BinaryExpression binaryExpression = (BinaryExpression) parent;
if (binaryExpression.astOperator() == BinaryOperator.ASSIGN) {
Expression lhs = binaryExpression.astLeft();
ResolvedNode resolved = context.resolve(lhs);
if (resolved instanceof ResolvedVariable) {
return (ResolvedVariable) resolved;
}
}
} else if (parent instanceof VariableDefinitionEntry) {
ResolvedNode resolved = context.resolve(parent);
if (resolved instanceof ResolvedVariable) {
return (ResolvedVariable) resolved;
}
}
return null;
}
private static boolean isBeginTransaction(@NonNull JavaContext context,
@NonNull MethodInvocation node) {
String methodName = node.astName().astValue();
assert methodName.equals(BEGIN_TRANSACTION) : methodName;
if (BEGIN_TRANSACTION.equals(methodName)) {
ResolvedNode resolved = context.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.isSubclassOf(FRAGMENT_MANAGER_CLS, false)
|| containingClass.isSubclassOf(FRAGMENT_MANAGER_V4_CLS,
false)) {
return true;
}
}
}
return false;
}
/**
* Visitor which checks whether an operation is "finished"; in the case
* of a FragmentTransaction we're looking for a "commit" call; in the
* case of a TypedArray we're looking for a "recycle", call, in the
* case of a database cursor we're looking for a "close" call, etc.
*/
private abstract static class FinishVisitor extends ForwardingAstVisitor {
protected final JavaContext mContext;
protected final List<ResolvedVariable> mVariables;
private boolean mContainsCleanup;
private boolean mEscapes;
public FinishVisitor(JavaContext context, @NonNull ResolvedVariable variable) {
mContext = context;
mVariables = Lists.newArrayList(variable);
}
public boolean isCleanedUp() {
return mContainsCleanup;
}
public boolean variableEscapes() {
return mEscapes;
}
@Override
public boolean visitNode(Node node) {
return mContainsCleanup || super.visitNode(node);
}
protected abstract boolean isCleanupCall(@NonNull MethodInvocation call);
@Override
public boolean visitMethodInvocation(MethodInvocation call) {
if (mContainsCleanup) {
return true;
}
super.visitMethodInvocation(call);
// Look for escapes
if (!mEscapes) {
for (Expression expression : call.astArguments()) {
if (expression instanceof VariableReference) {
ResolvedNode resolved = mContext.resolve(expression);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
mEscapes = true;
// Special case: MotionEvent.obtain(MotionEvent): passing in an
// event here does not recycle the event, and we also know it
// doesn't escape
if (OBTAIN.equals(call.astName().astValue())) {
ResolvedNode r = mContext.resolve(call);
if (r instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) r;
ResolvedClass cls = method.getContainingClass();
if (cls.matches(MOTION_EVENT_CLS)) {
mEscapes = false;
}
}
}
}
}
}
}
if (isCleanupCall(call)) {
mContainsCleanup = true;
return true;
} else {
return false;
}
}
@Override
public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) {
Expression initializer = node.astInitializer();
if (initializer instanceof VariableReference) {
ResolvedNode resolved = mContext.resolve(initializer);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
ResolvedNode resolvedVariable = mContext.resolve(node);
if (resolvedVariable instanceof ResolvedVariable) {
ResolvedVariable variable = (ResolvedVariable) resolvedVariable;
mVariables.add(variable);
} else if (resolvedVariable instanceof ResolvedField) {
mEscapes = true;
}
}
}
return super.visitVariableDefinitionEntry(node);
}
@Override
public boolean visitBinaryExpression(BinaryExpression node) {
if (node.astOperator() == BinaryOperator.ASSIGN) {
Expression rhs = node.astRight();
if (rhs instanceof VariableReference) {
ResolvedNode resolved = mContext.resolve(rhs);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
ResolvedNode resolvedLhs = mContext.resolve(node.astLeft());
if (resolvedLhs instanceof ResolvedVariable) {
ResolvedVariable variable = (ResolvedVariable) resolvedLhs;
mVariables.add(variable);
} else if (resolvedLhs instanceof ResolvedField) {
mEscapes = true;
}
}
}
}
return super.visitBinaryExpression(node);
}
@Override
public boolean visitReturn(Return node) {
Expression value = node.astValue();
if (value instanceof VariableReference) {
ResolvedNode resolved = mContext.resolve(value);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
mEscapes = true;
}
}
return super.visitReturn(node);
}
}
}
@@ -0,0 +1,265 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.ANDROID_VIEW_VIEW;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.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));
}
}
@@ -0,0 +1,199 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.io.File;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.Comment;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Node;
/**
* Looks for issues in Java comments
*/
public class CommentDetector extends Detector implements Detector.JavaScanner {
private static final String STOPSHIP_COMMENT = "STOPSHIP"; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
CommentDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Looks for hidden code */
public static final Issue EASTER_EGG = Issue.create(
"EasterEgg", //$NON-NLS-1$
"Code contains easter egg",
"An \"easter egg\" is code deliberately hidden in the code, both from potential " +
"users and even from other developers. This lint check looks for code which " +
"looks like it may be hidden from sight.",
Category.SECURITY,
6,
Severity.WARNING,
IMPLEMENTATION)
.setEnabledByDefault(false);
/** Looks for special comment markers intended to stop shipping the code */
public static final Issue STOP_SHIP = Issue.create(
"StopShip", //$NON-NLS-1$
"Code contains `STOPSHIP` marker",
"Using the comment `// STOPSHIP` can be used to flag code that is incomplete but " +
"checked in. This comment marker can be used to indicate that the code should not " +
"be shipped until the issue is addressed, and lint will look for these.",
Category.CORRECTNESS,
10,
Severity.WARNING,
IMPLEMENTATION)
.setEnabledByDefault(false);
private static final String ESCAPE_STRING = "\\u002a\\u002f"; //$NON-NLS-1$
/** Lombok's AST only passes comment nodes for Javadoc so I need to do manual token scanning
instead */
private static final boolean USE_AST = false;
/** Constructs a new {@link CommentDetector} check */
public CommentDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
if (USE_AST) {
return Collections.<Class<? extends Node>>singletonList(Comment.class);
} else {
return null;
}
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
// Lombok does not generate comment nodes for block and line comments, only for
// javadoc comments!
if (USE_AST) {
return new CommentChecker(context);
} else {
String source = context.getContents();
if (source == null) {
return null;
}
// Process the Java source such that we pass tokens to it
for (int i = 0, n = source.length() - 1; i < n; i++) {
char c = source.charAt(i);
if (c == '\\') {
i += 1;
} else if (c == '/') {
char next = source.charAt(i + 1);
if (next == '/') {
// Line comment
int start = i + 2;
int end = source.indexOf('\n', start);
if (end == -1) {
end = n;
}
checkComment(context, source, 0, start, end);
} else if (next == '*') {
// Block comment
int start = i + 2;
int end = source.indexOf("*/", start);
if (end == -1) {
end = n;
}
checkComment(context, source, 0, start, end);
}
}
}
return null;
}
}
private static class CommentChecker extends ForwardingAstVisitor {
private final JavaContext mContext;
public CommentChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitComment(Comment node) {
String contents = node.astContent();
checkComment(mContext, contents, node.getPosition().getStart(), 0, contents.length());
return super.visitComment(node);
}
}
private static void checkComment(
@NonNull Context context,
@NonNull String source,
int offset,
int start,
int end) {
char prev = 0;
char c;
for (int i = start; i < end - 2; i++, prev = c) {
c = source.charAt(i);
if (prev == '\\') {
if (c == 'u' || c == 'U') {
if (source.regionMatches(true, i - 1, ESCAPE_STRING,
0, ESCAPE_STRING.length())) {
Location location = Location.create(context.file, source,
offset + i - 1, offset + i - 1 + ESCAPE_STRING.length());
context.report(EASTER_EGG, location,
"Code might be hidden here; found unicode escape sequence " +
"which is interpreted as comment end, compiled code follows");
}
} else {
i++;
}
} else if (prev == 'S' && c == 'T' &&
source.regionMatches(i - 1, STOPSHIP_COMMENT, 0, STOPSHIP_COMMENT.length())) {
// TODO: Only flag this issue in release mode??
Location location = Location.create(context.file, source,
offset + i - 1, offset + i - 1 + STOPSHIP_COMMENT.length());
context.report(STOP_SHIP, location,
"`STOPSHIP` comment found; points to code which must be fixed prior " +
"to release");
}
}
}
}
@@ -0,0 +1,533 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
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.FrameNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
//import org.objectweb.asm.util.Printer;
/**
* A {@linkplain ControlFlowGraph} is a graph containing a node for each
* instruction in a method, and an edge for each possible control flow; usually
* just "next" for the instruction following the current instruction, but in the
* case of a branch such as an "if", multiple edges to each successive location,
* or with a "goto", a single edge to the jumped-to instruction.
* <p>
* It also adds edges for abnormal control flow, such as the possibility of a
* method call throwing a runtime exception.
*/
public class ControlFlowGraph {
/** Map from instructions to nodes */
private Map<AbstractInsnNode, Node> mNodeMap;
private MethodNode mMethod;
/**
* Creates a new {@link ControlFlowGraph} and populates it with the flow
* control for the given method. If the optional {@code initial} parameter is
* provided with an existing graph, then the graph is simply populated, not
* created. This allows subclassing of the graph instance, if necessary.
*
* @param initial usually null, but can point to an existing instance of a
* {@link ControlFlowGraph} in which that graph is reused (but
* populated with new edges)
* @param classNode the class containing the method to be analyzed
* @param method the method to be analyzed
* @return a {@link ControlFlowGraph} with nodes for the control flow in the
* given method
* @throws AnalyzerException if the underlying bytecode library is unable to
* analyze the method bytecode
*/
@NonNull
public static ControlFlowGraph create(
@Nullable ControlFlowGraph initial,
@NonNull ClassNode classNode,
@NonNull MethodNode method) throws AnalyzerException {
final ControlFlowGraph graph = initial != null ? initial : new ControlFlowGraph();
final InsnList instructions = method.instructions;
graph.mNodeMap = Maps.newHashMapWithExpectedSize(instructions.size());
graph.mMethod = method;
// Create a flow control graph using ASM5's analyzer. According to the ASM 4 guide
// (download.forge.objectweb.org/asm/asm4-guide.pdf) there are faster ways to construct
// it, but those require a lot more code.
Analyzer analyzer = new Analyzer(new BasicInterpreter()) {
@Override
protected void newControlFlowEdge(int insn, int successor) {
// Update the information as of whether the this object has been
// initialized at the given instruction.
AbstractInsnNode from = instructions.get(insn);
AbstractInsnNode to = instructions.get(successor);
graph.add(from, to);
}
@Override
protected boolean newControlFlowExceptionEdge(int insn, TryCatchBlockNode tcb) {
AbstractInsnNode from = instructions.get(insn);
graph.exception(from, tcb);
return super.newControlFlowExceptionEdge(insn, tcb);
}
@Override
protected boolean newControlFlowExceptionEdge(int insn, int successor) {
AbstractInsnNode from = instructions.get(insn);
AbstractInsnNode to = instructions.get(successor);
graph.exception(from, to);
return super.newControlFlowExceptionEdge(insn, successor);
}
};
analyzer.analyze(classNode.name, method);
return graph;
}
/**
* Checks whether there is a path from the given source node to the given
* destination node
*/
@SuppressWarnings("MethodMayBeStatic")
private boolean isConnected(@NonNull Node from,
@NonNull Node to, @NonNull Set<Node> seen) {
if (from == to) {
return true;
} else if (seen.contains(from)) {
return false;
}
seen.add(from);
List<Node> successors = from.successors;
List<Node> exceptions = from.exceptions;
if (exceptions != null) {
for (Node successor : exceptions) {
if (isConnected(successor, to, seen)) {
return true;
}
}
}
if (successors != null) {
for (Node successor : successors) {
if (isConnected(successor, to, seen)) {
return true;
}
}
}
return false;
}
/**
* Checks whether there is a path from the given source node to the given
* destination node
*/
public boolean isConnected(@NonNull Node from, @NonNull Node to) {
return isConnected(from, to, Sets.<Node>newIdentityHashSet());
}
/**
* Checks whether there is a path from the given instruction to the given
* instruction node
*/
public boolean isConnected(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
return isConnected(getNode(from), getNode(to));
}
/** A {@link Node} is a node in the control flow graph for a method, pointing to
* the instruction and its possible successors */
public static class Node {
/** The instruction */
public final AbstractInsnNode instruction;
/** Any normal successors (e.g. following instruction, or goto or conditional flow) */
public final List<Node> successors = new ArrayList<Node>(2);
/** Any abnormal successors (e.g. the handler to go to following an exception) */
public final List<Node> exceptions = new ArrayList<Node>(1);
/** A tag for use during depth-first-search iteration of the graph etc */
public int visit;
/**
* Constructs a new control graph node
*
* @param instruction the instruction to associate with this node
*/
public Node(@NonNull AbstractInsnNode instruction) {
this.instruction = instruction;
}
void addSuccessor(@NonNull Node node) {
if (!successors.contains(node)) {
successors.add(node);
}
}
void addExceptionPath(@NonNull Node node) {
if (!exceptions.contains(node)) {
exceptions.add(node);
}
}
/**
* Represents this instruction as a string, for debugging purposes
*
* @param includeAdjacent whether it should include a display of
* adjacent nodes as well
* @return a string representation
*/
@NonNull
public String toString(boolean includeAdjacent) {
StringBuilder sb = new StringBuilder(100);
sb.append(getId(instruction));
sb.append(':');
if (instruction instanceof LabelNode) {
//LabelNode l = (LabelNode) instruction;
//sb.append('L' + l.getLabel().getOffset() + ":");
//sb.append('L' + l.getLabel().info + ":");
sb.append("LABEL");
} else if (instruction instanceof LineNumberNode) {
sb.append("LINENUMBER ").append(((LineNumberNode)instruction).line);
} else if (instruction instanceof FrameNode) {
sb.append("FRAME");
} else {
int opcode = instruction.getOpcode();
String opcodeName = getOpcodeName(opcode);
sb.append(opcodeName);
if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
sb.append('(').append(((MethodInsnNode)instruction).name).append(')');
}
}
if (includeAdjacent) {
if (successors != null && !successors.isEmpty()) {
sb.append(" Next:");
for (Node successor : successors) {
sb.append(' ');
sb.append(successor.toString(false));
}
}
if (exceptions != null && !exceptions.isEmpty()) {
sb.append(" Exceptions:");
for (Node exception : exceptions) {
sb.append(' ');
sb.append(exception.toString(false));
}
}
sb.append('\n');
}
return sb.toString();
}
}
/** Adds an exception flow to this graph */
protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
getNode(from).addSuccessor(getNode(to));
}
/** Adds an exception flow to this graph */
protected void exception(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
// For now, these edges appear useless; we also get more specific
// information via the TryCatchBlockNode which we use instead.
//getNode(from).addExceptionPath(getNode(to));
}
/** Adds an exception try block node to this graph */
protected void exception(@NonNull AbstractInsnNode from, @NonNull TryCatchBlockNode tcb) {
// Add tcb's to all instructions in the range
LabelNode start = tcb.start;
LabelNode end = tcb.end; // exclusive
// Add exception edges for all method calls in the range
AbstractInsnNode curr = start;
Node handlerNode = getNode(tcb.handler);
while (curr != end && curr != null) {
if (curr.getType() == AbstractInsnNode.METHOD_INSN) {
// Method call; add exception edge to handler
if (tcb.type == null) {
// finally block: not an exception path
getNode(curr).addSuccessor(handlerNode);
}
getNode(curr).addExceptionPath(handlerNode);
}
curr = curr.getNext();
}
}
/**
* Looks up (and if necessary) creates a graph node for the given instruction
*
* @param instruction the instruction
* @return the control flow graph node corresponding to the given
* instruction
*/
@NonNull
public Node getNode(@NonNull AbstractInsnNode instruction) {
Node node = mNodeMap.get(instruction);
if (node == null) {
node = new Node(instruction);
mNodeMap.put(instruction, node);
}
return node;
}
/**
* Creates a human readable version of the graph
*
* @param start the starting instruction, or null if not known or to use the
* first instruction
* @return a string version of the graph
*/
@NonNull
public String toString(@Nullable Node start) {
StringBuilder sb = new StringBuilder(400);
AbstractInsnNode curr;
if (start != null) {
curr = start.instruction;
} else {
if (mNodeMap.isEmpty()) {
return "<empty>";
} else {
curr = mNodeMap.keySet().iterator().next();
while (curr.getPrevious() != null) {
curr = curr.getPrevious();
}
}
}
while (curr != null) {
Node node = mNodeMap.get(curr);
if (node != null) {
sb.append(node.toString(true));
}
curr = curr.getNext();
}
return sb.toString();
}
@Override
public String toString() {
return toString(null);
}
// ---- For debugging only ----
private static Map<Object, String> sIds = null;
private static int sNextId = 1;
private static String getId(Object object) {
if (sIds == null) {
sIds = Maps.newHashMap();
}
String id = sIds.get(object);
if (id == null) {
id = Integer.toString(sNextId++);
sIds.put(object, id);
}
return id;
}
/**
* Generates dot output of the graph. This can be used with
* graphwiz to visualize the graph. For example, if you
* save the output as graph1.gv you can run
* <pre>
* $ dot -Tps graph1.gv -o graph1.ps
* </pre>
* to generate a postscript file, which you can then view
* with "gv graph1.ps".
*
* (There are also some online web sites where you can
* paste in dot graphs and see the visualization right
* there in the browser.)
*
* @return a dot description of this control flow graph,
* useful for debugging
*/
public String toDot(@Nullable Set<Node> highlight) {
StringBuilder sb = new StringBuilder();
sb.append("digraph G {\n");
AbstractInsnNode instruction = mMethod.instructions.getFirst();
// Special start node
sb.append(" start -> ").append(getId(mNodeMap.get(instruction))).append(";\n");
sb.append(" start [shape=plaintext];\n");
while (instruction != null) {
Node node = mNodeMap.get(instruction);
if (node != null) {
if (node.successors != null) {
for (Node to : node.successors) {
sb.append(" ").append(getId(node)).append(" -> ").append(getId(to));
if (node.instruction instanceof JumpInsnNode) {
sb.append(" [label=\"");
if (((JumpInsnNode)node.instruction).label == to.instruction) {
sb.append("yes");
} else {
sb.append("no");
}
sb.append("\"]");
}
sb.append(";\n");
}
}
if (node.exceptions != null) {
for (Node to : node.exceptions) {
sb.append(getId(node)).append(" -> ").append(getId(to));
sb.append(" [label=\"exception\"];\n");
}
}
}
instruction = instruction.getNext();
}
// Labels
sb.append("\n");
for (Node node : mNodeMap.values()) {
instruction = node.instruction;
sb.append(" ").append(getId(node)).append(" ");
sb.append("[label=\"").append(dotDescribe(node)).append("\"");
if (highlight != null && highlight.contains(node)) {
sb.append(",shape=box,style=filled");
} else if (instruction instanceof LineNumberNode ||
instruction instanceof LabelNode ||
instruction instanceof FrameNode) {
sb.append(",shape=oval,style=dotted");
} else {
sb.append(",shape=box");
}
sb.append("];\n");
}
sb.append("}");
return sb.toString();
}
private static String dotDescribe(Node node) {
AbstractInsnNode instruction = node.instruction;
if (instruction instanceof LabelNode) {
return "Label";
} else if (instruction instanceof LineNumberNode) {
LineNumberNode lineNode = (LineNumberNode)instruction;
return "Line " + lineNode.line;
} else if (instruction instanceof FrameNode) {
return "Stack Frame";
} else if (instruction instanceof MethodInsnNode) {
MethodInsnNode method = (MethodInsnNode)instruction;
String cls = method.owner.substring(method.owner.lastIndexOf('/') + 1);
cls = cls.replace('$','.');
return "Call " + cls + "#" + method.name;
} else if (instruction instanceof FieldInsnNode) {
FieldInsnNode field = (FieldInsnNode) instruction;
String cls = field.owner.substring(field.owner.lastIndexOf('/') + 1);
cls = cls.replace('$','.');
return "Field " + cls + "#" + field.name;
} else if (instruction instanceof TypeInsnNode && instruction.getOpcode() == Opcodes.NEW) {
return "New " + ((TypeInsnNode)instruction).desc;
}
StringBuilder sb = new StringBuilder();
String opcodeName = getOpcodeName(instruction.getOpcode());
sb.append(opcodeName);
if (instruction instanceof IntInsnNode) {
IntInsnNode in = (IntInsnNode) instruction;
sb.append(" ").append(Integer.toString(in.operand));
} else if (instruction instanceof LdcInsnNode) {
LdcInsnNode ldc = (LdcInsnNode) instruction;
sb.append(" ");
if (ldc.cst instanceof String) {
sb.append("\\\"");
}
sb.append(ldc.cst);
if (ldc.cst instanceof String) {
sb.append("\\\"");
}
}
return sb.toString();
}
private static String getOpcodeName(int opcode) {
if (sOpcodeNames == null) {
sOpcodeNames = new String[255];
try {
Field[] fields = Opcodes.class.getDeclaredFields();
for (Field field : fields) {
if (field.getType() == int.class) {
String name = field.getName();
if (name.startsWith("ASM") || name.startsWith("V1_") ||
name.startsWith("ACC_") || name.startsWith("T_") ||
name.startsWith("H_") || name.startsWith("F_")) {
continue;
}
int val = field.getInt(null);
if (val >= 0 && val < sOpcodeNames.length) {
sOpcodeNames[val] = field.getName();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (opcode >= 0 && opcode < sOpcodeNames.length) {
String name = sOpcodeNames[opcode];
if (name != null) {
return name;
}
}
return Integer.toString(opcode);
}
private static String[] sOpcodeNames;
}
@@ -0,0 +1,171 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.CLASS_VIEWGROUP;
import static com.android.SdkConstants.DOT_LAYOUT_PARAMS;
import static com.android.SdkConstants.R_STYLEABLE_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.google.common.collect.Iterators;
import java.io.File;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.ClassDeclaration;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.MethodInvocation;
import lombok.ast.StrictListAccessor;
/**
* Makes sure that custom views use a declare styleable that matches
* the name of the custom view
*/
public class CustomViewDetector extends Detector implements Detector.JavaScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
CustomViewDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Mismatched style and class names */
public static final Issue ISSUE = Issue.create(
"CustomViewStyleable", //$NON-NLS-1$
"Mismatched Styleable/Custom View Name",
"The convention for custom views is to use a `declare-styleable` whose name " +
"matches the custom view class name. The IDE relies on this convention such that " +
"for example code completion can be offered for attributes in a custom view " +
"in layout XML resource files.\n" +
"\n" +
"(Similarly, layout parameter classes should use the suffix `_Layout`.)",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION);
private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$
/** Constructs a new {@link com.android.tools.lint.checks.CustomViewDetector} check */
public CustomViewDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(OBTAIN_STYLED_ATTRIBUTES);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
if (node.getParent() instanceof ExpressionStatement) {
if (!context.isContextMethod(node)) {
return;
}
StrictListAccessor<Expression, MethodInvocation> expressions = node.astArguments();
int size = expressions.size();
// Which parameter contains the styleable (attrs) ?
int parameterIndex;
if (size == 1) {
// obtainStyledAttributes(int[] attrs)
parameterIndex = 0;
} else {
// obtainStyledAttributes(int resid, int[] attrs)
// obtainStyledAttributes(AttributeSet set, int[] attrs)
// obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
parameterIndex = 1;
}
Expression expression = Iterators.get(node.astArguments().iterator(), parameterIndex,
null);
if (expression == null) {
return;
}
String s = expression.toString();
if (!s.startsWith(R_STYLEABLE_PREFIX)) {
return;
}
String styleableName = s.substring(R_STYLEABLE_PREFIX.length());
ClassDeclaration cls = JavaContext.findSurroundingClass(node);
if (cls == null) {
return;
}
ResolvedNode resolved = context.resolve(cls);
if (!(resolved instanceof ResolvedClass)) {
return;
}
String className = cls.astName().astValue();
ResolvedClass resolvedClass = (ResolvedClass) resolved;
if (resolvedClass.isSubclassOf(CLASS_VIEW, false)) {
if (!styleableName.equals(className)) {
String message = String.format(
"By convention, the custom view (`%1$s`) and the declare-styleable (`%2$s`) "
+ "should have the same name (various editor features rely on "
+ "this convention)",
className, styleableName);
context.report(ISSUE, node, context.getLocation(expression), message);
}
} else if (resolvedClass.isSubclassOf(CLASS_VIEWGROUP + DOT_LAYOUT_PARAMS, false)) {
ClassDeclaration outer = JavaContext.findSurroundingClass(cls.getParent());
if (outer == null) {
return;
}
String layoutClassName = outer.astName().astValue();
String expectedName = layoutClassName + "_Layout";
if (!styleableName.equals(expectedName)) {
String message = String.format(
"By convention, the declare-styleable (`%1$s`) for a layout parameter "
+ "class (`%2$s`) is expected to be the surrounding "
+ "class (`%3$s`) plus \"`_Layout`\", e.g. `%4$s`. "
+ "(Various editor features rely on this convention.)",
styleableName, className, layoutClassName, expectedName);
context.report(ISSUE, node, context.getLocation(expression), message);
}
}
}
}
}
@@ -0,0 +1,242 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.RESOURCE_CLZ_ID;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.collect.Maps;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import lombok.ast.ArrayAccess;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.Cast;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.If;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Select;
import lombok.ast.Statement;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Detector looking for cut & paste issues
*/
public class CutPasteDetector extends Detector implements Detector.JavaScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"CutPasteId", //$NON-NLS-1$
"Likely cut & paste mistakes",
"This lint check looks for cases where you have cut & pasted calls to " +
"`findViewById` but have forgotten to update the R.id field. It's possible " +
"that your code is simply (redundantly) looking up the field repeatedly, " +
"but lint cannot distinguish that from a case where you for example want to " +
"initialize fields `prev` and `next` and you cut & pasted `findViewById(R.id.prev)` " +
"and forgot to update the second initialization to `R.id.next`.",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
CutPasteDetector.class,
Scope.JAVA_FILE_SCOPE));
private Node mLastMethod;
private Map<String, MethodInvocation> mIds;
private Map<String, String> mLhs;
private Map<String, String> mCallOperands;
/** Constructs a new {@link CutPasteDetector} check */
public CutPasteDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("findViewById"); //$NON-NLS-1$
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation call) {
String lhs = getLhs(call);
if (lhs == null) {
return;
}
Node method = JavaContext.findSurroundingMethod(call);
if (method == null) {
return;
} else if (method != mLastMethod) {
mIds = Maps.newHashMap();
mLhs = Maps.newHashMap();
mCallOperands = Maps.newHashMap();
mLastMethod = method;
}
String callOperand = call.astOperand() != null ? call.astOperand().toString() : "";
Expression first = call.astArguments().first();
if (first instanceof Select) {
Select select = (Select) first;
String id = select.astIdentifier().astValue();
Expression operand = select.astOperand();
if (operand instanceof Select) {
Select type = (Select) operand;
if (type.astIdentifier().astValue().equals(RESOURCE_CLZ_ID)) {
if (mIds.containsKey(id)) {
if (lhs.equals(mLhs.get(id))) {
return;
}
if (!callOperand.equals(mCallOperands.get(id))) {
return;
}
MethodInvocation earlierCall = mIds.get(id);
if (!isReachableFrom(method, earlierCall, call)) {
return;
}
Location location = context.getLocation(call);
Location secondary = context.getLocation(earlierCall);
secondary.setMessage("First usage here");
location.setSecondary(secondary);
context.report(ISSUE, call, location, String.format(
"The id `%1$s` has already been looked up in this method; possible " +
"cut & paste error?", first.toString()));
} else {
mIds.put(id, call);
mLhs.put(id, lhs);
mCallOperands.put(id, callOperand);
}
}
}
}
}
@Nullable
private static String getLhs(@NonNull MethodInvocation call) {
Node parent = call.getParent();
if (parent instanceof Cast) {
parent = parent.getParent();
}
if (parent instanceof VariableDefinitionEntry) {
VariableDefinitionEntry vde = (VariableDefinitionEntry) parent;
return vde.astName().astValue();
} else if (parent instanceof BinaryExpression) {
BinaryExpression be = (BinaryExpression) parent;
Expression left = be.astLeft();
if (left instanceof VariableReference || left instanceof Select) {
return be.astLeft().toString();
} else if (left instanceof ArrayAccess) {
ArrayAccess aa = (ArrayAccess) left;
return aa.astOperand().toString();
}
}
return null;
}
private static boolean isReachableFrom(
@NonNull Node method,
@NonNull MethodInvocation from,
@NonNull MethodInvocation to) {
ReachableVisitor visitor = new ReachableVisitor(from, to);
method.accept(visitor);
return visitor.isReachable();
}
private static class ReachableVisitor extends ForwardingAstVisitor {
@NonNull private final MethodInvocation mFrom;
@NonNull private final MethodInvocation mTo;
private boolean mReachable;
private boolean mSeenEnd;
public ReachableVisitor(@NonNull MethodInvocation from, @NonNull MethodInvocation to) {
mFrom = from;
mTo = to;
}
boolean isReachable() {
return mReachable;
}
@Override
public boolean visitMethodInvocation(MethodInvocation node) {
if (node == mFrom) {
mReachable = true;
} else if (node == mTo) {
mSeenEnd = true;
}
return super.visitMethodInvocation(node);
}
@Override
public boolean visitIf(If node) {
Expression condition = node.astCondition();
Statement body = node.astStatement();
Statement elseBody = node.astElseStatement();
if (condition != null) {
condition.accept(this);
}
if (body != null) {
boolean wasReachable = mReachable;
body.accept(this);
mReachable = wasReachable;
}
if (elseBody != null) {
boolean wasReachable = mReachable;
elseBody.accept(this);
mReachable = wasReachable;
}
endVisit(node);
return false;
}
@Override
public boolean visitNode(Node node) {
return mSeenEnd;
}
}
}
@@ -0,0 +1,115 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.ConstructorInvocation;
/**
* Checks for errors related to Date Formats
*/
public class DateFormatDetector extends Detector implements JavaScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
DateFormatDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Constructing SimpleDateFormat without an explicit locale */
public static final Issue DATE_FORMAT = Issue.create(
"SimpleDateFormat", //$NON-NLS-1$
"Implied locale in date format",
"Almost all callers should use `getDateInstance()`, `getDateTimeInstance()`, or " +
"`getTimeInstance()` to get a ready-made instance of SimpleDateFormat suitable " +
"for the user's locale. The main reason you'd create an instance this class " +
"directly is because you need to format/parse a specific machine-readable format, " +
"in which case you almost certainly want to explicitly ask for US to ensure that " +
"you get ASCII digits (rather than, say, Arabic digits).\n" +
"\n" +
"Therefore, you should either use the form of the SimpleDateFormat constructor " +
"where you pass in an explicit locale, such as Locale.US, or use one of the " +
"get instance methods, or suppress this error if really know what you are doing.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/reference/java/text/SimpleDateFormat.html");//$NON-NLS-1$
public static final String LOCALE_CLS = "java.util.Locale"; //$NON-NLS-1$
public static final String SIMPLE_DATE_FORMAT_CLS = "java.text.SimpleDateFormat"; //$NON-NLS-1$
/** Constructs a new {@link DateFormatDetector} */
public DateFormatDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableConstructorTypes() {
return Collections.singletonList(SIMPLE_DATE_FORMAT_CLS);
}
@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull ConstructorInvocation node, @NonNull ResolvedMethod constructor) {
if (!specifiesLocale(constructor)) {
Location location = context.getLocation(node);
String message =
"To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, " +
"or `getTimeInstance()`, or use `new SimpleDateFormat(String template, " +
"Locale locale)` with for example `Locale.US` for ASCII dates.";
context.report(DATE_FORMAT, node, location, message);
}
}
private static boolean specifiesLocale(@NonNull ResolvedMethod method) {
for (int i = 0, n = method.getArgumentCount(); i < n; i++) {
TypeDescriptor argumentType = method.getArgumentType(i);
if (argumentType.matchesSignature(LOCALE_CLS)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,177 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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));
}
}
@@ -0,0 +1,196 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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));
}
}
@@ -0,0 +1,117 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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;
}
}
}
@@ -0,0 +1,674 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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());
}
}
}
@@ -0,0 +1,295 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.Handle;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.tools.lint.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 @", "/");
}
}
@@ -0,0 +1,147 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.DefaultPosition;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Position;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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);
}
}
}
@@ -0,0 +1,270 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.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;
}
}
@@ -0,0 +1,165 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.CLASS_FRAGMENT;
import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
import static com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.TypeMember;
/**
* Checks that Fragment subclasses can be instantiated via
* {link {@link Class#newInstance()}}: the class is public, static, and has
* a public null constructor.
* <p>
* This helps track down issues like
* http://stackoverflow.com/questions/8058809/fragment-activity-crashes-on-screen-rotate
* (and countless duplicates)
*/
public class FragmentDetector extends Detector implements JavaScanner {
/** Are fragment subclasses instantiatable? */
public static final Issue ISSUE = Issue.create(
"ValidFragment", //$NON-NLS-1$
"Fragment not instantiatable",
"From the Fragment documentation:\n" +
"*Every* fragment must have an empty constructor, so it can be instantiated when " +
"restoring its activity's state. It is strongly recommended that subclasses do not " +
"have other constructors with parameters, since these constructors will not be " +
"called when the fragment is re-instantiated; instead, arguments can be supplied " +
"by the caller with `setArguments(Bundle)` and later retrieved by the Fragment " +
"with `getArguments()`.",
Category.CORRECTNESS,
6,
Severity.FATAL,
new Implementation(
FragmentDetector.class,
Scope.JAVA_FILE_SCOPE)
).addMoreInfo(
"http://developer.android.com/reference/android/app/Fragment.html#Fragment()"); //$NON-NLS-1$
/** Constructs a new {@link FragmentDetector} */
public FragmentDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Arrays.asList(CLASS_FRAGMENT, CLASS_V4_FRAGMENT);
}
@Override
public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration node,
@NonNull Node declarationOrAnonymous, @NonNull ResolvedClass cls) {
if (node == null) {
return;
}
int flags = node.astModifiers().getEffectiveModifierFlags();
if ((flags & Modifier.ABSTRACT) != 0) {
return;
}
if ((flags & Modifier.PUBLIC) == 0) {
String message = String.format("This fragment class should be public (%1$s)",
cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
return;
}
if (cls.getContainingClass() != null && (flags & Modifier.STATIC) == 0) {
String message = String.format(
"This fragment inner class should be static (%1$s)", cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
return;
}
boolean hasDefaultConstructor = false;
boolean hasConstructor = false;
NormalTypeBody body = node.astBody();
if (body != null) {
for (TypeMember member : body.astMembers()) {
if (member instanceof ConstructorDeclaration) {
hasConstructor = true;
ConstructorDeclaration constructor = (ConstructorDeclaration) member;
if (constructor.astParameters().isEmpty()) {
// The constructor must be public
if (constructor.astModifiers().isPublic()) {
hasDefaultConstructor = true;
} else {
Location location = context.getLocation(
constructor.astTypeName());
context.report(ISSUE, constructor, location,
"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;
}
} else {
Location location = context.getLocation(constructor.astTypeName());
// TODO: Use separate issue for this which isn't an error
String message = "Avoid non-default constructors in fragments: "
+ "use a default constructor plus "
+ "`Fragment#setArguments(Bundle)` instead";
context.report(ISSUE, constructor, location, message);
}
}
}
}
if (!hasDefaultConstructor && hasConstructor) {
String message = String.format(
"This fragment should provide a default constructor (a public " +
"constructor with no arguments) (`%1$s`)",
cls.getName());
context.report(ISSUE, node, context.getLocation(node.astName()), message);
}
}
}
@@ -0,0 +1,234 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Collection;
import java.util.List;
/**
* Check which makes sure that a full-backup-content descriptor file is valid and logical
*/
public class FullBackupContentDetector extends ResourceXmlDetector implements JavaScanner {
/**
* Validation of {@code <full-backup-content>} XML elements
*/
public static final Issue ISSUE = Issue.create(
"FullBackupContent", //$NON-NLS-1$
"Valid Full Backup Content File",
"Ensures that a `<full-backup-content>` file, which is pointed to by a " +
"`android:fullBackupContent attribute` in the manifest file, is valid",
Category.CORRECTNESS,
5,
Severity.FATAL,
new Implementation(
FullBackupContentDetector.class,
Scope.RESOURCE_FILE_SCOPE));
@SuppressWarnings("SpellCheckingInspection")
private static final String DOMAIN_SHARED_PREF = "sharedpref";
private static final String DOMAIN_ROOT = "root";
private static final String DOMAIN_FILE = "file";
private static final String DOMAIN_DATABASE = "database";
private static final String DOMAIN_EXTERNAL = "external";
private static final String TAG_EXCLUDE = "exclude";
private static final String TAG_INCLUDE = "include";
private static final String TAG_FULL_BACKUP_CONTENT = "full-backup-content";
private static final String ATTR_PATH = "path";
private static final String ATTR_DOMAIN = "domain";
/**
* Constructs a new {@link FullBackupContentDetector}
*/
public FullBackupContentDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.XML;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root == null) {
return;
}
if (!TAG_FULL_BACKUP_CONTENT.equals(root.getTagName())) {
return;
}
List<Element> includes = Lists.newArrayList();
List<Element> excludes = Lists.newArrayList();
NodeList children = root.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) child;
String tag = element.getTagName();
if (TAG_INCLUDE.equals(tag)) {
includes.add(element);
} else if (TAG_EXCLUDE.equals(tag)) {
excludes.add(element);
} else {
// See FullBackup#validateInnerTagContents
context.report(ISSUE, element, context.getNameLocation(element),
String.format("Unexpected element `<%1$s>`", tag));
}
}
}
Multimap<String, String> includePaths = ArrayListMultimap.create(includes.size(), 4);
for (Element include : includes) {
String domain = validateDomain(context, include);
String path = validatePath(context, include);
if (domain == null) {
continue;
}
includePaths.put(domain, path);
}
for (Element exclude : excludes) {
String excludePath = validatePath(context, exclude);
if (excludePath.isEmpty()) {
continue;
}
String domain = validateDomain(context, exclude);
if (domain == null) {
continue;
}
if (includePaths.isEmpty()) {
// There is no <include> anywhere: that means that everything
// is considered included and there's no potential prefix mismatch
continue;
}
boolean hasPrefix = false;
Collection<String> included = includePaths.get(domain);
if (included == null) {
continue;
}
for (String includePath : included) {
if (excludePath.startsWith(includePath)) {
if (excludePath.equals(includePath)) {
Attr pathNode = exclude.getAttributeNode(ATTR_PATH);
assert pathNode != null;
Location location = context.getValueLocation(pathNode);
// Find corresponding include path so we can link to it in the
// chained location list
for (Element include : includes) {
Attr includePathNode = include.getAttributeNode(ATTR_PATH);
String includeDomain = include.getAttribute(ATTR_DOMAIN);
if (includePathNode != null
&& excludePath.equals(includePathNode.getValue())
&& domain.equals(includeDomain)) {
Location earlier = context.getLocation(includePathNode);
earlier.setMessage("Unnecessary/conflicting <include>");
location.setSecondary(earlier);
}
}
context.report(ISSUE, exclude, location,
String.format("Include `%1$s` is also excluded", excludePath));
}
hasPrefix = true;
break;
}
}
if (!hasPrefix) {
Attr pathNode = exclude.getAttributeNode(ATTR_PATH);
assert pathNode != null;
context.report(ISSUE, exclude, context.getValueLocation(pathNode),
String.format("`%1$s` is not in an included path", excludePath));
}
}
}
@NonNull
private static String validatePath(@NonNull XmlContext context, @NonNull Element element) {
Attr pathNode = element.getAttributeNode(ATTR_PATH);
if (pathNode == null) {
return "";
}
String value = pathNode.getValue();
if (value.contains("//")) {
context.report(ISSUE, element, context.getValueLocation(pathNode),
"Paths are not allowed to contain `//`");
} else if (value.contains("..")) {
context.report(ISSUE, element, context.getValueLocation(pathNode),
"Paths are not allowed to contain `..`");
} else if (value.contains("/")) {
String domain = element.getAttribute(ATTR_DOMAIN);
if (DOMAIN_SHARED_PREF.equals(domain) || DOMAIN_DATABASE.equals(domain)) {
context.report(ISSUE, element, context.getValueLocation(pathNode),
String.format("Subdirectories are not allowed for domain `%1$s`",
domain));
}
}
return value;
}
@Nullable
private static String validateDomain(@NonNull XmlContext context, @NonNull Element element) {
Attr domainNode = element.getAttributeNode(ATTR_DOMAIN);
if (domainNode == null) {
context.report(ISSUE, element, context.getLocation(element),
String.format("Missing domain attribute, expected one of %1$s",
Joiner.on(", ").join(VALID_DOMAINS)));
return null;
}
String domain = domainNode.getValue();
for (String availableDomain : VALID_DOMAINS) {
if (availableDomain.equals(domain)) {
return domain;
}
}
context.report(ISSUE, element, context.getValueLocation(domainNode),
String.format("Unexpected domain `%1$s`, expected one of %2$s", domain,
Joiner.on(", ").join(VALID_DOMAINS)));
return domain;
}
/** Valid domains; see FullBackup#getTokenForXmlDomain for authoritative list */
private static final String[] VALID_DOMAINS = new String[] {
DOMAIN_ROOT, DOMAIN_FILE, DOMAIN_DATABASE, DOMAIN_SHARED_PREF, DOMAIN_EXTERNAL
};
}
@@ -0,0 +1,156 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.client.api.JavaParser.ResolvedField;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import java.util.Collections;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.Expression;
import lombok.ast.IntegralLiteral;
import lombok.ast.MethodInvocation;
import lombok.ast.StrictListAccessor;
import lombok.ast.StringLiteral;
public class GetSignaturesDetector extends Detector implements Detector.JavaScanner {
public static final Issue ISSUE = Issue.create(
"PackageManagerGetSignatures", //$NON-NLS-1$
"Potential Multiple Certificate Exploit",
"Improper validation of app signatures could lead to issues where a malicious app " +
"submits itself to the Play Store with both its real certificate and a fake " +
"certificate and gains access to functionality or information it shouldn't " +
"have due to another application only checking for the fake certificate and " +
"ignoring the rest. Please make sure to validate all signatures returned " +
"by this method.",
Category.SECURITY,
8,
Severity.INFORMATIONAL,
new Implementation(
GetSignaturesDetector.class,
Scope.JAVA_FILE_SCOPE))
.addMoreInfo("https://bluebox.com/technical/android-fake-id-vulnerability/");
private static final String PACKAGE_MANAGER_CLASS = "android.content.pm.PackageManager"; //$NON-NLS-1$
private static final String GET_PACKAGE_INFO = "getPackageInfo"; //$NON-NLS-1$
private static final int GET_SIGNATURES_FLAG = 0x00000040; //$NON-NLS-1$
// ---- Implements JavaScanner ----
@Override
@Nullable
public List<String> getApplicableMethodNames() {
return Collections.singletonList(GET_PACKAGE_INFO);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
ResolvedNode resolved = context.resolve(node);
if (!(resolved instanceof ResolvedMethod) ||
!((ResolvedMethod) resolved).getContainingClass()
.isSubclassOf(PACKAGE_MANAGER_CLASS, false)) {
return;
}
StrictListAccessor<Expression, MethodInvocation> argumentList = node.astArguments();
// Ignore if the method doesn't fit our description.
if (argumentList != null && argumentList.size() == 2) {
TypeDescriptor firstParameterType = context.getType(argumentList.first());
if (firstParameterType != null
&& firstParameterType.matchesSignature(JavaParser.TYPE_STRING)) {
maybeReportIssue(calculateValue(context, argumentList.last()), context, node);
}
}
}
private static void maybeReportIssue(
int flagValue, JavaContext context, MethodInvocation node) {
if ((flagValue & GET_SIGNATURES_FLAG) != 0) {
context.report(ISSUE, node, context.getLocation(node.astArguments().last()),
"Reading app signatures from getPackageInfo: The app signatures "
+ "could be exploited if not validated properly; "
+ "see issue explanation for details.");
}
}
private static int calculateValue(JavaContext context, Expression expression) {
// This function assumes that the only inputs to the expression are static integer
// flags that combined via bitwise operands.
if (expression instanceof IntegralLiteral) {
return ((IntegralLiteral) expression).astIntValue();
}
ResolvedNode resolvedNode = context.resolve(expression);
if (resolvedNode instanceof ResolvedField) {
Object value = ((ResolvedField) resolvedNode).getValue();
if (value instanceof Integer) {
return (Integer) value;
}
}
if (expression instanceof BinaryExpression) {
BinaryExpression binaryExpression = (BinaryExpression) expression;
BinaryOperator operator = binaryExpression.astOperator();
int leftValue = calculateValue(context, binaryExpression.astLeft());
int rightValue = calculateValue(context, binaryExpression.astRight());
if (operator == BinaryOperator.BITWISE_OR) {
return leftValue | rightValue;
}
if (operator == BinaryOperator.BITWISE_AND) {
return leftValue & rightValue;
}
if (operator == BinaryOperator.BITWISE_XOR) {
return leftValue ^ rightValue;
}
}
return 0;
}
private static boolean isStringParameter(
@NonNull Expression expression, @NonNull JavaContext context) {
if (expression instanceof StringLiteral) {
return true;
} else {
ResolvedNode resolvedNode = context.resolve(expression);
if (resolvedNode instanceof ResolvedField) {
if (((ResolvedField) resolvedNode).getValue() instanceof String) {
return true;
}
}
}
return false;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,246 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.tools.lint.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;
}
}
@@ -0,0 +1,139 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.Node;
/**
* Checks that Handler implementations are top level classes or static.
* See the corresponding check in the android.os.Handler source code.
*/
public class HandlerDetector extends Detector implements Detector.JavaScanner {
/** Potentially leaking handlers */
public static final Issue ISSUE = Issue.create(
"HandlerLeak", //$NON-NLS-1$
"Handler reference leaks",
"Since this Handler is declared as an inner class, it may prevent the outer " +
"class from being garbage collected. If the Handler is using a Looper or " +
"MessageQueue for a thread other than the main thread, then there is no issue. " +
"If the Handler is using the Looper or MessageQueue of the main thread, you " +
"need to fix your Handler declaration, as follows: Declare the Handler as a " +
"static class; In the outer class, instantiate a WeakReference to the outer " +
"class and pass this object to your Handler when you instantiate the Handler; " +
"Make all references to members of the outer class using the WeakReference object.",
Category.PERFORMANCE,
4,
Severity.WARNING,
new Implementation(
HandlerDetector.class,
Scope.JAVA_FILE_SCOPE));
private static final String LOOPER_CLS = "android.os.Looper";
private static final String HANDLER_CLS = "android.os.Handler";
/** Constructs a new {@link HandlerDetector} */
public HandlerDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(HANDLER_CLS);
}
@Override
public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration,
@NonNull Node node, @NonNull ResolvedClass cls) {
if (!isInnerClass(declaration)) {
return;
}
if (isStaticClass(declaration)) {
return;
}
// Only flag handlers using the default looper
if (hasLooperConstructorParameter(cls)) {
return;
}
Node locationNode = node instanceof ClassDeclaration
? ((ClassDeclaration) node).astName() : node;
Location location = context.getLocation(locationNode);
context.report(ISSUE, location, String.format(
"This Handler class should be static or leaks might occur (%1$s)",
cls.getName()));
}
private static boolean isInnerClass(@Nullable ClassDeclaration node) {
return node == null || // null class declarations means anonymous inner class
JavaContext.getParentOfType(node, ClassDeclaration.class, true) != null;
}
private static boolean isStaticClass(@Nullable ClassDeclaration node) {
if (node == null) {
// A null class declaration means anonymous inner class, and these can't be static
return false;
}
int flags = node.astModifiers().getEffectiveModifierFlags();
return (flags & Modifier.STATIC) != 0;
}
private static boolean hasLooperConstructorParameter(@NonNull ResolvedClass cls) {
for (ResolvedMethod constructor : cls.getConstructors()) {
for (int i = 0, n = constructor.getArgumentCount(); i < n; i++) {
TypeDescriptor type = constructor.getArgumentType(i);
if (type.matchesSignature(LOOPER_CLS)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,97 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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");
}
}
}
@@ -0,0 +1,117 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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));
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,148 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import 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;
}
}
@@ -0,0 +1,387 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.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.lint.client.api.SdkInfo;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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`");
}
}
}
}
}
@@ -0,0 +1,309 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.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;
}
}
}
@@ -0,0 +1,594 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.lint.client.api.JavaParser.TYPE_INT;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.If;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
import lombok.ast.This;
import lombok.ast.Throw;
import lombok.ast.TypeReference;
import lombok.ast.TypeReferencePart;
import lombok.ast.UnaryExpression;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableReference;
/**
* Looks for performance issues in Java files, such as memory allocations during
* drawing operations and using HashMap instead of SparseArray.
*/
public class JavaPerformanceDetector extends Detector implements Detector.JavaScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
JavaPerformanceDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Allocating objects during a paint method */
public static final Issue PAINT_ALLOC = Issue.create(
"DrawAllocation", //$NON-NLS-1$
"Memory allocations within drawing code",
"You should avoid allocating objects during a drawing or layout operation. These " +
"are called frequently, so a smooth UI can be interrupted by garbage collection " +
"pauses caused by the object allocations.\n" +
"\n" +
"The way this is generally handled is to allocate the needed objects up front " +
"and to reuse them for each drawing operation.\n" +
"\n" +
"Some methods allocate memory on your behalf (such as `Bitmap.create`), and these " +
"should be handled in the same way.",
Category.PERFORMANCE,
9,
Severity.WARNING,
IMPLEMENTATION);
/** Using HashMaps where SparseArray would be better */
public static final Issue USE_SPARSE_ARRAY = Issue.create(
"UseSparseArrays", //$NON-NLS-1$
"HashMap can be replaced with SparseArray",
"For maps where the keys are of type integer, it's typically more efficient to " +
"use the Android `SparseArray` API. This check identifies scenarios where you might " +
"want to consider using `SparseArray` instead of `HashMap` for better performance.\n" +
"\n" +
"This is *particularly* useful when the value types are primitives like ints, " +
"where you can use `SparseIntArray` and avoid auto-boxing the values from `int` to " +
"`Integer`.\n" +
"\n" +
"If you need to construct a `HashMap` because you need to call an API outside of " +
"your control which requires a `Map`, you can suppress this warning using for " +
"example the `@SuppressLint` annotation.",
Category.PERFORMANCE,
4,
Severity.WARNING,
IMPLEMENTATION);
/** Using {@code new Integer()} instead of the more efficient {@code Integer.valueOf} */
public static final Issue USE_VALUE_OF = Issue.create(
"UseValueOf", //$NON-NLS-1$
"Should use `valueOf` instead of `new`",
"You should not call the constructor for wrapper classes directly, such as" +
"`new Integer(42)`. Instead, call the `valueOf` factory method, such as " +
"`Integer.valueOf(42)`. This will typically use less memory because common integers " +
"such as 0 and 1 will share a single instance.",
Category.PERFORMANCE,
4,
Severity.WARNING,
IMPLEMENTATION);
static final String ON_MEASURE = "onMeasure"; //$NON-NLS-1$
static final String ON_DRAW = "onDraw"; //$NON-NLS-1$
static final String ON_LAYOUT = "onLayout"; //$NON-NLS-1$
private static final String INTEGER = "Integer"; //$NON-NLS-1$
private static final String BOOLEAN = "Boolean"; //$NON-NLS-1$
private static final String BYTE = "Byte"; //$NON-NLS-1$
private static final String LONG = "Long"; //$NON-NLS-1$
private static final String CHARACTER = "Character"; //$NON-NLS-1$
private static final String DOUBLE = "Double"; //$NON-NLS-1$
private static final String FLOAT = "Float"; //$NON-NLS-1$
private static final String HASH_MAP = "HashMap"; //$NON-NLS-1$
private static final String SPARSE_ARRAY = "SparseArray"; //$NON-NLS-1$
private static final String CANVAS = "Canvas"; //$NON-NLS-1$
private static final String LAYOUT = "layout"; //$NON-NLS-1$
/** Constructs a new {@link JavaPerformanceDetector} check */
public JavaPerformanceDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements JavaScanner ----
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
List<Class<? extends Node>> types = new ArrayList<Class<? extends Node>>(3);
types.add(ConstructorInvocation.class);
types.add(MethodDeclaration.class);
types.add(MethodInvocation.class);
return types;
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return new PerformanceVisitor(context);
}
private static class PerformanceVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
private final boolean mCheckMaps;
private final boolean mCheckAllocations;
private final boolean mCheckValueOf;
/** Whether allocations should be "flagged" in the current method */
private boolean mFlagAllocations;
public PerformanceVisitor(JavaContext context) {
mContext = context;
mCheckAllocations = context.isEnabled(PAINT_ALLOC);
mCheckMaps = context.isEnabled(USE_SPARSE_ARRAY);
mCheckValueOf = context.isEnabled(USE_VALUE_OF);
}
@Override
public boolean visitMethodDeclaration(MethodDeclaration node) {
mFlagAllocations = isBlockedAllocationMethod(node);
return super.visitMethodDeclaration(node);
}
@Override
public boolean visitConstructorInvocation(ConstructorInvocation node) {
String typeName = null;
if (mCheckMaps) {
TypeReference reference = node.astTypeReference();
typeName = reference.astParts().last().astIdentifier().astValue();
// TODO: Should we handle factory method constructions of HashMaps as well,
// e.g. via Guava? This is a bit trickier since we need to infer the type
// arguments from the calling context.
if (typeName.equals(HASH_MAP)) {
checkHashMap(node, reference);
} else if (typeName.equals(SPARSE_ARRAY)) {
checkSparseArray(node, reference);
}
}
if (mCheckValueOf) {
if (typeName == null) {
TypeReference reference = node.astTypeReference();
typeName = reference.astParts().last().astIdentifier().astValue();
}
if ((typeName.equals(INTEGER)
|| typeName.equals(BOOLEAN)
|| typeName.equals(FLOAT)
|| typeName.equals(CHARACTER)
|| typeName.equals(LONG)
|| typeName.equals(DOUBLE)
|| typeName.equals(BYTE))
&& node.astTypeReference().astParts().size() == 1
&& node.astArguments().size() == 1) {
String argument = node.astArguments().first().toString();
mContext.report(USE_VALUE_OF, node, mContext.getLocation(node), getUseValueOfErrorMessage(
typeName, argument));
}
}
if (mFlagAllocations && !(node.getParent() instanceof Throw) && mCheckAllocations) {
// Make sure we're still inside the method declaration that marked
// mInDraw as true, in case we've left it and we're in a static
// block or something:
Node method = node;
while (method != null) {
if (method instanceof MethodDeclaration) {
break;
}
method = method.getParent();
}
if (method != null && isBlockedAllocationMethod(((MethodDeclaration) method))
&& !isLazilyInitialized(node)) {
reportAllocation(node);
}
}
return super.visitConstructorInvocation(node);
}
private void reportAllocation(Node node) {
mContext.report(PAINT_ALLOC, node, mContext.getLocation(node),
"Avoid object allocations during draw/layout operations (preallocate and " +
"reuse instead)");
}
@Override
public boolean visitMethodInvocation(MethodInvocation node) {
if (mFlagAllocations && node.astOperand() != null) {
// Look for forbidden methods
String methodName = node.astName().astValue();
if (methodName.equals("createBitmap") //$NON-NLS-1$
|| methodName.equals("createScaledBitmap")) { //$NON-NLS-1$
String operand = node.astOperand().toString();
if (operand.equals("Bitmap") //$NON-NLS-1$
|| operand.equals("android.graphics.Bitmap")) { //$NON-NLS-1$
if (!isLazilyInitialized(node)) {
reportAllocation(node);
}
}
} else if (methodName.startsWith("decode")) { //$NON-NLS-1$
// decodeFile, decodeByteArray, ...
String operand = node.astOperand().toString();
if (operand.equals("BitmapFactory") //$NON-NLS-1$
|| operand.equals("android.graphics.BitmapFactory")) { //$NON-NLS-1$
if (!isLazilyInitialized(node)) {
reportAllocation(node);
}
}
} else if (methodName.equals("getClipBounds")) { //$NON-NLS-1$
if (node.astArguments().isEmpty()) {
mContext.report(PAINT_ALLOC, node, mContext.getLocation(node),
"Avoid object allocations during draw operations: Use " +
"`Canvas.getClipBounds(Rect)` instead of `Canvas.getClipBounds()` " +
"which allocates a temporary `Rect`");
}
}
}
return super.visitMethodInvocation(node);
}
/**
* Check whether the given invocation is done as a lazy initialization,
* e.g. {@code if (foo == null) foo = new Foo();}.
* <p>
* This tries to also handle the scenario where the check is on some
* <b>other</b> variable - e.g.
* <pre>
* if (foo == null) {
* foo == init1();
* bar = new Bar();
* }
* </pre>
* or
* <pre>
* if (!initialized) {
* initialized = true;
* bar = new Bar();
* }
* </pre>
*/
private static boolean isLazilyInitialized(Node node) {
Node curr = node.getParent();
while (curr != null) {
if (curr instanceof MethodDeclaration) {
return false;
} else if (curr instanceof If) {
If ifNode = (If) curr;
// See if the if block represents a lazy initialization:
// compute all variable names seen in the condition
// (e.g. for "if (foo == null || bar != foo)" the result is "foo,bar"),
// and then compute all variables assigned to in the if body,
// and if there is an overlap, we'll consider the whole if block
// guarded (so lazily initialized and an allocation we won't complain
// about.)
List<String> assignments = new ArrayList<String>();
AssignmentTracker visitor = new AssignmentTracker(assignments);
ifNode.astStatement().accept(visitor);
if (!assignments.isEmpty()) {
List<String> references = new ArrayList<String>();
addReferencedVariables(references, ifNode.astCondition());
if (!references.isEmpty()) {
SetView<String> intersection = Sets.intersection(
new HashSet<String>(assignments),
new HashSet<String>(references));
return !intersection.isEmpty();
}
}
return false;
}
curr = curr.getParent();
}
return false;
}
/** Adds any variables referenced in the given expression into the given list */
private static void addReferencedVariables(Collection<String> variables,
Expression expression) {
if (expression instanceof BinaryExpression) {
BinaryExpression binary = (BinaryExpression) expression;
addReferencedVariables(variables, binary.astLeft());
addReferencedVariables(variables, binary.astRight());
} else if (expression instanceof UnaryExpression) {
UnaryExpression unary = (UnaryExpression) expression;
addReferencedVariables(variables, unary.astOperand());
} else if (expression instanceof VariableReference) {
VariableReference reference = (VariableReference) expression;
variables.add(reference.astIdentifier().astValue());
} else if (expression instanceof Select) {
Select select = (Select) expression;
if (select.astOperand() instanceof This) {
variables.add(select.astIdentifier().astValue());
}
}
}
/**
* Returns whether the given method declaration represents a method
* where allocating objects is not allowed for performance reasons
*/
private static boolean isBlockedAllocationMethod(MethodDeclaration node) {
return isOnDrawMethod(node) || isOnMeasureMethod(node) || isOnLayoutMethod(node)
|| isLayoutMethod(node);
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code protected void onDraw(Canvas canvas)}
*/
private static boolean isOnDrawMethod(MethodDeclaration node) {
if (ON_DRAW.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 1) {
VariableDefinition arg0 = parameters.first();
TypeReferencePart type = arg0.astTypeReference().astParts().last();
String typeName = type.getTypeName();
if (typeName.equals(CANVAS)) {
return true;
}
}
}
return false;
}
/**
* Returns true if this method looks like it's overriding
* android.view.View's
* {@code protected void onLayout(boolean changed, int left, int top,
* int right, int bottom)}
*/
private static boolean isOnLayoutMethod(MethodDeclaration node) {
if (ON_LAYOUT.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 5) {
Iterator<VariableDefinition> iterator = parameters.iterator();
if (!iterator.hasNext()) {
return false;
}
// Ensure that the argument list matches boolean, int, int, int, int
TypeReferencePart type = iterator.next().astTypeReference().astParts().last();
if (!type.getTypeName().equals(TYPE_BOOLEAN) || !iterator.hasNext()) {
return false;
}
for (int i = 0; i < 4; i++) {
type = iterator.next().astTypeReference().astParts().last();
if (!type.getTypeName().equals(TYPE_INT)) {
return false;
}
if (!iterator.hasNext()) {
return i == 3;
}
}
}
}
return false;
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)}
*/
private static boolean isOnMeasureMethod(MethodDeclaration node) {
if (ON_MEASURE.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 2) {
VariableDefinition arg0 = parameters.first();
VariableDefinition arg1 = parameters.last();
TypeReferencePart type1 = arg0.astTypeReference().astParts().last();
TypeReferencePart type2 = arg1.astTypeReference().astParts().last();
return TYPE_INT.equals(type1.getTypeName())
&& TYPE_INT.equals(type2.getTypeName());
}
}
return false;
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code public void layout(int l, int t, int r, int b)}
*/
private static boolean isLayoutMethod(MethodDeclaration node) {
if (LAYOUT.equals(node.astMethodName().astValue())) {
StrictListAccessor<VariableDefinition, MethodDeclaration> parameters =
node.astParameters();
if (parameters != null && parameters.size() == 4) {
Iterator<VariableDefinition> iterator = parameters.iterator();
for (int i = 0; i < 4; i++) {
if (!iterator.hasNext()) {
return false;
}
VariableDefinition next = iterator.next();
TypeReferencePart type = next.astTypeReference().astParts().last();
if (!TYPE_INT.equals(type.getTypeName())) {
return false;
}
}
return true;
}
}
return false;
}
/**
* Checks whether the given constructor call and type reference refers
* to a HashMap constructor call that is eligible for replacement by a
* SparseArray call instead
*/
private void checkHashMap(ConstructorInvocation node, TypeReference reference) {
// reference.hasTypeArguments returns false where it should not
StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments();
if (types != null && types.size() == 2) {
TypeReference first = types.first();
String typeName = first.getTypeName();
int minSdk = mContext.getMainProject().getMinSdk();
if (typeName.equals(INTEGER) || typeName.equals(BYTE)) {
String valueType = types.last().getTypeName();
if (valueType.equals(INTEGER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use new `SparseIntArray(...)` instead for better performance");
} else if (valueType.equals(LONG) && minSdk >= 18) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseLongArray(...)` instead for better performance");
} else if (valueType.equals(BOOLEAN)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
} else {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
String.format(
"Use `new SparseArray<%1$s>(...)` instead for better performance",
valueType));
}
} else if (typeName.equals(LONG) && (minSdk >= 16 ||
Boolean.TRUE == mContext.getMainProject().dependsOn(
SUPPORT_LIB_ARTIFACT))) {
boolean useBuiltin = minSdk >= 16;
String message = useBuiltin ?
"Use `new LongSparseArray(...)` instead for better performance" :
"Use `new android.support.v4.util.LongSparseArray(...)` instead for better performance";
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
message);
}
}
}
private void checkSparseArray(ConstructorInvocation node, TypeReference reference) {
// reference.hasTypeArguments returns false where it should not
StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments();
if (types != null && types.size() == 1) {
TypeReference first = types.first();
String valueType = first.getTypeName();
if (valueType.equals(INTEGER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseIntArray(...)` instead for better performance");
} else if (valueType.equals(BOOLEAN)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
}
}
}
}
private static String getUseValueOfErrorMessage(String typeName, String argument) {
// Keep in sync with {@link #getReplacedType} below
return String.format("Use `%1$s.valueOf(%2$s)` instead", typeName, argument);
}
/**
* For an error message for an {@link #USE_VALUE_OF} issue reported by this detector,
* returns the type being replaced. Intended to use for IDE quickfix implementations.
*/
@Nullable
public static String getReplacedType(@NonNull String message, @NonNull TextFormat format) {
message = format.toText(message);
int index = message.indexOf('.');
if (index != -1 && message.startsWith("Use ")) {
return message.substring(4, index);
}
return null;
}
/** Visitor which records variable names assigned into */
private static class AssignmentTracker extends ForwardingAstVisitor {
private final Collection<String> mVariables;
public AssignmentTracker(Collection<String> variables) {
mVariables = variables;
}
@Override
public boolean visitBinaryExpression(BinaryExpression node) {
BinaryOperator operator = node.astOperator();
if (operator == BinaryOperator.ASSIGN || operator == BinaryOperator.OR_ASSIGN) {
Expression left = node.astLeft();
String variable;
if (left instanceof Select && ((Select) left).astOperand() instanceof This) {
variable = ((Select) left).astIdentifier().astValue();
} else {
variable = left.toString();
}
mVariables.add(variable);
}
return super.visitBinaryExpression(node);
}
}
}
@@ -0,0 +1,321 @@
/*
* 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.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.client.api.JavaParser.ResolvedVariable;
import com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.google.common.collect.Maps;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.Cast;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.InlineIfExpression;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Looks for addJavascriptInterface calls on interfaces have been properly annotated
* with {@code @JavaScriptInterface}
*/
public class JavaScriptInterfaceDetector extends Detector implements Detector.JavaScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"JavascriptInterface", //$NON-NLS-1$
"Missing @JavascriptInterface on methods",
"As of API 17, you must annotate methods in objects registered with the " +
"`addJavascriptInterface` method with a `@JavascriptInterface` annotation.",
Category.SECURITY,
8,
Severity.ERROR,
new Implementation(
JavaScriptInterfaceDetector.class,
Scope.JAVA_FILE_SCOPE))
.addMoreInfo(
"http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)"); //$NON-NLS-1$
private static final String ADD_JAVASCRIPT_INTERFACE = "addJavascriptInterface"; //$NON-NLS-1$
private static final String JAVASCRIPT_INTERFACE_CLS = "android.webkit.JavascriptInterface"; //$NON-NLS-1$
private static final String WEB_VIEW_CLS = "android.webkit.WebView"; //$NON-NLS-1$
/** Constructs a new {@link JavaScriptInterfaceDetector} check */
public JavaScriptInterfaceDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW; // because it relies on class loading referenced javascript interface
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE);
}
@Override
public void visitMethod(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull MethodInvocation call) {
if (context.getMainProject().getTargetSdk() < 17) {
return;
}
if (call.astArguments().size() != 2) {
return;
}
if (!isCallOnWebView(context, call)) {
return;
}
Expression first = call.astArguments().first();
ResolvedNode resolved = context.resolve(first);
if (resolved instanceof ResolvedVariable) {
// We're passing in a variable to the addJavaScriptInterface method;
// the variable may be of a more generic type than the actual
// value assigned to it. For example, we may have a scenario like this:
// Object object = new SpecificType();
// addJavaScriptInterface(object, ...)
// Here the type of the variable is Object, but we know that it can
// contain objects of type SpecificType, so we should check that type instead.
Node method = JavaContext.findSurroundingMethod(call);
if (method != null) {
ConcreteTypeVisitor v = new ConcreteTypeVisitor(context, call);
method.accept(v);
resolved = v.getType();
if (resolved == null) {
return;
}
} else {
return;
}
} else if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
if (method.isConstructor()) {
resolved = method.getContainingClass();
} else {
TypeDescriptor returnType = method.getReturnType();
if (returnType != null) {
resolved = returnType.getTypeClass();
}
}
} else {
TypeDescriptor type = context.getType(first);
if (type != null) {
resolved = type.getTypeClass();
}
}
if (resolved instanceof ResolvedClass) {
ResolvedClass cls = (ResolvedClass) resolved;
if (isJavaScriptAnnotated(cls)) {
return;
}
Location location = context.getLocation(call.astName());
String message = String.format(
"None of the methods in the added interface (%1$s) have been annotated " +
"with `@android.webkit.JavascriptInterface`; they will not " +
"be visible in API 17", cls.getSimpleName());
context.report(ISSUE, call, location, message);
}
}
private static boolean isCallOnWebView(JavaContext context, MethodInvocation call) {
ResolvedNode resolved = context.resolve(call);
if (!(resolved instanceof ResolvedMethod)) {
return false;
}
ResolvedMethod method = (ResolvedMethod) resolved;
return method.getContainingClass().matches(WEB_VIEW_CLS);
}
private static boolean isJavaScriptAnnotated(ResolvedClass clz) {
while (clz != null) {
for (ResolvedAnnotation annotation : clz.getAnnotations()) {
if (annotation.getType().matchesSignature(JAVASCRIPT_INTERFACE_CLS)) {
return true;
}
}
for (ResolvedMethod method : clz.getMethods(false)) {
for (ResolvedAnnotation annotation : method.getAnnotations()) {
if (annotation.getType().matchesSignature(JAVASCRIPT_INTERFACE_CLS)) {
return true;
}
}
}
clz = clz.getSuperClass();
}
return false;
}
private static class ConcreteTypeVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
private final MethodInvocation mTargetCall;
private boolean mFoundCall;
private Map<Node, ResolvedClass> mTypes = Maps.newIdentityHashMap();
private Map<ResolvedVariable, ResolvedClass> mVariableTypes = Maps.newHashMap();
public ConcreteTypeVisitor(JavaContext context, MethodInvocation call) {
mContext = context;
mTargetCall = call;
}
public ResolvedClass getType() {
Expression first = mTargetCall.astArguments().first();
ResolvedClass resolvedClass = mTypes.get(first);
if (resolvedClass == null) {
ResolvedNode resolved = mContext.resolve(first);
if (resolved instanceof ResolvedVariable) {
resolvedClass = mVariableTypes.get(resolved);
if (resolvedClass == null) {
return ((ResolvedVariable)resolved).getType().getTypeClass();
}
}
}
return resolvedClass;
}
@Override
public boolean visitNode(Node node) {
return mFoundCall || super.visitNode(node);
}
@Override
public void afterVisitMethodInvocation(MethodInvocation node) {
if (node == mTargetCall) {
mFoundCall = true;
}
}
@Override
public void afterVisitConstructorInvocation(@NonNull ConstructorInvocation node) {
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
mTypes.put(node, method.getContainingClass());
} else {
// Implicit constructor?
TypeDescriptor type = mContext.getType(node);
if (type != null) {
ResolvedClass typeClass = type.getTypeClass();
if (typeClass != null) {
mTypes.put(node, typeClass);
}
}
}
}
@Override
public void afterVisitVariableReference(VariableReference node) {
if (mTypes.get(node) == null) {
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof ResolvedVariable) {
ResolvedClass resolvedClass = mVariableTypes.get(resolved);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
}
}
@Override
public void afterVisitBinaryExpression(BinaryExpression node) {
if (node.astOperator() == BinaryOperator.ASSIGN) {
Expression rhs = node.astRight();
ResolvedClass resolvedClass = mTypes.get(rhs);
if (resolvedClass != null) {
Expression lhs = node.astLeft();
mTypes.put(lhs, resolvedClass);
ResolvedNode variable = mContext.resolve(lhs);
if (variable instanceof ResolvedVariable) {
mVariableTypes.put((ResolvedVariable) variable, resolvedClass);
}
}
}
}
@Override
public void afterVisitInlineIfExpression(InlineIfExpression node) {
ResolvedClass resolvedClass = mTypes.get(node.astIfTrue());
if (resolvedClass == null) {
resolvedClass = mTypes.get(node.astIfFalse());
}
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
@Override
public void afterVisitVariableDefinitionEntry(VariableDefinitionEntry node) {
Expression initializer = node.astInitializer();
if (initializer != null) {
ResolvedClass resolvedClass = mTypes.get(initializer);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
ResolvedNode variable = mContext.resolve(node);
if (variable instanceof ResolvedVariable) {
mVariableTypes.put((ResolvedVariable) variable, resolvedClass);
}
}
}
}
@Override
public void afterVisitCast(Cast node) {
ResolvedClass resolvedClass = mTypes.get(node);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
}
}
@@ -0,0 +1,171 @@
/*
* 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.lint.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.lint.detector.api.LintUtils.stripIdPrefix;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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);
}
}
@@ -0,0 +1,446 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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.Document;
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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.ast.AstVisitor;
/**
* Checks for consistency in layouts across different resource folders
*/
public class LayoutConsistencyDetector extends LayoutDetector implements Detector.JavaScanner {
/** Map from layout resource names to a list of files defining that resource,
* and within each file the value is a map from string ids to the widget type
* used by that id in this file */
private final Map<String, List<Pair<File, Map<String, String>>>> mMap =
Maps.newHashMapWithExpectedSize(64);
/** Ids referenced from .java files. Only ids referenced from code are considered
* vital to be consistent among the layout variations (others could just have ids
* assigned to them in the layout either automatically by the layout editor or there
* in order to support RelativeLayout constraints etc, but not be problematic
* in findViewById calls.)
*/
private final Set<String> mRelevantIds = Sets.newLinkedHashSetWithExpectedSize(64);
/** Map from layout to id name to a list of locations */
private Map<String, Map<String, List<Location>>> mLocations;
/** Map from layout to id name to the error message to display for each */
private Map<String, Map<String, String>> mErrorMessages;
/** Inconsistent widget types */
public static final Issue INCONSISTENT_IDS = Issue.create(
"InconsistentLayout", //$NON-NLS-1$
"Inconsistent Layouts",
"This check ensures that a layout resource which is defined in multiple "
+ "resource folders, specifies the same set of widgets.\n"
+ "\n"
+ "This finds cases where you have accidentally forgotten to add "
+ "a widget to all variations of the layout, which could result "
+ "in a runtime crash for some resource configurations when a "
+ "`findViewById()` fails.\n"
+ "\n"
+ "There *are* cases where this is intentional. For example, you "
+ "may have a dedicated large tablet layout which adds some extra "
+ "widgets that are not present in the phone version of the layout. "
+ "As long as the code accessing the layout resource is careful to "
+ "handle this properly, it is valid. In that case, you can suppress "
+ "this lint check for the given extra or missing views, or the whole "
+ "layout",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
LayoutConsistencyDetector.class,
Scope.JAVA_AND_RESOURCE_FILES));
/** Constructs a consistency check */
public LayoutConsistencyDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root != null) {
if (context.getPhase() == 1) {
// Map from ids to types
Map<String,String> fileMap = Maps.newHashMapWithExpectedSize(10);
addIds(root, fileMap);
getFileMapList(context).add(Pair.of(context.file, fileMap));
} else {
String name = LintUtils.getLayoutName(context.file);
Map<String, List<Location>> map = mLocations.get(name);
if (map != null) {
lookupLocations(context, root, map);
}
}
}
}
@NonNull
private List<Pair<File, Map<String, String>>> getFileMapList(
@NonNull XmlContext context) {
String name = LintUtils.getLayoutName(context.file);
List<Pair<File, Map<String, String>>> list = mMap.get(name);
if (list == null) {
list = Lists.newArrayListWithCapacity(4);
mMap.put(name, list);
}
return list;
}
@Nullable
private static String getId(@NonNull Element element) {
String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
if (id != null && !id.isEmpty() && !id.startsWith(ANDROID_PREFIX)) {
return stripIdPrefix(id);
}
return null;
}
private static void addIds(Element element, Map<String,String> map) {
String id = getId(element);
if (id != null) {
String s = stripIdPrefix(id);
map.put(s, element.getTagName());
}
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
addIds((Element) child, map);
}
}
}
private static void lookupLocations(
@NonNull XmlContext context,
@NonNull Element element,
@NonNull Map<String, List<Location>> map) {
String id = getId(element);
if (id != null) {
if (map.containsKey(id)) {
if (context.getDriver().isSuppressed(context, INCONSISTENT_IDS, element)) {
map.remove(id);
return;
}
List<Location> locations = map.get(id);
if (locations == null) {
locations = Lists.newArrayList();
map.put(id, locations);
}
Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_ID);
assert attr != null;
Location location = context.getLocation(attr);
String folder = context.file.getParentFile().getName();
location.setMessage(String.format("Occurrence in %1$s", folder));
locations.add(location);
}
}
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
lookupLocations(context, (Element) child, map);
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
LintDriver driver = context.getDriver();
if (driver.getPhase() == 1) {
// First phase: gather all the ids and look for consistency issues.
// If any are found, request location computation in phase 2 by
// writing the ids needed for each layout in the {@link #mLocations} map.
for (Map.Entry<String,List<Pair<File,Map<String,String>>>> entry : mMap.entrySet()) {
String layout = entry.getKey();
List<Pair<File, Map<String, String>>> files = entry.getValue();
if (files.size() < 2) {
// No consistency problems for files that don't have resource variations
continue;
}
checkConsistentIds(layout, files);
}
if (mLocations != null) {
driver.requestRepeat(this, Scope.ALL_RESOURCES_SCOPE);
}
} else {
// Collect results and print
if (!mLocations.isEmpty()) {
reportErrors(context);
}
}
}
@NonNull
private Set<String> stripIrrelevantIds(@NonNull Set<String> ids) {
if (!mRelevantIds.isEmpty()) {
Set<String> stripped = new HashSet<String>(ids);
stripped.retainAll(mRelevantIds);
return stripped;
}
return Collections.emptySet();
}
private void checkConsistentIds(
@NonNull String layout,
@NonNull List<Pair<File, Map<String, String>>> files) {
int layoutCount = files.size();
assert layoutCount >= 2;
Map<File, Set<String>> idMap = getIdMap(files, layoutCount);
Set<String> inconsistent = getInconsistentIds(idMap);
if (inconsistent.isEmpty()) {
return;
}
if (mLocations == null) {
mLocations = Maps.newHashMap();
}
if (mErrorMessages == null) {
mErrorMessages = Maps.newHashMap();
}
// Map from each id, to a list of layout folders it is present in
int idCount = inconsistent.size();
Map<String, List<String>> presence = Maps.newHashMapWithExpectedSize(idCount);
Set<String> allLayouts = Sets.newHashSetWithExpectedSize(layoutCount);
for (Map.Entry<File, Set<String>> entry : idMap.entrySet()) {
File file = entry.getKey();
String folder = file.getParentFile().getName();
allLayouts.add(folder);
Set<String> ids = entry.getValue();
for (String id : ids) {
List<String> list = presence.get(id);
if (list == null) {
list = Lists.newArrayListWithExpectedSize(layoutCount);
presence.put(id, list);
}
list.add(folder);
}
}
// Compute lookup maps which will be used in phase 2 to initialize actual
// locations for the id references
Map<String, List<Location>> map = Maps.newHashMapWithExpectedSize(idCount);
mLocations.put(layout, map);
Map<String, String> messages = Maps.newHashMapWithExpectedSize(idCount);
mErrorMessages.put(layout, messages);
for (String id : inconsistent) {
map.put(id, null); // The locations will be filled in during the second phase
// Determine presence description for this id
String message;
List<String> layouts = presence.get(id);
Collections.sort(layouts);
Set<String> missingSet = new HashSet<String>(allLayouts);
missingSet.removeAll(layouts);
List<String> missing = new ArrayList<String>(missingSet);
Collections.sort(missing);
if (layouts.size() < layoutCount / 2) {
message = String.format(
"The id \"%1$s\" in layout \"%2$s\" is only present in the following "
+ "layout configurations: %3$s (missing from %4$s)",
id, layout,
LintUtils.formatList(layouts, Integer.MAX_VALUE),
LintUtils.formatList(missing, Integer.MAX_VALUE));
} else {
message = String.format(
"The id \"%1$s\" in layout \"%2$s\" is missing from the following layout "
+ "configurations: %3$s (present in %4$s)",
id, layout, LintUtils.formatList(missing, Integer.MAX_VALUE),
LintUtils.formatList(layouts, Integer.MAX_VALUE));
}
messages.put(id, message);
}
}
private static Set<String> getInconsistentIds(Map<File, Set<String>> idMap) {
Set<String> union = getAllIds(idMap);
Set<String> inconsistent = new HashSet<String>();
for (Map.Entry<File, Set<String>> entry : idMap.entrySet()) {
Set<String> ids = entry.getValue();
if (ids.size() < union.size()) {
Set<String> missing = new HashSet<String>(union);
missing.removeAll(ids);
inconsistent.addAll(missing);
}
}
return inconsistent;
}
private static Set<String> getAllIds(Map<File, Set<String>> idMap) {
Iterator<Set<String>> iterator = idMap.values().iterator();
assert iterator.hasNext();
Set<String> union = new HashSet<String>(iterator.next());
while (iterator.hasNext()) {
union.addAll(iterator.next());
}
return union;
}
private Map<File, Set<String>> getIdMap(List<Pair<File, Map<String, String>>> files,
int layoutCount) {
Map<File, Set<String>> idMap = new HashMap<File, Set<String>>(layoutCount);
for (Pair<File, Map<String, String>> pair : files) {
File file = pair.getFirst();
Map<String, String> typeMap = pair.getSecond();
Set<String> ids = typeMap.keySet();
idMap.put(file, stripIrrelevantIds(ids));
}
return idMap;
}
private void reportErrors(Context context) {
List<String> layouts = new ArrayList<String>(mLocations.keySet());
Collections.sort(layouts);
for (String layout : layouts) {
Map<String, List<Location>> locationMap = mLocations.get(layout);
Map<String, String> messageMap = mErrorMessages.get(layout);
assert locationMap != null;
assert messageMap != null;
List<String> ids = new ArrayList<String>(locationMap.keySet());
Collections.sort(ids);
for (String id : ids) {
String message = messageMap.get(id);
List<Location> locations = locationMap.get(id);
if (locations != null) {
Location location = chainLocations(locations);
context.report(INCONSISTENT_IDS, location, message);
}
}
}
}
@NonNull
private static Location chainLocations(@NonNull List<Location> locations) {
assert !locations.isEmpty();
// Sort locations by the file parent folders
if (locations.size() > 1) {
Collections.sort(locations, new Comparator<Location>() {
@Override
public int compare(Location location1, Location location2) {
File file1 = location1.getFile();
File file2 = location2.getFile();
String folder1 = file1.getParentFile().getName();
String folder2 = file2.getParentFile().getName();
return folder1.compareTo(folder2);
}
});
// Chain locations together
Iterator<Location> iterator = locations.iterator();
assert iterator.hasNext();
Location prev = iterator.next();
while (iterator.hasNext()) {
Location next = iterator.next();
prev.setSecondary(next);
prev = next;
}
}
return locations.get(0);
}
// ---- Implements JavaScanner ----
@Override
public boolean appliesToResourceRefs() {
return true;
}
@Override
public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull lombok.ast.Node node, @NonNull String type, @NonNull String name,
boolean isFramework) {
if (!isFramework && type.equals(ResourceType.ID.getName())) {
mRelevantIds.add(name);
}
}
}
@@ -0,0 +1,269 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.tools.lint.checks.ViewHolderDetector.INFLATE;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
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.ResourceType;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.kxml2.io.KXmlParser;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import lombok.ast.AstVisitor;
import lombok.ast.Expression;
import lombok.ast.MethodInvocation;
import lombok.ast.NullLiteral;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
/**
* Looks for layout inflation calls passing null as the view root
*/
public class LayoutInflationDetector extends LayoutDetector implements Detector.JavaScanner {
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
LayoutInflationDetector.class,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.JAVA_FILE_SCOPE);
/** Passing in a null parent to a layout inflater */
public static final Issue ISSUE = Issue.create(
"InflateParams", //$NON-NLS-1$
"Layout Inflation without a Parent",
"When inflating a layout, avoid passing in null as the parent view, since " +
"otherwise any layout parameters on the root of the inflated layout will be ignored.",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("http://www.doubleencore.com/2013/05/layout-inflation-as-intended");
private static final String ERROR_MESSAGE =
"Avoid passing `null` as the view root (needed to resolve "
+ "layout parameters on the inflated layout's root element)";
/** Constructs a new {@link LayoutInflationDetector} check */
public LayoutInflationDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPendingErrors != null) {
for (Pair<String,Location> pair : mPendingErrors) {
String inflatedLayout = pair.getFirst();
if (mLayoutsWithRootLayoutParams == null ||
!mLayoutsWithRootLayoutParams.contains(inflatedLayout)) {
// No root layout parameters on the inflated layout: no need to complain
continue;
}
Location location = pair.getSecond();
context.report(ISSUE, location, ERROR_MESSAGE);
}
}
}
// ---- Implements XmlScanner ----
private Set<String> mLayoutsWithRootLayoutParams;
private List<Pair<String,Location>> mPendingErrors;
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root != null) {
NamedNodeMap attributes = root.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
if (attribute.getLocalName() != null
&& attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
if (mLayoutsWithRootLayoutParams == null) {
mLayoutsWithRootLayoutParams = Sets.newHashSetWithExpectedSize(20);
}
mLayoutsWithRootLayoutParams.add(LintUtils.getBaseName(context.file.getName()));
break;
}
}
}
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(INFLATE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
assert node.astName().astValue().equals(INFLATE);
if (node.astOperand() == null) {
return;
}
StrictListAccessor<Expression, MethodInvocation> arguments = node.astArguments();
if (arguments.size() < 2) {
return;
}
Iterator<Expression> iterator = arguments.iterator();
Expression first = iterator.next();
Expression second = iterator.next();
if (!(second instanceof NullLiteral) || !(first instanceof Select)) {
return;
}
Select select = (Select) first;
Expression operand = select.astOperand();
if (operand instanceof Select) {
Select rLayout = (Select) operand;
if (rLayout.astIdentifier().astValue().equals(ResourceType.LAYOUT.getName()) &&
rLayout.astOperand().toString().endsWith(SdkConstants.R_CLASS)) {
String layoutName = select.astIdentifier().astValue();
if (context.getScope().contains(Scope.RESOURCE_FILE)) {
// We're doing a full analysis run: we can gather this information
// incrementally
if (!context.getDriver().isSuppressed(context, ISSUE, node)) {
if (mPendingErrors == null) {
mPendingErrors = Lists.newArrayList();
}
Location location = context.getLocation(second);
mPendingErrors.add(Pair.of(layoutName, location));
}
} else if (hasLayoutParams(context, layoutName)) {
context.report(ISSUE, node, context.getLocation(second), ERROR_MESSAGE);
}
}
}
super.visitMethod(context, visitor, node);
}
private static boolean hasLayoutParams(@NonNull JavaContext context, String name) {
LintClient client = context.getClient();
if (!client.supportsProjectResources()) {
return true; // not certain
}
Project project = context.getProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return true; // not certain
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.LAYOUT, name);
if (items == null || items.isEmpty()) {
return false;
}
for (ResourceItem item : items) {
ResourceFile source = item.getSource();
if (source == null) {
return true; // not certain
}
File file = source.getFile();
if (file.exists()) {
try {
String s = context.getClient().readFile(file);
if (hasLayoutParams(new StringReader(s))) {
return true;
}
} catch (Exception e) {
context.log(e, "Could not read/parse inflated layout");
return true; // not certain
}
}
}
return false;
}
@VisibleForTesting
static boolean hasLayoutParams(@NonNull Reader reader)
throws XmlPullParserException, IOException {
KXmlParser parser = new KXmlParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(reader);
while (true) {
int event = parser.next();
if (event == XmlPullParser.START_TAG) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
String prefix = parser.getAttributePrefix(i);
if (prefix != null && !prefix.isEmpty() &&
ANDROID_URI.equals(parser.getNamespace(prefix))) {
return true;
}
}
}
return false;
} else if (event == XmlPullParser.END_DOCUMENT) {
return false;
}
}
}
}
@@ -0,0 +1,184 @@
/*
* 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.lint.checks;
import static com.android.SdkConstants.FORMAT_METHOD;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.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;
}
}
}
@@ -0,0 +1,437 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.ResourceContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.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;
}
}
@@ -0,0 +1,327 @@
/*
* 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.lint.checks;
import static com.android.tools.lint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.ClassDeclaration;
import lombok.ast.Expression;
import lombok.ast.If;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Select;
import lombok.ast.StringLiteral;
import lombok.ast.VariableReference;
/**
* Detector for finding inefficiencies and errors in logging calls.
*/
public class LogDetector extends Detector implements Detector.JavaScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LogDetector.class, Scope.JAVA_FILE_SCOPE);
/** Log call missing surrounding if */
public static final Issue CONDITIONAL = Issue.create(
"LogConditional", //$NON-NLS-1$
"Unconditional Logging Calls",
"The BuildConfig class (available in Tools 17) provides a constant, \"DEBUG\", " +
"which indicates whether the code is being built in release mode or in debug " +
"mode. In release mode, you typically want to strip out all the logging calls. " +
"Since the compiler will automatically remove all code which is inside a " +
"\"if (false)\" check, surrounding your logging calls with a check for " +
"BuildConfig.DEBUG is a good idea.\n" +
"\n" +
"If you *really* intend for the logging to be present in release mode, you can " +
"suppress this warning with a @SuppressLint annotation for the intentional " +
"logging calls.",
Category.PERFORMANCE,
5,
Severity.WARNING,
IMPLEMENTATION).setEnabledByDefault(false);
/** Mismatched tags between isLogging and log calls within it */
public static final Issue WRONG_TAG = Issue.create(
"LogTagMismatch", //$NON-NLS-1$
"Mismatched Log Tags",
"When guarding a `Log.v(tag, ...)` call with `Log.isLoggable(tag)`, the " +
"tag passed to both calls should be the same. Similarly, the level passed " +
"in to `Log.isLoggable` should typically match the type of `Log` call, e.g. " +
"if checking level `Log.DEBUG`, the corresponding `Log` call should be `Log.d`, " +
"not `Log.i`.",
Category.CORRECTNESS,
5,
Severity.ERROR,
IMPLEMENTATION);
/** Log tag is too long */
public static final Issue LONG_TAG = Issue.create(
"LongLogTag", //$NON-NLS-1$
"Too Long Log Tags",
"Log tags are only allowed to be at most 23 tag characters long.",
Category.CORRECTNESS,
5,
Severity.ERROR,
IMPLEMENTATION);
@SuppressWarnings("SpellCheckingInspection")
private static final String IS_LOGGABLE = "isLoggable"; //$NON-NLS-1$
private static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$
private static final String PRINTLN = "println"; //$NON-NLS-1$
// ---- Implements Detector.JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
"d", //$NON-NLS-1$
"e", //$NON-NLS-1$
"i", //$NON-NLS-1$
"v", //$NON-NLS-1$
"w", //$NON-NLS-1$
PRINTLN,
IS_LOGGABLE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, @NonNull MethodInvocation node) {
ResolvedNode resolved = context.resolve(node);
if (!(resolved instanceof ResolvedMethod)) {
return;
}
ResolvedMethod method = (ResolvedMethod) resolved;
if (!method.getContainingClass().matches(LOG_CLS)) {
return;
}
String name = node.astName().astValue();
boolean withinConditional = IS_LOGGABLE.equals(name) ||
checkWithinConditional(context, node.getParent(), node);
// See if it's surrounded by an if statement (and it's one of the non-error, spammy
// log methods (info, verbose, etc))
if (("i".equals(name) || "d".equals(name) || "v".equals(name) || PRINTLN.equals(name))
&& !withinConditional
&& performsWork(context, node)
&& context.isEnabled(CONDITIONAL)) {
String message = String.format("The log call Log.%1$s(...) should be " +
"conditional: surround with `if (Log.isLoggable(...))` or " +
"`if (BuildConfig.DEBUG) { ... }`",
node.astName().toString());
context.report(CONDITIONAL, node, context.getLocation(node), message);
}
// Check tag length
if (context.isEnabled(LONG_TAG)) {
int tagArgumentIndex = PRINTLN.equals(name) ? 1 : 0;
if (method.getArgumentCount() > tagArgumentIndex
&& method.getArgumentType(tagArgumentIndex).matchesSignature(TYPE_STRING)
&& node.astArguments().size() == method.getArgumentCount()) {
Iterator<Expression> iterator = node.astArguments().iterator();
if (tagArgumentIndex == 1) {
iterator.next();
}
Node argument = iterator.next();
String tag = ConstantEvaluator.evaluateString(context, argument, true);
if (tag != null && tag.length() > 23) {
String message = String.format(
"The logging tag can be at most 23 characters, was %1$d (%2$s)",
tag.length(), tag);
context.report(LONG_TAG, node, context.getLocation(node), message);
}
}
}
}
/** Returns true if the given logging call performs "work" to compute the message */
private static boolean performsWork(
@NonNull JavaContext context,
@NonNull MethodInvocation node) {
int messageArgumentIndex = PRINTLN.equals(node.astName().astValue()) ? 2 : 1;
if (node.astArguments().size() >= messageArgumentIndex) {
Iterator<Expression> iterator = node.astArguments().iterator();
Node argument = null;
for (int i = 0; i <= messageArgumentIndex; i++) {
argument = iterator.next();
}
if (argument == null) {
return false;
}
if (argument instanceof StringLiteral || argument instanceof VariableReference) {
return false;
}
if (argument instanceof BinaryExpression) {
String string = ConstantEvaluator.evaluateString(context, argument, false);
//noinspection VariableNotUsedInsideIf
if (string != null) { // does it resolve to a constant?
return false;
}
} else if (argument instanceof Select) {
String string = ConstantEvaluator.evaluateString(context, argument, false);
//noinspection VariableNotUsedInsideIf
if (string != null) {
return false;
}
}
// Method invocations etc
return true;
}
return false;
}
private static boolean checkWithinConditional(
@NonNull JavaContext context,
@Nullable Node curr,
@NonNull MethodInvocation logCall) {
while (curr != null) {
if (curr instanceof If) {
If ifNode = (If) curr;
if (ifNode.astCondition() instanceof MethodInvocation) {
MethodInvocation call = (MethodInvocation) ifNode.astCondition();
if (IS_LOGGABLE.equals(call.astName().astValue())) {
checkTagConsistent(context, logCall, call);
}
}
return true;
} else if (curr instanceof MethodInvocation
|| curr instanceof ClassDeclaration) { // static block
break;
}
curr = curr.getParent();
}
return false;
}
/** Checks that the tag passed to Log.s and Log.isLoggable match */
private static void checkTagConsistent(JavaContext context, MethodInvocation logCall,
MethodInvocation call) {
Iterator<Expression> isLogIterator = call.astArguments().iterator();
Iterator<Expression> logIterator = logCall.astArguments().iterator();
if (!isLogIterator.hasNext() || !logIterator.hasNext()) {
return;
}
Expression isLoggableTag = isLogIterator.next();
Expression logTag = logIterator.next();
//String callName = logCall.astName().astValue();
String logCallName = logCall.astName().astValue();
boolean isPrintln = PRINTLN.equals(logCallName);
if (isPrintln) {
if (!logIterator.hasNext()) {
return;
}
logTag = logIterator.next();
}
if (logTag != null) {
if (!isLoggableTag.toString().equals(logTag.toString())) {
ResolvedNode resolved1 = context.resolve(isLoggableTag);
ResolvedNode resolved2 = context.resolve(logTag);
if ((resolved1 == null || resolved2 == null || !resolved1.equals(resolved2))
&& context.isEnabled(WRONG_TAG)) {
Location location = context.getLocation(logTag);
Location alternate = context.getLocation(isLoggableTag);
alternate.setMessage("Conflicting tag");
location.setSecondary(alternate);
String isLoggableDescription = resolved1 != null ? resolved1
.getName()
: isLoggableTag.toString();
String logCallDescription = resolved2 != null ? resolved2.getName()
: logTag.toString();
String message = String.format(
"Mismatched tags: the `%1$s()` and `isLoggable()` calls typically " +
"should pass the same tag: `%2$s` versus `%3$s`",
logCallName,
isLoggableDescription,
logCallDescription);
context.report(WRONG_TAG, call, location, message);
}
}
}
// Check log level versus the actual log call type (e.g. flag
// if (Log.isLoggable(TAG, Log.DEBUG) Log.info(TAG, "something")
if (logCallName.length() != 1 || !isLogIterator.hasNext()) { // e.g. println
return;
}
Expression isLoggableLevel = isLogIterator.next();
if (isLoggableLevel == null) {
return;
}
String levelString = isLoggableLevel.toString();
if (isLoggableLevel instanceof Select) {
levelString = ((Select)isLoggableLevel).astIdentifier().astValue();
}
if (levelString.isEmpty()) {
return;
}
char levelChar = Character.toLowerCase(levelString.charAt(0));
if (logCallName.charAt(0) == levelChar || !context.isEnabled(WRONG_TAG)) {
return;
}
switch (levelChar) {
case 'd':
case 'e':
case 'i':
case 'v':
case 'w':
break;
default:
// Some other char; e.g. user passed in a literal value or some
// local constant or variable alias
return;
}
String expectedCall = String.valueOf(levelChar);
String message = String.format(
"Mismatched logging levels: when checking `isLoggable` level `%1$s`, the " +
"corresponding log call should be `Log.%2$s`, not `Log.%3$s`",
levelString, expectedCall, logCallName);
Location location = context.getLocation(logCall.astName());
Location alternate = context.getLocation(isLoggableLevel);
alternate.setMessage("Conflicting tag");
location.setSecondary(alternate);
context.report(WRONG_TAG, call, location, message);
}
}
@@ -0,0 +1,994 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.*;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.resources.ResourceUrl;
import com.android.tools.lint.detector.api.*;
import com.google.common.collect.Maps;
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.*;
import static com.android.SdkConstants.*;
import static com.android.xml.AndroidManifest.*;
/**
* Checks for issues in AndroidManifest files such as declaring elements in the
* wrong order.
*/
public class ManifestDetector extends Detector implements Detector.XmlScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
ManifestDetector.class,
Scope.MANIFEST_SCOPE
);
/** Wrong order of elements in the manifest */
public static final Issue ORDER = Issue.create(
"ManifestOrder", //$NON-NLS-1$
"Incorrect order of elements in manifest",
"The <application> tag should appear after the elements which declare " +
"which version you need, which features you need, which libraries you " +
"need, and so on. In the past there have been subtle bugs (such as " +
"themes not getting applied correctly) when the `<application>` tag appears " +
"before some of these other elements, so it's best to order your " +
"manifest in the logical dependency order.",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION);
/** Missing a {@code <uses-sdk>} element */
public static final Issue USES_SDK = Issue.create(
"UsesMinSdkAttributes", //$NON-NLS-1$
"Minimum SDK and target SDK attributes not defined",
"The manifest should contain a `<uses-sdk>` element which defines the " +
"minimum API Level required for the application to run, " +
"as well as the target version (the highest API level you have tested " +
"the version for.)",
Category.CORRECTNESS,
9,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/guide/topics/manifest/uses-sdk-element.html"); //$NON-NLS-1$
/** Using a targetSdkVersion that isn't recent */
public static final Issue TARGET_NEWER = Issue.create(
"OldTargetApi", //$NON-NLS-1$
"Target SDK attribute is not targeting latest version",
"When your application runs on a version of Android that is more recent than your " +
"`targetSdkVersion` specifies that it has been tested with, various compatibility " +
"modes kick in. This ensures that your application continues to work, but it may " +
"look out of place. For example, if the `targetSdkVersion` is less than 14, your " +
"app may get an option button in the UI.\n" +
"\n" +
"To fix this issue, set the `targetSdkVersion` to the highest available value. Then " +
"test your app to make sure everything works correctly. You may want to consult " +
"the compatibility notes to see what changes apply to each version you are adding " +
"support for: " +
"http://developer.android.com/reference/android/os/Build.VERSION_CODES.html",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/reference/android/os/Build.VERSION_CODES.html"); //$NON-NLS-1$
/** Using multiple {@code <uses-sdk>} elements */
public static final Issue MULTIPLE_USES_SDK = Issue.create(
"MultipleUsesSdk", //$NON-NLS-1$
"Multiple `<uses-sdk>` elements in the manifest",
"The `<uses-sdk>` element should appear just once; the tools will *not* merge the " +
"contents of all the elements so if you split up the attributes across multiple " +
"elements, only one of them will take effect. To fix this, just merge all the " +
"attributes from the various elements into a single <uses-sdk> element.",
Category.CORRECTNESS,
6,
Severity.FATAL,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/guide/topics/manifest/uses-sdk-element.html"); //$NON-NLS-1$
/** Missing a {@code <uses-sdk>} element */
public static final Issue WRONG_PARENT = Issue.create(
"WrongManifestParent", //$NON-NLS-1$
"Wrong manifest parent",
"The `<uses-library>` element should be defined as a direct child of the " +
"`<application>` tag, not the `<manifest>` tag or an `<activity>` tag. Similarly, " +
"a `<uses-sdk>` tag much be declared at the root level, and so on. This check " +
"looks for incorrect declaration locations in the manifest, and complains " +
"if an element is found in the wrong place.",
Category.CORRECTNESS,
6,
Severity.FATAL,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$
/** Missing a {@code <uses-sdk>} element */
public static final Issue DUPLICATE_ACTIVITY = Issue.create(
"DuplicateActivity", //$NON-NLS-1$
"Activity registered more than once",
"An activity should only be registered once in the manifest. If it is " +
"accidentally registered more than once, then subtle errors can occur, " +
"since attribute declarations from the two elements are not merged, so " +
"you may accidentally remove previous declarations.",
Category.CORRECTNESS,
5,
Severity.FATAL,
IMPLEMENTATION);
/** Not explicitly defining allowBackup */
public static final Issue ALLOW_BACKUP = Issue.create(
"AllowBackup", //$NON-NLS-1$
"Missing `allowBackup` attribute",
"The allowBackup attribute determines if an application's data can be backed up " +
"and restored. It is documented at " +
"http://developer.android.com/reference/android/R.attr.html#allowBackup\n" +
"\n" +
"By default, this flag is set to `true`. When this flag is set to `true`, " +
"application data can be backed up and restored by the user using `adb backup` " +
"and `adb restore`.\n" +
"\n" +
"This may have security consequences for an application. `adb backup` allows " +
"users who have enabled USB debugging to copy application data off of the " +
"device. Once backed up, all application data can be read by the user. " +
"`adb restore` allows creation of application data from a source specified " +
"by the user. Following a restore, applications should not assume that the " +
"data, file permissions, and directory permissions were created by the " +
"application itself.\n" +
"\n" +
"Setting `allowBackup=\"false\"` opts an application out of both backup and " +
"restore.\n" +
"\n" +
"To fix this warning, decide whether your application should support backup, " +
"and explicitly set `android:allowBackup=(true|false)\"`",
Category.SECURITY,
3,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/reference/android/R.attr.html#allowBackup");
/** Conflicting permission names */
public static final Issue UNIQUE_PERMISSION = Issue.create(
"UniquePermission", //$NON-NLS-1$
"Permission names are not unique",
"The unqualified names or your permissions must be unique. The reason for this " +
"is that at build time, the `aapt` tool will generate a class named `Manifest` " +
"which contains a field for each of your permissions. These fields are named " +
"using your permission unqualified names (i.e. the name portion after the last " +
"dot).\n" +
"\n" +
"If more than one permission maps to the same field name, that field will " +
"arbitrarily name just one of them.",
Category.CORRECTNESS,
6,
Severity.FATAL,
IMPLEMENTATION);
/** Using a resource for attributes that do not allow it */
public static final Issue SET_VERSION = Issue.create(
"MissingVersion", //$NON-NLS-1$
"Missing application name/version",
"You should define the version information for your application.\n" +
"`android:versionCode`: An integer value that represents the version of the " +
"application code, relative to other versions.\n" +
"\n" +
"`android:versionName`: A string value that represents the release version of " +
"the application code, as it should be shown to users.",
Category.CORRECTNESS,
2,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/tools/publishing/versioning.html#appversioning");
/** Using a resource for attributes that do not allow it */
public static final Issue ILLEGAL_REFERENCE = Issue.create(
"IllegalResourceRef", //$NON-NLS-1$
"Name and version must be integer or string, not resource",
"For the `versionCode` attribute, you have to specify an actual integer " +
"literal; you cannot use an indirection with a `@dimen/name` resource. " +
"Similarly, the `versionName` attribute should be an actual string, not " +
"a string resource url.",
Category.CORRECTNESS,
8,
Severity.WARNING,
IMPLEMENTATION);
/** Declaring a uses-feature multiple time */
public static final Issue DUPLICATE_USES_FEATURE = Issue.create(
"DuplicateUsesFeature", //$NON-NLS-1$
"Feature declared more than once",
"A given feature should only be declared once in the manifest.",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION);
/** Not explicitly defining application icon */
public static final Issue APPLICATION_ICON = Issue.create(
"MissingApplicationIcon", //$NON-NLS-1$
"Missing application icon",
"You should set an icon for the application as whole because there is no " +
"default. This attribute must be set as a reference to a drawable resource " +
"containing the image (for example `@drawable/icon`).",
Category.ICONS,
5,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/tools/publishing/preparing.html#publishing-configure"); //$NON-NLS-1$
/** Malformed Device Admin */
public static final Issue DEVICE_ADMIN = Issue.create(
"DeviceAdmin", //$NON-NLS-1$
"Malformed Device Admin",
"If you register a broadcast receiver which acts as a device admin, you must also " +
"register an `<intent-filter>` for the action " +
"`android.app.action.DEVICE_ADMIN_ENABLED`, without any `<data>`, such that the " +
"device admin can be activated/deactivated.\n" +
"\n" +
"To do this, add\n" +
"`<intent-filter>`\n" +
" `<action android:name=\"android.app.action.DEVICE_ADMIN_ENABLED\" />`\n" +
"`</intent-filter>`\n" +
"to your `<receiver>`.",
Category.CORRECTNESS,
7,
Severity.WARNING,
IMPLEMENTATION);
/** Using a mock location in a non-debug-specific manifest file */
public static final Issue MOCK_LOCATION = Issue.create(
"MockLocation", //$NON-NLS-1$
"Using mock location provider in production",
"Using a mock location provider (by requiring the permission " +
"`android.permission.ACCESS_MOCK_LOCATION`) should *only* be done " +
"in debug builds (or from tests). In Gradle projects, that means you should only " +
"request this permission in a test or debug source set specific manifest file.\n" +
"\n" +
"To fix this, create a new manifest file in the debug folder and move " +
"the `<uses-permission>` element there. A typical path to a debug manifest " +
"override file in a Gradle project is src/debug/AndroidManifest.xml.",
Category.CORRECTNESS,
8,
Severity.FATAL,
IMPLEMENTATION);
/** Defining a value that is overridden by Gradle */
public static final Issue GRADLE_OVERRIDES = Issue.create(
"GradleOverrides", //$NON-NLS-1$
"Value overridden by Gradle build script",
"The value of (for example) `minSdkVersion` is only used if it is not specified in " +
"the `build.gradle` build scripts. When specified in the Gradle build scripts, " +
"the manifest value is ignored and can be misleading, so should be removed to " +
"avoid ambiguity.",
Category.CORRECTNESS,
4,
Severity.WARNING,
IMPLEMENTATION);
/** Using drawable rather than mipmap launcher icons */
public static final Issue MIPMAP = Issue.create(
"MipmapIcons", //$NON-NLS-1$
"Use Mipmap Launcher Icons",
"Launcher icons should be provided in the `mipmap` resource directory. " +
"This is the same as the `drawable` resource directory, except resources in " +
"the `mipmap` directory will not get stripped out when creating density-specific " +
"APKs.\n" +
"\n" +
"In certain cases, the Launcher app may use a higher resolution asset (than " +
"would normally be computed for the device) to display large app shortcuts. " +
"If drawables for densities other than the device's resolution have been " +
"stripped out, then the app shortcut could appear blurry.\n" +
"\n" +
"To fix this, move your launcher icons from `drawable-`dpi to `mipmap-`dpi " +
"and change references from @drawable/ and R.drawable to @mipmap/ and R.mipmap.\n" +
"In Android Studio this lint warning has a quickfix to perform this automatically.",
Category.ICONS,
5,
Severity.WARNING,
IMPLEMENTATION);
/** Permission name of mock location permission */
public static final String MOCK_LOCATION_PERMISSION =
"android.permission.ACCESS_MOCK_LOCATION"; //$NON-NLS-1$
/** Constructs a new {@link ManifestDetector} check */
public ManifestDetector() {
}
private boolean mSeenApplication;
/** Number of times we've seen the <uses-sdk> element */
private int mSeenUsesSdk;
/** Activities we've encountered */
private Set<String> mActivities;
/** Features we've encountered */
private Set<String> mUsesFeatures;
/** Permission basenames */
private Map<String, String> mPermissionNames;
/** Handle to the {@code <application>} tag */
private Location.Handle mApplicationTagHandle;
/** Whether we've seen an application icon definition in any of the manifest files (or
* if a manifest tag warning for this has been explicitly disabled) */
private boolean mSeenAppIcon;
/** Whether we've seen an allow backup definition in any of the manifest files (or
* if a manifest tag warning for this has been explicitly disabled) */
private boolean mSeenAllowBackup;
@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 void beforeCheckFile(@NonNull Context context) {
mSeenApplication = false;
mSeenUsesSdk = 0;
mActivities = new HashSet<String>();
mUsesFeatures = new HashSet<String>();
}
@Override
public void afterCheckFile(@NonNull Context context) {
XmlContext xmlContext = (XmlContext) context;
Element element = xmlContext.document.getDocumentElement();
if (element != null) {
checkDocumentElement(xmlContext, element);
}
if (mSeenUsesSdk == 0 && context.isEnabled(USES_SDK)
// Not required in Gradle projects; typically defined in build.gradle instead
// and inserted at build time
&& !context.getMainProject().isGradleProject()) {
context.report(USES_SDK, Location.create(context.file),
"Manifest should specify a minimum API level with " +
"`<uses-sdk android:minSdkVersion=\"?\" />`; if it really supports " +
"all versions of Android set it to 1.");
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (!mSeenAllowBackup && context.isEnabled(ALLOW_BACKUP)
&& !context.getProject().isLibrary()
&& context.getMainProject().getMinSdk() >= 4) {
Location location = getMainApplicationTagLocation(context);
context.report(ALLOW_BACKUP, location,
"Should explicitly set `android:allowBackup` to `true` or " +
"`false` (it's `true` by default, and that can have some security " +
"implications for the application's data)");
}
if (!context.getMainProject().isLibrary()
&& !mSeenAppIcon && context.isEnabled(APPLICATION_ICON)) {
Location location = getMainApplicationTagLocation(context);
context.report(APPLICATION_ICON, location,
"Should explicitly set `android:icon`, there is no default");
}
}
@Nullable
private Location getMainApplicationTagLocation(@NonNull Context context) {
if (mApplicationTagHandle != null) {
return mApplicationTagHandle.resolve();
}
List<File> manifestFiles = context.getMainProject().getManifestFiles();
if (!manifestFiles.isEmpty()) {
return Location.create(manifestFiles.get(0));
}
return null;
}
private static void checkDocumentElement(XmlContext context, Element element) {
Attr codeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_VERSION_CODE);
if (codeNode != null && codeNode.getValue().startsWith(PREFIX_RESOURCE_REF)
&& context.isEnabled(ILLEGAL_REFERENCE)) {
context.report(ILLEGAL_REFERENCE, element, context.getLocation(codeNode),
"The `android:versionCode` cannot be a resource url, it must be "
+ "a literal integer");
} else if (codeNode == null && context.isEnabled(SET_VERSION)
// Not required in Gradle projects; typically defined in build.gradle instead
// and inserted at build time
&& !context.getMainProject().isGradleProject()) {
context.report(SET_VERSION, element, context.getLocation(element),
"Should set `android:versionCode` to specify the application version");
}
Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_VERSION_NAME);
if (nameNode == null && context.isEnabled(SET_VERSION)
// Not required in Gradle projects; typically defined in build.gradle instead
// and inserted at build time
&& !context.getMainProject().isGradleProject()) {
context.report(SET_VERSION, element, context.getLocation(element),
"Should set `android:versionName` to specify the application version");
}
checkOverride(context, element, ATTR_VERSION_CODE);
checkOverride(context, element, ATTR_VERSION_NAME);
Attr pkgNode = element.getAttributeNode(ATTR_PACKAGE);
if (pkgNode != null) {
String pkg = pkgNode.getValue();
if (pkg.contains("${") && context.getMainProject().isGradleProject()) {
context.report(GRADLE_OVERRIDES, pkgNode, context.getLocation(pkgNode),
"Cannot use placeholder for the package in the manifest; "
+ "set `applicationId` in `build.gradle` instead");
}
}
}
private static void checkOverride(XmlContext context, Element element, String attributeName) {
Project project = context.getProject();
Attr attribute = element.getAttributeNodeNS(ANDROID_URI, attributeName);
if (project.isGradleProject() && attribute != null && context.isEnabled(GRADLE_OVERRIDES)) {
Variant variant = project.getCurrentVariant();
if (variant != null) {
ProductFlavor flavor = variant.getMergedFlavor();
String gradleValue = null;
if (ATTR_MIN_SDK_VERSION.equals(attributeName)) {
try {
ApiVersion minSdkVersion = flavor.getMinSdkVersion();
gradleValue = minSdkVersion != null ? minSdkVersion.getApiString() : null;
} catch (Throwable e) {
// TODO: REMOVE ME
// This method was added in the 0.11 model. We'll need to drop support
// for 0.10 shortly but until 0.11 is available this is a stopgap measure
}
} else if (ATTR_TARGET_SDK_VERSION.equals(attributeName)) {
try {
ApiVersion targetSdkVersion = flavor.getTargetSdkVersion();
gradleValue = targetSdkVersion != null ? targetSdkVersion.getApiString() : null;
} catch (Throwable e) {
// TODO: REMOVE ME
// This method was added in the 0.11 model. We'll need to drop support
// for 0.10 shortly but until 0.11 is available this is a stopgap measure
}
} else if (ATTR_VERSION_CODE.equals(attributeName)) {
Integer versionCode = flavor.getVersionCode();
if (versionCode != null) {
gradleValue = versionCode.toString();
}
} else if (ATTR_VERSION_NAME.equals(attributeName)) {
gradleValue = flavor.getVersionName();
} else {
assert false : attributeName;
return;
}
if (gradleValue != null) {
String manifestValue = attribute.getValue();
String message = String.format("This `%1$s` value (`%2$s`) is not used; it is "
+ "always overridden by the value specified in the Gradle build "
+ "script (`%3$s`)", attributeName, manifestValue, gradleValue);
context.report(GRADLE_OVERRIDES, attribute, context.getLocation(attribute),
message);
}
}
}
}
// ---- Implements Detector.XmlScanner ----
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_APPLICATION,
TAG_USES_PERMISSION,
TAG_PERMISSION,
"permission-tree", //$NON-NLS-1$
"permission-group", //$NON-NLS-1$
TAG_USES_SDK,
"uses-configuration", //$NON-NLS-1$
TAG_USES_FEATURE,
"supports-screens", //$NON-NLS-1$
"compatible-screens", //$NON-NLS-1$
"supports-gl-texture", //$NON-NLS-1$
TAG_USES_LIBRARY,
TAG_ACTIVITY,
TAG_SERVICE,
TAG_PROVIDER,
TAG_RECEIVER
);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
Node parentNode = element.getParentNode();
boolean isReceiver = tag.equals(TAG_RECEIVER);
if (isReceiver) {
checkDeviceAdmin(context, element);
}
if (tag.equals(TAG_USES_LIBRARY) || tag.equals(TAG_ACTIVITY) || tag.equals(TAG_SERVICE)
|| tag.equals(TAG_PROVIDER) || isReceiver) {
if (!TAG_APPLICATION.equals(parentNode.getNodeName())
&& context.isEnabled(WRONG_PARENT)) {
context.report(WRONG_PARENT, element, context.getLocation(element),
String.format(
"The `<%1$s>` element must be a direct child of the <application> element",
tag));
}
if (tag.equals(TAG_ACTIVITY)) {
Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (nameNode != null) {
String name = nameNode.getValue();
if (!name.isEmpty()) {
String pkg = context.getMainProject().getPackage();
if (name.charAt(0) == '.') {
name = pkg + name;
} else if (name.indexOf('.') == -1) {
name = pkg + '.' + name;
}
if (mActivities.contains(name)) {
String message = String.format(
"Duplicate registration for activity `%1$s`", name);
context.report(DUPLICATE_ACTIVITY, element,
context.getLocation(nameNode), message);
} else {
mActivities.add(name);
}
}
}
checkMipmapIcon(context, element);
}
return;
}
if (parentNode != element.getOwnerDocument().getDocumentElement()
&& context.isEnabled(WRONG_PARENT)) {
context.report(WRONG_PARENT, element, context.getLocation(element),
String.format(
"The `<%1$s>` element must be a direct child of the " +
"`<manifest>` root element", tag));
}
if (tag.equals(TAG_USES_SDK)) {
mSeenUsesSdk++;
if (mSeenUsesSdk == 2) { // Only warn when we encounter the first one
Location location = context.getLocation(element);
// Link up *all* encountered locations in the document
NodeList elements = element.getOwnerDocument().getElementsByTagName(TAG_USES_SDK);
Location secondary = null;
for (int i = elements.getLength() - 1; i >= 0; i--) {
Element e = (Element) elements.item(i);
if (e != element) {
Location l = context.getLocation(e);
l.setSecondary(secondary);
l.setMessage("Also appears here");
secondary = l;
}
}
location.setSecondary(secondary);
if (context.isEnabled(MULTIPLE_USES_SDK)) {
context.report(MULTIPLE_USES_SDK, element, location,
"There should only be a single `<uses-sdk>` element in the manifest:" +
" merge these together");
}
return;
}
if (!element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) {
if (context.isEnabled(USES_SDK) && !context.getMainProject().isGradleProject()) {
context.report(USES_SDK, element, context.getLocation(element),
"`<uses-sdk>` tag should specify a minimum API level with " +
"`android:minSdkVersion=\"?\"`");
}
} else {
Attr codeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION);
if (codeNode != null && codeNode.getValue().startsWith(PREFIX_RESOURCE_REF)
&& context.isEnabled(ILLEGAL_REFERENCE)) {
context.report(ILLEGAL_REFERENCE, element, context.getLocation(codeNode),
"The `android:minSdkVersion` cannot be a resource url, it must be "
+ "a literal integer (or string if a preview codename)");
}
checkOverride(context, element, ATTR_MIN_SDK_VERSION);
}
if (!element.hasAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION)) {
// Warn if not setting target SDK -- but only if the min SDK is somewhat
// old so there's some compatibility stuff kicking in (such as the menu
// button etc)
if (context.isEnabled(USES_SDK) && !context.getMainProject().isGradleProject()) {
context.report(USES_SDK, element, context.getLocation(element),
"`<uses-sdk>` tag should specify a target API level (the " +
"highest verified version; when running on later versions, " +
"compatibility behaviors may be enabled) with " +
"`android:targetSdkVersion=\"?\"`");
}
} else {
checkOverride(context, element, ATTR_TARGET_SDK_VERSION);
if (context.isEnabled(TARGET_NEWER)) {
Attr targetSdkVersionNode = element.getAttributeNodeNS(ANDROID_URI,
ATTR_TARGET_SDK_VERSION);
if (targetSdkVersionNode != null) {
String target = targetSdkVersionNode.getValue();
try {
int api = Integer.parseInt(target);
if (api < context.getClient().getHighestKnownApiLevel()) {
context.report(TARGET_NEWER, element,
context.getLocation(targetSdkVersionNode),
"Not targeting the latest versions of Android; compatibility " +
"modes apply. Consider testing and updating this version. " +
"Consult the `android.os.Build.VERSION_CODES` javadoc for details.");
}
} catch (NumberFormatException nufe) {
// Ignore: AAPT will enforce this.
}
}
}
}
Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION);
if (nameNode != null && nameNode.getValue().startsWith(PREFIX_RESOURCE_REF)
&& context.isEnabled(ILLEGAL_REFERENCE)) {
context.report(ILLEGAL_REFERENCE, element, context.getLocation(nameNode),
"The `android:targetSdkVersion` cannot be a resource url, it must be "
+ "a literal integer (or string if a preview codename)");
}
}
if (tag.equals(TAG_PERMISSION)) {
Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (nameNode != null) {
String name = nameNode.getValue();
String base = name.substring(name.lastIndexOf('.') + 1);
if (mPermissionNames == null) {
mPermissionNames = Maps.newHashMap();
} else if (mPermissionNames.containsKey(base)) {
String prevName = mPermissionNames.get(base);
Location location = context.getLocation(nameNode);
NodeList siblings = element.getParentNode().getChildNodes();
for (int i = 0, n = siblings.getLength(); i < n; i++) {
Node node = siblings.item(i);
if (node == element) {
break;
} else if (node.getNodeType() == Node.ELEMENT_NODE) {
Element sibling = (Element) node;
String suffix = '.' + base;
if (sibling.getTagName().equals(TAG_PERMISSION)) {
String b = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (b.endsWith(suffix)) {
Location prevLocation = context.getLocation(node);
prevLocation.setMessage("Previous permission here");
location.setSecondary(prevLocation);
break;
}
}
}
}
String message = String.format("Permission name `%1$s` is not unique " +
"(appears in both `%2$s` and `%3$s`)", base, prevName, name);
context.report(UNIQUE_PERMISSION, element, location, message);
}
mPermissionNames.put(base, name);
}
}
if (tag.equals(TAG_USES_PERMISSION)) {
Attr name = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (name != null && name.getValue().equals(MOCK_LOCATION_PERMISSION)
&& context.getMainProject().isGradleProject()
&& !isDebugOrTestManifest(context, context.file)
&& context.isEnabled(MOCK_LOCATION)) {
String message = "Mock locations should only be requested in a test or " +
"debug-specific manifest file (typically `src/debug/AndroidManifest.xml`)";
Location location = context.getLocation(name);
context.report(MOCK_LOCATION, element, location, message);
}
}
if (tag.equals(TAG_APPLICATION)) {
mSeenApplication = true;
boolean recordLocation = false;
if (element.hasAttributeNS(ANDROID_URI, ATTR_ALLOW_BACKUP)
|| context.getDriver().isSuppressed(context, ALLOW_BACKUP, element)) {
mSeenAllowBackup = true;
} else {
recordLocation = true;
}
if (element.hasAttributeNS(ANDROID_URI, ATTR_ICON)
|| context.getDriver().isSuppressed(context, APPLICATION_ICON, element)) {
checkMipmapIcon(context, element);
mSeenAppIcon = true;
} else {
recordLocation = true;
}
if (recordLocation && !context.getProject().isLibrary() &&
(mApplicationTagHandle == null || isMainManifest(context, context.file))) {
mApplicationTagHandle = context.createLocationHandle(element);
}
Attr fullBackupNode = element.getAttributeNodeNS(ANDROID_URI, "fullBackupContent");
if (fullBackupNode != null &&
fullBackupNode.getValue().startsWith(PREFIX_RESOURCE_REF) &&
context.getClient().supportsProjectResources()) {
AbstractResourceRepository resources = context.getClient()
.getProjectResources(context.getProject(), true);
ResourceUrl url = ResourceUrl.parse(fullBackupNode.getValue());
if (url != null && !url.framework
&& resources != null
&& !resources.hasResourceItem(url.type, url.name)) {
Location location = context.getValueLocation(fullBackupNode);
context.report(ALLOW_BACKUP, location,
"Missing `<full-backup-content>` resource");
}
} else if (fullBackupNode == null && context.getMainProject().getTargetSdk() >= 23) {
Location location = context.getLocation(element);
context.report(ALLOW_BACKUP, location,
"Should explicitly set `android:fullBackupContent` to `true` or `false` "
+ "to opt-in to or out of full app data back-up and restore, or "
+ "alternatively to an `@xml` resource which specifies which "
+ "files to backup");
} else if (fullBackupNode == null && hasGcmReceiver(element)) {
Location location = context.getLocation(element);
context.report(ALLOW_BACKUP, location,
"Should explicitly set `android:fullBackupContent` to avoid backing up "
+ "the GCM device specific regId.");
}
} else if (mSeenApplication) {
if (context.isEnabled(ORDER)) {
context.report(ORDER, element, context.getLocation(element),
String.format("`<%1$s>` tag appears after `<application>` tag", tag));
}
// Don't complain for *every* element following the <application> tag
mSeenApplication = false;
}
if (tag.equals(TAG_USES_FEATURE)) {
Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (nameNode != null) {
String name = nameNode.getValue();
if (!name.isEmpty()) {
if (mUsesFeatures.contains(name)) {
String message = String.format(
"Duplicate declaration of uses-feature `%1$s`", name);
context.report(DUPLICATE_USES_FEATURE, element,
context.getLocation(nameNode), message);
} else {
mUsesFeatures.add(name);
}
}
}
}
}
/**
* Returns true if the given application element has a receiver with an intent filter
* action for GCM receive
*/
private static boolean hasGcmReceiver(@NonNull Element application) {
NodeList applicationChildren = application.getChildNodes();
for (int i1 = 0, n1 = applicationChildren.getLength(); i1 < n1; i1++) {
Node applicationChild = applicationChildren.item(i1);
if (applicationChild.getNodeType() == Node.ELEMENT_NODE
&& TAG_RECEIVER.equals(applicationChild.getNodeName())) {
NodeList receiverChildren = applicationChild.getChildNodes();
for (int i2 = 0, n2 = receiverChildren.getLength(); i2 < n2; i2++) {
Node receiverChild = receiverChildren.item(i2);
if (receiverChild.getNodeType() == Node.ELEMENT_NODE
&& TAG_INTENT_FILTER.equals(receiverChild.getNodeName())) {
NodeList filterChildren = receiverChild.getChildNodes();
for (int i3 = 0, n3 = filterChildren.getLength(); i3 < n3; i3++) {
Node filterChild = filterChildren.item(i3);
if (filterChild.getNodeType() == Node.ELEMENT_NODE
&& NODE_ACTION.equals(filterChild.getNodeName())) {
Element action = (Element) filterChild;
String name = action.getAttributeNS(ANDROID_URI, ATTR_NAME);
if ("com.google.android.c2dm.intent.RECEIVE".equals(name)) {
return true;
}
}
}
}
}
}
}
return false;
}
private static void checkMipmapIcon(@NonNull XmlContext context, @NonNull Element element) {
Attr attribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_ICON);
if (attribute == null) {
return;
}
String icon = attribute.getValue();
if (icon.startsWith(DRAWABLE_PREFIX)) {
if (TAG_ACTIVITY.equals(element.getTagName()) && !isLaunchableActivity(element)) {
return;
}
if (context.isEnabled(MIPMAP)
// Only complain if this app is skipping some densities
&& context.getProject().getApplicableDensities() != null) {
context.report(MIPMAP, element, context.getLocation(attribute),
"Should use `@mipmap` instead of `@drawable` for launcher icons");
}
}
}
@SuppressWarnings("SpellCheckingInspection")
private static boolean isLaunchableActivity(@NonNull Element element) {
if (!TAG_ACTIVITY.equals(element.getTagName())) {
return false;
}
for (Element child : LintUtils.getChildren(element)) {
if (child.getTagName().equals(TAG_INTENT_FILTER)) {
for (Element innerChild : LintUtils.getChildren(child)) {
if (innerChild.getTagName().equals("category")) { //$NON-NLS-1$
String categoryString = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
return "android.intent.category.LAUNCHER".equals(categoryString);
}
}
}
}
return false;
}
/** Returns true iff the given manifest file is the main manifest file */
private static boolean isMainManifest(XmlContext context, File manifestFile) {
if (!context.getProject().isGradleProject()) {
// In non-gradle projects, just one manifest per project
return true;
}
AndroidProject model = context.getProject().getGradleProjectModel();
return model == null || manifestFile
.equals(model.getDefaultConfig().getSourceProvider().getManifestFile());
}
/**
* Returns true iff the given manifest file is in a debug-specific source set,
* or a test source set
*/
private static boolean isDebugOrTestManifest(
@NonNull XmlContext context,
@NonNull File manifestFile) {
AndroidProject model = context.getProject().getGradleProjectModel();
if (model != null) {
// Quickly check if it's the main manifest first; that's the most likely scenario
if (manifestFile.equals(model.getDefaultConfig().getSourceProvider().getManifestFile())) {
return false;
}
// Debug build type?
for (BuildTypeContainer container : model.getBuildTypes()) {
if (container.getBuildType().isDebuggable()) {
if (manifestFile.equals(container.getSourceProvider().getManifestFile())) {
return true;
}
}
}
// Test source set?
for (ProductFlavorContainer container : model.getProductFlavors()) {
for (SourceProviderContainer extra : container.getExtraSourceProviders()) {
String artifactName = extra.getArtifactName();
if (AndroidProject.ARTIFACT_ANDROID_TEST.equals(artifactName)
&& manifestFile.equals(extra.getSourceProvider().getManifestFile())) {
return true;
}
}
}
}
return false;
}
private static void checkDeviceAdmin(XmlContext context, Element element) {
List<Element> children = LintUtils.getChildren(element);
boolean requiredIntentFilterFound = false;
boolean deviceAdmin = false;
Attr locationNode = null;
for (Element child : children) {
String tagName = child.getTagName();
if (tagName.equals(TAG_INTENT_FILTER) && !requiredIntentFilterFound) {
boolean dataFound = false;
boolean actionFound = false;
for (Element filterChild : LintUtils.getChildren(child)) {
String filterTag = filterChild.getTagName();
if (filterTag.equals(NODE_ACTION)) {
String name = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
if ("android.app.action.DEVICE_ADMIN_ENABLED".equals(name)) { //$NON-NLS-1$
actionFound = true;
}
} else if (filterTag.equals(NODE_DATA)) {
dataFound = true;
}
}
if (actionFound && !dataFound) {
requiredIntentFilterFound = true;
}
} else if (tagName.equals(NODE_METADATA)) {
Attr valueNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (valueNode != null) {
String name = valueNode.getValue();
if ("android.app.device_admin".equals(name)) { //$NON-NLS-1$
deviceAdmin = true;
locationNode = valueNode;
}
}
}
}
if (deviceAdmin && !requiredIntentFilterFound && context.isEnabled(DEVICE_ADMIN)) {
context.report(DEVICE_ADMIN, locationNode, context.getLocation(locationNode),
"You must have an intent filter for action "
+ "`android.app.action.DEVICE_ADMIN_ENABLED`");
}
}
}
@@ -0,0 +1,184 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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);
}
}
}
}
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.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*/);
}
}
}
@@ -0,0 +1,217 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_FOREGROUND;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.R_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.VIEW_INCLUDE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.Handle;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.Pair;
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.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.ast.AstVisitor;
import lombok.ast.Expression;
import lombok.ast.MethodInvocation;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
/**
* Checks whether a root FrameLayout can be replaced with a {@code <merge>} tag.
*/
public class MergeRootFrameLayoutDetector extends LayoutDetector implements Detector.JavaScanner {
/**
* Set of layouts that we want to enable the warning for. We only warn for
* {@code <FrameLayout>}'s that are the root of a layout included from
* another layout, or directly referenced via a {@code setContentView} call.
*/
private Set<String> mWhitelistedLayouts;
/**
* Set of pending [layout, location] pairs where the given layout is a
* FrameLayout that perhaps should be replaced by a {@code <merge>} tag (if
* the layout is included or set as the content view. This must be processed
* after the whole project has been scanned since the set of includes etc
* can be encountered after the included layout.
*/
private List<Pair<String, Location.Handle>> mPending;
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"MergeRootFrame", //$NON-NLS-1$
"FrameLayout can be replaced with `<merge>` tag",
"If a `<FrameLayout>` is the root of a layout and does not provide background " +
"or padding etc, it can often be replaced with a `<merge>` tag which is slightly " +
"more efficient. Note that this depends on context, so make sure you understand " +
"how the `<merge>` tag works before proceeding.",
Category.PERFORMANCE,
4,
Severity.WARNING,
new Implementation(
MergeRootFrameLayoutDetector.class,
EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE)))
.addMoreInfo(
"http://android-developers.blogspot.com/2009/03/android-layout-tricks-3-optimize-by.html"); //$NON-NLS-1$
/** Constructs a new {@link MergeRootFrameLayoutDetector} */
public MergeRootFrameLayoutDetector() {
}
@Override
@NonNull
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA);
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPending != null && mWhitelistedLayouts != null) {
// Process all the root FrameLayouts that are eligible, and generate
// suggestions for <merge> replacements for any layouts that are included
// from other layouts
for (Pair<String, Handle> pair : mPending) {
String layout = pair.getFirst();
if (mWhitelistedLayouts.contains(layout)) {
Handle handle = pair.getSecond();
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ISSUE, (Node) clientData)) {
return;
}
}
Location location = handle.resolve();
context.report(ISSUE, location,
"This `<FrameLayout>` can be replaced with a `<merge>` tag");
}
}
}
}
// Implements XmlScanner
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(VIEW_INCLUDE, FRAME_LAYOUT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
if (tag.equals(VIEW_INCLUDE)) {
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());
whiteListLayout(layout);
}
} else {
assert tag.equals(FRAME_LAYOUT);
if (LintUtils.isRootElement(element) &&
((isWidthFillParent(element) && isHeightFillParent(element)) ||
!element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY))
&& !element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_FOREGROUND)
&& !hasPadding(element)) {
String layout = LintUtils.getLayoutName(context.file);
Handle handle = context.createLocationHandle(element);
handle.setClientData(element);
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mPending == null) {
mPending = new ArrayList<Pair<String,Handle>>();
}
mPending.add(Pair.of(layout, handle));
}
}
}
private void whiteListLayout(String layout) {
if (mWhitelistedLayouts == null) {
mWhitelistedLayouts = new HashSet<String>();
}
mWhitelistedLayouts.add(layout);
}
// Implements JavaScanner
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("setContentView"); //$NON-NLS-1$
}
@Override
public void visitMethod(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
StrictListAccessor<Expression, MethodInvocation> argumentList = node.astArguments();
if (argumentList != null && argumentList.size() == 1) {
Expression argument = argumentList.first();
if (argument instanceof Select) {
String expression = argument.toString();
if (expression.startsWith(R_LAYOUT_RESOURCE_PREFIX)) {
whiteListLayout(expression.substring(R_LAYOUT_RESOURCE_PREFIX.length()));
}
}
}
}
}
@@ -0,0 +1,527 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.Handle;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.tools.lint.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;
}
}
@@ -0,0 +1,94 @@
/*
* 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.lint.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.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.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");
}
}
}

Some files were not shown because too many files have changed in this diff Show More