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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user