diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java
new file mode 100644
index 00000000000..e8a4bb5c25b
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java
@@ -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.
+ *
+ * It operates in two phases:
+ *
+ * - 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.
+ *
- 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.
+ *
- 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.
+ *
+ * It also notifies all the detectors before and after the document is processed
+ * such that they can do pre- and post-processing.
+ *
+ * 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.
+ */
+@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> mMethodNameToChecks =
+ new HashMap>();
+ private final Map> mMethodOwnerToChecks =
+ new HashMap>();
+ private final List mFullClassChecks = new ArrayList();
+
+ private final List extends Detector> mAllDetectors;
+ private List[] mNodeTypeDetectors;
+
+ // Really want this:
+ // & 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 names = scanner.getApplicableCallNames();
+ if (names != null) {
+ checkFullClass = false;
+ for (String element : names) {
+ List list = mMethodNameToChecks.get(element);
+ if (list == null) {
+ list = new ArrayList();
+ mMethodNameToChecks.put(element, list);
+ }
+ list.add(scanner);
+ }
+ }
+
+ Collection owners = scanner.getApplicableCallOwners();
+ if (owners != null) {
+ checkFullClass = false;
+ for (String element : owners) {
+ List list = mMethodOwnerToChecks.get(element);
+ if (list == null) {
+ list = new ArrayList();
+ 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 checks = mNodeTypeDetectors[type];
+ if (checks == null) {
+ checks = new ArrayList();
+ 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 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 scanners = mNodeTypeDetectors[type];
+ if (scanners != null) {
+ for (ClassScanner scanner : scanners) {
+ scanner.checkInstruction(context, classNode, method, instruction);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for (Detector detector : mAllDetectors) {
+ detector.afterCheckFile(context);
+ }
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CircularDependencyException.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CircularDependencyException.java
new file mode 100644
index 00000000000..337eb279dfe
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CircularDependencyException.java
@@ -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
+ *
+ * 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.
+ */
+@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;
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java
new file mode 100644
index 00000000000..c4d3eb5eb5d
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java
@@ -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 {
+ 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 fromClassPath(
+ @NonNull LintClient client,
+ @NonNull List classPath,
+ boolean sort) {
+ if (!classPath.isEmpty()) {
+ List libraryEntries = new ArrayList(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 fromClassFiles(
+ @NonNull LintClient client,
+ @NonNull List classFiles, @NonNull List classFolders,
+ boolean sort) {
+ List entries = new ArrayList(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 entries,
+ @NonNull List 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 classFiles = new ArrayList();
+ 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 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 createSuperClassMap(
+ @NonNull LintClient client,
+ @NonNull List libraryEntries,
+ @NonNull List classEntries) {
+ int size = libraryEntries.size() + classEntries.size();
+ Map 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 createSuperClassMap(
+ @NonNull LintClient client,
+ @NonNull List entries) {
+ Map 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 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 mMap;
+
+ public SuperclassVisitor(Map 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);
+ }
+ }
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CompositeIssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CompositeIssueRegistry.java
new file mode 100644
index 00000000000..70b6a09b74a
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CompositeIssueRegistry.java
@@ -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.
+ *
+ * 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.
+ */
+class CompositeIssueRegistry extends IssueRegistry {
+ private final List myRegistries;
+ private List myIssues;
+
+ public CompositeIssueRegistry(@NonNull List registries) {
+ myRegistries = registries;
+ }
+
+ @NonNull
+ @Override
+ public List getIssues() {
+ if (myIssues == null) {
+ List issues = Lists.newArrayListWithExpectedSize(200);
+ for (IssueRegistry registry : myRegistries) {
+ issues.addAll(registry.getIssues());
+ }
+ myIssues = issues;
+ }
+
+ return myIssues;
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java
new file mode 100644
index 00000000000..64334a4eba5
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java
@@ -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.
+ *
+ * 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.
+ */
+@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 must 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() {
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java
new file mode 100644
index 00000000000..ca7e76e7ccc
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java
@@ -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.
+ *
+ * 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.
+ */
+@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> mSuppressed;
+
+ /** Map from id to regular expressions. */
+ @Nullable
+ private Map> mRegexps;
+
+ /**
+ * Map from id to custom {@link Severity} override
+ */
+ private Map 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 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 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 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>();
+ mSeverity = new HashMap();
+
+ 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 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 paths = mSuppressed.get(id);
+ if (paths == null) {
+ paths = new ArrayList(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 ids, int n,
+ @NonNull String regexp, boolean silent) {
+ try {
+ if (mRegexps == null) {
+ mRegexps = new HashMap>();
+ }
+ Pattern pattern = Pattern.compile(regexp);
+ for (String id : ids) {
+ List paths = mRegexps.get(id);
+ if (paths == null) {
+ paths = new ArrayList(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(
+ "\n" + //$NON-NLS-1$
+ "\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 idSet = new HashSet();
+ for (String id : mSuppressed.keySet()) {
+ idSet.add(id);
+ }
+ for (String id : mSeverity.keySet()) {
+ idSet.add(id);
+ }
+ List ids = new ArrayList(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 regexps = mRegexps != null ? mRegexps.get(id) : null;
+ List 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(""); //$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 paths = mSuppressed.get(issue.getId());
+ if (paths == null) {
+ paths = new ArrayList();
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java
new file mode 100644
index 00000000000..2f546594ce6
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java
@@ -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}
+ *
+ * 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.
+ */
+@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 PARENTS = Maps.newHashMapWithExpectedSize(CLASS_COUNT);
+ private static final Set 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 INTERFACES = new HashMap(2);
+ static {
+ INTERFACES.put(CHECKED_TEXT_VIEW, CHECKABLE);
+ INTERFACES.put(COMPOUND_BUTTON, CHECKABLE);
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java
new file mode 100644
index 00000000000..09e70dc7e6b
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java
@@ -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
+ *
+ * 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.
+ */
+@Beta
+public abstract class IssueRegistry {
+ private static List sCategories;
+ private static Map sIdToIssue;
+ private static Map, List> 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 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) {
+ 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 getIssuesForScope(@NonNull EnumSet scope) {
+ List list = sScopeIssues.get(scope);
+ if (list == null) {
+ List issues = getIssues();
+ if (scope.equals(Scope.ALL)) {
+ list = issues;
+ } else {
+ list = new ArrayList(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,
+ @Nullable Map> scopeToDetectors) {
+
+ List issues = getIssuesForScope(scope);
+ if (issues.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ Set> detectorClasses = new HashSet>();
+ Map, EnumSet> detectorToScope =
+ new HashMap, EnumSet>();
+
+ for (Issue issue : issues) {
+ Implementation implementation = issue.getImplementation();
+ Class extends Detector> detectorClass = implementation.getDetectorClass();
+ EnumSet 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 s = detectorToScope.get(detectorClass);
+ if (s == null) {
+ detectorToScope.put(detectorClass, issueScope);
+ } else if (!s.containsAll(issueScope)) {
+ EnumSet union = EnumSet.copyOf(s);
+ union.addAll(issueScope);
+ detectorToScope.put(detectorClass, union);
+ }
+ }
+ }
+
+ List detectors = new ArrayList(detectorClasses.size());
+ for (Class extends Detector> clz : detectorClasses) {
+ try {
+ Detector detector = clz.newInstance();
+ detectors.add(detector);
+
+ if (scopeToDetectors != null) {
+ EnumSet union = detectorToScope.get(clz);
+ for (Scope s : union) {
+ List list = scopeToDetectors.get(s);
+ if (list == null) {
+ list = new ArrayList();
+ 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 getCategories() {
+ if (sCategories == null) {
+ final Set categories = new HashSet();
+ for (Issue issue : getIssues()) {
+ categories.add(issue.getCategory());
+ }
+ List sorted = new ArrayList(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 issues = getIssues();
+ sIdToIssue = new HashMap(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.
+ *
+ * NOTE: This is only intended for testing purposes.
+ */
+ @VisibleForTesting
+ protected static void reset() {
+ sIdToIssue = null;
+ sCategories = null;
+ sScopeIssues = Maps.newHashMap();
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java
new file mode 100644
index 00000000000..0262691fc7e
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java
@@ -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;
+
+/**
+ *
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).
+ *
+ * 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.
+ */
+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> sCache;
+
+ private final List myIssues;
+
+ @NonNull
+ static IssueRegistry get(@NonNull LintClient client, @NonNull File jarFile) throws IOException,
+ ClassNotFoundException, IllegalAccessException, InstantiationException {
+ if (sCache == null) {
+ sCache = new HashMap>();
+ } else {
+ SoftReference 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(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 getIssues() {
+ return myIssues;
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java
new file mode 100644
index 00000000000..3aa55d42868
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java
@@ -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.
+ *
+ * NOTE: This is not public or final API; if you rely on this be prepared
+ * to adjust your code for the next tools release.
+ */
+@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 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 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 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 getConstructors();
+
+ /** Returns the methods defined in this class, and optionally any methods inherited from any superclasses as well */
+ @NonNull
+ public abstract Iterable 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 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 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 reference. 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 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 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();
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java
new file mode 100644
index 00000000000..e852925bc19
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java
@@ -0,0 +1,1398 @@
+/*
+ * 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.ANDROID_PKG;
+import static com.android.SdkConstants.R_CLASS;
+
+import com.android.annotations.NonNull;
+import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
+import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
+import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
+import com.android.tools.lint.detector.api.Detector;
+import com.android.tools.lint.detector.api.Detector.JavaScanner;
+import com.android.tools.lint.detector.api.Detector.XmlScanner;
+import com.android.tools.lint.detector.api.JavaContext;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import lombok.ast.AlternateConstructorInvocation;
+import lombok.ast.Annotation;
+import lombok.ast.AnnotationDeclaration;
+import lombok.ast.AnnotationElement;
+import lombok.ast.AnnotationMethodDeclaration;
+import lombok.ast.AnnotationValueArray;
+import lombok.ast.ArrayAccess;
+import lombok.ast.ArrayCreation;
+import lombok.ast.ArrayDimension;
+import lombok.ast.ArrayInitializer;
+import lombok.ast.Assert;
+import lombok.ast.AstVisitor;
+import lombok.ast.BinaryExpression;
+import lombok.ast.Block;
+import lombok.ast.BooleanLiteral;
+import lombok.ast.Break;
+import lombok.ast.Case;
+import lombok.ast.Cast;
+import lombok.ast.Catch;
+import lombok.ast.CharLiteral;
+import lombok.ast.ClassDeclaration;
+import lombok.ast.ClassLiteral;
+import lombok.ast.Comment;
+import lombok.ast.CompilationUnit;
+import lombok.ast.ConstructorDeclaration;
+import lombok.ast.ConstructorInvocation;
+import lombok.ast.Continue;
+import lombok.ast.Default;
+import lombok.ast.DoWhile;
+import lombok.ast.EmptyDeclaration;
+import lombok.ast.EmptyStatement;
+import lombok.ast.EnumConstant;
+import lombok.ast.EnumDeclaration;
+import lombok.ast.EnumTypeBody;
+import lombok.ast.Expression;
+import lombok.ast.ExpressionStatement;
+import lombok.ast.FloatingPointLiteral;
+import lombok.ast.For;
+import lombok.ast.ForEach;
+import lombok.ast.ForwardingAstVisitor;
+import lombok.ast.Identifier;
+import lombok.ast.If;
+import lombok.ast.ImportDeclaration;
+import lombok.ast.InlineIfExpression;
+import lombok.ast.InstanceInitializer;
+import lombok.ast.InstanceOf;
+import lombok.ast.IntegralLiteral;
+import lombok.ast.InterfaceDeclaration;
+import lombok.ast.KeywordModifier;
+import lombok.ast.LabelledStatement;
+import lombok.ast.MethodDeclaration;
+import lombok.ast.MethodInvocation;
+import lombok.ast.Modifiers;
+import lombok.ast.Node;
+import lombok.ast.NormalTypeBody;
+import lombok.ast.NullLiteral;
+import lombok.ast.PackageDeclaration;
+import lombok.ast.Return;
+import lombok.ast.Select;
+import lombok.ast.StaticInitializer;
+import lombok.ast.StringLiteral;
+import lombok.ast.Super;
+import lombok.ast.SuperConstructorInvocation;
+import lombok.ast.Switch;
+import lombok.ast.Synchronized;
+import lombok.ast.This;
+import lombok.ast.Throw;
+import lombok.ast.Try;
+import lombok.ast.TypeReference;
+import lombok.ast.TypeReferencePart;
+import lombok.ast.TypeVariable;
+import lombok.ast.UnaryExpression;
+import lombok.ast.VariableDeclaration;
+import lombok.ast.VariableDefinition;
+import lombok.ast.VariableDefinitionEntry;
+import lombok.ast.VariableReference;
+import lombok.ast.While;
+
+/**
+ * Specialized visitor for running detectors on a Java AST.
+ * It operates in three phases:
+ *
+ * - First, it computes a set of maps where it generates a map from each
+ * significant AST attribute (such as method call names) to a list
+ * of detectors to consult whenever that attribute is encountered.
+ * Examples of "attributes" are method names, Android resource identifiers,
+ * and general AST node types such as "cast" nodes etc. These are
+ * defined on the {@link JavaScanner} interface.
+ *
- Second, it iterates over the document a single time, delegating to
+ * the detectors found at each relevant AST attribute.
+ *
- Finally, it calls the remaining visitors (those that need to process a
+ * whole document on their own).
+ *
+ * It also notifies all the detectors before and after the document is processed
+ * such that they can do pre- and post-processing.
+ */
+public class JavaVisitor {
+ /** Default size of lists holding detectors of the same type for a given node type */
+ private static final int SAME_TYPE_COUNT = 8;
+
+ private final Map> mMethodDetectors =
+ Maps.newHashMapWithExpectedSize(40);
+ private final Map> mConstructorDetectors =
+ Maps.newHashMapWithExpectedSize(12);
+ private Set mConstructorSimpleNames;
+ private final List mResourceFieldDetectors =
+ new ArrayList();
+ private final List mAllDetectors;
+ private final List mFullTreeDetectors;
+ private final Map, List> mNodeTypeDetectors =
+ new HashMap, List>(16);
+ private final JavaParser mParser;
+ private final Map> mSuperClassDetectors =
+ new HashMap>();
+
+ /**
+ * Number of fatal exceptions (internal errors, usually from ECJ) we've
+ * encountered; we don't log each and every one to avoid massive log spam
+ * in code which triggers this condition
+ */
+ private static int sExceptionCount;
+ /** Max number of logs to include */
+ private static final int MAX_REPORTED_CRASHES = 20;
+
+ JavaVisitor(@NonNull JavaParser parser, @NonNull List detectors) {
+ mParser = parser;
+ mAllDetectors = new ArrayList(detectors.size());
+ mFullTreeDetectors = new ArrayList(detectors.size());
+
+ for (Detector detector : detectors) {
+ VisitingDetector v = new VisitingDetector(detector, (JavaScanner) detector);
+ mAllDetectors.add(v);
+
+ List applicableSuperClasses = detector.applicableSuperClasses();
+ if (applicableSuperClasses != null) {
+ for (String fqn : applicableSuperClasses) {
+ List list = mSuperClassDetectors.get(fqn);
+ if (list == null) {
+ list = new ArrayList(SAME_TYPE_COUNT);
+ mSuperClassDetectors.put(fqn, list);
+ }
+ list.add(v);
+ }
+ continue;
+ }
+
+ List> nodeTypes = detector.getApplicableNodeTypes();
+ if (nodeTypes != null) {
+ for (Class extends Node> type : nodeTypes) {
+ List list = mNodeTypeDetectors.get(type);
+ if (list == null) {
+ list = new ArrayList(SAME_TYPE_COUNT);
+ mNodeTypeDetectors.put(type, list);
+ }
+ list.add(v);
+ }
+ }
+
+ List names = detector.getApplicableMethodNames();
+ if (names != null) {
+ // not supported in Java visitors; adding a method invocation node is trivial
+ // for that case.
+ assert names != XmlScanner.ALL;
+
+ for (String name : names) {
+ List list = mMethodDetectors.get(name);
+ if (list == null) {
+ list = new ArrayList(SAME_TYPE_COUNT);
+ mMethodDetectors.put(name, list);
+ }
+ list.add(v);
+ }
+ }
+
+ List types = detector.getApplicableConstructorTypes();
+ if (types != null) {
+ // not supported in Java visitors; adding a method invocation node is trivial
+ // for that case.
+ assert types != XmlScanner.ALL;
+ if (mConstructorSimpleNames == null) {
+ mConstructorSimpleNames = Sets.newHashSet();
+ }
+ for (String type : types) {
+ List list = mConstructorDetectors.get(type);
+ if (list == null) {
+ list = new ArrayList(SAME_TYPE_COUNT);
+ mConstructorDetectors.put(type, list);
+ mConstructorSimpleNames.add(type.substring(type.lastIndexOf('.')+1));
+ }
+ list.add(v);
+ }
+ }
+
+ if (detector.appliesToResourceRefs()) {
+ mResourceFieldDetectors.add(v);
+ } else if ((names == null || names.isEmpty())
+ && (nodeTypes == null || nodeTypes.isEmpty())
+ && (types == null || types.isEmpty())) {
+ mFullTreeDetectors.add(v);
+ }
+ }
+ }
+
+ void visitFile(@NonNull JavaContext context) {
+ Node compilationUnit = null;
+ try {
+ compilationUnit = mParser.parseJava(context);
+ if (compilationUnit == null) {
+ // No need to log this; the parser should be reporting
+ // a full warning (such as IssueRegistry#PARSER_ERROR)
+ // with details, location, etc.
+ return;
+ }
+ context.setCompilationUnit(compilationUnit);
+
+ for (VisitingDetector v : mAllDetectors) {
+ v.setContext(context);
+ v.getDetector().beforeCheckFile(context);
+ }
+
+ if (!mSuperClassDetectors.isEmpty()) {
+ SuperclassVisitor visitor = new SuperclassVisitor(context);
+ compilationUnit.accept(visitor);
+ }
+
+ for (VisitingDetector v : mFullTreeDetectors) {
+ AstVisitor visitor = v.getVisitor();
+ compilationUnit.accept(visitor);
+ }
+
+ if (!mMethodDetectors.isEmpty() || !mResourceFieldDetectors.isEmpty() ||
+ !mConstructorDetectors.isEmpty()) {
+ AstVisitor visitor = new DelegatingJavaVisitor(context);
+ compilationUnit.accept(visitor);
+ } else if (!mNodeTypeDetectors.isEmpty()) {
+ AstVisitor visitor = new DispatchVisitor();
+ compilationUnit.accept(visitor);
+ }
+
+ for (VisitingDetector v : mAllDetectors) {
+ v.getDetector().afterCheckFile(context);
+ }
+ } catch (RuntimeException e) {
+ if (sExceptionCount++ > MAX_REPORTED_CRASHES) {
+ // No need to keep spamming the user that a lot of the files
+ // are tripping up ECJ, they get the picture.
+ return;
+ }
+
+ // Work around ECJ bugs; see https://code.google.com/p/android/issues/detail?id=172268
+ // Don't allow lint bugs to take down the whole build. TRY to log this as a
+ // lint error instead!
+ StringBuilder sb = new StringBuilder(100);
+ sb.append("Unexpected failure during lint analysis of ");
+ sb.append(context.file.getName());
+ sb.append(" (this is a bug in lint or one of the libraries it depends on)\n");
+
+ StackTraceElement[] stackTrace = e.getStackTrace();
+ int count = 0;
+ for (StackTraceElement frame : stackTrace) {
+ if (count > 0) {
+ sb.append("->");
+ }
+
+ String className = frame.getClassName();
+ sb.append(className.substring(className.lastIndexOf('.') + 1));
+ sb.append('.').append(frame.getMethodName());
+ sb.append('(');
+ sb.append(frame.getFileName()).append(':').append(frame.getLineNumber());
+ sb.append(')');
+ count++;
+ // Only print the top 3-4 frames such that we can identify the bug
+ if (count == 4) {
+ break;
+ }
+ }
+ Throwable throwable = null; // NOT e: this makes for very noisy logs
+ //noinspection ConstantConditions
+ context.log(throwable, sb.toString());
+ } finally {
+ if (compilationUnit != null) {
+ mParser.dispose(context, compilationUnit);
+ }
+ }
+ }
+
+ public void prepare(@NonNull List contexts) {
+ mParser.prepareJavaParse(contexts);
+ }
+
+ public void dispose() {
+ mParser.dispose();
+ }
+
+ private static class VisitingDetector {
+ private AstVisitor mVisitor; // construct lazily, and clear out on context switch!
+ private JavaContext mContext;
+ public final Detector mDetector;
+ public final JavaScanner mJavaScanner;
+
+ public VisitingDetector(@NonNull Detector detector, @NonNull JavaScanner javaScanner) {
+ mDetector = detector;
+ mJavaScanner = javaScanner;
+ }
+
+ @NonNull
+ public Detector getDetector() {
+ return mDetector;
+ }
+
+ @NonNull
+ public JavaScanner getJavaScanner() {
+ return mJavaScanner;
+ }
+
+ public void setContext(@NonNull JavaContext context) {
+ mContext = context;
+
+ // The visitors are one-per-context, so clear them out here and construct
+ // lazily only if needed
+ mVisitor = null;
+ }
+
+ @NonNull
+ AstVisitor getVisitor() {
+ if (mVisitor == null) {
+ mVisitor = mDetector.createJavaVisitor(mContext);
+ if (mVisitor == null) {
+ mVisitor = new ForwardingAstVisitor() {
+ };
+ }
+ }
+ return mVisitor;
+ }
+ }
+
+ private class SuperclassVisitor extends ForwardingAstVisitor {
+ private JavaContext mContext;
+
+ public SuperclassVisitor(@NonNull JavaContext context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean visitClassDeclaration(ClassDeclaration node) {
+ ResolvedNode resolved = mContext.resolve(node);
+ if (!(resolved instanceof ResolvedClass)) {
+ return true;
+ }
+
+ ResolvedClass resolvedClass = (ResolvedClass) resolved;
+ ResolvedClass cls = resolvedClass;
+ while (cls != null) {
+ String fqcn = cls.getSignature();
+ if (fqcn != null) {
+ List list = mSuperClassDetectors.get(fqcn);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getJavaScanner().checkClass(mContext, node, node, resolvedClass);
+ }
+ }
+ }
+
+ cls = cls.getSuperClass();
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean visitConstructorInvocation(ConstructorInvocation node) {
+ NormalTypeBody anonymous = node.astAnonymousClassBody();
+ if (anonymous != null) {
+ ResolvedNode resolved = mContext.resolve(anonymous);
+ if (resolved instanceof ResolvedMethod) {
+ resolved = ((ResolvedMethod) resolved).getContainingClass();
+ }
+ if (!(resolved instanceof ResolvedClass)) {
+ return true;
+ }
+
+ ResolvedClass resolvedClass = (ResolvedClass) resolved;
+ ResolvedClass cls = resolvedClass;
+ while (cls != null) {
+ String fqcn = cls.getSignature();
+ if (fqcn != null) {
+ List list = mSuperClassDetectors.get(fqcn);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getJavaScanner().checkClass(mContext, null, anonymous,
+ resolvedClass);
+ }
+ }
+ }
+
+ cls = cls.getSuperClass();
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean visitImportDeclaration(ImportDeclaration node) {
+ return true;
+ }
+ }
+
+ /**
+ * Generic dispatcher which visits all nodes (once) and dispatches to
+ * specific visitors for each node. Each visitor typically only wants to
+ * look at a small part of a tree, such as a method call or a class
+ * declaration, so this means we avoid visiting all "uninteresting" nodes in
+ * the tree repeatedly.
+ */
+ private class DispatchVisitor extends ForwardingAstVisitor {
+ @Override
+ public void endVisit(Node node) {
+ for (VisitingDetector v : mAllDetectors) {
+ v.getVisitor().endVisit(node);
+ }
+ }
+
+ @Override
+ public boolean visitAlternateConstructorInvocation(AlternateConstructorInvocation node) {
+ List list =
+ mNodeTypeDetectors.get(AlternateConstructorInvocation.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitAlternateConstructorInvocation(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitAnnotation(Annotation node) {
+ List list = mNodeTypeDetectors.get(Annotation.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitAnnotation(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitAnnotationDeclaration(AnnotationDeclaration node) {
+ List list = mNodeTypeDetectors.get(AnnotationDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitAnnotationDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitAnnotationElement(AnnotationElement node) {
+ List list = mNodeTypeDetectors.get(AnnotationElement.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitAnnotationElement(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitAnnotationMethodDeclaration(AnnotationMethodDeclaration node) {
+ List list =
+ mNodeTypeDetectors.get(AnnotationMethodDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitAnnotationMethodDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitAnnotationValueArray(AnnotationValueArray node) {
+ List list = mNodeTypeDetectors.get(AnnotationValueArray.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitAnnotationValueArray(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitArrayAccess(ArrayAccess node) {
+ List list = mNodeTypeDetectors.get(ArrayAccess.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitArrayAccess(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitArrayCreation(ArrayCreation node) {
+ List list = mNodeTypeDetectors.get(ArrayCreation.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitArrayCreation(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitArrayDimension(ArrayDimension node) {
+ List list = mNodeTypeDetectors.get(ArrayDimension.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitArrayDimension(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitArrayInitializer(ArrayInitializer node) {
+ List list = mNodeTypeDetectors.get(ArrayInitializer.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitArrayInitializer(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitAssert(Assert node) {
+ List list = mNodeTypeDetectors.get(Assert.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitAssert(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitBinaryExpression(BinaryExpression node) {
+ List list = mNodeTypeDetectors.get(BinaryExpression.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitBinaryExpression(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitBlock(Block node) {
+ List list = mNodeTypeDetectors.get(Block.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitBlock(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitBooleanLiteral(BooleanLiteral node) {
+ List list = mNodeTypeDetectors.get(BooleanLiteral.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitBooleanLiteral(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitBreak(Break node) {
+ List list = mNodeTypeDetectors.get(Break.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitBreak(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitCase(Case node) {
+ List list = mNodeTypeDetectors.get(Case.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitCase(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitCast(Cast node) {
+ List list = mNodeTypeDetectors.get(Cast.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitCast(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitCatch(Catch node) {
+ List list = mNodeTypeDetectors.get(Catch.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitCatch(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitCharLiteral(CharLiteral node) {
+ List list = mNodeTypeDetectors.get(CharLiteral.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitCharLiteral(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitClassDeclaration(ClassDeclaration node) {
+ List list = mNodeTypeDetectors.get(ClassDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitClassDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitClassLiteral(ClassLiteral node) {
+ List list = mNodeTypeDetectors.get(ClassLiteral.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitClassLiteral(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitComment(Comment node) {
+ List list = mNodeTypeDetectors.get(Comment.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitComment(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitCompilationUnit(CompilationUnit node) {
+ List list = mNodeTypeDetectors.get(CompilationUnit.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitCompilationUnit(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitConstructorDeclaration(ConstructorDeclaration node) {
+ List list = mNodeTypeDetectors.get(ConstructorDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitConstructorDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitConstructorInvocation(ConstructorInvocation node) {
+ List list = mNodeTypeDetectors.get(ConstructorInvocation.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitConstructorInvocation(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitContinue(Continue node) {
+ List list = mNodeTypeDetectors.get(Continue.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitContinue(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitDefault(Default node) {
+ List list = mNodeTypeDetectors.get(Default.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitDefault(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitDoWhile(DoWhile node) {
+ List list = mNodeTypeDetectors.get(DoWhile.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitDoWhile(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitEmptyDeclaration(EmptyDeclaration node) {
+ List list = mNodeTypeDetectors.get(EmptyDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitEmptyDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitEmptyStatement(EmptyStatement node) {
+ List list = mNodeTypeDetectors.get(EmptyStatement.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitEmptyStatement(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitEnumConstant(EnumConstant node) {
+ List list = mNodeTypeDetectors.get(EnumConstant.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitEnumConstant(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitEnumDeclaration(EnumDeclaration node) {
+ List list = mNodeTypeDetectors.get(EnumDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitEnumDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitEnumTypeBody(EnumTypeBody node) {
+ List list = mNodeTypeDetectors.get(EnumTypeBody.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitEnumTypeBody(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitExpressionStatement(ExpressionStatement node) {
+ List list = mNodeTypeDetectors.get(ExpressionStatement.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitExpressionStatement(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitFloatingPointLiteral(FloatingPointLiteral node) {
+ List list = mNodeTypeDetectors.get(FloatingPointLiteral.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitFloatingPointLiteral(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitFor(For node) {
+ List list = mNodeTypeDetectors.get(For.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitFor(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitForEach(ForEach node) {
+ List list = mNodeTypeDetectors.get(ForEach.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitForEach(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitIdentifier(Identifier node) {
+ List list = mNodeTypeDetectors.get(Identifier.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitIdentifier(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitIf(If node) {
+ List list = mNodeTypeDetectors.get(If.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitIf(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitImportDeclaration(ImportDeclaration node) {
+ List list = mNodeTypeDetectors.get(ImportDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitImportDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitInlineIfExpression(InlineIfExpression node) {
+ List list = mNodeTypeDetectors.get(InlineIfExpression.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitInlineIfExpression(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitInstanceInitializer(InstanceInitializer node) {
+ List list = mNodeTypeDetectors.get(InstanceInitializer.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitInstanceInitializer(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitInstanceOf(InstanceOf node) {
+ List list = mNodeTypeDetectors.get(InstanceOf.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitInstanceOf(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitIntegralLiteral(IntegralLiteral node) {
+ List list = mNodeTypeDetectors.get(IntegralLiteral.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitIntegralLiteral(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitInterfaceDeclaration(InterfaceDeclaration node) {
+ List list = mNodeTypeDetectors.get(InterfaceDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitInterfaceDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitKeywordModifier(KeywordModifier node) {
+ List list = mNodeTypeDetectors.get(KeywordModifier.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitKeywordModifier(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitLabelledStatement(LabelledStatement node) {
+ List list = mNodeTypeDetectors.get(LabelledStatement.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitLabelledStatement(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitMethodDeclaration(MethodDeclaration node) {
+ List list = mNodeTypeDetectors.get(MethodDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitMethodDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitMethodInvocation(MethodInvocation node) {
+ List list = mNodeTypeDetectors.get(MethodInvocation.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitMethodInvocation(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitModifiers(Modifiers node) {
+ List list = mNodeTypeDetectors.get(Modifiers.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitModifiers(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitNormalTypeBody(NormalTypeBody node) {
+ List list = mNodeTypeDetectors.get(NormalTypeBody.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitNormalTypeBody(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitNullLiteral(NullLiteral node) {
+ List list = mNodeTypeDetectors.get(NullLiteral.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitNullLiteral(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitPackageDeclaration(PackageDeclaration node) {
+ List list = mNodeTypeDetectors.get(PackageDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitPackageDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitParseArtefact(Node node) {
+ List list = mNodeTypeDetectors.get(Node.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitParseArtefact(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitReturn(Return node) {
+ List list = mNodeTypeDetectors.get(Return.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitReturn(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitSelect(Select node) {
+ List list = mNodeTypeDetectors.get(Select.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitSelect(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitStaticInitializer(StaticInitializer node) {
+ List list = mNodeTypeDetectors.get(StaticInitializer.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitStaticInitializer(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitStringLiteral(StringLiteral node) {
+ List list = mNodeTypeDetectors.get(StringLiteral.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitStringLiteral(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitSuper(Super node) {
+ List list = mNodeTypeDetectors.get(Super.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitSuper(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitSuperConstructorInvocation(SuperConstructorInvocation node) {
+ List list = mNodeTypeDetectors.get(SuperConstructorInvocation.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitSuperConstructorInvocation(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitSwitch(Switch node) {
+ List list = mNodeTypeDetectors.get(Switch.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitSwitch(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitSynchronized(Synchronized node) {
+ List list = mNodeTypeDetectors.get(Synchronized.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitSynchronized(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitThis(This node) {
+ List list = mNodeTypeDetectors.get(This.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitThis(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitThrow(Throw node) {
+ List list = mNodeTypeDetectors.get(Throw.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitThrow(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitTry(Try node) {
+ List list = mNodeTypeDetectors.get(Try.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitTry(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitTypeReference(TypeReference node) {
+ List list = mNodeTypeDetectors.get(TypeReference.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitTypeReference(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitTypeReferencePart(TypeReferencePart node) {
+ List list = mNodeTypeDetectors.get(TypeReferencePart.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitTypeReferencePart(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitTypeVariable(TypeVariable node) {
+ List list = mNodeTypeDetectors.get(TypeVariable.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitTypeVariable(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitUnaryExpression(UnaryExpression node) {
+ List list = mNodeTypeDetectors.get(UnaryExpression.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitUnaryExpression(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitVariableDeclaration(VariableDeclaration node) {
+ List list = mNodeTypeDetectors.get(VariableDeclaration.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitVariableDeclaration(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitVariableDefinition(VariableDefinition node) {
+ List list = mNodeTypeDetectors.get(VariableDefinition.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitVariableDefinition(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) {
+ List list = mNodeTypeDetectors.get(VariableDefinitionEntry.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitVariableDefinitionEntry(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitVariableReference(VariableReference node) {
+ List list = mNodeTypeDetectors.get(VariableReference.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitVariableReference(node);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean visitWhile(While node) {
+ List list = mNodeTypeDetectors.get(While.class);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getVisitor().visitWhile(node);
+ }
+ }
+ return false;
+ }
+ }
+
+ /** Performs common AST searches for method calls and R-type-field references.
+ * Note that this is a specialized form of the {@link DispatchVisitor}. */
+ private class DelegatingJavaVisitor extends DispatchVisitor {
+ private final JavaContext mContext;
+ private final boolean mVisitResources;
+ private final boolean mVisitMethods;
+ private final boolean mVisitConstructors;
+
+ public DelegatingJavaVisitor(JavaContext context) {
+ mContext = context;
+
+ mVisitMethods = !mMethodDetectors.isEmpty();
+ mVisitConstructors = !mConstructorDetectors.isEmpty();
+ mVisitResources = !mResourceFieldDetectors.isEmpty();
+ }
+
+ @Override
+ public boolean visitSelect(Select node) {
+ if (mVisitResources) {
+ // R.type.name
+ if (node.astOperand() instanceof Select) {
+ Select select = (Select) node.astOperand();
+ if (select.astOperand() instanceof VariableReference) {
+ VariableReference reference = (VariableReference) select.astOperand();
+ if (reference.astIdentifier().astValue().equals(R_CLASS)) {
+ String type = select.astIdentifier().astValue();
+ String name = node.astIdentifier().astValue();
+
+ // R -could- be referenced locally and really have been
+ // imported as "import android.R;" in the import statements,
+ // but this is not recommended (and in fact there's a specific
+ // lint rule warning against it)
+ boolean isFramework = false;
+
+ for (VisitingDetector v : mResourceFieldDetectors) {
+ JavaScanner detector = v.getJavaScanner();
+ //noinspection ConstantConditions
+ detector.visitResourceReference(mContext, v.getVisitor(),
+ node, type, name, isFramework);
+ }
+
+ return super.visitSelect(node);
+ }
+ }
+ }
+
+ // Arbitrary packages -- android.R.type.name, foo.bar.R.type.name
+ if (node.astIdentifier().astValue().equals(R_CLASS)) {
+ Node parent = node.getParent();
+ if (parent instanceof Select) {
+ Node grandParent = parent.getParent();
+ if (grandParent instanceof Select) {
+ Select select = (Select) grandParent;
+ String name = select.astIdentifier().astValue();
+ Expression typeOperand = select.astOperand();
+ if (typeOperand instanceof Select) {
+ Select typeSelect = (Select) typeOperand;
+ String type = typeSelect.astIdentifier().astValue();
+ boolean isFramework = node.astOperand().toString().equals(
+ ANDROID_PKG);
+ for (VisitingDetector v : mResourceFieldDetectors) {
+ JavaScanner detector = v.getJavaScanner();
+ detector.visitResourceReference(mContext, v.getVisitor(),
+ node, type, name, isFramework);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return super.visitSelect(node);
+ }
+
+ @Override
+ public boolean visitMethodInvocation(MethodInvocation node) {
+ if (mVisitMethods) {
+ String methodName = node.astName().astValue();
+ List list = mMethodDetectors.get(methodName);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getJavaScanner().visitMethod(mContext, v.getVisitor(), node);
+ }
+ }
+ }
+
+ return super.visitMethodInvocation(node);
+ }
+
+ @Override
+ public boolean visitConstructorInvocation(ConstructorInvocation node) {
+ if (mVisitConstructors) {
+ TypeReference typeReference = node.astTypeReference();
+ if (typeReference != null) {
+ TypeReferencePart last = typeReference.astParts().last();
+ if (last != null) {
+ String name = last.astIdentifier().astValue();
+ if (mConstructorSimpleNames.contains(name)) {
+ ResolvedNode resolved = mContext.resolve(node);
+ if (resolved instanceof ResolvedMethod) {
+ ResolvedMethod method = (ResolvedMethod) resolved;
+ String type = method.getContainingClass().getSignature();
+ List list = mConstructorDetectors.get(type);
+ if (list != null) {
+ for (VisitingDetector v : list) {
+ v.getJavaScanner().visitConstructor(mContext,
+ v.getVisitor(), node, method);
+ }
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+ return super.visitConstructorInvocation(node);
+ }
+ }
+}
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java
new file mode 100755
index 00000000000..fcb4dfe14d3
--- /dev/null
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java
@@ -0,0 +1,1053 @@
+/*
+ * 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.CLASS_FOLDER;
+import static com.android.SdkConstants.DOT_AAR;
+import static com.android.SdkConstants.DOT_JAR;
+import static com.android.SdkConstants.GEN_FOLDER;
+import static com.android.SdkConstants.LIBS_FOLDER;
+import static com.android.SdkConstants.RES_FOLDER;
+import static com.android.SdkConstants.SRC_FOLDER;
+import static com.android.tools.lint.detector.api.LintUtils.endsWith;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.repository.ResourceVisibilityLookup;
+import com.android.ide.common.res2.AbstractResourceRepository;
+import com.android.ide.common.res2.ResourceItem;
+import com.android.prefs.AndroidLocation;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkVersionInfo;
+import com.android.sdklib.repository.local.LocalSdk;
+import com.android.tools.lint.detector.api.Context;
+import com.android.tools.lint.detector.api.Detector;
+import com.android.tools.lint.detector.api.Issue;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.android.tools.lint.detector.api.Location;
+import com.android.tools.lint.detector.api.Project;
+import com.android.tools.lint.detector.api.Severity;
+import com.android.tools.lint.detector.api.TextFormat;
+import com.android.utils.XmlUtils;
+import com.google.common.annotations.Beta;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Information about the tool embedding the lint analyzer. IDEs and other tools
+ * implementing lint support will extend this to integrate logging, displaying errors,
+ * etc.
+ *
+ * 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.
+ */
+@Beta
+public abstract class LintClient {
+ private static final String PROP_BIN_DIR = "com.android.tools.lint.bindir"; //$NON-NLS-1$
+
+ /**
+ * Returns a configuration for use by the given project. The configuration
+ * provides information about which issues are enabled, any customizations
+ * to the severity of an issue, etc.
+ *
+ * By default this method returns a {@link DefaultConfiguration}.
+ *
+ * @param project the project to obtain a configuration for
+ * @return a configuration, never null.
+ */
+ public Configuration getConfiguration(@NonNull Project project) {
+ return DefaultConfiguration.create(this, project, null);
+ }
+
+ /**
+ * Report the given issue. This method will only be called if the configuration
+ * provided by {@link #getConfiguration(Project)} has reported the corresponding
+ * issue as enabled and has not filtered out the issue with its
+ * {@link Configuration#ignore(Context,Issue,Location,String)} method.
+ *
+ * @param context the context used by the detector when the issue was found
+ * @param issue the issue that was found
+ * @param severity the severity of the issue
+ * @param location the location of the issue
+ * @param message the associated user message
+ * @param format the format of the description and location descriptions
+ */
+ public abstract void report(
+ @NonNull Context context,
+ @NonNull Issue issue,
+ @NonNull Severity severity,
+ @Nullable Location location,
+ @NonNull String message,
+ @NonNull TextFormat format);
+
+ /**
+ * Send an exception or error message (with warning severity) to the log
+ *
+ * @param exception the exception, possibly null
+ * @param format the error message using {@link String#format} syntax, possibly null
+ * (though in that case the exception should not be null)
+ * @param args any arguments for the format string
+ */
+ public void log(
+ @Nullable Throwable exception,
+ @Nullable String format,
+ @Nullable Object... args) {
+ log(Severity.WARNING, exception, format, args);
+ }
+
+ /**
+ * Send an exception or error message to the log
+ *
+ * @param severity the severity of the warning
+ * @param exception the exception, possibly null
+ * @param format the error message using {@link String#format} syntax, possibly null
+ * (though in that case the exception should not be null)
+ * @param args any arguments for the format string
+ */
+ public abstract void log(
+ @NonNull Severity severity,
+ @Nullable Throwable exception,
+ @Nullable String format,
+ @Nullable Object... args);
+
+ /**
+ * Returns a {@link XmlParser} to use to parse XML
+ *
+ * @return a new {@link XmlParser}, or null if this client does not support
+ * XML analysis
+ */
+ @Nullable
+ public abstract XmlParser getXmlParser();
+
+ /**
+ * Returns a {@link JavaParser} to use to parse Java
+ *
+ * @param project the project to parse, if known (this can be used to look up
+ * the class path for type attribution etc, and it can also be used
+ * to more efficiently process a set of files, for example to
+ * perform type attribution for multiple units in a single pass)
+ * @return a new {@link JavaParser}, or null if this client does not
+ * support Java analysis
+ */
+ @Nullable
+ public abstract JavaParser getJavaParser(@Nullable Project project);
+
+ /**
+ * Returns an optimal detector, if applicable. By default, just returns the
+ * original detector, but tools can replace detectors using this hook with a version
+ * that takes advantage of native capabilities of the tool.
+ *
+ * @param detectorClass the class of the detector to be replaced
+ * @return the new detector class, or just the original detector (not null)
+ */
+ @NonNull
+ public Class extends Detector> replaceDetector(
+ @NonNull Class extends Detector> detectorClass) {
+ return detectorClass;
+ }
+
+ /**
+ * Reads the given text file and returns the content as a string
+ *
+ * @param file the file to read
+ * @return the string to return, never null (will be empty if there is an
+ * I/O error)
+ */
+ @NonNull
+ public abstract String readFile(@NonNull File file);
+
+ /**
+ * Reads the given binary file and returns the content as a byte array.
+ * By default this method will read the bytes from the file directly,
+ * but this can be customized by a client if for example I/O could be
+ * held in memory and not flushed to disk yet.
+ *
+ * @param file the file to read
+ * @return the bytes in the file, never null
+ * @throws IOException if the file does not exist, or if the file cannot be
+ * read for some reason
+ */
+ @NonNull
+ public byte[] readBytes(@NonNull File file) throws IOException {
+ return Files.toByteArray(file);
+ }
+
+ /**
+ * Returns the list of source folders for Java source files
+ *
+ * @param project the project to look up Java source file locations for
+ * @return a list of source folders to search for .java files
+ */
+ @NonNull
+ public List getJavaSourceFolders(@NonNull Project project) {
+ return getClassPath(project).getSourceFolders();
+ }
+
+ /**
+ * Returns the list of output folders for class files
+ *
+ * @param project the project to look up class file locations for
+ * @return a list of output folders to search for .class files
+ */
+ @NonNull
+ public List getJavaClassFolders(@NonNull Project project) {
+ return getClassPath(project).getClassFolders();
+
+ }
+
+ /**
+ * Returns the list of Java libraries
+ *
+ * @param project the project to look up jar dependencies for
+ * @return a list of jar dependencies containing .class files
+ */
+ @NonNull
+ public List getJavaLibraries(@NonNull Project project) {
+ return getClassPath(project).getLibraries();
+ }
+
+ /**
+ * Returns the list of source folders for test source files
+ *
+ * @param project the project to look up test source file locations for
+ * @return a list of source folders to search for .java files
+ */
+ @NonNull
+ public List getTestSourceFolders(@NonNull Project project) {
+ return getClassPath(project).getTestSourceFolders();
+ }
+
+ /**
+ * Returns the resource folders.
+ *
+ * @param project the project to look up the resource folder for
+ * @return a list of files pointing to the resource folders, possibly empty
+ */
+ @NonNull
+ public List getResourceFolders(@NonNull Project project) {
+ File res = new File(project.getDir(), RES_FOLDER);
+ if (res.exists()) {
+ return Collections.singletonList(res);
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns the {@link SdkInfo} to use for the given project.
+ *
+ * @param project the project to look up an {@link SdkInfo} for
+ * @return an {@link SdkInfo} for the project
+ */
+ @NonNull
+ public SdkInfo getSdkInfo(@NonNull Project project) {
+ // By default no per-platform SDK info
+ return new DefaultSdkInfo();
+ }
+
+ /**
+ * Returns a suitable location for storing cache files. Note that the
+ * directory may not exist.
+ *
+ * @param create if true, attempt to create the cache dir if it does not
+ * exist
+ * @return a suitable location for storing cache files, which may be null if
+ * the create flag was false, or if for some reason the directory
+ * could not be created
+ */
+ @Nullable
+ public File getCacheDir(boolean create) {
+ String home = System.getProperty("user.home");
+ String relative = ".android" + File.separator + "cache"; //$NON-NLS-1$ //$NON-NLS-2$
+ File dir = new File(home, relative);
+ if (create && !dir.exists()) {
+ if (!dir.mkdirs()) {
+ return null;
+ }
+ }
+ return dir;
+ }
+
+ /**
+ * Returns the File corresponding to the system property or the environment variable
+ * for {@link #PROP_BIN_DIR}.
+ * This property is typically set by the SDK/tools/lint[.bat] wrapper.
+ * It denotes the path of the wrapper on disk.
+ *
+ * @return A new File corresponding to {@link LintClient#PROP_BIN_DIR} or null.
+ */
+ @Nullable
+ private static File getLintBinDir() {
+ // First check the Java properties (e.g. set using "java -jar ... -Dname=value")
+ String path = System.getProperty(PROP_BIN_DIR);
+ if (path == null || path.isEmpty()) {
+ // If not found, check environment variables.
+ path = System.getenv(PROP_BIN_DIR);
+ }
+ if (path != null && !path.isEmpty()) {
+ File file = new File(path);
+ if (file.exists()) {
+ return file;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the File pointing to the user's SDK install area. This is generally
+ * the root directory containing the lint tool (but also platforms/ etc).
+ *
+ * @return a file pointing to the user's install area
+ */
+ @Nullable
+ public File getSdkHome() {
+ File binDir = getLintBinDir();
+ if (binDir != null) {
+ assert binDir.getName().equals("tools");
+
+ File root = binDir.getParentFile();
+ if (root != null && root.isDirectory()) {
+ return root;
+ }
+ }
+
+ String home = System.getenv("ANDROID_HOME"); //$NON-NLS-1$
+ if (home != null) {
+ return new File(home);
+ }
+
+ return null;
+ }
+
+ /**
+ * Locates an SDK resource (relative to the SDK root directory).
+ *
+ * TODO: Consider switching to a {@link URL} return type instead.
+ *
+ * @param relativePath A relative path (using {@link File#separator} to
+ * separate path components) to the given resource
+ * @return a {@link File} pointing to the resource, or null if it does not
+ * exist
+ */
+ @Nullable
+ public File findResource(@NonNull String relativePath) {
+ File top = getSdkHome();
+ if (top == null) {
+ throw new IllegalArgumentException("Lint must be invoked with the System property "
+ + PROP_BIN_DIR + " pointing to the ANDROID_SDK tools directory");
+ }
+
+ File file = new File(top, relativePath);
+ if (file.exists()) {
+ return file;
+ } else {
+ return null;
+ }
+ }
+
+ private Map mProjectInfo;
+
+ /**
+ * Returns true if this project is a Gradle-based Android project
+ *
+ * @param project the project to check
+ * @return true if this is a Gradle-based project
+ */
+ public boolean isGradleProject(Project project) {
+ // This is not an accurate test; specific LintClient implementations (e.g.
+ // IDEs or a gradle-integration of lint) have more context and can perform a more accurate
+ // check
+ if (new File(project.getDir(), SdkConstants.FN_BUILD_GRADLE).exists()) {
+ return true;
+ }
+
+ File parent = project.getDir().getParentFile();
+ if (parent != null && parent.getName().equals(SdkConstants.FD_SOURCES)) {
+ File root = parent.getParentFile();
+ if (root != null && new File(root, SdkConstants.FN_BUILD_GRADLE).exists()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Information about class paths (sources, class files and libraries)
+ * usually associated with a project.
+ */
+ protected static class ClassPathInfo {
+ private final List mClassFolders;
+ private final List mSourceFolders;
+ private final List mLibraries;
+ private final List mTestFolders;
+
+ public ClassPathInfo(
+ @NonNull List sourceFolders,
+ @NonNull List classFolders,
+ @NonNull List libraries,
+ @NonNull List testFolders) {
+ mSourceFolders = sourceFolders;
+ mClassFolders = classFolders;
+ mLibraries = libraries;
+ mTestFolders = testFolders;
+ }
+
+ @NonNull
+ public List getSourceFolders() {
+ return mSourceFolders;
+ }
+
+ @NonNull
+ public List getClassFolders() {
+ return mClassFolders;
+ }
+
+ @NonNull
+ public List getLibraries() {
+ return mLibraries;
+ }
+
+ public List getTestSourceFolders() {
+ return mTestFolders;
+ }
+ }
+
+ /**
+ * Considers the given project as an Eclipse project and returns class path
+ * information for the project - the source folder(s), the output folder and
+ * any libraries.
+ *
+ * Callers will not cache calls to this method, so if it's expensive to compute
+ * the classpath info, this method should perform its own caching.
+ *
+ * @param project the project to look up class path info for
+ * @return a class path info object, never null
+ */
+ @NonNull
+ protected ClassPathInfo getClassPath(@NonNull Project project) {
+ ClassPathInfo info;
+ if (mProjectInfo == null) {
+ mProjectInfo = Maps.newHashMap();
+ info = null;
+ } else {
+ info = mProjectInfo.get(project);
+ }
+
+ if (info == null) {
+ List sources = new ArrayList