Lint: Update diagnostics because of the new Uast. Also, these diagnostics are from AS 2.2.

This commit is contained in:
Yan Zhulanow
2016-10-13 20:24:37 +03:00
committed by Yan Zhulanow
parent 4c4d6a4ad4
commit c2ddd943f9
152 changed files with 27552 additions and 7266 deletions
-1
View File
@@ -10,7 +10,6 @@
<orderEntry type="library" name="guava" level="project" />
<orderEntry type="module" module-name="uast-common" />
<orderEntry type="module" module-name="uast-java" />
<orderEntry type="module" module-name="uast-android" />
<orderEntry type="library" name="android-plugin" level="project" />
<orderEntry type="library" name="intellij-core" level="project" />
<orderEntry type="module" module-name="android-annotations" />
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.ANDROID_PKG;
import com.android.annotations.NonNull;
import com.android.resources.ResourceType;
import org.jetbrains.uast.UExpression;
public class AndroidReference {
public final UExpression node;
private final String rPackage;
private final ResourceType type;
private final String name;
// getPackage() can be empty if not a package-qualified import (e.g. android.R.id.name).
@NonNull
public String getPackage() {
return rPackage;
}
@NonNull
public ResourceType getType() {
return type;
}
@NonNull
public String getName() {
return name;
}
boolean isFramework() {
return rPackage.equals(ANDROID_PKG);
}
public AndroidReference(
UExpression node,
String rPackage,
ResourceType type,
String name) {
this.node = node;
this.rPackage = rPackage;
this.type = type;
this.name = name;
}
}
@@ -21,9 +21,18 @@ import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.google.common.annotations.Beta;
import org.jetbrains.org.objectweb.asm.tree.*;
import java.util.*;
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.
@@ -81,9 +90,9 @@ class AsmVisitor {
if (names != null) {
checkFullClass = false;
for (String element : names) {
List<ClassScanner> list = mMethodNameToChecks.get(element);
List<Detector.ClassScanner> list = mMethodNameToChecks.get(element);
if (list == null) {
list = new ArrayList<ClassScanner>();
list = new ArrayList<Detector.ClassScanner>();
mMethodNameToChecks.put(element, list);
}
list.add(scanner);
@@ -94,9 +103,9 @@ class AsmVisitor {
if (owners != null) {
checkFullClass = false;
for (String element : owners) {
List<ClassScanner> list = mMethodOwnerToChecks.get(element);
List<Detector.ClassScanner> list = mMethodOwnerToChecks.get(element);
if (list == null) {
list = new ArrayList<ClassScanner>();
list = new ArrayList<Detector.ClassScanner>();
mMethodOwnerToChecks.put(element, list);
}
list.add(scanner);
@@ -16,14 +16,19 @@
package com.android.tools.klint.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.jetbrains.org.objectweb.asm.ClassReader;
import org.jetbrains.org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import java.io.File;
import java.io.FileInputStream;
@@ -35,10 +40,6 @@ import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static org.jetbrains.org.objectweb.asm.Opcodes.ASM5;
/** A class, present either as a .class file on disk, or inside a .jar file. */
@VisibleForTesting
class ClassEntry implements Comparable<ClassEntry> {
@@ -27,7 +27,7 @@ import com.google.common.annotations.Beta;
/**
* Lint configuration for an Android project such as which specific rules to include,
* which specific rules to exclude, and which specific errors to ignore.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -16,6 +16,9 @@
package com.android.tools.klint.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.klint.detector.api.Context;
@@ -28,22 +31,35 @@ 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.*;
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.*;
import java.util.*;
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;
import static com.android.SdkConstants.CURRENT_PLATFORM;
import static com.android.SdkConstants.PLATFORM_WINDOWS;
/**
* Default implementation of a {@link Configuration} which reads and writes
* configuration data into {@code lint.xml} in the project directory.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -251,7 +267,7 @@ public class DefaultConfiguration extends Configuration {
}, mClient);
mClient.report(new Context(driver, mProject, mProject, mConfigFile),
IssueRegistry.LINT_ERROR,
mProject.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR),
mProject.getConfiguration(driver).getSeverity(IssueRegistry.LINT_ERROR),
Location.create(mConfigFile), message, TextFormat.RAW);
}
@@ -348,7 +364,6 @@ public class DefaultConfiguration extends Configuration {
}
}
@VisibleForTesting
@NonNull
public static String globToRegexp(@NonNull String glob) {
StringBuilder sb = new StringBuilder(glob.length() * 2);
@@ -16,6 +16,52 @@
package com.android.tools.klint.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;
@@ -26,8 +72,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static com.android.SdkConstants.*;
/**
* Default simple implementation of an {@link SdkInfo}
* <p>
@@ -85,7 +129,7 @@ class DefaultSdkInfo extends SdkInfo {
if (parent.equals(child)) {
return true;
}
if (implementsInterface(child, parentType)) {
if (implementsInterface(child, parent)) {
return true;
}
child = PARENTS.get(child);
@@ -102,7 +146,7 @@ class DefaultSdkInfo extends SdkInfo {
return interfaceName.equals(INTERFACES.get(className));
}
// Strip off type parameters, e.g. AdapterView<?> => AdapterView
// Strip off type parameters, e.g. AdapterView<?> AdapterView
private static String getRawType(String type) {
if (type != null) {
int index = type.indexOf('<');
@@ -18,7 +18,6 @@ package com.android.tools.klint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
@@ -27,8 +26,16 @@ import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.google.common.annotations.Beta;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.*;
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
@@ -38,8 +45,8 @@ import java.util.*;
*/
@Beta
public abstract class IssueRegistry {
private static List<Category> sCategories;
private static Map<String, Issue> sIdToIssue;
private static volatile List<Category> sCategories;
private static volatile Map<String, Issue> sIdToIssue;
private static Map<EnumSet<Scope>, List<Issue>> sScopeIssues = Maps.newHashMap();
/**
@@ -265,19 +272,31 @@ public abstract class IssueRegistry {
*
* @return an iterator for all the categories, never null
*/
@SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
@NonNull
public List<Category> getCategories() {
if (sCategories == null) {
final Set<Category> categories = new HashSet<Category>();
for (Issue issue : getIssues()) {
categories.add(issue.getCategory());
List<Category> categories = sCategories;
if (categories == null) {
synchronized (IssueRegistry.class) {
categories = sCategories;
if (categories == null) {
sCategories = categories = Collections.unmodifiableList(createCategoryList());
}
}
List<Category> sorted = new ArrayList<Category>(categories);
Collections.sort(sorted);
sCategories = Collections.unmodifiableList(sorted);
}
return sCategories;
return categories;
}
@NonNull
private List<Category> createCategoryList() {
Set<Category> categorySet = Sets.newHashSetWithExpectedSize(20);
for (Issue issue : getIssues()) {
categorySet.add(issue.getCategory());
}
List<Category> sorted = new ArrayList<Category>(categorySet);
Collections.sort(sorted);
return sorted;
}
/**
@@ -286,27 +305,39 @@ public abstract class IssueRegistry {
* @param id the id to be checked
* @return the corresponding issue, or null
*/
@SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
@Nullable
public final Issue getIssue(@NonNull String id) {
if (sIdToIssue == null) {
List<Issue> issues = getIssues();
sIdToIssue = new HashMap<String, Issue>(issues.size());
for (Issue issue : issues) {
sIdToIssue.put(issue.getId(), issue);
Map<String, Issue> map = sIdToIssue;
if (map == null) {
synchronized (IssueRegistry.class) {
map = sIdToIssue;
if (map == null) {
map = createIdToIssueMap();
sIdToIssue = map;
}
}
sIdToIssue.put(PARSER_ERROR.getId(), PARSER_ERROR);
sIdToIssue.put(LINT_ERROR.getId(), LINT_ERROR);
}
return sIdToIssue.get(id);
return map.get(id);
}
@NonNull
private Map<String, Issue> createIdToIssueMap() {
List<Issue> issues = getIssues();
Map<String, Issue> map = Maps.newHashMapWithExpectedSize(issues.size() + 2);
for (Issue issue : issues) {
map.put(issue.getId(), issue);
}
map.put(PARSER_ERROR.getId(), PARSER_ERROR);
map.put(LINT_ERROR.getId(), LINT_ERROR);
return map;
}
/**
* Reset the registry such that it recomputes its available issues.
* <p>
* NOTE: This is only intended for testing purposes.
*/
@VisibleForTesting
protected static void reset() {
sIdToIssue = null;
sCategories = null;
@@ -15,23 +15,32 @@
*/
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.DOT_CLASS;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.EnumSet;
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.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* <p> An {@link IssueRegistry} for a custom lint rule jar file. The rule jar should provide a
@@ -46,15 +55,24 @@ 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 final String MF_LINT_REGISTRY_OLD = "Lint-Registry"; //$NON-NLS-1$
private static final String MF_LINT_REGISTRY = "Lint-Registry-v2"; //$NON-NLS-1$
private static Map<File, SoftReference<JarFileIssueRegistry>> sCache;
private final List<Issue> myIssues;
private boolean mHasLegacyDetectors;
/** True if one or more java detectors were found that use the old Lombok-based API */
public boolean hasLegacyDetectors() {
return mHasLegacyDetectors;
}
@NonNull
static IssueRegistry get(@NonNull LintClient client, @NonNull File jarFile) throws IOException,
ClassNotFoundException, IllegalAccessException, InstantiationException {
static JarFileIssueRegistry get(@NonNull LintClient client, @NonNull File jarFile)
throws IOException, ClassNotFoundException, IllegalAccessException,
InstantiationException {
if (sCache == null) {
sCache = new HashMap<File, SoftReference<JarFileIssueRegistry>>();
} else {
@@ -67,6 +85,9 @@ class JarFileIssueRegistry extends IssueRegistry {
}
}
// Ensure that the scope-to-detector map doesn't return stale results
IssueRegistry.reset();
JarFileIssueRegistry registry = new JarFileIssueRegistry(client, jarFile);
sCache.put(jarFile, new SoftReference<JarFileIssueRegistry>(registry));
return registry;
@@ -78,19 +99,48 @@ class JarFileIssueRegistry extends IssueRegistry {
myIssues = Lists.newArrayList();
JarFile jarFile = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
jarFile = new JarFile(file);
Manifest manifest = jarFile.getManifest();
Attributes attrs = manifest.getMainAttributes();
Object object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY));
boolean isLegacy = false;
if (object == null) {
object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY_OLD));
//noinspection VariableNotUsedInsideIf
if (object != null) {
// It's an old rule. We don't yet conclude that
// mHasLegacyDetectors=true
// because the lint checks may not be Java related.
isLegacy = true;
}
}
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},
ClassLoader loader = client.createUrlClassLoader(new URL[]{url},
JarFileIssueRegistry.class.getClassLoader());
Class<?> registryClass = Class.forName(className, true, loader);
IssueRegistry registry = (IssueRegistry) registryClass.newInstance();
myIssues.addAll(registry.getIssues());
if (isLegacy) {
// If it's an old registry, look through the issues to see if it
// provides Java scanning and if so create the old style visitors
for (Issue issue : myIssues) {
EnumSet<Scope> scope = issue.getImplementation().getScope();
if (scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.JAVA_LIBRARIES)
|| scope.contains(Scope.ALL_JAVA_FILES)) {
mHasLegacyDetectors = true;
break;
}
}
}
if (loader instanceof URLClassLoader) {
loadAndCloseURLClassLoader(client, file, (URLClassLoader)loader);
}
} else {
client.log(Severity.ERROR, null,
"Custom lint rule jar %1$s does not contain a valid registry manifest key " +
@@ -105,6 +155,79 @@ class JarFileIssueRegistry extends IssueRegistry {
}
}
/**
* Work around http://bugs.java.com/bugdatabase/view_bug.do?bug_id=5041014 :
* URLClassLoader, on Windows, locks the .jar file forever.
* As of Java 7, there's a workaround: you can call close() when you're "done"
* with the file. We'll do that here. However, the whole point of the
* {@linkplain JarFileIssueRegistry} is that when lint is run over and over again
* as the user is editing in the IDE and we're background checking the code, we
* don't to keep loading the custom view classes over and over again: we want to
* cache them. Therefore, just closing the URLClassLoader right away isn't great
* either. However, it turns out it's safe to close the URLClassLoader once you've
* loaded the classes you need, since the URLClassLoader will continue to serve
* those classes even after its close() methods has been called.
* <p>
* Therefore, if we can call close() on this URLClassLoader, we'll proactively load
* all class files we find in the .jar file, then close it.
*
* @param client the client to report errors to
* @param file the .jar file
* @param loader the URLClassLoader we should close
*/
private static void loadAndCloseURLClassLoader(
@NonNull LintClient client,
@NonNull File file,
@NonNull URLClassLoader loader) {
try {
// Proactively close out the .jar file. This is only available on Java 7.
Method closeMethod = loader.getClass().getDeclaredMethod("close");
// But first, proactively load all classes:
try {
InputStream inputStream = new FileInputStream(file);
try {
JarInputStream jarInputStream = new JarInputStream(inputStream);
try {
ZipEntry entry = jarInputStream.getNextEntry();
while (entry != null) {
String name = entry.getName();
// Load non-inner-classes
if (name.endsWith(DOT_CLASS)) {
// Strip .class suffix and change .jar file path (/)
// to class name (.'s).
name = name.substring(0,
name.length() - DOT_CLASS.length());
name = name.replace('/', '.');
try {
Class.forName(name, true, loader);
} catch (Throwable e) {
client.log(Severity.ERROR, e,
"Failed to prefetch " + name + " from " + file);
}
}
entry = jarInputStream.getNextEntry();
}
} finally {
jarInputStream.close();
}
} finally {
inputStream.close();
}
} catch (Throwable ignore) {
} finally {
// Finally close the URL class loader
try {
closeMethod.invoke(loader);
} catch (Throwable ignore) {
// Couldn't close. This is unlikely.
}
}
} catch (NoSuchMethodException ignore) {
// No close method - we're on 1.6
}
}
@NonNull
@Override
public List<Issue> getIssues() {
@@ -0,0 +1,266 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.ClassContext;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMember;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiType;
import java.io.File;
@SuppressWarnings("MethodMayBeStatic") // Some of these methods may be overridden by LintClients
public abstract class JavaEvaluator {
public abstract boolean extendsClass(
@Nullable PsiClass cls,
@NonNull String className,
boolean strict);
public abstract boolean implementsInterface(
@NonNull PsiClass cls,
@NonNull String interfaceName,
boolean strict);
public boolean isMemberInSubClassOf(
@NonNull PsiMember method,
@NonNull String className,
boolean strict) {
PsiClass containingClass = method.getContainingClass();
return containingClass != null && extendsClass(containingClass, className, strict);
}
public static boolean isMemberInClass(
@Nullable PsiMember method,
@NonNull String className) {
if (method == null) {
return false;
}
PsiClass containingClass = method.getContainingClass();
return containingClass != null && className.equals(containingClass.getQualifiedName());
}
public int getParameterCount(@NonNull PsiMethod method) {
return method.getParameterList().getParametersCount();
}
/**
* Checks whether the class extends a super class or implements a given interface. Like calling
* both {@link #extendsClass(PsiClass, String, boolean)} and {@link
* #implementsInterface(PsiClass, String, boolean)}.
*/
public boolean inheritsFrom(
@NonNull PsiClass cls,
@NonNull String className,
boolean strict) {
return extendsClass(cls, className, strict) || implementsInterface(cls, className, strict);
}
/**
* Returns true if the given method (which is typically looked up by resolving a method call) is
* either a method in the exact given class, or if {@code allowInherit} is true, a method in a
* class possibly extending the given class, and if the parameter types are the exact types
* specified.
*
* @param method the method in question
* @param className the class name the method should be defined in or inherit from (or
* if null, allow any class)
* @param allowInherit whether we allow checking for inheritance
* @param argumentTypes the names of the types of the parameters
* @return true if this method is defined in the given class and with the given parameters
*/
public boolean methodMatches(
@NonNull PsiMethod method,
@Nullable String className,
boolean allowInherit,
@NonNull String... argumentTypes) {
if (className != null && allowInherit) {
if (!isMemberInSubClassOf(method, className, false)) {
return false;
}
}
return parametersMatch(method, argumentTypes);
}
/**
* Returns true if the given method's parameters are the exact types specified.
*
* @param method the method in question
* @param argumentTypes the names of the types of the parameters
* @return true if this method is defined in the given class and with the given parameters
*/
public boolean parametersMatch(
@NonNull PsiMethod method,
@NonNull String... argumentTypes) {
PsiParameterList parameterList = method.getParameterList();
if (parameterList.getParametersCount() != argumentTypes.length) {
return false;
}
PsiParameter[] parameters = parameterList.getParameters();
for (int i = 0; i < parameters.length; i++) {
PsiType type = parameters[i].getType();
if (!type.getCanonicalText().equals(argumentTypes[i])) {
return false;
}
}
return true;
}
/** Returns true if the given type matches the given fully qualified type name */
public boolean parameterHasType(
@Nullable PsiMethod method,
int parameterIndex,
@NonNull String typeName) {
if (method == null) {
return false;
}
PsiParameterList parameterList = method.getParameterList();
return parameterList.getParametersCount() > parameterIndex
&& typeMatches(parameterList.getParameters()[parameterIndex].getType(), typeName);
}
/** Returns true if the given type matches the given fully qualified type name */
public boolean typeMatches(
@Nullable PsiType type,
@NonNull String typeName) {
return type != null && type.getCanonicalText().equals(typeName);
}
@Nullable
public PsiElement resolve(@NonNull PsiElement element) {
if (element instanceof PsiReference) {
return ((PsiReference)element).resolve();
} else if (element instanceof PsiMethodCallExpression) {
PsiElement resolved = ((PsiMethodCallExpression) element).resolveMethod();
if (resolved != null) {
return resolved;
}
}
return null;
}
public boolean isPublic(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.PUBLIC);
}
return false;
}
public boolean isStatic(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.STATIC);
}
return false;
}
public boolean isPrivate(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.PRIVATE);
}
return false;
}
public boolean isAbstract(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.ABSTRACT);
}
return false;
}
public boolean isFinal(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.FINAL);
}
return false;
}
@Nullable
public PsiMethod getSuperMethod(@Nullable PsiMethod method) {
if (method == null) {
return null;
}
final PsiMethod[] superMethods = method.findSuperMethods();
if (superMethods.length > 0) {
return superMethods[0];
}
return null;
}
@NonNull
public String getInternalName(@NonNull PsiClass psiClass) {
String qualifiedName = psiClass.getQualifiedName();
if (qualifiedName == null) {
qualifiedName = psiClass.getName();
if (qualifiedName == null) {
assert psiClass instanceof PsiAnonymousClass;
//noinspection ConstantConditions
return getInternalName(psiClass.getContainingClass());
}
}
return ClassContext.getInternalName(qualifiedName);
}
@NonNull
public String getInternalName(@NonNull PsiClassType psiClassType) {
return ClassContext.getInternalName(psiClassType.getCanonicalText());
}
@Nullable
public abstract PsiClass findClass(@NonNull String qualifiedName);
@Nullable
public abstract PsiClassType getClassType(@Nullable PsiClass psiClass);
@NonNull
public abstract PsiAnnotation[] getAllAnnotations(@NonNull PsiModifierListOwner owner,
boolean inHierarchy);
@Nullable
public abstract PsiAnnotation findAnnotationInHierarchy(
@NonNull PsiModifierListOwner listOwner,
@NonNull String... annotationNames);
@Nullable
public abstract PsiAnnotation findAnnotation(
@Nullable PsiModifierListOwner listOwner,
@NonNull String... annotationNames);
@Nullable
public abstract File getFile(@NonNull PsiFile file);
}
@@ -1,38 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.client.api;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UastVisitorExtension;
import org.jetbrains.uast.UastConverter;
import org.jetbrains.uast.java.JavaUastLanguagePlugin;
import java.util.List;
public class JavaLintLanguageExtension extends LintLanguageExtension {
@NotNull
@Override
public UastConverter getConverter() {
return JavaUastLanguagePlugin.INSTANCE.getConverter();
}
@NotNull
@Override
public List<UastVisitorExtension> getVisitorExtensions() {
return JavaUastLanguagePlugin.INSTANCE.getVisitorExtensions();
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -14,19 +14,21 @@
* limitations under the License.
*/
package com.android.tools.lint.client.api;
package com.android.tools.klint.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.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.client.api.JavaParser.ResolvedClass;
import com.android.tools.klint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.klint.client.api.JavaParser.ResolvedNode;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaScanner;
import com.android.tools.klint.detector.api.Detector.XmlScanner;
import com.android.tools.klint.detector.api.JavaContext;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -285,6 +287,16 @@ public class JavaVisitor {
return;
}
if (e.getClass().getSimpleName().equals("IndexNotReadyException")) {
// Attempting to access PSI during startup before indices are ready; ignore these.
// See http://b.android.com/176644 for an example.
return;
} else if (e.getClass().getSimpleName().equals("ProcessCanceledException")) {
// Cancelling inspections in the IDE
context.getDriver().cancel();
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!
@@ -293,11 +305,13 @@ public class JavaVisitor {
sb.append(context.file.getName());
sb.append(" (this is a bug in lint or one of the libraries it depends on)\n");
sb.append(e.getClass().getSimpleName());
sb.append(':');
StackTraceElement[] stackTrace = e.getStackTrace();
int count = 0;
for (StackTraceElement frame : stackTrace) {
if (count > 0) {
sb.append("->");
sb.append("<-");
}
String className = frame.getClassName();
@@ -322,6 +336,24 @@ public class JavaVisitor {
}
}
/**
* For testing only: returns the number of exceptions thrown during Java AST analysis
*
* @return the number of internal errors found
*/
@VisibleForTesting
public static int getCrashCount() {
return sExceptionCount;
}
/**
* For testing only: clears the crash counter
*/
@VisibleForTesting
public static void clearCrashCount() {
sExceptionCount = 0;
}
public void prepare(@NonNull List<JavaContext> contexts) {
mParser.prepareJavaParse(contexts);
}
@@ -330,6 +362,29 @@ public class JavaVisitor {
mParser.dispose();
}
@Nullable
private static Set<String> getInterfaceNames(
@Nullable Set<String> addTo,
@NonNull ResolvedClass cls) {
Iterable<ResolvedClass> interfaces = cls.getInterfaces();
for (ResolvedClass resolvedInterface : interfaces) {
String name = resolvedInterface.getName();
if (addTo == null) {
addTo = Sets.newHashSet();
} else if (addTo.contains(name)) {
// Superclasses can explicitly implement the same interface,
// so keep track of visited interfaces as we traverse up the
// super class chain to avoid checking the same interface
// more than once.
continue;
}
addTo.add(name);
getInterfaceNames(addTo, resolvedInterface);
}
return addTo;
}
private static class VisitingDetector {
private AstVisitor mVisitor; // construct lazily, and clear out on context switch!
private JavaContext mContext;
@@ -388,18 +443,37 @@ public class JavaVisitor {
ResolvedClass resolvedClass = (ResolvedClass) resolved;
ResolvedClass cls = resolvedClass;
int depth = 0;
while (cls != null) {
String fqcn = cls.getSignature();
if (fqcn != null) {
List<VisitingDetector> list = mSuperClassDetectors.get(fqcn);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, node, node, resolvedClass);
List<VisitingDetector> list = mSuperClassDetectors.get(cls.getName());
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, node, node, resolvedClass);
}
}
// Check interfaces too
Set<String> interfaceNames = getInterfaceNames(null, cls);
if (interfaceNames != null) {
for (String name : interfaceNames) {
list = mSuperClassDetectors.get(name);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, node, node,
resolvedClass);
}
}
}
}
cls = cls.getSuperClass();
depth++;
if (depth == 500) {
// Shouldn't happen in practice; this prevents the IDE from
// hanging if the user has accidentally typed in an incorrect
// super class which creates a cycle.
break;
}
}
return false;
@@ -409,10 +483,7 @@ public class JavaVisitor {
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();
}
ResolvedNode resolved = mContext.resolve(node.astTypeReference());
if (!(resolved instanceof ResolvedClass)) {
return true;
}
@@ -420,13 +491,24 @@ public class JavaVisitor {
ResolvedClass resolvedClass = (ResolvedClass) resolved;
ResolvedClass cls = resolvedClass;
while (cls != null) {
String fqcn = cls.getSignature();
if (fqcn != null) {
List<VisitingDetector> list = mSuperClassDetectors.get(fqcn);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, null, anonymous,
resolvedClass);
List<VisitingDetector> list = mSuperClassDetectors.get(cls.getName());
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, null, anonymous,
resolvedClass);
}
}
// Check interfaces too
Set<String> interfaceNames = getInterfaceNames(null, cls);
if (interfaceNames != null) {
for (String name : interfaceNames) {
list = mSuperClassDetectors.get(name);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, null, anonymous,
resolvedClass);
}
}
}
}
@@ -1377,7 +1459,7 @@ public class JavaVisitor {
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
String type = method.getContainingClass().getSignature();
String type = method.getContainingClass().getName();
List<VisitingDetector> list = mConstructorDetectors.get(type);
if (list != null) {
for (VisitingDetector v : list) {
+219 -36
View File
@@ -16,16 +16,30 @@
package com.android.tools.klint.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.FD_ASSETS;
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.klint.detector.api.LintUtils.endsWith;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.Dependencies;
import com.android.builder.model.Variant;
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.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.sdklib.repository.local.LocalSdk;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Issue;
@@ -40,7 +54,7 @@ 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.jetbrains.uast.UastLanguagePlugin;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -49,17 +63,21 @@ import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.*;
import static com.android.SdkConstants.*;
import static com.android.tools.klint.detector.api.LintUtils.endsWith;
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.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -67,6 +85,16 @@ import static com.android.tools.klint.detector.api.LintUtils.endsWith;
public abstract class LintClient {
private static final String PROP_BIN_DIR = "com.android.tools.lint.bindir"; //$NON-NLS-1$
protected LintClient(@NonNull String clientName) {
//noinspection AssignmentToStaticFieldFromInstanceMethod
sClientName = clientName;
}
protected LintClient() {
//noinspection AssignmentToStaticFieldFromInstanceMethod
sClientName = "unknown";
}
/**
* Returns a configuration for use by the given project. The configuration
* provides information about which issues are enabled, any customizations
@@ -75,15 +103,17 @@ public abstract class LintClient {
* By default this method returns a {@link DefaultConfiguration}.
*
* @param project the project to obtain a configuration for
* @param driver the current driver, if any
* @return a configuration, never null.
*/
public Configuration getConfiguration(@NonNull Project project) {
@NonNull
public Configuration getConfiguration(@NonNull Project project, @Nullable LintDriver driver) {
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
* provided by {@link #getConfiguration(Project,LintDriver)} has reported the corresponding
* issue as enabled and has not filtered out the issue with its
* {@link Configuration#ignore(Context,Issue,Location,String)} method.
* <p>
@@ -98,7 +128,7 @@ public abstract class LintClient {
@NonNull Context context,
@NonNull Issue issue,
@NonNull Severity severity,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@NonNull TextFormat format);
@@ -141,6 +171,19 @@ public abstract class LintClient {
@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
@@ -207,12 +250,15 @@ public abstract class LintClient {
/**
* Returns the list of Java libraries
*
* @param project the project to look up jar dependencies for
* @param project the project to look up jar dependencies for
* @param includeProvided If true, included provided libraries too (libraries that are not
* packaged with the app, but are provided for compilation purposes and
* are assumed to be present in the running environment)
* @return a list of jar dependencies containing .class files
*/
@NonNull
public List<File> getJavaLibraries(@NonNull Project project) {
return getClassPath(project).getLibraries();
public List<File> getJavaLibraries(@NonNull Project project, boolean includeProvided) {
return getClassPath(project).getLibraries(includeProvided);
}
/**
@@ -242,6 +288,22 @@ public abstract class LintClient {
return Collections.emptyList();
}
/**
* Returns the asset folders.
*
* @param project the project to look up the asset folder for
* @return a list of files pointing to the asset folders, possibly empty
*/
@NonNull
public List<File> getAssetFolders(@NonNull Project project) {
File assets = new File(project.getDir(), FD_ASSETS);
if (assets.exists()) {
return Collections.singletonList(assets);
}
return Collections.emptyList();
}
/**
* Returns the {@link SdkInfo} to use for the given project.
*
@@ -381,15 +443,6 @@ public abstract class LintClient {
return false;
}
@Nullable
public com.intellij.openapi.project.Project getProject() {
return null;
}
public List<UastLanguagePlugin> getLanguagePlugins() {
return Collections.emptyList();
}
/**
* Information about class paths (sources, class files and libraries)
* usually associated with a project.
@@ -398,16 +451,19 @@ public abstract class LintClient {
private final List<File> mClassFolders;
private final List<File> mSourceFolders;
private final List<File> mLibraries;
private final List<File> mNonProvidedLibraries;
private final List<File> mTestFolders;
public ClassPathInfo(
@NonNull List<File> sourceFolders,
@NonNull List<File> classFolders,
@NonNull List<File> libraries,
@NonNull List<File> nonProvidedLibraries,
@NonNull List<File> testFolders) {
mSourceFolders = sourceFolders;
mClassFolders = classFolders;
mLibraries = libraries;
mNonProvidedLibraries = nonProvidedLibraries;
mTestFolders = testFolders;
}
@@ -422,8 +478,8 @@ public abstract class LintClient {
}
@NonNull
public List<File> getLibraries() {
return mLibraries;
public List<File> getLibraries(boolean includeProvided) {
return includeProvided ? mLibraries : mNonProvidedLibraries;
}
public List<File> getTestSourceFolders() {
@@ -497,7 +553,7 @@ public abstract class LintClient {
File[] jars = libs.listFiles();
if (jars != null) {
for (File jar : jars) {
if (LintUtils.endsWith(jar.getPath(), DOT_JAR)
if (endsWith(jar.getPath(), DOT_JAR)
&& !libraries.contains(jar)) {
libraries.add(jar);
}
@@ -556,7 +612,7 @@ public abstract class LintClient {
}
}
info = new ClassPathInfo(sources, classes, libraries, tests);
info = new ClassPathInfo(sources, classes, libraries, libraries, tests);
mProjectInfo.put(project, info);
}
@@ -568,7 +624,7 @@ public abstract class LintClient {
* projects are unique for a directory (in case we process a library project
* before its including project for example)
*/
private Map<File, Project> mDirToProject;
protected Map<File, Project> mDirToProject;
/**
* Returns a project for the given directory. This should return the same
@@ -645,7 +701,7 @@ public abstract class LintClient {
mDirToProject.put(canonicalDir, project);
}
private Set<File> mProjectDirs = Sets.newHashSet();
protected Set<File> mProjectDirs = Sets.newHashSet();
/**
* Create a project for the given directory
@@ -756,6 +812,32 @@ public abstract class LintClient {
return max;
}
/**
* Returns the specific version of the build tools being used for the given project, if known
*
* @param project the project in question
*
* @return the build tools version in use by the project, or null if not known
*/
@Nullable
public BuildToolInfo getBuildTools(@NonNull Project project) {
//TODO
//AndroidSdkHandler sdk = getSdk();
//// Build systems like Eclipse and ant just use the latest available
//// build tools, regardless of project metadata. In Gradle, this
//// method is overridden to use the actual build tools specified in the
//// project.
//if (sdk != null) {
// IAndroidTarget compileTarget = getCompileTarget(project);
// if (compileTarget != null) {
// return compileTarget.getBuildToolInfo();
// }
// return sdk.getLatestBuildTool(getRepositoryLogger(), false);
//}
return null;
}
/**
* Returns the super class for the given class name, which should be in VM
* format (e.g. java/lang/Integer, not java.lang.Integer, and using $ rather
@@ -807,7 +889,7 @@ public abstract class LintClient {
*/
@NonNull
public Map<String, String> createSuperClassMap(@NonNull Project project) {
List<File> libraries = project.getJavaLibraries();
List<File> libraries = project.getJavaLibraries(true);
List<File> classFolders = project.getJavaClassFolders();
List<ClassEntry> classEntries = ClassEntry.fromClassPath(this, classFolders, true);
if (libraries.isEmpty()) {
@@ -904,10 +986,41 @@ public abstract class LintClient {
@SuppressWarnings("MethodMayBeStatic") // Intentionally instance method so it can be overridden
@NonNull
public List<File> findRuleJars(@NonNull Project project) {
if (project.getDir().getPath().endsWith(DOT_AAR)) {
File lintJar = new File(project.getDir(), "lint.jar"); //$NON-NLS-1$
if (lintJar.exists()) {
return Collections.singletonList(lintJar);
if (project.isGradleProject()) {
if (project.isLibrary()) {
AndroidLibrary model = project.getGradleLibraryModel();
if (model != null) {
File lintJar = model.getLintJar();
if (lintJar.exists()) {
return Collections.singletonList(lintJar);
}
}
} else if (project.getSubset() != null) {
// Probably just analyzing a single file: we still want to look for custom
// rules applicable to the file
List<File> rules = null;
final Variant variant = project.getCurrentVariant();
if (variant != null) {
Collection<AndroidLibrary> libraries = variant.getMainArtifact()
.getDependencies().getLibraries();
for (AndroidLibrary library : libraries) {
File lintJar = library.getLintJar();
if (lintJar.exists()) {
if (rules == null) {
rules = Lists.newArrayListWithExpectedSize(4);
}
rules.add(lintJar);
}
}
if (rules != null) {
return rules;
}
}
} else if (project.getDir().getPath().endsWith(DOT_AAR)) {
File lintJar = new File(project.getDir(), "lint.jar"); //$NON-NLS-1$
if (lintJar.exists()) {
return Collections.singletonList(lintJar);
}
}
}
@@ -921,7 +1034,7 @@ public abstract class LintClient {
* settings.
*
* @param url the URL to read
* @return a {@link java.net.URLConnection} or null
* @return a {@link URLConnection} or null
* @throws IOException if any kind of IO exception occurs
*/
@Nullable
@@ -929,7 +1042,7 @@ public abstract class LintClient {
return url.openConnection();
}
/** Closes a connection previously returned by {@link #openConnection(java.net.URL)} */
/** Closes a connection previously returned by {@link #openConnection(URL)} */
public void closeConnection(@NonNull URLConnection connection) throws IOException {
if (connection instanceof HttpURLConnection) {
((HttpURLConnection)connection).disconnect();
@@ -946,7 +1059,7 @@ public abstract class LintClient {
*/
@SuppressWarnings("MethodMayBeStatic") // Intentionally instance method so it can be overridden
public boolean isProjectDirectory(@NonNull File dir) {
return LintUtils.isManifestFolder(dir) || Project.isAospFrameworksProject(dir);
return LintUtils.isManifestFolder(dir) || Project.isAospFrameworksRelatedProject(dir);
}
/**
@@ -986,6 +1099,17 @@ public abstract class LintClient {
return registry;
}
/**
* Creates a {@link ClassLoader} which can load in a set of Jar files.
*
* @param urls the URLs
* @param parent the parent class loader
* @return a new class loader
*/
public ClassLoader createUrlClassLoader(@NonNull URL[] urls, @NonNull ClassLoader parent) {
return new URLClassLoader(urls, parent);
}
/**
* Returns true if this client supports project resource repository lookup via
* {@link #getProjectResources(Project,boolean)}
@@ -1034,4 +1158,63 @@ public abstract class LintClient {
}
return mResourceVisibility;
}
/**
* The client name returned by {@link #getClientName()} when running in
* Android Studio/IntelliJ IDEA
*/
public static final String CLIENT_STUDIO = "studio";
/**
* The client name returned by {@link #getClientName()} when running in
* Gradle
*/
public static final String CLIENT_GRADLE = "gradle";
/**
* The client name returned by {@link #getClientName()} when running in
* the CLI (command line interface) version of lint, {@code lint}
*/
public static final String CLIENT_CLI = "cli";
/**
* The client name returned by {@link #getClientName()} when running in
* some unknown client
*/
public static final String CLIENT_UNKNOWN = "unknown";
/** The client name. */
@NonNull
private static String sClientName = CLIENT_UNKNOWN;
/**
* Returns the name of the embedding client. It could be not just
* {@link #CLIENT_STUDIO}, {@link #CLIENT_GRADLE}, {@link #CLIENT_CLI}
* etc but other values too as lint is integrated in other embedding contexts.
*
* @return the name of the embedding client
*/
@NonNull
public static String getClientName() {
return sClientName;
}
/**
* Returns true if the embedding client currently running lint is Android Studio
* (or IntelliJ IDEA)
*
* @return true if running in Android Studio / IntelliJ IDEA
*/
public static boolean isStudio() {
return CLIENT_STUDIO.equals(sClientName);
}
/**
* Returns true if the embedding client currently running lint is Gradle
*
* @return true if running in Gradle
*/
public static boolean isGradle() {
return CLIENT_GRADLE.equals(sClientName);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,61 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.client.api;
import com.android.annotations.Nullable;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import org.jetbrains.uast.UastLanguagePlugin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public abstract class LintLanguageExtension implements UastLanguagePlugin {
public static final ExtensionPointName<LintLanguageExtension> EP_NAME =
ExtensionPointName.create("com.android.tools.klint.client.api.lintLanguageExtension");
public static boolean isFileSupported(@Nullable Project project, String name) {
LintLanguageExtension[] extensions = getExtensions(project);
for (LintLanguageExtension ext : extensions) {
if (ext.getConverter().isFileSupported(name)) {
return true;
}
}
return false;
}
public static List<UastLanguagePlugin> getPlugins(@Nullable Project project) {
if (project == null) {
return Collections.emptyList();
}
LintLanguageExtension[] languageExtensions = project.getExtensions(EP_NAME);
List<UastLanguagePlugin> converters = new ArrayList<UastLanguagePlugin>(languageExtensions.length);
Collections.addAll(converters, languageExtensions);
return converters;
}
public static LintLanguageExtension[] getExtensions(@Nullable Project project) {
if (project == null) {
return new LintLanguageExtension[0];
}
return project.getExtensions(EP_NAME);
}
}
@@ -172,6 +172,7 @@ public class LintRequest {
* @param project the project to look up the main project for
* @return the main project
*/
@SuppressWarnings("MethodMayBeStatic")
@NonNull
public Project getMainProject(@NonNull Project project) {
return project;
@@ -15,6 +15,13 @@
*/
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FD_ASSETS;
import static com.android.tools.klint.detector.api.Detector.OtherFileScanner;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Context;
@@ -25,10 +32,11 @@ import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.util.*;
import static com.android.SdkConstants.*;
import static com.android.tools.klint.detector.api.Detector.OtherFileScanner;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Visitor for "other" files: files that aren't java sources,
@@ -94,16 +102,16 @@ class OtherFileVisitor {
}
}
if (scopes.contains(Scope.SOURCE_FILE)) {
if (scopes.contains(Scope.JAVA_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(DOT_JAVA)) {
if (file.getPath().endsWith(".kt")) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.SOURCE_FILE, files);
mFiles.put(Scope.JAVA_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
@@ -111,7 +119,7 @@ class OtherFileVisitor {
collectFiles(files, srcFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.SOURCE_FILE, files);
mFiles.put(Scope.JAVA_FILE, files);
}
}
}
@@ -24,10 +24,20 @@ import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.ResourceContext;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
import org.w3c.dom.*;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
/**
* Specialized visitor for running detectors on resources: typically XML documents,
@@ -1,11 +1,11 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,26 +16,303 @@
package com.android.tools.klint.client.api;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import org.jetbrains.uast.*;
import com.android.annotations.Nullable;
import com.android.resources.ResourceType;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.JavaContext;
import com.google.common.base.Joiner;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiVariable;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.UResolvable;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.jetbrains.uast.UVariable;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.java.JavaAbstractUExpression;
import org.jetbrains.uast.java.JavaUVariableDeclarationsExpression;
import java.util.Collections;
import java.util.List;
public class UastLintUtils {
public static boolean isChildOfExpression(
@NonNull UExpression child,
@NonNull UExpression parent) {
UElement current = child;
while (current != null) {
if (current.equals(parent)) {
return true;
} else if (!(current instanceof UExpression)) {
return false;
}
current = current.getParent();
@NonNull
public static String getClassName(PsiClassType type) {
PsiClass psiClass = type.resolve();
if (psiClass == null) {
return type.getClassName();
} else {
return getClassName(psiClass);
}
return false;
}
@NonNull
public static String getClassName(PsiClass psiClass) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(psiClass.getName());
psiClass = psiClass.getContainingClass();
while (psiClass != null) {
stringBuilder.insert(0, psiClass.getName() + ".");
psiClass = psiClass.getContainingClass();
}
return stringBuilder.toString();
}
@Nullable
public static UExpression findLastAssignment(
@NonNull PsiVariable variable,
@NonNull UElement call,
@NonNull JavaContext context) {
UElement lastAssignment = null;
if (variable instanceof UVariable) {
variable = ((UVariable) variable).getPsi();
}
if (!variable.hasModifierProperty(PsiModifier.FINAL) &&
(variable instanceof PsiLocalVariable || variable instanceof PsiParameter)) {
UMethod containingFunction = UastUtils.getContainingUMethod(call);
if (containingFunction != null) {
ConstantEvaluator.LastAssignmentFinder
finder = new ConstantEvaluator.LastAssignmentFinder(
variable, call, context, null,
(variable instanceof PsiParameter) ? 1 : 0);
containingFunction.accept(finder);
lastAssignment = finder.getLastAssignment();
}
} else {
lastAssignment = context.getUastContext().getInitializerBody(variable);
}
if (lastAssignment instanceof UExpression) {
return (UExpression) lastAssignment;
}
return null;
}
@Nullable
public static String getReferenceName(UReferenceExpression expression) {
if (expression instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) expression).getIdentifier();
} else if (expression instanceof UQualifiedReferenceExpression) {
UExpression selector = ((UQualifiedReferenceExpression) expression).getSelector();
if (selector instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) selector).getIdentifier();
}
}
return null;
}
@Nullable
public static Object findLastValue(
@NonNull PsiVariable variable,
@NonNull UElement call,
@NonNull JavaContext context,
@NonNull ConstantEvaluator evaluator) {
Object value = null;
if (!variable.hasModifierProperty(PsiModifier.FINAL) &&
(variable instanceof PsiLocalVariable || variable instanceof PsiParameter)) {
UMethod containingFunction = UastUtils.getContainingUMethod(call);
if (containingFunction != null) {
ConstantEvaluator.LastAssignmentFinder
finder = new ConstantEvaluator.LastAssignmentFinder(
variable, call, context, evaluator, 1);
containingFunction.getUastBody().accept(finder);
value = finder.getCurrentValue();
}
} else {
UExpression initializer = context.getUastContext().getInitializerBody(variable);
if (initializer != null) {
value = initializer.evaluate();
}
}
return value;
}
@Nullable
private static AndroidReference toAndroidReference(UQualifiedReferenceExpression expression) {
List<String> path = UastUtils.asQualifiedPath(expression);
String packageNameFromResolved = null;
PsiClass containingClass = UastUtils.getContainingClass(expression.resolve());
if (containingClass != null) {
String containingClassFqName = containingClass.getQualifiedName();
if (containingClassFqName != null) {
int i = containingClassFqName.lastIndexOf(".R.");
if (i >= 0) {
packageNameFromResolved = containingClassFqName.substring(0, i);
}
}
}
if (path == null) {
return null;
}
int size = path.size();
if (size < 3) {
return null;
}
String r = path.get(size - 3);
if (!r.equals(SdkConstants.R_CLASS)) {
return null;
}
String packageName = packageNameFromResolved != null
? packageNameFromResolved
: Joiner.on('.').join(path.subList(0, size - 3));
String type = path.get(size - 2);
String name = path.get(size - 1);
ResourceType resourceType = null;
for (ResourceType value : ResourceType.values()) {
if (value.getName().equals(type)) {
resourceType = value;
break;
}
}
if (resourceType == null) {
return null;
}
return new AndroidReference(expression, packageName, resourceType, name);
}
@Nullable
public static AndroidReference toAndroidReferenceViaResolve(UElement element) {
if (element instanceof UQualifiedReferenceExpression
&& element instanceof JavaAbstractUExpression) {
AndroidReference ref = toAndroidReference((UQualifiedReferenceExpression) element);
if (ref != null) {
return ref;
}
}
PsiElement declaration;
if (element instanceof UVariable) {
declaration = ((UVariable) element).getPsi();
} else if (element instanceof UResolvable) {
declaration = ((UResolvable) element).resolve();
} else {
return null;
}
if (declaration == null && element instanceof USimpleNameReferenceExpression
&& element instanceof JavaAbstractUExpression) {
// R class can't be resolved in tests so we need to use heuristics to calc the reference
UExpression maybeQualified = UastUtils.getQualifiedParentOrThis((UExpression) element);
if (maybeQualified instanceof UQualifiedReferenceExpression) {
AndroidReference ref = toAndroidReference(
(UQualifiedReferenceExpression) maybeQualified);
if (ref != null) {
return ref;
}
}
}
if (!(declaration instanceof PsiVariable)) {
return null;
}
PsiVariable variable = (PsiVariable) declaration;
if (!(variable instanceof PsiField)
|| variable.getType() != PsiType.INT
|| !variable.hasModifierProperty(PsiModifier.STATIC)
|| !variable.hasModifierProperty(PsiModifier.FINAL)) {
return null;
}
PsiClass resTypeClass = ((PsiField) variable).getContainingClass();
if (resTypeClass == null || !resTypeClass.hasModifierProperty(PsiModifier.STATIC)) {
return null;
}
PsiClass rClass = resTypeClass.getContainingClass();
if (rClass == null || rClass.getContainingClass() != null || !"R".equals(rClass.getName())) {
return null;
}
String packageName = ((PsiJavaFile) rClass.getContainingFile()).getPackageName();
if (packageName.isEmpty()) {
return null;
}
String resourceTypeName = resTypeClass.getName();
ResourceType resourceType = null;
for (ResourceType value : ResourceType.values()) {
if (value.getName().equals(resourceTypeName)) {
resourceType = value;
break;
}
}
if (resourceType == null) {
return null;
}
String resourceName = variable.getName();
UExpression node;
if (element instanceof UExpression) {
node = (UExpression) element;
} else if (element instanceof UVariable) {
node = new JavaUVariableDeclarationsExpression(
null, Collections.singletonList(((UVariable) element)));
} else {
throw new IllegalArgumentException("element must be an expression or an UVariable");
}
return new AndroidReference(node, packageName, resourceType, resourceName);
}
public static boolean areIdentifiersEqual(UExpression first, UExpression second) {
String firstIdentifier = getIdentifier(first);
String secondIdentifier = getIdentifier(second);
return firstIdentifier != null && secondIdentifier != null
&& firstIdentifier.equals(secondIdentifier);
}
@Nullable
public static String getIdentifier(UExpression expression) {
if (expression instanceof ULiteralExpression) {
expression.asRenderString();
} else if (expression instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) expression).getIdentifier();
} else if (expression instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualified = (UQualifiedReferenceExpression) expression;
String receiverIdentifier = getIdentifier(qualified.getReceiver());
String selectorIdentifier = getIdentifier(qualified.getSelector());
if (receiverIdentifier == null || selectorIdentifier == null) {
return null;
}
return receiverIdentifier + "." + selectorIdentifier;
}
return null;
}
}
@@ -22,6 +22,7 @@ import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -29,7 +30,7 @@ import org.w3c.dom.Node;
/**
* A wrapper for an XML parser. This allows tools integrating lint to map directly
* to builtin services, such as already-parsed data structures in XML editors.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -22,7 +22,7 @@ import com.google.common.annotations.Beta;
/**
* A category is a container for related issues.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -107,7 +107,36 @@ public final class Category implements Comparable<Category> {
}
@Override
public int compareTo(Category other) {
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Category category = (Category) o;
//noinspection SimplifiableIfStatement
if (!mName.equals(category.mName)) {
return false;
}
return mParent != null ? mParent.equals(category.mParent) : category.mParent == null;
}
@Override
public String toString() {
return getFullName();
}
@Override
public int hashCode() {
return mName.hashCode();
}
@Override
public int compareTo(@NonNull Category other) {
if (other.mPriority == mPriority) {
if (mParent == other) {
return 1;
@@ -115,7 +144,13 @@ public final class Category implements Comparable<Category> {
return -1;
}
}
return other.mPriority - mPriority;
int delta = other.mPriority - mPriority;
if (delta != 0) {
return delta;
}
return mName.compareTo(other.mName);
}
/** Issues related to running lint itself */
@@ -139,9 +174,6 @@ public final class Category implements Comparable<Category> {
/** Issues related to internationalization */
public static final Category I18N = create("Internationalization", 50);
/** Issues related to right to left and bi-directional text support */
public static final Category RTL = create("Bi-directional Text", 40);
// Sub categories
/** Issues related to icons */
@@ -152,4 +184,7 @@ public final class Category implements Comparable<Category> {
/** Issues related to messages/strings */
public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95);
/** Issues related to right to left and bidirectional text support */
public static final Category RTL = create(I18N, "Bidirectional Text", 40);
}
@@ -16,22 +16,36 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.tools.klint.detector.api.Location.SearchDirection.BACKWARD;
import static com.android.tools.klint.detector.api.Location.SearchDirection.EOL_BACKWARD;
import static com.android.tools.klint.detector.api.Location.SearchDirection.FORWARD;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.detector.api.Location.SearchDirection;
import com.android.tools.klint.detector.api.Location.SearchHints;
import com.android.utils.AsmUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import org.jetbrains.org.objectweb.asm.Type;
import org.jetbrains.org.objectweb.asm.tree.*;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.File;
import java.util.List;
import static com.android.SdkConstants.*;
/**
* A {@link Context} used when checking .class files.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -148,11 +162,11 @@ public class ClassContext extends Context {
if (source == null) {
source = file.getName();
if (source.endsWith(DOT_CLASS)) {
source = source.substring(0, source.length() - DOT_CLASS.length()) + DOT_JAVA;
source = source.substring(0, source.length() - DOT_CLASS.length()) + ".kt";
}
int index = source.indexOf('$');
if (index != -1) {
source = source.substring(0, index) + DOT_JAVA;
source = source.substring(0, index) + ".kt";
}
}
if (source != null) {
@@ -248,7 +262,7 @@ public class ClassContext extends Context {
*/
@NonNull
public Location getLocationForLine(int line, @Nullable String patternStart,
@Nullable String patternEnd, @Nullable Location.SearchHints hints) {
@Nullable String patternEnd, @Nullable SearchHints hints) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
// ASM line numbers are 1-based, and lint line numbers are 0-based
@@ -269,8 +283,8 @@ public class ClassContext extends Context {
* Detectors should only call this method if an error applies to the whole class
* scope and there is no specific method or field that applies to the error.
* If so, use
* {@link #report(Issue, org.jetbrains.org.objectweb.asm.tree.MethodNode, org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode, Location, String)} or
* {@link #report(Issue, org.jetbrains.org.objectweb.asm.tree.FieldNode, Location, String)}, such that
* {@link #report(Issue, org.objectweb.asm.tree.MethodNode, org.objectweb.asm.tree.AbstractInsnNode, Location, String)} or
* {@link #report(Issue, org.objectweb.asm.tree.FieldNode, Location, String)}, such that
* suppress annotations are checked.
*
* @param issue the issue to report
@@ -280,7 +294,7 @@ public class ClassContext extends Context {
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (mDriver.isSuppressed(issue, mClassNode)) {
return;
@@ -344,7 +358,7 @@ public class ClassContext extends Context {
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (method != null && mDriver.isSuppressed(issue, mClassNode, method, instruction)) {
return;
@@ -365,7 +379,7 @@ public class ClassContext extends Context {
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (field != null && mDriver.isSuppressed(issue, field)) {
return;
@@ -387,7 +401,7 @@ public class ClassContext extends Context {
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, method, instruction, location, message);
@@ -406,7 +420,7 @@ public class ClassContext extends Context {
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, field, location, message);
@@ -502,7 +516,7 @@ public class ClassContext extends Context {
}
return getLocationForLine(findLineNumber(classNode), pattern, null,
Location.SearchHints.create(Location.SearchDirection.BACKWARD).matchJavaSymbol());
SearchHints.create(BACKWARD).matchJavaSymbol());
}
@Nullable
@@ -543,21 +557,21 @@ public class ClassContext extends Context {
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
Location.SearchDirection searchMode;
SearchDirection searchMode;
if (methodNode.name.equals(CONSTRUCTOR_NAME)) {
searchMode = Location.SearchDirection.EOL_BACKWARD;
searchMode = EOL_BACKWARD;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName.substring(classNode.superName.lastIndexOf('/') + 1);
} else {
pattern = classNode.name.substring(classNode.name.lastIndexOf('$') + 1);
}
} else {
searchMode = Location.SearchDirection.BACKWARD;
searchMode = BACKWARD;
pattern = methodNode.name;
}
return getLocationForLine(findLineNumber(methodNode), pattern, null,
Location.SearchHints.create(searchMode).matchJavaSymbol());
SearchHints.create(searchMode).matchJavaSymbol());
}
/**
@@ -569,7 +583,7 @@ public class ClassContext extends Context {
*/
@NonNull
public Location getLocation(@NonNull AbstractInsnNode instruction) {
Location.SearchHints hints = Location.SearchHints.create(Location.SearchDirection.FORWARD).matchJavaSymbol();
SearchHints hints = SearchHints.create(FORWARD).matchJavaSymbol();
String pattern = null;
if (instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
@@ -685,7 +699,7 @@ public class ClassContext extends Context {
// If class name contains $, it's not an ambiguous inner class name.
if (fqcn.indexOf('$') != -1) {
return fqcn.replace('.', '/');
return AsmUtils.toInternalName(fqcn);
}
// Let's assume that components that start with Caps are class names.
StringBuilder sb = new StringBuilder(fqcn.length());
@@ -16,6 +16,11 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.SUPPRESS_ALL;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.Configuration;
@@ -29,8 +34,6 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import static com.android.SdkConstants.*;
/**
* Context passed to the detectors during an analysis run. It provides
* information about the file being analyzed, it allows shared properties (so
@@ -100,7 +103,7 @@ public class Context {
mDriver = driver;
mProject = project;
mMainProject = main;
mConfiguration = project.getConfiguration();
mConfiguration = project.getConfiguration(driver);
}
/**
@@ -245,23 +248,33 @@ public class Context {
* Reports an issue. Convenience wrapper around {@link LintClient#report}
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param location the location of the issue
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
//noinspection ConstantConditions
if (location == null) {
// Misbehaving third-party lint detectors
assert false : issue;
return;
}
if (location == Location.NONE) {
// Detector reported error for issue in a non-applicable location etc
return;
}
Configuration configuration = mConfiguration;
// If this error was computed for a context where the context corresponds to
// a project instead of a file, the actual error may be in a different project (e.g.
// a library project), so adjust the configuration as necessary.
if (location != null && location.getFile() != null) {
Project project = mDriver.findProjectFor(location.getFile());
if (project != null) {
configuration = project.getConfiguration();
}
Project project = mDriver.findProjectFor(location.getFile());
if (project != null) {
configuration = project.getConfiguration(mDriver);
}
// If an error occurs in a library project, but you've disabled that check in the
@@ -293,7 +306,7 @@ public class Context {
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, location, message);
@@ -348,7 +361,7 @@ public class Context {
// Java and XML files are handled in sub classes (XmlContext, JavaContext)
String path = file.getPath();
if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_GRADLE)) {
if (path.endsWith(DOT_JAVA) || path.endsWith(".kt") || path.endsWith(DOT_GRADLE)) {
return JavaContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(DOT_XML)) {
return XmlContext.SUPPRESS_COMMENT_PREFIX;
File diff suppressed because it is too large Load Diff
@@ -24,7 +24,7 @@ import java.util.EnumSet;
* An {@linkplain Implementation} of an {@link Issue} maps to the {@link Detector}
* class responsible for analyzing the issue, as well as the {@link Scope} required
* by the detector to perform its analysis.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -16,7 +16,10 @@
package com.android.tools.klint.detector.api;
import static com.android.tools.klint.detector.api.TextFormat.RAW;
import com.android.annotations.NonNull;
import com.android.tools.klint.client.api.Configuration;
import com.google.common.annotations.Beta;
import java.util.ArrayList;
@@ -32,7 +35,7 @@ import java.util.List;
* multiple different issues as it's analyzing code, and we want to be able to
* different severities for different issues, the ability to suppress one but
* not other issues from the same detector, and so on.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -139,7 +142,7 @@ public final class Issue implements Comparable<Issue> {
*/
@NonNull
public String getBriefDescription(@NonNull TextFormat format) {
return TextFormat.RAW.convertTo(mBriefDescription, format);
return RAW.convertTo(mBriefDescription, format);
}
/**
@@ -153,7 +156,7 @@ public final class Issue implements Comparable<Issue> {
*/
@NonNull
public String getExplanation(@NonNull TextFormat format) {
return TextFormat.RAW.convertTo(mExplanation, format);
return RAW.convertTo(mExplanation, format);
}
/**
@@ -16,29 +16,86 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.tools.klint.client.api.JavaParser.ResolvedNode;
import static com.android.tools.klint.client.api.JavaParser.TypeDescriptor;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.JavaParser;
import com.android.tools.klint.client.api.JavaParser.ResolvedClass;
import com.android.tools.klint.client.api.LintDriver;
import org.jetbrains.annotations.NotNull;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.google.common.collect.Iterators;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiEnumConstant;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiLabeledStatement;
import com.intellij.psi.PsiMember;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiNameIdentifierOwner;
import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiSwitchStatement;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.psi.PsiElementBacked;
import org.jetbrains.uast.psi.UElementWithLocation;
import java.io.File;
import java.util.List;
import java.util.Iterator;
import static com.android.SdkConstants.CLASS_CONTEXT;
import lombok.ast.AnnotationElement;
import lombok.ast.AnnotationMethodDeclaration;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.ConstructorInvocation;
import lombok.ast.EnumConstant;
import lombok.ast.Expression;
import lombok.ast.LabelledStatement;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Position;
import lombok.ast.TypeDeclaration;
import lombok.ast.VariableReference;
/**
* A {@link Context} used when checking Java files.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
public class JavaContext extends Context implements UastAndroidContext {
public class JavaContext extends Context {
static final String SUPPRESS_COMMENT_PREFIX = "//noinspection "; //$NON-NLS-1$
/**
* The parse tree
*
* @deprecated Use {@link #mJavaFile} instead (see {@link JavaPsiScanner})
*/
@Deprecated
private Node mCompilationUnit;
/** The parse tree */
private UFile mCompilationUnit;
private PsiJavaFile mJavaFile;
private UFile mUFile;
/** The parser which produced the parse tree */
private final JavaParser mParser;
/**
* Constructs a {@link JavaContext} for running lint on the given file, with
@@ -58,23 +115,184 @@ public class JavaContext extends Context implements UastAndroidContext {
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file) {
@NonNull File file,
@NonNull JavaParser parser) {
super(driver, project, main, file);
mParser = parser;
}
@NonNull
public UastContext getUastContext() {
return mParser.getUastContext();
}
/**
* Returns a location for the given node
*
* @param node the AST node to get a location for
* @return a location for the given node
*/
@Override
public Location getLocation(@Nullable UElement element) {
return UastAndroidContext.DefaultImpls.getLocation(this, element);
@NonNull
public Location getLocation(@NonNull Node node) {
return mParser.getLocation(this, node);
}
/**
* Returns a location for the given node range (from the starting offset of the first node to
* the ending offset of the second node).
*
* @param from the AST node to get a starting location from
* @param fromDelta Offset delta to apply to the starting offset
* @param to the AST node to get a ending location from
* @param toDelta Offset delta to apply to the ending offset
* @return a location for the given node
*/
@NonNull
public Location getRangeLocation(
@NonNull Node from,
int fromDelta,
@NonNull Node to,
int toDelta) {
return mParser.getRangeLocation(this, from, fromDelta, to, toDelta);
}
/**
* Returns a location for the given node range (from the starting offset of the first node to
* the ending offset of the second node).
*
* @param from the AST node to get a starting location from
* @param fromDelta Offset delta to apply to the starting offset
* @param to the AST node to get a ending location from
* @param toDelta Offset delta to apply to the ending offset
* @return a location for the given node
*/
@NonNull
public Location getRangeLocation(
@NonNull PsiElement from,
int fromDelta,
@NonNull PsiElement to,
int toDelta) {
return mParser.getRangeLocation(this, from, fromDelta, to, toDelta);
}
/**
* Returns a {@link Location} for the given node. This attempts to pick a shorter
* location range than the entire node; for a class or method for example, it picks
* the name node (if found). For statement constructs such as a {@code switch} statement
* it will highlight the keyword, etc.
*
* @param node the AST node to create a location for
* @return a location for the given node
*/
@NonNull
public Location getNameLocation(@NonNull Node node) {
return mParser.getNameLocation(this, node);
}
/**
* Returns a {@link Location} for the given node. This attempts to pick a shorter
* location range than the entire node; for a class or method for example, it picks
* the name node (if found). For statement constructs such as a {@code switch} statement
* it will highlight the keyword, etc.
*
* @param element the AST node to create a location for
* @return a location for the given node
*/
@NonNull
public Location getNameLocation(@NonNull PsiElement element) {
if (element instanceof PsiSwitchStatement) {
// Just use keyword
return mParser.getRangeLocation(this, element, 0, 6); // 6: "switch".length()
}
return mParser.getNameLocation(this, element);
}
@NonNull
public Location getUastNameLocation(@NonNull UElement element) {
if (element instanceof UDeclaration) {
UElement nameIdentifier = ((UDeclaration) element).getUastAnchor();
if (nameIdentifier != null) {
return getUastLocation(nameIdentifier);
}
} else if (element instanceof PsiNameIdentifierOwner) {
PsiElement nameIdentifier = ((PsiNameIdentifierOwner) element).getNameIdentifier();
if (nameIdentifier != null) {
return getLocation(nameIdentifier);
}
} else if (element instanceof UCallExpression) {
UElement methodReference = ((UCallExpression) element).getMethodIdentifier();
if (methodReference != null) {
return getUastLocation(methodReference);
}
}
return getUastLocation(element);
}
@NonNull
public Location getLocation(@NonNull PsiElement node) {
return mParser.getLocation(this, node);
}
@NonNull
public Location getUastLocation(@Nullable UElement node) {
if (node == null) {
return Location.NONE;
}
if (node instanceof UElementWithLocation) {
UFile file = UastUtils.getContainingFile(node);
if (file == null) {
return Location.NONE;
}
File ioFile = UastUtils.getIoFile(file);
if (ioFile == null) {
return Location.NONE;
}
UElementWithLocation segment = (UElementWithLocation) node;
return Location.create(ioFile, file.getPsi().getText(),
segment.getStartOffset(), segment.getEndOffset());
} else if (node instanceof PsiElementBacked) {
PsiElement psiElement = ((PsiElementBacked) node).getPsi();
if (psiElement != null) {
TextRange range = psiElement.getTextRange();
UFile containingFile = getUFile();
if (containingFile == null) {
return Location.NONE;
}
File file = this.file;
if (!containingFile.equals(getUFile())) {
// Reporting an error in a different file.
if (getDriver().getScope().size() == 1) {
// Don't bother with this error if it's in a different file during single-file analysis
return Location.NONE;
}
VirtualFile virtualFile = containingFile.getPsi().getVirtualFile();
if (virtualFile == null) {
return Location.NONE;
}
file = VfsUtilCore.virtualToIoFile(virtualFile);
}
return Location.create(file, getContents(), range.getStartOffset(),
range.getEndOffset());
}
}
return Location.NONE;
}
@NonNull
public JavaParser getParser() {
return mParser;
}
@NonNull
public JavaEvaluator getEvaluator() {
return mParser.getEvaluator();
}
@Nullable
public UFile getCompilationUnit() {
public Node getCompilationUnit() {
return mCompilationUnit;
}
@@ -84,12 +302,36 @@ public class JavaContext extends Context implements UastAndroidContext {
*
* @param compilationUnit the parse tree
*/
public void setCompilationUnit(@Nullable UFile compilationUnit) {
public void setCompilationUnit(@Nullable Node compilationUnit) {
mCompilationUnit = compilationUnit;
}
/**
* Returns the {@link UFile}.
*
* @return the parsed UFile
*/
@Nullable
public UFile getUFile() {
return mUFile;
}
/**
* Sets the compilation result. Not intended for client usage; the lint infrastructure
* will set this when a context has been processed
*
* @param javaFile the parse tree
*/
public void setJavaFile(@Nullable PsiJavaFile javaFile) {
mJavaFile = javaFile;
}
public void setUFile(@Nullable UFile file) {
mUFile = file;
}
@Override
public void report(@NonNull Issue issue, @Nullable Location location,
public void report(@NonNull Issue issue, @NonNull Location location,
@NonNull String message) {
if (mDriver.isSuppressed(this, issue, mCompilationUnit)) {
return;
@@ -108,60 +350,503 @@ public class JavaContext extends Context implements UastAndroidContext {
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
@Override
public void report(
@NonNull Issue issue,
@Nullable UElement scope,
@Nullable Location location,
@NonNull String message) {
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
public void report(
@NonNull Issue issue,
@Nullable PsiElement scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
public void report(
@NonNull Issue issue,
@Nullable UElement scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/** UDeclaration is a PsiElement, so it's impossible to call report(Issue, UElement, ...)
* without an explicit cast. */
public void reportUast(
@NonNull Issue issue,
@Nullable UElement scope,
@NonNull Location location,
@NonNull String message) {
report(issue, scope, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
/**
* @deprecated Use {@link PsiTreeUtil#getParentOfType(PsiElement, Class[])}
* with PsiMethod.class instead
*/
@Deprecated
@Nullable
public static Node findSurroundingMethod(Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {
return scope;
}
scope = scope.getParent();
}
return null;
}
/**
* @deprecated Use {@link PsiTreeUtil#getParentOfType(PsiElement, Class[])}
* with PsiMethod.class instead
*/
@Deprecated
@Nullable
public static ClassDeclaration findSurroundingClass(@Nullable Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == ClassDeclaration.class) {
return (ClassDeclaration) scope;
}
scope = scope.getParent();
}
return null;
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
/**
* @deprecated Use {@link #isSuppressedWithComment(PsiElement, Issue)} instead
*/
@Deprecated
public boolean isSuppressedWithComment(@NonNull Node scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
Position position = scope.getPosition();
if (position == null) {
return false;
}
int start = position.getStart();
return isSuppressedWithComment(start, issue);
}
public boolean isSuppressedWithComment(@NonNull UElement scope, @NonNull Issue issue) {
return false;
}
public boolean isSuppressedWithComment(@NonNull PsiElement scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
TextRange textRange = scope.getTextRange();
if (textRange == null) {
return false;
}
int start = textRange.getStartOffset();
return isSuppressedWithComment(start, issue);
}
/**
* @deprecated Location handles aren't needed for AST nodes anymore; just use the
* {@link PsiElement} from the AST
*/
@Deprecated
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
/**
* @deprecated Use PsiElement resolve methods (varies by AST node type, e.g.
* {@link PsiMethodCallExpression#resolveMethod()}
*/
@Deprecated
@Nullable
public ResolvedNode resolve(@NonNull Node node) {
return mParser.resolve(this, node);
}
/**
* @deprecated Use {@link JavaEvaluator#findClass(String)} instead
*/
@Deprecated
@Nullable
public ResolvedClass findClass(@NonNull String fullyQualifiedName) {
return mParser.findClass(this, fullyQualifiedName);
}
/**
* @deprecated Use {@link PsiExpression#getType()} )} instead
*/
@Deprecated
@Nullable
public TypeDescriptor getType(@NonNull Node node) {
return mParser.getType(this, node);
}
/**
* @deprecated Use {@link #getMethodName(PsiElement)} instead
*/
@Deprecated
@Nullable
public static String getMethodName(@NonNull Node call) {
if (call instanceof MethodInvocation) {
return ((MethodInvocation)call).astName().astValue();
} else if (call instanceof ConstructorInvocation) {
return ((ConstructorInvocation)call).astTypeReference().getTypeName();
} else if (call instanceof EnumConstant) {
return ((EnumConstant)call).astName().astValue();
} else {
return null;
}
}
@Nullable
public static String getMethodName(@NonNull PsiElement call) {
if (call instanceof PsiMethodCallExpression) {
return ((PsiMethodCallExpression)call).getMethodExpression().getReferenceName();
} else if (call instanceof PsiNewExpression) {
PsiJavaCodeReferenceElement classReference = ((PsiNewExpression) call).getClassReference();
if (classReference != null) {
return classReference.getReferenceName();
} else {
return null;
}
} else if (call instanceof PsiEnumConstant) {
return ((PsiEnumConstant)call).getName();
} else {
return null;
}
}
@Nullable
public static String getMethodName(@NonNull UElement call) {
if (call instanceof UEnumConstant) {
return ((UEnumConstant)call).getName();
} else if (call instanceof UCallExpression) {
String methodName = ((UCallExpression) call).getMethodName();
if (methodName != null) {
return methodName;
} else {
return UastUtils.getQualifiedName(((UCallExpression) call).getClassReference());
}
} else {
return null;
}
}
/**
* Searches for a name node corresponding to the given node
* @return the name node to use, if applicable
* @deprecated Use {@link #findNameElement(PsiElement)} instead
*/
@Deprecated
@Nullable
public static Node findNameNode(@NonNull Node node) {
if (node instanceof TypeDeclaration) {
// ClassDeclaration, AnnotationDeclaration, EnumDeclaration, InterfaceDeclaration
return ((TypeDeclaration) node).astName();
} else if (node instanceof MethodDeclaration) {
return ((MethodDeclaration)node).astMethodName();
} else if (node instanceof ConstructorDeclaration) {
return ((ConstructorDeclaration)node).astTypeName();
} else if (node instanceof MethodInvocation) {
return ((MethodInvocation)node).astName();
} else if (node instanceof ConstructorInvocation) {
return ((ConstructorInvocation)node).astTypeReference();
} else if (node instanceof EnumConstant) {
return ((EnumConstant)node).astName();
} else if (node instanceof AnnotationElement) {
return ((AnnotationElement)node).astName();
} else if (node instanceof AnnotationMethodDeclaration) {
return ((AnnotationMethodDeclaration)node).astMethodName();
} else if (node instanceof VariableReference) {
return ((VariableReference)node).astIdentifier();
} else if (node instanceof LabelledStatement) {
return ((LabelledStatement)node).astLabel();
}
return null;
}
/**
* Searches for a name node corresponding to the given node
* @return the name node to use, if applicable
*/
@Nullable
public static PsiElement findNameElement(@NonNull PsiElement element) {
if (element instanceof PsiClass) {
if (element instanceof PsiAnonymousClass) {
return ((PsiAnonymousClass)element).getBaseClassReference();
}
return ((PsiClass) element).getNameIdentifier();
} else if (element instanceof PsiMethod) {
return ((PsiMethod) element).getNameIdentifier();
} else if (element instanceof PsiMethodCallExpression) {
return ((PsiMethodCallExpression) element).getMethodExpression().
getReferenceNameElement();
} else if (element instanceof PsiNewExpression) {
return ((PsiNewExpression) element).getClassReference();
} else if (element instanceof PsiField) {
return ((PsiField)element).getNameIdentifier();
} else if (element instanceof PsiAnnotation) {
return ((PsiAnnotation)element).getNameReferenceElement();
} else if (element instanceof PsiReferenceExpression) {
return ((PsiReferenceExpression) element).getReferenceNameElement();
} else if (element instanceof PsiLabeledStatement) {
return ((PsiLabeledStatement)element).getLabelIdentifier();
}
return null;
}
@Deprecated
@NonNull
public static Iterator<Expression> getParameters(@NonNull Node call) {
if (call instanceof MethodInvocation) {
return ((MethodInvocation) call).astArguments().iterator();
} else if (call instanceof ConstructorInvocation) {
return ((ConstructorInvocation) call).astArguments().iterator();
} else if (call instanceof EnumConstant) {
return ((EnumConstant) call).astArguments().iterator();
} else {
return Iterators.emptyIterator();
}
}
@Deprecated
@Nullable
public static Node getParameter(@NonNull Node call, int parameter) {
Iterator<Expression> iterator = getParameters(call);
for (int i = 0; i < parameter - 1; i++) {
if (!iterator.hasNext()) {
return null;
}
iterator.next();
}
return iterator.hasNext() ? iterator.next() : null;
}
/**
* Returns true if the given method invocation node corresponds to a call on a
* {@code android.content.Context}
*
* @param node the method call node
* @return true iff the method call is on a class extending context
* @deprecated use {@link JavaEvaluator#isMemberInSubClassOf(PsiMember, String, boolean)} instead
*/
public boolean isContextMethod(@NonNull UCallExpression node) {
@Deprecated
public boolean isContextMethod(@NonNull MethodInvocation node) {
// Method name used in many other contexts where it doesn't have the
// same semantics; only use this one if we can resolve types
// and we're certain this is the Context method
UFunction resolved = node.resolve(this);
UClass containingClass = UastUtils.getContainingClass(resolved);
if (resolved != null && containingClass != null) {
if (containingClass.isSubclassOf(CLASS_CONTEXT)) {
ResolvedNode resolved = resolve(node);
if (resolved instanceof JavaParser.ResolvedMethod) {
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.isSubclassOf(CLASS_CONTEXT, false)) {
return true;
}
}
return false;
}
@NotNull
@Override
public JavaContext getLintContext() {
return this;
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz) {
return getParentOfType(element, clz, true);
}
@NotNull
@Override
public List<UastLanguagePlugin> getLanguagePlugins() {
return getClient().getLanguagePlugins();
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz,
boolean strict) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null) {
if (clz.isInstance(element)) {
//noinspection unchecked
return (T) element;
}
element = element.getParent();
}
return null;
}
@org.jetbrains.annotations.Nullable
@Override
public UElement convert(@Nullable Object element) {
return UastContext.DefaultImpls.convert(this, element);
/**
* Returns the first ancestor node of the given type, stopping at the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param terminators optional node types to terminate the search at
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(@Nullable Node element,
@NonNull Class<T> clz,
boolean strict,
@NonNull Class<? extends Node>... terminators) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null && !clz.isInstance(element)) {
for (Class<?> terminator : terminators) {
if (terminator.isInstance(element)) {
return null;
}
}
element = element.getParent();
}
//noinspection unchecked
return (T) element;
}
/**
* Returns the first sibling of the given node that is of the given class
*
* @param sibling the sibling to search from
* @param clz the type to look for
* @param <T> the type
* @return the first sibling of the given type, or null
* @deprecated Use {@link PsiTreeUtil#getNextSiblingOfType(PsiElement, Class)} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getNextSiblingOfType(@Nullable Node sibling,
@NonNull Class<T> clz) {
if (sibling == null) {
return null;
}
Node parent = sibling.getParent();
if (parent == null) {
return null;
}
Iterator<Node> iterator = parent.getChildren().iterator();
while (iterator.hasNext()) {
if (iterator.next() == sibling) {
break;
}
}
while (iterator.hasNext()) {
Node child = iterator.next();
if (clz.isInstance(child)) {
//noinspection unchecked
return (T) child;
}
}
return null;
}
/**
* Returns the given argument of the given call
*
* @param call the call containing arguments
* @param index the index of the target argument
* @return the argument at the given index
* @throws IllegalArgumentException if index is outside the valid range
*/
@Deprecated
@NonNull
public static Node getArgumentNode(@NonNull MethodInvocation call, int index) {
int i = 0;
for (Expression parameter : call.astArguments()) {
if (i == index) {
return parameter;
}
i++;
}
throw new IllegalArgumentException(Integer.toString(index));
}
}
@@ -16,17 +16,27 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import org.w3c.dom.Element;
import static com.android.SdkConstants.*;
import org.w3c.dom.Element;
/**
* Abstract class specifically intended for layout detectors which provides some
* common utility methods shared by layout detectors.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
+237 -69
View File
@@ -16,6 +16,41 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LOCALE;
import static com.android.SdkConstants.BIN_FOLDER;
import static com.android.SdkConstants.DOT_GIF;
import static com.android.SdkConstants.DOT_JPEG;
import static com.android.SdkConstants.DOT_JPG;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_WEBP;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FN_BUILD_GRADLE;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.UTF_8;
import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER;
import static com.android.ide.common.resources.configuration.LocaleQualifier.BCP_47_PREFIX;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BYTE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BYTE_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_CHAR;
import static com.android.tools.klint.client.api.JavaParser.TYPE_CHARACTER_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INTEGER_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_SHORT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_SHORT_WRAPPER;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidProject;
@@ -38,32 +73,45 @@ import com.android.tools.klint.client.api.LintClient;
import com.android.utils.PositionXmlParser;
import com.android.utils.SdkUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.jetbrains.uast.UFile;
import org.jetbrains.uast.UImportStatement;
import org.jetbrains.org.objectweb.asm.Opcodes;
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode;
import org.jetbrains.org.objectweb.asm.tree.ClassNode;
import org.jetbrains.org.objectweb.asm.tree.FieldNode;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiImportStatement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiWhiteSpace;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UParenthesizedExpression;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static com.android.SdkConstants.*;
import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER;
import static com.android.ide.common.resources.configuration.LocaleQualifier.BCP_47_PREFIX;
import lombok.ast.ImportDeclaration;
/**
* Useful utility methods related to lint.
@@ -238,6 +286,22 @@ public class LintUtils {
return element == element.getOwnerDocument().getDocumentElement();
}
/**
* Returns the corresponding R field name for the given XML resource name
* @param styleName the XML name
* @return the corresponding R field name
*/
public static String getFieldName(@NonNull String styleName) {
for (int i = 0, n = styleName.length(); i < n; i++) {
char c = styleName.charAt(i);
if (c == '.' || c == '-' || c == ':') {
return styleName.replace('.', '_').replace('-', '_').replace(':', '_');
}
}
return styleName;
}
/**
* Returns the given id without an {@code @id/} or {@code @+id} prefix
*
@@ -559,7 +623,7 @@ public class LintUtils {
text = new String(data, offset, length, charset);
} catch (UnsupportedEncodingException e) {
try {
if (charset != defaultCharset) {
if (!charset.equals(defaultCharset)) {
text = new String(data, offset, length, defaultCharset);
}
} catch (UnsupportedEncodingException u) {
@@ -771,9 +835,12 @@ public class LintUtils {
* @param fullyQualifiedName the fully qualified class name
* @return true if the given imported name refers to the given fully
* qualified name
* @deprecated Use PSI element hierarchies instead where type resolution is more directly
* available (call {@link PsiImportStatement#resolve()})
*/
@Deprecated
public static boolean isImported(
@Nullable UFile compilationUnit,
@Nullable lombok.ast.Node compilationUnit,
@NonNull String fullyQualifiedName) {
if (compilationUnit == null) {
return false;
@@ -782,27 +849,26 @@ public class LintUtils {
int dotLength = fullyQualifiedName.length() - dotIndex;
boolean imported = false;
for (UImportStatement importStatement : compilationUnit.getImportStatements()) {
String fqn = importStatement.getFqNameToImport();
if (fqn == null) {
continue;
}
if (fqn.equals(fullyQualifiedName)) {
return true;
} else if (fullyQualifiedName.regionMatches(dotIndex, fqn,
fqn.length() - dotLength, dotLength)) {
// This import is importing the class name using some other prefix, so there
// fully qualified class name cannot be imported under that name
return false;
} else if (importStatement.isStarImport()
&& fqn.regionMatches(0, fqn, 0, dotIndex + 1)) {
imported = true;
// but don't break -- keep searching in case there's a non-wildcard
// import of the specific class name, e.g. if we're looking for
// android.content.SharedPreferences.Editor, don't match on the following:
// import android.content.SharedPreferences.*;
// import foo.bar.Editor;
for (lombok.ast.Node rootNode : compilationUnit.getChildren()) {
if (rootNode instanceof ImportDeclaration) {
ImportDeclaration importDeclaration = (ImportDeclaration) rootNode;
String fqn = importDeclaration.asFullyQualifiedName();
if (fqn.equals(fullyQualifiedName)) {
return true;
} else if (fullyQualifiedName.regionMatches(dotIndex, fqn,
fqn.length() - dotLength, dotLength)) {
// This import is importing the class name using some other prefix, so there
// fully qualified class name cannot be imported under that name
return false;
} else if (importDeclaration.astStarImport()
&& fqn.regionMatches(0, fqn, 0, dotIndex + 1)) {
imported = true;
// but don't break -- keep searching in case there's a non-wildcard
// import of the specific class name, e.g. if we're looking for
// android.content.SharedPreferences.Editor, don't match on the following:
// import android.content.SharedPreferences.*;
// import foo.bar.Editor;
}
}
}
@@ -1045,7 +1111,7 @@ public class LintUtils {
}
return new AndroidVersion(api.getApiLevel(), null);
}
/**
* Looks for a certain string within a larger string, which should immediately follow
* the given prefix and immediately precede the given suffix.
@@ -1130,40 +1196,6 @@ public class LintUtils {
return Collections.emptyList();
}
/**
* Escapes the given property file value (right hand side of property assignment)
* as required by the property file format (e.g. escapes colons and backslashes)
*
* @param value the value to be escaped
* @return the escaped value
*/
@NonNull
public static String escapePropertyValue(@NonNull String value) {
// Slow, stupid implementation, but is 100% compatible with Java's property file
// implementation
Properties properties = new Properties();
properties.setProperty("k", value); // key doesn't matter
StringWriter writer = new StringWriter();
try {
properties.store(writer, null);
String s = writer.toString();
int end = s.length();
// Writer inserts trailing newline
String lineSeparator = SdkUtils.getLineSeparator();
if (s.endsWith(lineSeparator)) {
end -= lineSeparator.length();
}
int start = s.indexOf('=');
assert start != -1 : s;
return s.substring(start + 1, end);
}
catch (IOException e) {
return value; // shouldn't happen; we're not going to disk
}
}
/**
* Returns the locale for the given parent folder.
*
@@ -1216,4 +1248,140 @@ public class LintUtils {
return "en".equals(locale.getLanguage()); //$NON-NLS-1$
}
}
/**
* Create a {@link Location} for an error in the top level build.gradle file.
* This is necessary when we're doing an analysis based on the Gradle interpreted model,
* not from parsing Gradle files - and the model doesn't provide source positions.
* @param project the project containing the gradle file being analyzed
* @return location for the top level gradle file if it exists, otherwise fall back to
* the project directory.
*/
public static Location guessGradleLocation(@NonNull Project project) {
File dir = project.getDir();
Location location;
File topLevel = new File(dir, FN_BUILD_GRADLE);
if (topLevel.exists()) {
location = Location.create(topLevel);
} else {
location = Location.create(dir);
}
return location;
}
/**
* Returns true if the given element is the null literal
*
* @param element the element to check
* @return true if the element is "null"
*/
public static boolean isNullLiteral(@Nullable PsiElement element) {
return element instanceof PsiLiteral && "null".equals(element.getText());
}
public static boolean isTrueLiteral(@Nullable PsiElement element) {
return element instanceof PsiLiteral && "true".equals(element.getText());
}
public static boolean isFalseLiteral(@Nullable PsiElement element) {
return element instanceof PsiLiteral && "false".equals(element.getText());
}
@Nullable
public static PsiElement skipParentheses(@Nullable PsiElement element) {
while (element instanceof PsiParenthesizedExpression) {
element = element.getParent();
}
return element;
}
@Nullable
public static UElement skipParentheses(@Nullable UElement element) {
while (element instanceof UParenthesizedExpression) {
element = element.getContainingElement();
}
return element;
}
@Nullable
public static PsiElement nextNonWhitespace(@Nullable PsiElement element) {
if (element != null) {
element = element.getNextSibling();
while (element instanceof PsiWhiteSpace) {
element = element.getNextSibling();
}
}
return element;
}
@Nullable
public static PsiElement prevNonWhitespace(@Nullable PsiElement element) {
if (element != null) {
element = element.getPrevSibling();
while (element instanceof PsiWhiteSpace) {
element = element.getPrevSibling();
}
}
return element;
}
public static boolean isString(@NonNull PsiType type) {
if (type instanceof PsiClassType) {
final String shortName = ((PsiClassType)type).getClassName();
if (!Objects.equal(shortName, CommonClassNames.JAVA_LANG_STRING_SHORT)) {
return false;
}
}
return CommonClassNames.JAVA_LANG_STRING.equals(type.getCanonicalText());
}
@Nullable
public static String getAutoBoxedType(@NonNull String primitive) {
if (TYPE_INT.equals(primitive)) {
return TYPE_INTEGER_WRAPPER;
} else if (TYPE_LONG.equals(primitive)) {
return TYPE_LONG_WRAPPER;
} else if (TYPE_CHAR.equals(primitive)) {
return TYPE_CHARACTER_WRAPPER;
} else if (TYPE_FLOAT.equals(primitive)) {
return TYPE_FLOAT_WRAPPER;
} else if (TYPE_DOUBLE.equals(primitive)) {
return TYPE_DOUBLE_WRAPPER;
} else if (TYPE_BOOLEAN.equals(primitive)) {
return TYPE_BOOLEAN_WRAPPER;
} else if (TYPE_SHORT.equals(primitive)) {
return TYPE_SHORT_WRAPPER;
} else if (TYPE_BYTE.equals(primitive)) {
return TYPE_BYTE_WRAPPER;
}
return null;
}
@Nullable
public static String getPrimitiveType(@NonNull String autoBoxedType) {
if (TYPE_INTEGER_WRAPPER.equals(autoBoxedType)) {
return TYPE_INT;
} else if (TYPE_LONG_WRAPPER.equals(autoBoxedType)) {
return TYPE_LONG;
} else if (TYPE_CHARACTER_WRAPPER.equals(autoBoxedType)) {
return TYPE_CHAR;
} else if (TYPE_FLOAT_WRAPPER.equals(autoBoxedType)) {
return TYPE_FLOAT;
} else if (TYPE_DOUBLE_WRAPPER.equals(autoBoxedType)) {
return TYPE_DOUBLE;
} else if (TYPE_BOOLEAN_WRAPPER.equals(autoBoxedType)) {
return TYPE_BOOLEAN;
} else if (TYPE_SHORT_WRAPPER.equals(autoBoxedType)) {
return TYPE_SHORT;
} else if (TYPE_BYTE_WRAPPER.equals(autoBoxedType)) {
return TYPE_BYTE;
}
return null;
}
}
@@ -21,13 +21,15 @@ import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.tools.klint.client.api.JavaParser;
import com.google.common.annotations.Beta;
import com.intellij.psi.PsiElement;
import java.io.File;
/**
* Location information for a warning
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -42,6 +44,13 @@ public class Location {
private Location mSecondary;
private Object mClientData;
/**
* Special marker location which means location not available, or not applicable, or filtered out, etc.
* For example, the infrastructure may return {@link #NONE} if you ask {@link JavaParser#getLocation(JavaContext, PsiElement)}
* for an element which is not in the current file during an incremental lint run in a single file.
*/
public static final Location NONE = new Location(new File("NONE"), null, null);
/**
* (Private constructor, use one of the factory methods
* {@link Location#create(File)},
@@ -355,7 +364,14 @@ public class Location {
index = findNextMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else {
assert direction == SearchDirection.NEAREST;
assert direction == SearchDirection.NEAREST ||
direction == SearchDirection.EOL_NEAREST;
int lineEnd = contents.indexOf('\n', offset);
if (lineEnd == -1) {
lineEnd = contents.length();
}
offset = lineEnd;
int before = findPreviousMatch(contents, offset, patternStart, hints);
int after = findNextMatch(contents, offset, patternStart, hints);
@@ -366,12 +382,27 @@ public class Location {
} else if (after == -1) {
index = before;
line = adjustLine(contents, line, offset, index);
} else if (offset - before < after - offset) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
index = after;
line = adjustLine(contents, line, offset, index);
int newLinesBefore = 0;
for (int i = before; i < offset; i++) {
if (contents.charAt(i) == '\n') {
newLinesBefore++;
}
}
int newLinesAfter = 0;
for (int i = offset; i < after; i++) {
if (contents.charAt(i) == '\n') {
newLinesAfter++;
}
}
if (newLinesBefore < newLinesAfter || newLinesBefore == newLinesAfter
&& offset - before < after - offset) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
index = after;
line = adjustLine(contents, line, offset, index);
}
}
}
@@ -674,6 +705,12 @@ public class Location {
* the match that is closest
*/
NEAREST,
/**
* Search both forwards and backwards from the end of the given line, and prefer
* the match that is closest
*/
EOL_NEAREST,
}
/**
@@ -20,7 +20,7 @@ import com.google.common.annotations.Beta;
/**
* Information about a position in a file/document.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
+185 -62
View File
@@ -16,21 +16,49 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_LIBRARY;
import static com.android.SdkConstants.ANDROID_LIBRARY_REFERENCE_FORMAT;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.APPCOMPAT_LIB_ARTIFACT;
import static com.android.SdkConstants.ATTR_MIN_SDK_VERSION;
import static com.android.SdkConstants.ATTR_PACKAGE;
import static com.android.SdkConstants.ATTR_TARGET_SDK_VERSION;
import static com.android.SdkConstants.FN_ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
import static com.android.SdkConstants.OLD_PROGUARD_FILE;
import static com.android.SdkConstants.PROGUARD_CONFIG;
import static com.android.SdkConstants.PROJECT_PROPERTIES;
import static com.android.SdkConstants.RES_FOLDER;
import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT;
import static com.android.SdkConstants.TAG_USES_SDK;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_API;
import static com.android.sdklib.SdkVersionInfo.LOWEST_ACTIVE_API;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.FilterData;
import com.android.build.OutputFile;
import com.android.builder.model.*;
import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidArtifactOutput;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.ProductFlavor;
import com.android.builder.model.ProductFlavorContainer;
import com.android.builder.model.Variant;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.resources.Density;
import com.android.resources.ResourceFolderType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.tools.klint.client.api.CircularDependencyException;
import com.android.tools.klint.client.api.Configuration;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.client.api.SdkInfo;
import com.google.common.annotations.Beta;
import com.google.common.base.CharMatcher;
@@ -40,6 +68,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -48,13 +77,16 @@ import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.SdkConstants.*;
import static com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_API;
/**
* A project contains information about an Android project being scanned for
* Lint errors.
@@ -93,9 +125,11 @@ public class Project {
protected List<File> mManifestFiles;
protected List<File> mJavaSourceFolders;
protected List<File> mJavaClassFolders;
protected List<File> mNonProvidedJavaLibraries;
protected List<File> mJavaLibraries;
protected List<File> mTestSourceFolders;
protected List<File> mResourceFolders;
protected List<File> mAssetFolders;
protected List<Project> mDirectLibraries;
protected List<Project> mAllLibraries;
protected boolean mReportIssues = true;
@@ -104,6 +138,7 @@ public class Project {
protected Boolean mAppCompat;
private Map<String, String> mSuperClassMap;
private ResourceVisibilityLookup mResourceVisibility;
private BuildToolInfo mBuildTools;
/**
* Creates a new {@link Project} for the given directory.
@@ -155,7 +190,7 @@ public class Project {
public AndroidProject getGradleProjectModel() {
return null;
}
/**
* Returns the project model for this project if it corresponds to
* a Gradle library. This is the case if both
@@ -292,6 +327,18 @@ public class Project {
} else {
mDirectLibraries = Collections.emptyList();
}
if (isAospBuildEnvironment()) {
if (isAospFrameworksRelatedProject(mDir)) {
// No manifest file for this project: just init the manifest values here
mManifestMinSdk = mManifestTargetSdk = new AndroidVersion(HIGHEST_KNOWN_API, null);
} else if (mBuildSdk == -1) {
// only set BuildSdk for projects other than frameworks and
// the ones that don't have one set in project.properties.
mBuildSdk = getClient().getHighestKnownApiLevel();
}
}
}
@Override
@@ -348,7 +395,7 @@ public class Project {
@NonNull
public List<File> getJavaSourceFolders() {
if (mJavaSourceFolders == null) {
if (isAospFrameworksProject(mDir)) {
if (isAospFrameworksRelatedProject(mDir)) {
return Collections.singletonList(new File(mDir, "java")); //$NON-NLS-1$
}
if (isAospBuildEnvironment()) {
@@ -373,9 +420,9 @@ public class Project {
public List<File> getJavaClassFolders() {
if (mJavaClassFolders == null) {
if (isAospFrameworksProject(mDir)) {
File top = mDir.getParentFile().getParentFile().getParentFile();
String top = getAospTop();
if (top != null) {
File out = new File(top, "out");
File out = new File(top, "out"); //$NON-NLS-1$
if (out.exists()) {
String relative =
"target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar";
@@ -406,20 +453,39 @@ public class Project {
* library projects which are processed in a separate pass with their own
* source and class folders.
*
* @param includeProvided If true, included provided libraries too (libraries
* that are not packaged with the app, but are provided
* for compilation purposes and are assumed to be present
* in the running environment)
* @return a list of .jar files (or class folders) that this project depends
* on.
*/
@NonNull
public List<File> getJavaLibraries() {
if (mJavaLibraries == null) {
// AOSP builds already merge libraries and class folders into
// the single classes.jar file, so these have already been processed
// in getJavaClassFolders.
mJavaLibraries = mClient.getJavaLibraries(this);
public List<File> getJavaLibraries(boolean includeProvided) {
if (includeProvided) {
if (mJavaLibraries == null) {
// AOSP builds already merge libraries and class folders into
// the single classes.jar file, so these have already been processed
// in getJavaClassFolders.
mJavaLibraries = mClient.getJavaLibraries(this, true);
if (isAospBuildEnvironment()) {
// We still need to add the support-annotations library in the case of AOSP
File out = new File(getAospTop(), "out");
String relative = "target/common/obj/JAVA_LIBRARIES/"
+ "android-support-annotations_intermediates/classes";
File annotationsDir = new File(out, relative.replace('/', File.separatorChar));
if (annotationsDir.exists()) {
mJavaLibraries.add(annotationsDir);
}
}
}
return mJavaLibraries;
} else {
if (mNonProvidedJavaLibraries == null) {
mNonProvidedJavaLibraries = mClient.getJavaLibraries(this, false);
}
return mNonProvidedJavaLibraries;
}
return mJavaLibraries;
}
/**
@@ -437,17 +503,17 @@ public class Project {
}
/**
* Returns the resource folder.
* Returns the resource folders.
*
* @return a file pointing to the resource folder, or null if the project
* does not contain any resources
* @return a list of files pointing to the resource folders, which might be empty if the project
* does not provide any resources.
*/
@NonNull
public List<File> getResourceFolders() {
if (mResourceFolders == null) {
List<File> folders = mClient.getResourceFolders(this);
if (folders.size() == 1 && isAospFrameworksProject(mDir)) {
if (folders.size() == 1 && isAospFrameworksRelatedProject(mDir)) {
// No manifest file for this project: just init the manifest values here
mManifestMinSdk = mManifestTargetSdk = new AndroidVersion(HIGHEST_KNOWN_API, null);
File folder = new File(folders.get(0), RES_FOLDER);
@@ -460,7 +526,21 @@ public class Project {
}
return mResourceFolders;
}
/**
* Returns the asset folders.
*
* @return a list of files pointing to the asset folders, which might be empty if the project
* does not provide any resources.
*/
@NonNull
public List<File> getAssetFolders() {
if (mAssetFolders == null) {
mAssetFolders = mClient.getAssetFolders(this);
}
return mAssetFolders;
}
/**
@@ -536,12 +616,13 @@ public class Project {
/**
* Gets the configuration associated with this project
*
* @param driver the current driver, if any
* @return the configuration associated with this project
*/
@NonNull
public Configuration getConfiguration() {
public Configuration getConfiguration(@Nullable LintDriver driver) {
if (mConfiguration == null) {
mConfiguration = mClient.getConfiguration(this);
mConfiguration = mClient.getConfiguration(this, driver);
}
return mConfiguration;
}
@@ -553,9 +634,6 @@ public class Project {
*/
@Nullable
public String getPackage() {
//assert !mLibrary; // Should call getPackage on the master project, not the library
// Assertion disabled because you might be running lint on a standalone library project.
return mPackage;
}
@@ -613,6 +691,20 @@ public class Project {
return mBuildSdk;
}
/**
* Returns the specific version of the build tools being used, if known
*
* @return the build tools version in use, or null if not known
*/
@Nullable
public BuildToolInfo getBuildTools() {
if (mBuildTools == null) {
mBuildTools = mClient.getBuildTools(this);
}
return mBuildTools;
}
/**
* Returns the target used to build the project, or null if not known
*
@@ -919,7 +1011,7 @@ public class Project {
private static Boolean sAospBuild;
/** Is lint running in an AOSP build environment */
private static boolean isAospBuildEnvironment() {
public static boolean isAospBuildEnvironment() {
if (sAospBuild == null) {
sAospBuild = getAospTop() != null;
}
@@ -928,29 +1020,51 @@ public class Project {
}
/**
* Is this the frameworks AOSP project? Needs some hardcoded support since
* Is this the frameworks or related AOSP project? Needs some hardcoded support since
* it doesn't have a manifest file, etc.
*
* A frameworks AOSP projects can be any directory under "frameworks" that
* 1. Is not the "support" directory (which uses the public support annotations)
* 2. Doesn't have an AndroidManifest.xml (it's an app instead)
*
* @param dir the project directory to check
* @return true if this looks like the frameworks/base/core project
* @return true if this looks like the frameworks/dir project and does not have
* an AndroidManifest.xml
*/
public static boolean isAospFrameworksRelatedProject(@NonNull File dir) {
if (isAospBuildEnvironment()) {
File frameworks = new File(getAospTop(), "frameworks"); //$NON-NLS-1$
String frameworksDir = frameworks.getAbsolutePath();
String supportDir = new File(frameworks, "support").getAbsolutePath(); //$NON-NLS-1$
if (dir.exists()
&& !dir.getAbsolutePath().startsWith(supportDir)
&& dir.getAbsolutePath().startsWith(frameworksDir)
&& !(new File(dir, FN_ANDROID_MANIFEST_XML).exists())) {
return true;
}
}
return false;
}
/**
* Is this the actual frameworks project.
* @param dir the project directory to check.
* @return true if this is the frameworks project.
*/
public static boolean isAospFrameworksProject(@NonNull File dir) {
if (!dir.getPath().endsWith("core")) { //$NON-NLS-1$
String top = getAospTop();
if (top != null) {
File toCompare = new File(top, "frameworks" //$NON-NLS-1$
+ File.separator + "base" //$NON-NLS-1$
+ File.separator + "core"); //$NON-NLS-1$
try {
return dir.getCanonicalFile().equals(toCompare) && dir.exists();
} catch (IOException e) {
return false;
}
} else {
return false;
}
File parent = dir.getParentFile();
if (parent == null || !parent.getName().equals("base")) { //$NON-NLS-1$
return false;
}
parent = parent.getParentFile();
//noinspection RedundantIfStatement
if (parent == null || !parent.getName().equals("frameworks")) { //$NON-NLS-1$
return false;
}
return true;
}
/** Get the root AOSP dir, if any */
@@ -1025,6 +1139,13 @@ public class Project {
// some Android.mk files do some complicated things with it - and most
// projects use the same module name as the directory name.
String moduleName = mDir.getName();
try {
// Get the actual directory name instead of '.' that's possible
// when using this via CLI.
moduleName = mDir.getCanonicalFile().getName();
} catch (IOException ioe) {
// pass
}
String top = getAospTop();
final String[] outFolders = new String[] {
@@ -1092,32 +1213,34 @@ public class Project {
/** In an AOSP build environment, identify the currently built image version, if available */
private static AndroidVersion findCurrentAospVersion() {
if (sCurrentVersion == null) {
File apiDir = new File(getAospTop(), "frameworks/base/api" //$NON-NLS-1$
File versionMk = new File(getAospTop(), "build/core/version_defaults.mk" //$NON-NLS-1$
.replace('/', File.separatorChar));
File[] apiFiles = apiDir.listFiles();
if (apiFiles == null) {
if (!versionMk.exists()) {
sCurrentVersion = AndroidVersion.DEFAULT;
return sCurrentVersion;
}
int max = 1;
for (File apiFile : apiFiles) {
String name = apiFile.getName();
int index = name.indexOf('.');
if (index > 0) {
String base = name.substring(0, index);
if (Character.isDigit(base.charAt(0))) {
int sdkVersion = LOWEST_ACTIVE_API;
try {
Pattern p = Pattern.compile("PLATFORM_SDK_VERSION\\s*:=\\s*(.*)");
List<String> lines = Files.readLines(versionMk, Charsets.UTF_8);
for (String line : lines) {
line = line.trim();
Matcher matcher = p.matcher(line);
if (matcher.matches()) {
String version = matcher.group(1);
try {
int version = Integer.parseInt(base);
if (version > max) {
max = version;
}
} catch (NumberFormatException nufe) {
sdkVersion = Integer.parseInt(version);
} catch (NumberFormatException nfe) {
// pass
}
break;
}
}
} catch (IOException io) {
// pass
}
sCurrentVersion = new AndroidVersion(max, null);
sCurrentVersion = new AndroidVersion(sdkVersion, null);
}
return sCurrentVersion;
@@ -1137,7 +1260,7 @@ public class Project {
public Boolean dependsOn(@NonNull String artifact) {
if (SUPPORT_LIB_ARTIFACT.equals(artifact)) {
if (mSupportLib == null) {
for (File file : getJavaLibraries()) {
for (File file : getJavaLibraries(true)) {
String name = file.getName();
if (name.equals("android-support-v4.jar") //$NON-NLS-1$
|| name.startsWith("support-v4-")) { //$NON-NLS-1$
@@ -1162,7 +1285,7 @@ public class Project {
return mSupportLib;
} else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) {
if (mAppCompat == null) {
for (File file : getJavaLibraries()) {
for (File file : getJavaLibraries(true)) {
String name = file.getName();
if (name.startsWith("appcompat-v7-")) { //$NON-NLS-1$
mAppCompat = true;
@@ -28,7 +28,7 @@ import java.io.File;
* A {@link com.android.tools.lint.detector.api.Context} used when checking resource files
* (both bitmaps and XML files; for XML files a subclass of this context is used:
* {@link com.android.tools.lint.detector.api.XmlContext}.)
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -0,0 +1,690 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_PKG;
import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.SdkConstants.CLASS_FRAGMENT;
import static com.android.SdkConstants.CLASS_RESOURCES;
import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
import static com.android.SdkConstants.R_CLASS;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.tools.klint.client.api.UastLintUtils.toAndroidReferenceViaResolve;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.AndroidReference;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.UastLintUtils;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiConditionalExpression;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.UParenthesizedExpression;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
/** Evaluates constant expressions */
public class ResourceEvaluator {
/**
* Marker ResourceType used to signify that an expression is of type {@code @ColorInt},
* which isn't actually a ResourceType but one we want to specifically compare with.
* We're using {@link ResourceType#PUBLIC} because that one won't appear in the R
* class (and ResourceType is an enum we can't just create new constants for.)
*/
public static final ResourceType COLOR_INT_MARKER_TYPE = ResourceType.PUBLIC;
/**
* Marker ResourceType used to signify that an expression is of type {@code @Px},
* which isn't actually a ResourceType but one we want to specifically compare with.
* We're using {@link ResourceType#DECLARE_STYLEABLE} because that one doesn't
* have a corresponding {@code *Res} constant (and ResourceType is an enum we can't
* just create new constants for.)
*/
public static final ResourceType PX_MARKER_TYPE = ResourceType.DECLARE_STYLEABLE;
public static final String COLOR_INT_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "ColorInt"; //$NON-NLS-1$
public static final String PX_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "Px"; //$NON-NLS-1$
public static final String RES_SUFFIX = "Res";
public static final String CLS_TYPED_ARRAY = "android.content.res.TypedArray";
private final JavaContext mContext;
private final JavaEvaluator mEvaluator;
private boolean mAllowDereference = true;
/**
* Creates a new resource evaluator
*
* @param context Java context
*/
public ResourceEvaluator(JavaContext context) {
mContext = context;
mEvaluator = context.getEvaluator();
}
/**
* Whether we allow dereferencing resources when computing constants;
* e.g. if we ask for the resource for {@code x} when the code is
* {@code x = getString(R.string.name)}, if {@code allowDereference} is
* true we'll return R.string.name, otherwise we'll return null.
*
* @return this for constructor chaining
*/
public ResourceEvaluator allowDereference(boolean allow) {
mAllowDereference = allow;
return this;
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource url (type and name)
*/
@Nullable
public static ResourceUrl getResource(
@NonNull JavaContext context,
@NonNull PsiElement element) {
return new ResourceEvaluator(context).getResource(element);
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource url (type and name)
*/
@Nullable
public static ResourceUrl getResource(
@NonNull JavaContext context,
@NonNull UElement element) {
return new ResourceEvaluator(context).getResource(element);
}
/**
* Evaluates the given node and returns the resource types implied by the given element,
* if any.
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource types
*/
@Nullable
public static EnumSet<ResourceType> getResourceTypes(
@NonNull JavaContext context,
@NonNull PsiElement element) {
return new ResourceEvaluator(context).getResourceTypes(element);
}
/**
* Evaluates the given node and returns the resource types implied by the given element,
* if any.
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource types
*/
@Nullable
public static EnumSet<ResourceType> getResourceTypes(
@NonNull JavaContext context,
@NonNull UElement element) {
return new ResourceEvaluator(context).getResourceTypes(element);
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param element the node to compute the constant value for
* @return the corresponding constant value - a String, an Integer, a Float, and so on
*/
@Nullable
public ResourceUrl getResource(@Nullable UElement element) {
if (element == null) {
return null;
}
if (element instanceof UIfExpression) {
UIfExpression expression = (UIfExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResource(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResource(expression.getElseExpression());
}
} else if (element instanceof UParenthesizedExpression) {
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
return getResource(parenthesizedExpression.getExpression());
} else if (mAllowDereference && element instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualifiedExpression = (UQualifiedReferenceExpression) element;
UExpression selector = qualifiedExpression.getSelector();
if ((selector instanceof UCallExpression)) {
UCallExpression call = (UCallExpression) selector;
PsiMethod function = call.resolve();
PsiClass containingClass = UastUtils.getContainingClass(function);
if (function != null && containingClass != null) {
String qualifiedName = containingClass.getQualifiedName();
String name = call.getMethodName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
List<UExpression> args = call.getValueArguments();
if (!args.isEmpty()) {
return getResource(args.get(0));
}
}
}
}
}
if (element instanceof UReferenceExpression) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return url;
}
PsiElement resolved = ((UReferenceExpression) element).resolve();
if (resolved instanceof PsiVariable) {
PsiVariable variable = (PsiVariable) resolved;
UElement lastAssignment =
UastLintUtils.findLastAssignment(
variable, element, mContext);
if (lastAssignment != null) {
return getResource(lastAssignment);
}
return null;
}
}
return null;
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param element the node to compute the constant value for
* @return the corresponding constant value - a String, an Integer, a Float, and so on
*/
@Nullable
public ResourceUrl getResource(@Nullable PsiElement element) {
if (element == null) {
return null;
}
if (element instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResource(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResource(expression.getElseExpression());
}
} else if (element instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
return getResource(parenthesizedExpression.getExpression());
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
PsiReferenceExpression expression = call.getMethodExpression();
PsiMethod method = call.resolveMethod();
if (method != null && method.getContainingClass() != null) {
String qualifiedName = method.getContainingClass().getQualifiedName();
String name = expression.getReferenceName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
PsiExpression[] args = call.getArgumentList().getExpressions();
if (args.length > 0) {
return getResource(args[0]);
}
}
}
} else if (element instanceof PsiReference) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return url;
}
PsiElement resolved = ((PsiReference) element).resolve();
if (resolved instanceof PsiField) {
url = getResourceConstant(resolved);
if (url != null) {
return url;
}
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
return getResource(field.getInitializer());
}
return null;
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
for (PsiElement e : prevStatement.getDeclaredElements()) {
if (variable.equals(e)) {
return getResource(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement) prev)
.getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign
= (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return getResource(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
}
return null;
}
/**
* Evaluates the given node and returns the resource types applicable to the
* node, if any.
*
* @param element the element to compute the types for
* @return the corresponding resource types
*/
@Nullable
public EnumSet<ResourceType> getResourceTypes(@Nullable UElement element) {
if (element == null) {
return null;
}
if (element instanceof UIfExpression) {
UIfExpression expression = (UIfExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResourceTypes(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResourceTypes(expression.getElseExpression());
} else {
EnumSet<ResourceType> left = getResourceTypes(
expression.getThenExpression());
EnumSet<ResourceType> right = getResourceTypes(
expression.getElseExpression());
if (left == null) {
return right;
} else if (right == null) {
return left;
} else {
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
copy.addAll(right);
return copy;
}
}
} else if (element instanceof UParenthesizedExpression) {
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
return getResourceTypes(parenthesizedExpression.getExpression());
} else if ((element instanceof UQualifiedReferenceExpression && mAllowDereference)
|| element instanceof UCallExpression) {
UElement probablyCallExpression = element;
if (element instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualifiedExpression =
(UQualifiedReferenceExpression) element;
probablyCallExpression = qualifiedExpression.getSelector();
}
if ((probablyCallExpression instanceof UCallExpression)) {
UCallExpression call = (UCallExpression) probablyCallExpression;
PsiMethod method = call.resolve();
PsiClass containingClass = UastUtils.getContainingClass(method);
if (method != null && containingClass != null) {
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
if (types != null) {
return types;
}
String qualifiedName = containingClass.getQualifiedName();
String name = call.getMethodName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
List<UExpression> args = call.getValueArguments();
if (!args.isEmpty()) {
types = getResourceTypes(args.get(0));
if (types != null) {
return types;
}
}
}
}
}
}
if (element instanceof UReferenceExpression) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return EnumSet.of(url.type);
}
PsiElement resolved = ((UReferenceExpression) element).resolve();
if (resolved instanceof PsiVariable) {
PsiVariable variable = (PsiVariable) resolved;
UElement lastAssignment =
UastLintUtils.findLastAssignment(variable, element, mContext);
if (lastAssignment != null) {
return getResourceTypes(lastAssignment);
}
return null;
}
}
return null;
}
/**
* Evaluates the given node and returns the resource types applicable to the
* node, if any.
*
* @param element the element to compute the types for
* @return the corresponding resource types
*/
@Nullable
public EnumSet<ResourceType> getResourceTypes(@Nullable PsiElement element) {
if (element == null) {
return null;
}
if (element instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResourceTypes(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResourceTypes(expression.getElseExpression());
} else {
EnumSet<ResourceType> left = getResourceTypes(
expression.getThenExpression());
EnumSet<ResourceType> right = getResourceTypes(
expression.getElseExpression());
if (left == null) {
return right;
} else if (right == null) {
return left;
} else {
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
copy.addAll(right);
return copy;
}
}
} else if (element instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
return getResourceTypes(parenthesizedExpression.getExpression());
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
PsiReferenceExpression expression = call.getMethodExpression();
PsiMethod method = call.resolveMethod();
if (method != null && method.getContainingClass() != null) {
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
if (types != null) {
return types;
}
String qualifiedName = method.getContainingClass().getQualifiedName();
String name = expression.getReferenceName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
PsiExpression[] args = call.getArgumentList().getExpressions();
if (args.length > 0) {
types = getResourceTypes(args[0]);
if (types != null) {
return types;
}
}
}
}
} else if (element instanceof PsiReference) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return EnumSet.of(url.type);
}
PsiElement resolved = ((PsiReference) element).resolve();
if (resolved instanceof PsiField) {
url = getResourceConstant(resolved);
if (url != null) {
return EnumSet.of(url.type);
}
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
return getResourceTypes(field.getInitializer());
}
return null;
} else if (resolved instanceof PsiParameter) {
return getTypesFromAnnotations((PsiParameter)resolved);
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
for (PsiElement e : prevStatement.getDeclaredElements()) {
if (variable.equals(e)) {
return getResourceTypes(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement) prev)
.getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign
= (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return getResourceTypes(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
}
return null;
}
@Nullable
private EnumSet<ResourceType> getTypesFromAnnotations(PsiModifierListOwner owner) {
if (mEvaluator == null) {
return null;
}
for (PsiAnnotation annotation : mEvaluator.getAllAnnotations(owner, true)) {
String signature = annotation.getQualifiedName();
if (signature == null) {
continue;
}
if (signature.equals(COLOR_INT_ANNOTATION)) {
return EnumSet.of(COLOR_INT_MARKER_TYPE);
}
if (signature.equals(PX_ANNOTATION)) {
return EnumSet.of(PX_MARKER_TYPE);
}
if (signature.endsWith(RES_SUFFIX)
&& signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
String typeString = signature
.substring(SUPPORT_ANNOTATIONS_PREFIX.length(),
signature.length() - RES_SUFFIX.length())
.toLowerCase(Locale.US);
ResourceType type = ResourceType.getEnum(typeString);
if (type != null) {
return EnumSet.of(type);
} else if (typeString.equals("any")) { // @AnyRes
return getAnyRes();
}
}
}
return null;
}
/** Returns a resource URL based on the field reference in the code */
@Nullable
public static ResourceUrl getResourceConstant(@NonNull PsiElement node) {
// R.type.name
if (node instanceof PsiReferenceExpression) {
PsiReferenceExpression expression = (PsiReferenceExpression) node;
if (expression.getQualifier() instanceof PsiReferenceExpression) {
PsiReferenceExpression select = (PsiReferenceExpression) expression.getQualifier();
if (select.getQualifier() instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) select
.getQualifier();
if (R_CLASS.equals(reference.getReferenceName())) {
String typeName = select.getReferenceName();
String name = expression.getReferenceName();
ResourceType type = ResourceType.getEnum(typeName);
if (type != null && name != null) {
boolean isFramework =
reference.getQualifier() instanceof PsiReferenceExpression
&& ANDROID_PKG
.equals(((PsiReferenceExpression) reference.
getQualifier()).getReferenceName());
return ResourceUrl.create(type, name, isFramework, false);
}
}
}
}
} else if (node instanceof PsiField) {
PsiField field = (PsiField) node;
PsiClass typeClass = field.getContainingClass();
if (typeClass != null) {
PsiClass rClass = typeClass.getContainingClass();
if (rClass != null && R_CLASS.equals(rClass.getName())) {
String name = field.getName();
ResourceType type = ResourceType.getEnum(typeClass.getName());
if (type != null && name != null) {
String qualifiedName = rClass.getQualifiedName();
boolean isFramework = qualifiedName != null
&& qualifiedName.startsWith(ANDROID_PKG_PREFIX);
return ResourceUrl.create(type, name, isFramework, false);
}
}
}
}
return null;
}
/** Returns a resource URL based on the field reference in the code */
@Nullable
public static ResourceUrl getResourceConstant(@NonNull UElement node) {
AndroidReference androidReference = toAndroidReferenceViaResolve(node);
if (androidReference == null) {
return null;
}
String name = androidReference.getName();
ResourceType type = androidReference.getType();
boolean isFramework = androidReference.getPackage().equals("android");
return ResourceUrl.create(type, name, isFramework, false);
}
private static EnumSet<ResourceType> getAnyRes() {
EnumSet<ResourceType> types = EnumSet.allOf(ResourceType.class);
types.remove(ResourceEvaluator.COLOR_INT_MARKER_TYPE);
types.remove(ResourceEvaluator.PX_MARKER_TYPE);
return types;
}
}
@@ -26,7 +26,7 @@ import java.io.File;
* Specialized detector intended for XML resources. Detectors that apply to XML
* resources should extend this detector instead since it provides special
* iteration hooks that are more efficient.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -16,17 +16,25 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_PROPERTIES;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
import static com.android.SdkConstants.OLD_PROGUARD_FILE;
import static com.android.SdkConstants.RES_FOLDER;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import org.jetbrains.uast.UastConverterUtils;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import static com.android.SdkConstants.*;
/**
* The scope of a detector is the set of files a detector must consider when
* performing its analysis. This can be used to determine when issues are
@@ -54,7 +62,7 @@ public enum Scope {
BINARY_RESOURCE_FILE,
/**
* The analysis considers the resource folders
* The analysis considers the resource folders (which also includes asset folders)
*/
RESOURCE_FOLDER,
@@ -72,14 +80,14 @@ public enum Scope {
* Issues which are only affected by a single Java source file can be
* checked for incrementally when a Java source file is edited.
*/
SOURCE_FILE,
JAVA_FILE,
/**
* The analysis considers <b>all</b> the Java source files together.
* <p>
* This flag is mutually exclusive with {@link #SOURCE_FILE}.
* This flag is mutually exclusive with {@link #JAVA_FILE}.
*/
ALL_SOURCE_FILES,
ALL_JAVA_FILES,
/**
* The analysis only considers a single Java class file at a time.
@@ -104,7 +112,8 @@ public enum Scope {
/**
* The analysis considers classes in the libraries for this project. These
* will be analyzed before the classes themselves.
* will be analyzed before the classes themselves. NOTE: This excludes
* provided libraries.
*/
JAVA_LIBRARIES,
@@ -135,10 +144,10 @@ public enum Scope {
if (size == 2) {
// When single checking a Java source file, we check both its Java source
// and the associated class files
return scopes.contains(SOURCE_FILE) && scopes.contains(CLASS_FILE);
return scopes.contains(JAVA_FILE) && scopes.contains(CLASS_FILE);
} else {
return size == 1 &&
(scopes.contains(SOURCE_FILE)
(scopes.contains(JAVA_FILE)
|| scopes.contains(CLASS_FILE)
|| scopes.contains(RESOURCE_FILE)
|| scopes.contains(PROGUARD_FILE)
@@ -183,8 +192,8 @@ public enum Scope {
scope.add(MANIFEST);
} else if (name.endsWith(DOT_XML)) {
scope.add(RESOURCE_FILE);
} else if (name.endsWith(DOT_JAVA)) {
scope.add(SOURCE_FILE);
} else if (name.endsWith(".kt")) {
scope.add(JAVA_FILE);
} else if (name.endsWith(DOT_CLASS)) {
scope.add(CLASS_FILE);
} else if (name.endsWith(DOT_GRADLE)) {
@@ -202,9 +211,6 @@ public enum Scope {
scope.add(RESOURCE_FILE);
scope.add(BINARY_RESOURCE_FILE);
scope.add(RESOURCE_FOLDER);
} else if (UastConverterUtils.isFileSupported(
project.getClient().getLanguagePlugins(), name)) {
scope.add(SOURCE_FILE);
}
}
} else {
@@ -226,7 +232,7 @@ public enum Scope {
/** Scope-set used for detectors which scan all resources */
public static final EnumSet<Scope> ALL_RESOURCES_SCOPE = EnumSet.of(ALL_RESOURCE_FILES);
/** Scope-set used for detectors which are affected by a single Java source file */
public static final EnumSet<Scope> SOURCE_FILE_SCOPE = EnumSet.of(SOURCE_FILE);
public static final EnumSet<Scope> JAVA_FILE_SCOPE = EnumSet.of(JAVA_FILE);
/** Scope-set used for detectors which are affected by a single Java class file */
public static final EnumSet<Scope> CLASS_FILE_SCOPE = EnumSet.of(CLASS_FILE);
/** Scope-set used for detectors which are affected by a single Gradle build file */
@@ -243,8 +249,8 @@ public enum Scope {
public static final EnumSet<Scope> MANIFEST_AND_RESOURCE_SCOPE =
EnumSet.of(Scope.MANIFEST, Scope.RESOURCE_FILE);
/** Scope-set used for detectors which are affected by single XML and Java source files */
public static final EnumSet<Scope> SOURCE_AND_RESOURCE_FILES =
EnumSet.of(RESOURCE_FILE, SOURCE_FILE);
public static final EnumSet<Scope> JAVA_AND_RESOURCE_FILES =
EnumSet.of(RESOURCE_FILE, JAVA_FILE);
/** Scope-set used for analyzing individual class files and all resource files */
public static final EnumSet<Scope> CLASS_AND_ALL_RESOURCE_FILES =
EnumSet.of(ALL_RESOURCE_FILES, CLASS_FILE);
@@ -22,7 +22,7 @@ import com.google.common.annotations.Beta;
/**
* Severity of an issue found by lint
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -27,7 +27,6 @@ import com.android.utils.XmlUtils;
* also be converted to plain text and to HTML markup, using the
* {@link #convertTo(String, TextFormat)} method.
*
* @see Issue#getDescription(TextFormat)
* @see Issue#getExplanation(TextFormat)
* @see Issue#getBriefDescription(TextFormat)
*/
@@ -56,7 +55,16 @@ public enum TextFormat {
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags)
*/
HTML;
HTML,
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags).
* This is like {@link #HTML}, but it does not escape unicode characters with entities.
* <p>
* (This is used for example in the IDE, where some partial HTML support in some
* label widgets support some HTML markup, but not numeric code character entities.)
*/
HTML_WITH_UNICODE;
/**
* Converts the given text to HTML
@@ -102,6 +110,7 @@ public enum TextFormat {
return message;
case TEXT:
case HTML:
case HTML_WITH_UNICODE:
return to.fromRaw(message);
}
}
@@ -111,6 +120,7 @@ public enum TextFormat {
case RAW:
return message;
case HTML:
case HTML_WITH_UNICODE:
return XmlUtils.toXmlTextValue(message);
}
}
@@ -118,6 +128,20 @@ public enum TextFormat {
switch (to) {
case HTML:
return message;
case HTML_WITH_UNICODE:
return removeNumericEntities(message);
case RAW:
case TEXT: {
return to.fromHtml(message);
}
}
}
case HTML_WITH_UNICODE: {
switch (to) {
case HTML:
case HTML_WITH_UNICODE:
return message;
case RAW:
case TEXT: {
return to.fromHtml(message);
@@ -137,51 +161,103 @@ public enum TextFormat {
// Drop all tags; replace all entities, insert newlines
// (this won't do wrapping)
StringBuilder sb = new StringBuilder(html.length());
boolean inPre = false;
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '<') {
// Scan forward to the end
if (html.startsWith("<br>", i) ||
html.startsWith("<br />", i) ||
html.startsWith("<BR>", i) ||
html.startsWith("<BR />", i)) {
sb.append('\n');
} else if (html.startsWith("<!--")) {
i = Math.max(i, html.indexOf("-->", i));
// Strip comments
if (html.startsWith("<!--", i)) {
int end = html.indexOf("-->", i);
if (end == -1) {
break; // Unclosed comment
} else {
i = end + 2;
}
continue;
}
// Tags: scan forward to the end
int begin;
boolean isEndTag = false;
if (html.startsWith("</", i)) {
begin = i + 2;
isEndTag = true;
} else {
begin = i + 1;
}
i = html.indexOf('>', i);
if (i == -1) {
// Unclosed tag
break;
}
int end = i;
if (html.charAt(i - 1) == '/') {
end--;
isEndTag = true;
}
// TODO: Handle <pre> such that we don't collapse spaces and reformat there!
// (We do need to strip out tags and expand entities)
String tag = html.substring(begin, end).trim();
if (tag.equalsIgnoreCase("br")) {
sb.append('\n');
} else if (tag.equalsIgnoreCase("p") // Most common block tags
|| tag.equalsIgnoreCase("div")
|| tag.equalsIgnoreCase("pre")
|| tag.equalsIgnoreCase("blockquote")
|| tag.equalsIgnoreCase("dl")
|| tag.equalsIgnoreCase("dd")
|| tag.equalsIgnoreCase("dt")
|| tag.equalsIgnoreCase("ol")
|| tag.equalsIgnoreCase("ul")
|| tag.equalsIgnoreCase("li")
|| tag.length() == 2 && tag.startsWith("h")
&& Character.isDigit(tag.charAt(1))) {
// Block tag: ensure new line
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '\n') {
sb.append('\n');
}
if (tag.equals("li") && !isEndTag) {
sb.append("* ");
}
if (tag.equalsIgnoreCase("pre")) {
inPre = !isEndTag;
}
}
} else if (c == '&') {
int end = html.indexOf(';', i);
if (end > i) {
String entity = html.substring(i, end + 1);
sb.append(XmlUtils.fromXmlAttributeValue(entity));
String s = XmlUtils.fromXmlAttributeValue(entity);
if (s.startsWith("&")) {
// Not an XML entity; for example, &nbsp;
// Sadly Guava's HtmlEscapes don't handle this either.
if (entity.equalsIgnoreCase("&nbsp;")) {
s = " ";
} else if (entity.startsWith("&#")) {
try {
int value = Integer.parseInt(entity.substring(2));
s = Character.toString((char)value);
} catch (NumberFormatException ignore) {
}
}
}
sb.append(s);
i = end;
} else {
sb.append(c);
}
} else if (c == '\n') {
sb.append(' ');
} else if (Character.isWhitespace(c)) {
if (inPre) {
sb.append(c);
} else if (sb.length() == 0
|| !Character.isWhitespace(sb.charAt(sb.length() - 1))) {
sb.append(' ');
}
} else {
sb.append(c);
}
}
// Collapse repeated spaces
String s = sb.toString();
sb.setLength(0);
boolean wasSpace = false;
for (int i = 0, n = s.length(); i < n; i++) {
char c = s.charAt(i);
if (c == '\t') { // we keep newlines; came from <br>'s
c = ' ';
}
boolean isSpace = c == ' ';
if (!isSpace || !wasSpace) {
wasSpace = isSpace;
sb.append(c);
}
}
s = sb.toString();
// Line-wrap
s = SdkUtils.wrap(s, 60, null);
@@ -194,16 +270,17 @@ public enum TextFormat {
/** Converts to this output format from the given raw-format text */
@NonNull
private String fromRaw(@NonNull String text) {
assert this == HTML || this == TEXT : this;
assert this == HTML || this == HTML_WITH_UNICODE || this == TEXT : this;
StringBuilder sb = new StringBuilder(3 * text.length() / 2);
boolean html = this == HTML;
boolean html = this == HTML || this == HTML_WITH_UNICODE;
boolean escapeUnicode = this == HTML;
char prev = 0;
int flushIndex = 0;
int n = text.length();
for (int i = 0; i < n; i++) {
char c = text.charAt(i);
if ((c == '*' || c == '`' && i < n - 1)) {
if ((c == '*' || c == '`') && i < n - 1) {
// Scout ahead for range end
if (!Character.isLetterOrDigit(prev)
&& !Character.isWhitespace(text.charAt(i + 1))) {
@@ -212,15 +289,15 @@ public enum TextFormat {
int end = text.indexOf(c, i + 1);
if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
appendEscapedText(sb, text, html, flushIndex, i, escapeUnicode);
}
if (html) {
String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$
sb.append('<').append(tag).append('>');
appendEscapedText(sb, text, html, i + 1, end);
appendEscapedText(sb, text, html, i + 1, end, escapeUnicode);
sb.append('<').append('/').append(tag).append('>');
} else {
appendEscapedText(sb, text, html, i + 1, end);
appendEscapedText(sb, text, html, i + 1, end, escapeUnicode);
}
flushIndex = end + 1;
i = flushIndex - 1; // -1: account for the i++ in the loop
@@ -243,7 +320,7 @@ public enum TextFormat {
}
if (end > i + HTTP_PREFIX.length()) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
appendEscapedText(sb, text, html, flushIndex, i, escapeUnicode);
}
String url = text.substring(i, end);
@@ -261,14 +338,42 @@ public enum TextFormat {
}
if (flushIndex < n) {
appendEscapedText(sb, text, html, flushIndex, n);
appendEscapedText(sb, text, html, flushIndex, n, escapeUnicode);
}
return sb.toString();
}
private static String removeNumericEntities(@NonNull String html) {
if (!html.contains("&#")) {
return html;
}
StringBuilder sb = new StringBuilder(html.length());
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '&' && i < n - 1 && html.charAt(i + 1) == '#') {
int end = html.indexOf(';', i + 2);
if (end != -1) {
String decimal = html.substring(i + 2, end);
try {
c = (char)Integer.parseInt(decimal);
sb.append(c);
i = end;
continue;
} catch (NumberFormatException ignore) {
// fall through to not escape this
}
}
}
sb.append(c);
}
return sb.toString();
}
private static void appendEscapedText(@NonNull StringBuilder sb, @NonNull String text,
boolean html, int start, int end) {
boolean html, int start, int end, boolean escapeUnicode) {
if (html) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
@@ -279,7 +384,7 @@ public enum TextFormat {
} else if (c == '\n') {
sb.append("<br/>\n");
} else {
if (c > 255) {
if (c > 255 && escapeUnicode) {
sb.append("&#"); //$NON-NLS-1$
sb.append(Integer.toString(c));
sb.append(';');
@@ -0,0 +1,417 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.detector.api;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.klint.client.api.JavaParser.TYPE_CHAR;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import static com.android.tools.klint.detector.api.JavaContext.getParentOfType;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaParser.DefaultTypeDescriptor;
import com.android.tools.klint.client.api.JavaParser.ResolvedClass;
import com.android.tools.klint.client.api.JavaParser.ResolvedField;
import com.android.tools.klint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.klint.client.api.JavaParser.ResolvedNode;
import com.android.tools.klint.client.api.JavaParser.ResolvedVariable;
import com.android.tools.klint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.klint.client.api.UastLintUtils;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UVariable;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import java.util.ListIterator;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.BooleanLiteral;
import lombok.ast.Cast;
import lombok.ast.CharLiteral;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.FloatingPointLiteral;
import lombok.ast.InlineIfExpression;
import lombok.ast.IntegralLiteral;
import lombok.ast.Literal;
import lombok.ast.Node;
import lombok.ast.NullLiteral;
import lombok.ast.Statement;
import lombok.ast.StringLiteral;
import lombok.ast.UnaryExpression;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Evaluates the types of nodes. This goes deeper than
* {@link JavaContext#getType(Node)} in that it analyzes the
* flow and for example figures out that if you ask for the type of {@code var}
* in this code snippet:
* <pre>
* Object o = new StringBuilder();
* Object var = o;
* </pre>
* it will return "java.lang.StringBuilder".
* <p>
* <b>NOTE:</b> This type evaluator does not (yet) compute the correct
* types when involving implicit type conversions, so be careful
* if using this for primitives; e.g. for "int * long" it might return
* the type "int".
*/
public class TypeEvaluator {
private final JavaContext mContext;
/**
* Creates a new constant evaluator
*
* @param context the context to use to resolve field references, if any
*/
public TypeEvaluator(@Nullable JavaContext context) {
mContext = context;
}
/**
* Returns the inferred type of the given node
* @deprecated Use {@link #evaluate(PsiElement)} instead
*/
@Deprecated
@Nullable
public TypeDescriptor evaluate(@NonNull Node node) {
ResolvedNode resolved = null;
if (mContext != null) {
resolved = mContext.resolve(node);
}
if (resolved instanceof ResolvedMethod) {
TypeDescriptor type;
ResolvedMethod method = (ResolvedMethod) resolved;
if (method.isConstructor()) {
ResolvedClass containingClass = method.getContainingClass();
type = containingClass.getType();
} else {
type = method.getReturnType();
}
return type;
}
if (resolved instanceof ResolvedField) {
ResolvedField field = (ResolvedField) resolved;
Node astNode = field.findAstNode();
if (astNode instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration)astNode;
VariableDefinition definition = declaration.astDefinition();
if (definition != null) {
VariableDefinitionEntry first = definition.astVariables().first();
if (first != null) {
Expression initializer = first.astInitializer();
if (initializer != null) {
TypeDescriptor type = evaluate(initializer);
if (type != null) {
return type;
}
}
}
}
}
return field.getType();
}
if (node instanceof VariableReference) {
Statement statement = getParentOfType(node, Statement.class, false);
if (statement != null) {
ListIterator<Node> iterator = statement.getParent().getChildren().listIterator();
while (iterator.hasNext()) {
if (iterator.next() == statement) {
if (iterator.hasPrevious()) { // should always be true
iterator.previous();
}
break;
}
}
String targetName = ((VariableReference) node).astIdentifier().astValue();
while (iterator.hasPrevious()) {
Node previous = iterator.previous();
if (previous instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration) previous;
VariableDefinition definition = declaration.astDefinition();
for (VariableDefinitionEntry entry : definition.astVariables()) {
if (entry.astInitializer() != null && entry.astName().astValue()
.equals(targetName)) {
return evaluate(entry.astInitializer());
}
}
} else if (previous instanceof ExpressionStatement) {
ExpressionStatement expressionStatement = (ExpressionStatement) previous;
Expression expression = expressionStatement.astExpression();
if (expression instanceof BinaryExpression &&
((BinaryExpression) expression).astOperator()
== BinaryOperator.ASSIGN) {
BinaryExpression binaryExpression = (BinaryExpression) expression;
if (targetName.equals(binaryExpression.astLeft().toString())) {
return evaluate(binaryExpression.astRight());
}
}
}
}
}
} else if (node instanceof Cast) {
Cast cast = (Cast) node;
if (mContext != null) {
ResolvedNode typeReference = mContext.resolve(cast.astTypeReference());
if (typeReference instanceof ResolvedClass) {
return ((ResolvedClass) typeReference).getType();
}
}
TypeDescriptor viewType = evaluate(cast.astOperand());
if (viewType != null) {
return viewType;
}
} else if (node instanceof Literal) {
if (node instanceof NullLiteral) {
return null;
} else if (node instanceof BooleanLiteral) {
return new DefaultTypeDescriptor(TYPE_BOOLEAN);
} else if (node instanceof StringLiteral) {
return new DefaultTypeDescriptor(TYPE_STRING);
} else if (node instanceof CharLiteral) {
return new DefaultTypeDescriptor(TYPE_CHAR);
} else if (node instanceof IntegralLiteral) {
IntegralLiteral literal = (IntegralLiteral) node;
// Don't combine to ?: since that will promote astIntValue to a long
if (literal.astMarkedAsLong()) {
return new DefaultTypeDescriptor(TYPE_LONG);
} else {
return new DefaultTypeDescriptor(TYPE_INT);
}
} else if (node instanceof FloatingPointLiteral) {
FloatingPointLiteral literal = (FloatingPointLiteral) node;
// Don't combine to ?: since that will promote astFloatValue to a double
if (literal.astMarkedAsFloat()) {
return new DefaultTypeDescriptor(TYPE_FLOAT);
} else {
return new DefaultTypeDescriptor(TYPE_DOUBLE);
}
}
} else if (node instanceof UnaryExpression) {
return evaluate(((UnaryExpression) node).astOperand());
} else if (node instanceof InlineIfExpression) {
InlineIfExpression expression = (InlineIfExpression) node;
if (expression.astIfTrue() != null) {
return evaluate(expression.astIfTrue());
} else if (expression.astIfFalse() != null) {
return evaluate(expression.astIfFalse());
}
} else if (node instanceof BinaryExpression) {
BinaryExpression expression = (BinaryExpression) node;
BinaryOperator operator = expression.astOperator();
switch (operator) {
case LOGICAL_OR:
case LOGICAL_AND:
case EQUALS:
case NOT_EQUALS:
case GREATER:
case GREATER_OR_EQUAL:
case LESS:
case LESS_OR_EQUAL:
return new DefaultTypeDescriptor(TYPE_BOOLEAN);
}
TypeDescriptor type = evaluate(expression.astLeft());
if (type != null) {
return type;
}
return evaluate(expression.astRight());
}
if (resolved instanceof ResolvedVariable) {
ResolvedVariable variable = (ResolvedVariable) resolved;
return variable.getType();
}
return null;
}
/**
* Returns the inferred type of the given node
*/
@Nullable
public PsiType evaluate(@Nullable PsiElement node) {
if (node == null) {
return null;
}
PsiElement resolved = null;
if (node instanceof PsiReference) {
resolved = ((PsiReference) node).resolve();
}
if (resolved instanceof PsiMethod) {
PsiMethod method = (PsiMethod) resolved;
if (method.isConstructor()) {
PsiClass containingClass = method.getContainingClass();
if (containingClass != null && mContext != null) {
return mContext.getEvaluator().getClassType(containingClass);
}
} else {
return method.getReturnType();
}
}
if (resolved instanceof PsiField) {
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
PsiType type = evaluate(field.getInitializer());
if (type != null) {
return type;
}
}
return field.getType();
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(node, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
for (PsiElement element : ((PsiDeclarationStatement)prev).getDeclaredElements()) {
if (variable.equals(element)) {
return evaluate(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement)prev).getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign = (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return evaluate(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
return variable.getType();
} else if (node instanceof PsiExpression) {
PsiExpression expression = (PsiExpression) node;
return expression.getType();
}
return null;
}
@Nullable
public static PsiType evaluate(@NonNull JavaContext context, @Nullable UElement node) {
if (node == null) {
return null;
}
UElement resolved = node;
if (resolved instanceof UReferenceExpression) {
resolved = UastUtils.tryResolveUDeclaration(resolved, context.getUastContext());
}
if (resolved instanceof UMethod) {
return ((UMethod) resolved).getPsi().getReturnType();
} else if (resolved instanceof UVariable) {
UVariable variable = (UVariable) resolved;
UElement lastAssignment = UastLintUtils.findLastAssignment(variable, node, context);
if (lastAssignment != null) {
return evaluate(context, lastAssignment);
}
return variable.getType();
} else if (resolved instanceof UCallExpression) {
if (UastExpressionUtils.isMethodCall(resolved)) {
PsiMethod resolvedMethod = ((UCallExpression) resolved).resolve();
return resolvedMethod != null ? resolvedMethod.getReturnType() : null;
} else {
return ((UCallExpression) resolved).getExpressionType();
}
} else if (resolved instanceof UExpression) {
return ((UExpression) resolved).getExpressionType();
}
return null;
}
/**
* Evaluates the given node and returns the likely type of the instance. Convenience
* wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
* the result.
*
* @param context the context to use to resolve field references, if any
* @param node the node to compute the type for
* @return the corresponding type descriptor, if found
* @deprecated Use {@link #evaluate(JavaContext, PsiElement)} instead
*/
@Deprecated
@Nullable
public static TypeDescriptor evaluate(@NonNull JavaContext context, @NonNull Node node) {
return new TypeEvaluator(context).evaluate(node);
}
/**
* Evaluates the given node and returns the likely type of the instance. Convenience
* wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
* the result.
*
* @param context the context to use to resolve field references, if any
* @param node the node to compute the type for
* @return the corresponding type descriptor, if found
*/
@Nullable
public static PsiType evaluate(@NonNull JavaContext context, @NonNull PsiElement node) {
return new TypeEvaluator(context).evaluate(node);
}
}
@@ -22,6 +22,7 @@ import com.android.resources.ResourceFolderType;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.client.api.XmlParser;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -30,7 +31,7 @@ import java.io.File;
/**
* A {@link Context} used when checking XML files.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -132,7 +133,7 @@ public class XmlContext extends ResourceContext {
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
@@ -153,7 +154,7 @@ public class XmlContext extends ResourceContext {
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
@@ -162,7 +163,7 @@ public class XmlContext extends ResourceContext {
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
// Warn if clients use the non-scoped form? No, there are cases where an
// XML detector's error isn't applicable to one particular location (or it's