Lint: copy diagnostics from IDEA (br143)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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() {
|
||||
}
|
||||
}
|
||||
+609
@@ -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();
|
||||
}
|
||||
}
|
||||
+113
@@ -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
+1053
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);
|
||||
}
|
||||
}
|
||||
+1265
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();
|
||||
}
|
||||
+1348
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);
|
||||
}
|
||||
}
|
||||
+61
@@ -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("<"); //$NON-NLS-1$
|
||||
} else if (c == '&') {
|
||||
sb.append("&"); //$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(" "); //$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);
|
||||
}
|
||||
}
|
||||
+132
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
@@ -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;
|
||||
}
|
||||
}
|
||||
+239
@@ -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;
|
||||
}
|
||||
*/
|
||||
}
|
||||
+2181
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;
|
||||
}
|
||||
}
|
||||
+187
@@ -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;
|
||||
}
|
||||
}
|
||||
+109
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+287
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+308
@@ -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;
|
||||
}
|
||||
}
|
||||
+90
@@ -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;
|
||||
}
|
||||
}
|
||||
+127
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+265
@@ -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));
|
||||
}
|
||||
}
|
||||
+117
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+295
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+234
@@ -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
|
||||
};
|
||||
}
|
||||
+156
@@ -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;
|
||||
}
|
||||
}
|
||||
+97
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -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;
|
||||
}
|
||||
}
|
||||
+387
@@ -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`");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+309
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+594
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+321
@@ -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);
|
||||
}
|
||||
}
|
||||
+446
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+269
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+437
@@ -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`");
|
||||
}
|
||||
}
|
||||
}
|
||||
+184
@@ -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*/);
|
||||
}
|
||||
}
|
||||
}
|
||||
+217
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+527
@@ -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
Reference in New Issue
Block a user