Lint: Update diagnostics because of the new Uast. Also, these diagnostics are from AS 2.2.
This commit is contained in:
committed by
Yan Zhulanow
parent
4c4d6a4ad4
commit
c2ddd943f9
@@ -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>
|
||||
*/
|
||||
|
||||
+24
-9
@@ -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;
|
||||
|
||||
+127
-4
@@ -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);
|
||||
}
|
||||
-38
@@ -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) {
|
||||
|
||||
Executable → Regular
+219
-36
@@ -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
-61
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||
|
||||
+1941
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
*/
|
||||
|
||||
Executable → Regular
+237
-69
@@ -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>
|
||||
*/
|
||||
|
||||
Executable → Regular
+185
-62
@@ -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>
|
||||
*/
|
||||
|
||||
+690
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
// Sadly Guava's HtmlEscapes don't handle this either.
|
||||
if (entity.equalsIgnoreCase(" ")) {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user