diff --git a/.idea/dictionaries/yan.xml b/.idea/dictionaries/yan.xml index f3381349a06..803ebf0adb6 100644 --- a/.idea/dictionaries/yan.xml +++ b/.idea/dictionaries/yan.xml @@ -2,6 +2,7 @@ kapt + uast \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 519d3ee12ad..021661f45d5 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -83,7 +83,6 @@ - diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/state/KotlinTypeMapper.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/state/KotlinTypeMapper.java index ee4de236d06..be53feee31c 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/state/KotlinTypeMapper.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/state/KotlinTypeMapper.java @@ -417,7 +417,7 @@ public class KotlinTypeMapper { } @NotNull - private Type mapType( + public Type mapType( @NotNull KotlinType kotlinType, @Nullable final JvmSignatureWriter signatureVisitor, @NotNull TypeMappingMode mode diff --git a/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightMethod.kt b/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightMethod.kt index 62a5cae89a8..d350de370c0 100644 --- a/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightMethod.kt +++ b/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightMethod.kt @@ -205,7 +205,7 @@ sealed class KtLightMethodImpl( delegate: PsiMethod, origin: LightMemberOrigin?, containingClass: KtLightClass ) : KtLightMethodImpl(delegate, origin, containingClass) - private class KtLightAnnotationMethod( + class KtLightAnnotationMethod( override val clsDelegate: PsiAnnotationMethod, origin: LightMemberOrigin?, containingClass: KtLightClass diff --git a/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightParameter.java b/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightParameter.java index dccb1b1296a..ead19724b00 100644 --- a/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightParameter.java +++ b/compiler/light-classes/src/org/jetbrains/kotlin/asJava/elements/KtLightParameter.java @@ -185,4 +185,9 @@ public class KtLightParameter extends LightParameter implements KtLightDeclarati } return super.isEquivalentTo(another); } + + @Override + public boolean equals(Object obj) { + return obj instanceof PsiElement && isEquivalentTo((PsiElement) obj); + } } diff --git a/idea/src/META-INF/android-lint.xml b/idea/src/META-INF/android-lint.xml index 24303fb9627..4c41551d09a 100644 --- a/idea/src/META-INF/android-lint.xml +++ b/idea/src/META-INF/android-lint.xml @@ -1,22 +1,37 @@ + + + + + + + + + + + + + @@ -35,49 +50,72 @@ + + + + + + + + - + + + + + + + + + + + + - - + + + + + - - + + + @@ -85,13 +123,13 @@ - - - - + + + \ No newline at end of file diff --git a/plugins/lint/lint-api/lint-api.iml b/plugins/lint/lint-api/lint-api.iml index 6ce6a410525..148f696c544 100644 --- a/plugins/lint/lint-api/lint-api.iml +++ b/plugins/lint/lint-api/lint-api.iml @@ -10,7 +10,6 @@ - diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AndroidReference.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AndroidReference.java new file mode 100644 index 00000000000..db26881cc6f --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AndroidReference.java @@ -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; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java index 33d83b3c836..ff68e23d66d 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java @@ -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 list = mMethodNameToChecks.get(element); + List list = mMethodNameToChecks.get(element); if (list == null) { - list = new ArrayList(); + list = new ArrayList(); mMethodNameToChecks.put(element, list); } list.add(scanner); @@ -94,9 +103,9 @@ class AsmVisitor { if (owners != null) { checkFullClass = false; for (String element : owners) { - List list = mMethodOwnerToChecks.get(element); + List list = mMethodOwnerToChecks.get(element); if (list == null) { - list = new ArrayList(); + list = new ArrayList(); mMethodOwnerToChecks.put(element, list); } list.add(scanner); diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java index 2479f236dc4..dd0e0e86d4d 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java @@ -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 { diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java index 8d2a60314fe..5952b101f6b 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java @@ -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. - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java index 0a2278d0937..eb2d9f09ae6 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java @@ -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. - *

+ *

* 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. */ @@ -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); diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java index 5097822ad59..cc7b157a2c4 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java @@ -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} *

@@ -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('<'); diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java index 552ea3e2cea..bc822ec73c0 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java @@ -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 sCategories; - private static Map sIdToIssue; + private static volatile List sCategories; + private static volatile Map sIdToIssue; private static Map, List> 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 getCategories() { - if (sCategories == null) { - final Set categories = new HashSet(); - for (Issue issue : getIssues()) { - categories.add(issue.getCategory()); + List categories = sCategories; + if (categories == null) { + synchronized (IssueRegistry.class) { + categories = sCategories; + if (categories == null) { + sCategories = categories = Collections.unmodifiableList(createCategoryList()); + } } - List sorted = new ArrayList(categories); - Collections.sort(sorted); - sCategories = Collections.unmodifiableList(sorted); } - return sCategories; + return categories; + } + + @NonNull + private List createCategoryList() { + Set categorySet = Sets.newHashSetWithExpectedSize(20); + for (Issue issue : getIssues()) { + categorySet.add(issue.getCategory()); + } + List sorted = new ArrayList(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 issues = getIssues(); - sIdToIssue = new HashMap(issues.size()); - for (Issue issue : issues) { - sIdToIssue.put(issue.getId(), issue); + Map 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 createIdToIssueMap() { + List issues = getIssues(); + Map 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. - *

- * NOTE: This is only intended for testing purposes. */ - @VisibleForTesting protected static void reset() { sIdToIssue = null; sCategories = null; diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java index bcafb754a1c..79b4dca57b5 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java @@ -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; /** *

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> sCache; private final List 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>(); } 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(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 = 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. + *

+ * 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 getIssues() { diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaEvaluator.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaEvaluator.java new file mode 100644 index 00000000000..36b8dd1be0f --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaEvaluator.java @@ -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); +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaLintLanguageExtension.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaLintLanguageExtension.java deleted file mode 100644 index 546dee07971..00000000000 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaLintLanguageExtension.java +++ /dev/null @@ -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 getVisitorExtensions() { - return JavaUastLanguagePlugin.INSTANCE.getVisitorExtensions(); - } -} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java index 4fd83c82036..96763856e15 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java @@ -16,31 +16,350 @@ package com.android.tools.klint.client.api; +import static com.android.SdkConstants.ATTR_VALUE; + import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.detector.api.ClassContext; +import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.DefaultPosition; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Position; import com.google.common.annotations.Beta; +import com.google.common.base.Splitter; +import com.intellij.mock.MockProject; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UElement; import org.jetbrains.uast.UFile; +import org.jetbrains.uast.UastContext; + +import java.io.File; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.List; + +import lombok.ast.Catch; +import lombok.ast.For; +import lombok.ast.Identifier; +import lombok.ast.If; +import lombok.ast.Node; +import lombok.ast.Return; +import lombok.ast.StrictListAccessor; +import lombok.ast.Switch; +import lombok.ast.Throw; +import lombok.ast.TypeReference; +import lombok.ast.TypeReferencePart; +import lombok.ast.While; /** * A wrapper for a Java parser. This allows tools integrating lint to map directly * to builtin services, such as already-parsed data structures in Java editors. - *

+ *

* NOTE: This is not public or final API; if you rely on this be prepared * to adjust your code for the next tools release. */ +// Currently ships with deprecated API support +@SuppressWarnings({"deprecation", "UnusedParameters"}) @Beta public abstract class JavaParser { - public static final String TYPE_OBJECT = "java.lang.Object"; //$NON-NLS-1$ - public static final String TYPE_STRING = "java.lang.String"; //$NON-NLS-1$ - public static final String TYPE_INT = "int"; //$NON-NLS-1$ - public static final String TYPE_LONG = "long"; //$NON-NLS-1$ - public static final String TYPE_CHAR = "char"; //$NON-NLS-1$ - public static final String TYPE_FLOAT = "float"; //$NON-NLS-1$ - public static final String TYPE_DOUBLE = "double"; //$NON-NLS-1$ - public static final String TYPE_BOOLEAN = "boolean"; //$NON-NLS-1$ - public static final String TYPE_SHORT = "short"; //$NON-NLS-1$ - public static final String TYPE_BYTE = "byte"; //$NON-NLS-1$ - public static final String TYPE_NULL = "null"; //$NON-NLS-1$ + public static final String TYPE_OBJECT = "java.lang.Object"; + public static final String TYPE_STRING = "java.lang.String"; + public static final String TYPE_INT = "int"; + public static final String TYPE_LONG = "long"; + public static final String TYPE_CHAR = "char"; + public static final String TYPE_FLOAT = "float"; + public static final String TYPE_DOUBLE = "double"; + public static final String TYPE_BOOLEAN = "boolean"; + public static final String TYPE_SHORT = "short"; + public static final String TYPE_BYTE = "byte"; + public static final String TYPE_NULL = "null"; + public static final String TYPE_INTEGER_WRAPPER = "java.lang.Integer"; + public static final String TYPE_BOOLEAN_WRAPPER = "java.lang.Boolean"; + public static final String TYPE_BYTE_WRAPPER = "java.lang.Byte"; + public static final String TYPE_SHORT_WRAPPER = "java.lang.Short"; + public static final String TYPE_LONG_WRAPPER = "java.lang.Long"; + public static final String TYPE_DOUBLE_WRAPPER = "java.lang.Double"; + public static final String TYPE_FLOAT_WRAPPER = "java.lang.Float"; + public static final String TYPE_CHARACTER_WRAPPER = "java.lang.Character"; + + /** + * Prepare to parse the given contexts. This method will be called before + * a series of {@link #parseJava(JavaContext)} calls, which allows some + * parsers to do up front global computation in case they want to more + * efficiently process multiple files at the same time. This allows a single + * type-attribution pass for example, which is a lot more efficient than + * performing global type analysis over and over again for each individual + * file + * + * @param contexts a list of contexts to be parsed + */ + public abstract void prepareJavaParse(@NonNull List contexts); + + /** + * Parse the file pointed to by the given context. + * + * @param context the context pointing to the file to be parsed, typically + * via {@link Context#getContents()} but the file handle ( + * {@link Context#file} can also be used to map to an existing + * editor buffer in the surrounding tool, etc) + * @return the compilation unit node for the file + * @deprecated Use {@link #parseJavaToPsi(JavaContext)} instead + */ + @Deprecated + @Nullable + public Node parseJava(@NonNull JavaContext context) { + return null; + } + + /** + * Parse the file pointed to by the given context. + * + * @param context the context pointing to the file to be parsed, typically + * via {@link Context#getContents()} but the file handle ( + * {@link Context#file} can also be used to map to an existing + * editor buffer in the surrounding tool, etc) + * @return the compilation unit node for the file + */ + @Nullable + public abstract PsiJavaFile parseJavaToPsi(@NonNull JavaContext context); + + /** + * Returns an evaluator which can perform various resolution tasks, + * evaluate inheritance lookup etc. + * + * @return an evaluator + */ + @NonNull + public abstract JavaEvaluator getEvaluator(); + + public abstract Project getIdeaProject(); + + public abstract UastContext getUastContext(); + + /** + * Returns a {@link Location} for the given node + * + * @param context information about the file being parsed + * @param node the node to create a location for + * @return a location for the given node + * @deprecated Use {@link #getNameLocation(JavaContext, PsiElement)} instead + */ + @Deprecated + @NonNull + public Location getLocation(@NonNull JavaContext context, @NonNull Node node) { + // No longer mandatory to override for children; this is a deprecated API + return Location.NONE; + } + + /** + * Returns a {@link Location} for the given element + * + * @param context information about the file being parsed + * @param element the element to create a location for + * @return a location for the given node + */ + @SuppressWarnings("MethodMayBeStatic") // subclasses may want to override/optimize + @NonNull + public Location getLocation(@NonNull JavaContext context, @NonNull PsiElement element) { + TextRange range = element.getTextRange(); + UFile uFile = (UFile) getUastContext().convertElementWithParent(element.getContainingFile(), UFile.class); + if (uFile == null) { + return Location.NONE; + } + + PsiFile containingFile = uFile.getPsi(); + File file = context.file; + if (containingFile != context.getUFile().getPsi()) { + // Reporting an error in a different file. + if (context.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.getVirtualFile(); + if (virtualFile == null) { + return Location.NONE; + } + file = VfsUtilCore.virtualToIoFile(virtualFile); + } + return Location.create(file, context.getContents(), range.getStartOffset(), + range.getEndOffset()); + } + + /** + * Returns a {@link Location} for the given node range (from the starting offset of the first + * node to the ending offset of the second node). + * + * @param context information about the file being parsed + * @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 + * @deprecated Use {@link #getRangeLocation(JavaContext, PsiElement, int, PsiElement, int)} + * instead + */ + @Deprecated + @NonNull + public abstract Location getRangeLocation( + @NonNull JavaContext context, + @NonNull Node from, + int fromDelta, + @NonNull Node to, + int toDelta); + + /** + * Returns a {@link Location} for the given node range (from the starting offset of the first + * node to the ending offset of the second node). + * + * @param context information about the file being parsed + * @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 + */ + @SuppressWarnings("MethodMayBeStatic") // subclasses may want to override/optimize + @NonNull + public Location getRangeLocation( + @NonNull JavaContext context, + @NonNull PsiElement from, + int fromDelta, + @NonNull PsiElement to, + int toDelta) { + String contents = context.getContents(); + int start = Math.max(0, from.getTextRange().getStartOffset() + fromDelta); + int end = Math.min(contents == null ? Integer.MAX_VALUE : contents.length(), + to.getTextRange().getEndOffset() + toDelta); + return Location.create(context.file, contents, start, end); + } + + /** + * Like {@link #getRangeLocation(JavaContext, PsiElement, int, PsiElement, int)} + * but both offsets are relative to the starting offset of the given node. This is + * sometimes more convenient than operating relative to the ending offset when you + * have a fixed range in mind. + * + * @param context information about the file being parsed + * @param from the AST node to get a starting location from + * @param fromDelta Offset delta to apply to the starting offset + * @param toDelta Offset delta to apply to the starting offset + * @return a location for the given node + */ + @SuppressWarnings("MethodMayBeStatic") // subclasses may want to override/optimize + @NonNull + public Location getRangeLocation( + @NonNull JavaContext context, + @NonNull PsiElement from, + int fromDelta, + int toDelta) { + return getRangeLocation(context, from, fromDelta, from, + -(from.getTextRange().getLength() - 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 context information about the file being parsed + * @param node the node to create a location for + * @return a location for the given node + * @deprecated Use {@link #getNameLocation(JavaContext, PsiElement)} instead + */ + @Deprecated + @NonNull + public Location getNameLocation(@NonNull JavaContext context, @NonNull Node node) { + Node nameNode = JavaContext.findNameNode(node); + if (nameNode != null) { + node = nameNode; + } else { + if (node instanceof Switch + || node instanceof For + || node instanceof If + || node instanceof While + || node instanceof Throw + || node instanceof Return) { + // Lint doesn't want to highlight the entire statement/block associated + // with this node, it wants to just highlight the keyword. + Location location = getLocation(context, node); + Position start = location.getStart(); + if (start != null) { + // The Lombok classes happen to have the same length as the target keyword + int length = node.getClass().getSimpleName().length(); + return Location.create(location.getFile(), start, + new DefaultPosition(start.getLine(), start.getColumn() + length, + start.getOffset() + length)); + } + } + } + + return getLocation(context, 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 context information about the file being parsed + * @param element the node to create a location for + * @return a location for the given node + */ + @NonNull + public Location getNameLocation(@NonNull JavaContext context, @NonNull PsiElement element) { + PsiElement nameNode = JavaContext.findNameElement(element); + if (nameNode != null) { + element = nameNode; + } + + return getLocation(context, element); + } + /** + * Creates a light-weight handle to a location for the given node. It can be + * turned into a full fledged location by + * {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}. + * + * @param context the context providing the node + * @param node the node (element or attribute) to create a location handle + * for + * @return a location handle + * @deprecated Use PSI instead (where handles aren't necessary; use PsiElement directly) + */ + @Deprecated + @NonNull + public abstract Location.Handle createLocationHandle(@NonNull JavaContext context, + @NonNull Node node); + + /** + * Dispose any data structures held for the given context. + * @param context information about the file previously parsed + * @param compilationUnit the compilation unit being disposed + * @deprecated Use {@link #dispose(JavaContext, PsiJavaFile)} instead + */ + @Deprecated + public void dispose(@NonNull JavaContext context, @NonNull Node compilationUnit) { + } + + /** + * Dispose any data structures held for the given context. + * @param context information about the file previously parsed + * @param compilationUnit the compilation unit being disposed + */ + public void dispose(@NonNull JavaContext context, @NonNull PsiFile compilationUnit) { + } /** * Dispose any data structures held for the given context. @@ -49,4 +368,711 @@ public abstract class JavaParser { */ public void dispose(@NonNull JavaContext context, @NonNull UFile compilationUnit) { } + + /** + * Dispose any remaining data structures held for all contexts. + * Typically frees up any resources allocated by + * {@link #prepareJavaParse(List)} + */ + public void dispose() { + } + + /** + * Resolves the given expression node: computes the declaration for the given symbol + * + * @param context information about the file being parsed + * @param node the node to resolve + * @return a node representing the resolved fully type: class/interface/annotation, + * field, method or variable + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + @Nullable + public ResolvedNode resolve(@NonNull JavaContext context, @NonNull Node node) { + return null; + } + + /** + * Finds the given type, if possible (which should be reachable from the compilation + * patch of the given node. + * + * @param context information about the file being parsed + * @param fullyQualifiedName the fully qualified name of the class to look up + * @return the class, or null if not found + */ + @Nullable + public ResolvedClass findClass( + @NonNull JavaContext context, + @NonNull String fullyQualifiedName) { + return null; + } + + /** + * Returns the set of exception types handled by the given catch block. + *

+ * This is a workaround for the fact that the Lombok AST API (and implementation) + * doesn't support multi-catch statements. + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + public List getCatchTypes(@NonNull JavaContext context, + @NonNull Catch catchBlock) { + TypeReference typeReference = catchBlock.astExceptionDeclaration().astTypeReference(); + return Collections.singletonList(new DefaultTypeDescriptor( + typeReference.getTypeName())); + } + + /** + * Gets the type of the given node + * + * @param context information about the file being parsed + * @param node the node to look up the type for + * @return the type of the node, if known + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + @Nullable + public TypeDescriptor getType(@NonNull JavaContext context, @NonNull Node node) { + return null; + } + + /** + * Runs the given runnable under a readlock such that it can access the PSI + * + * @param runnable the runnable to be run + */ + public abstract void runReadAction(@NonNull Runnable runnable); + + /** + * A description of a type, such as a primitive int or the android.app.Activity class + * @deprecated Use {@link PsiType} instead + */ + @SuppressWarnings("unused") + @Deprecated + public abstract static class TypeDescriptor { + /** + * Returns the fully qualified name of the type, such as "int" or "android.app.Activity" + * */ + @NonNull public abstract String getName(); + + /** Returns the simple name of this class */ + @NonNull + public String getSimpleName() { + // This doesn't handle inner classes properly, so subclasses with more + // accurate type information will override to handle it correctly. + String name = getName(); + int index = name.lastIndexOf('.'); + if (index != -1) { + return name.substring(index + 1); + } + return name; + } + + /** + * Returns the full signature of the type, which is normally the same as {@link #getName()} + * but for arrays can include []'s, for generic methods can include generics parameters + * etc + */ + @NonNull public abstract String getSignature(); + + /** + * Computes the internal class name of the given fully qualified class name. + * For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar. + * This should only be called for class types, not primitives. + * + * @return the internal class name + */ + @NonNull public String getInternalName() { + return ClassContext.getInternalName(getName()); + } + + public abstract boolean matchesName(@NonNull String name); + + /** + * Returns true if the given TypeDescriptor represents an array + * @return true if this type represents an array + */ + public abstract boolean isArray(); + + /** + * Returns true if the given TypeDescriptor represents a primitive + * @return true if this type represents a primitive + */ + public abstract boolean isPrimitive(); + + public abstract boolean matchesSignature(@NonNull String signature); + + @NonNull + public TypeReference getNode() { + TypeReference typeReference = new TypeReference(); + StrictListAccessor parts = typeReference.astParts(); + for (String part : Splitter.on('.').split(getName())) { + Identifier identifier = Identifier.of(part); + parts.addToEnd(new TypeReferencePart().astIdentifier(identifier)); + } + + return typeReference; + } + + /** If the type is not primitive, returns the class of the type if known */ + @Nullable + public abstract ResolvedClass getTypeClass(); + + @Override + public abstract boolean equals(Object o); + + @Override + public String toString() { + return getName(); + } + } + + /** + * Convenience implementation of {@link TypeDescriptor} + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + public static class DefaultTypeDescriptor extends TypeDescriptor { + + private String mName; + + public DefaultTypeDescriptor(String name) { + mName = name; + } + + @NonNull + @Override + public String getName() { + return mName; + } + + @NonNull + @Override + public String getSignature() { + return getName(); + } + + @Override + public boolean matchesName(@NonNull String name) { + return mName.equals(name); + } + + @Override + public boolean isArray() { + return mName.endsWith("[]"); + } + + @Override + public boolean isPrimitive() { + return mName.indexOf('.') != -1; + } + + @Override + public boolean matchesSignature(@NonNull String signature) { + return matchesName(signature); + } + + @Override + public String toString() { + return getSignature(); + } + + @Override + @Nullable + public ResolvedClass getTypeClass() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DefaultTypeDescriptor that = (DefaultTypeDescriptor) o; + + return !(mName != null ? !mName.equals(that.mName) : that.mName != null); + + } + + @Override + public int hashCode() { + return mName != null ? mName.hashCode() : 0; + } + } + + /** + * A resolved declaration from an AST Node reference + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @SuppressWarnings("unused") + @Deprecated + public abstract static class ResolvedNode { + @NonNull + public abstract String getName(); + + /** Returns the signature of the resolved node */ + public abstract String getSignature(); + + public abstract int getModifiers(); + + @Override + public String toString() { + return getSignature(); + } + + /** Returns any annotations defined on this node */ + @NonNull + public abstract Iterable getAnnotations(); + + /** + * Searches for the annotation of the given type on this node + * + * @param type the fully qualified name of the annotation to check + * @return the annotation, or null if not found + */ + @Nullable + public ResolvedAnnotation getAnnotation(@NonNull String type) { + for (ResolvedAnnotation annotation : getAnnotations()) { + if (annotation.getType().matchesSignature(type)) { + return annotation; + } + } + + return null; + } + + /** + * Returns true if this element is in the given package (or optionally, in one of its sub + * packages) + * + * @param pkg the package name + * @param includeSubPackages whether to include subpackages + * @return true if the element is in the given package + */ + public boolean isInPackage(@NonNull String pkg, boolean includeSubPackages) { + return getSignature().startsWith(pkg); + } + + /** + * Attempts to find the corresponding AST node, if possible. This won't work if for example + * the resolved node is from a binary (such as a compiled class in a .jar) or if the + * underlying parser doesn't support it. + *

+ * Note that looking up the AST node can result in different instances for each lookup. + * + * @return an AST node, if possible. + */ + @Nullable + public Node findAstNode() { + return null; + } + } + + /** + * A resolved class declaration (class, interface, enumeration or annotation) + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @SuppressWarnings("unused") + @Deprecated + public abstract static class ResolvedClass extends ResolvedNode { + /** Returns the fully qualified name of this class */ + @Override + @NonNull + public abstract String getName(); + + /** Returns the simple name of this class */ + @NonNull + public abstract String getSimpleName(); + + /** Returns the package name of this class */ + @NonNull + public String getPackageName() { + String name = getName(); + String simpleName = getSimpleName(); + if (name.length() > simpleName.length() + 1) { + return name.substring(0, name.length() - simpleName.length() - 1); + } + return name; + } + + /** Returns whether this class' fully qualified name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @Nullable + public abstract ResolvedClass getSuperClass(); + + @NonNull + public abstract Iterable getInterfaces(); + + @Nullable + public abstract ResolvedClass getContainingClass(); + + public abstract boolean isInterface(); + public abstract boolean isEnum(); + + public TypeDescriptor getType() { + return new DefaultTypeDescriptor(getName()); + } + + /** + * Determines whether this class extends the given name. If strict is true, + * it will not consider C extends C true. + *

+ * The target must be a class; to check whether this class extends an interface, + * use {@link #isImplementing(String,boolean)} instead. If you're not sure, use + * {@link #isInheritingFrom(String, boolean)}. + * + * @param name the fully qualified class name + * @param strict if true, do not consider a class to be extending itself + * @return true if this class extends the given class + */ + public abstract boolean isSubclassOf(@NonNull String name, boolean strict); + + /** + * Determines whether this is implementing the given interface. + *

+ * The target must be an interface; to check whether this class extends a class, + * use {@link #isSubclassOf(String, boolean)} instead. If you're not sure, use + * {@link #isInheritingFrom(String, boolean)}. + * + * @param name the fully qualified interface name + * @param strict if true, do not consider a class to be extending itself + * @return true if this class implements the given interface + */ + public abstract boolean isImplementing(@NonNull String name, boolean strict); + + /** + * Determines whether this class extends or implements the class of the given name. + * If strict is true, it will not consider C extends C true. + *

+ * For performance reasons, if you know that the target is a class, consider using + * {@link #isSubclassOf(String, boolean)} instead, and if the target is an interface, + * consider using {@link #isImplementing(String,boolean)}. + * + * @param name the fully qualified class name + * @param strict if true, do not consider a class to be inheriting from itself + * @return true if this class extends or implements the given class + */ + public abstract boolean isInheritingFrom(@NonNull String name, boolean strict); + + @NonNull + public abstract Iterable getConstructors(); + + /** Returns the methods defined in this class, and optionally any methods inherited from any superclasses as well */ + @NonNull + public abstract Iterable getMethods(boolean includeInherited); + + /** Returns the methods of a given name defined in this class, and optionally any methods inherited from any superclasses as well */ + @NonNull + public abstract Iterable getMethods(@NonNull String name, boolean includeInherited); + + /** Returns the fields defined in this class, and optionally any fields declared in any superclasses as well */ + @NonNull + public abstract Iterable getFields(boolean includeInherited); + + /** Returns the named field defined in this class, or optionally inherited from a superclass */ + @Nullable + public abstract ResolvedField getField(@NonNull String name, boolean includeInherited); + + /** Returns the package containing this class */ + @Nullable + public abstract ResolvedPackage getPackage(); + + @Override + public boolean isInPackage(@NonNull String pkg, boolean includeSubPackages) { + String packageName = getPackageName(); + + //noinspection SimplifiableIfStatement + if (pkg.equals(packageName)) { + return true; + } + + return includeSubPackages && packageName.length() > pkg.length() && + packageName.charAt(pkg.length()) == '.' && + packageName.startsWith(pkg); + } + } + + /** + * A method or constructor declaration + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @SuppressWarnings("unused") + @Deprecated + public abstract static class ResolvedMethod extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this method name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract ResolvedClass getContainingClass(); + + public abstract int getArgumentCount(); + + @NonNull + public abstract TypeDescriptor getArgumentType(int index); + + /** Returns true if the parameter at the given index matches the given type signature */ + public boolean argumentMatchesType(int index, @NonNull String signature) { + return getArgumentType(index).matchesSignature(signature); + } + + @Nullable + public abstract TypeDescriptor getReturnType(); + + public boolean isConstructor() { + return getReturnType() == null; + } + + /** Returns any annotations defined on the given parameter of this method */ + @NonNull + public abstract Iterable getParameterAnnotations(int index); + + /** + * Searches for the annotation of the given type on the method + * + * @param type the fully qualified name of the annotation to check + * @param parameterIndex the index of the parameter to look up + * @return the annotation, or null if not found + */ + @Nullable + public ResolvedAnnotation getParameterAnnotation(@NonNull String type, + int parameterIndex) { + for (ResolvedAnnotation annotation : getParameterAnnotations(parameterIndex)) { + if (annotation.getType().matchesSignature(type)) { + return annotation; + } + } + + return null; + } + + /** Returns the super implementation of the given method, if any */ + @Nullable + public ResolvedMethod getSuperMethod() { + if ((getModifiers() & Modifier.PRIVATE) != 0) { + // Private methods aren't overriding anything + return null; + } + ResolvedClass cls = getContainingClass().getSuperClass(); + if (cls != null) { + String methodName = getName(); + int argCount = getArgumentCount(); + for (ResolvedMethod method : cls.getMethods(methodName, true)) { + if (argCount != method.getArgumentCount()) { + continue; + } + boolean sameTypes = true; + for (int arg = 0; arg < argCount; arg++) { + if (!method.getArgumentType(arg).equals(getArgumentType(arg))) { + sameTypes = false; + break; + } + } + if (sameTypes) { + if ((method.getModifiers() & Modifier.PRIVATE) != 0) { + // Normally can't override private methods - unless they're + // in the same compilation unit where the compiler will create + // an accessor method to trampoline over to it. + // + // Compare compilation units: + if (haveSameCompilationUnit(getContainingClass(), + method.getContainingClass())) { + return method; + } else { + // We can stop the search; this is invalid (you can't have a + // private method in the middle of a chain; the compiler would + // complain about weaker access) + return null; + } + } + return method; + } + } + } + + return null; + } + + @Override + public boolean isInPackage(@NonNull String pkg, boolean includeSubPackages) { + String packageName = getContainingClass().getPackageName(); + + //noinspection SimplifiableIfStatement + if (pkg.equals(packageName)) { + return true; + } + + return includeSubPackages && packageName.length() > pkg.length() && + packageName.charAt(pkg.length()) == '.' && + packageName.startsWith(pkg); + } + } + + /** + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + private static boolean haveSameCompilationUnit(@Nullable ResolvedClass cls1, + @Nullable ResolvedClass cls2) { + if (cls1 == null || cls2 == null) { + return false; + } + //noinspection ConstantConditions + while (cls1.getContainingClass() != null) { + cls1 = cls1.getContainingClass(); + } + //noinspection ConstantConditions + while (cls2.getContainingClass() != null) { + cls2 = cls2.getContainingClass(); + } + return cls1.equals(cls2); + } + + /** + * A field declaration + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + public abstract static class ResolvedField extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this field name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract TypeDescriptor getType(); + + @Nullable + public abstract ResolvedClass getContainingClass(); + + @Nullable + public abstract Object getValue(); + + @Nullable + public String getContainingClassName() { + ResolvedClass containingClass = getContainingClass(); + return containingClass != null ? containingClass.getName() : null; + } + + @Override + public boolean isInPackage(@NonNull String pkg, boolean includeSubPackages) { + ResolvedClass containingClass = getContainingClass(); + if (containingClass == null) { + return false; + } + + String packageName = containingClass.getPackageName(); + + //noinspection SimplifiableIfStatement + if (pkg.equals(packageName)) { + return true; + } + + return includeSubPackages && packageName.length() > pkg.length() && + packageName.charAt(pkg.length()) == '.' && + packageName.startsWith(pkg); + } + } + + /** + * An annotation reference. Note that this refers to a usage of an annotation, + * not a declaraton of an annotation. You can call {@link #getClassType()} to + * find the declaration for the annotation. + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + public abstract static class ResolvedAnnotation extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this field name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract TypeDescriptor getType(); + + /** Returns the {@link ResolvedClass} which defines the annotation */ + @Nullable + public abstract ResolvedClass getClassType(); + + public static class Value { + @NonNull public final String name; + @Nullable public final Object value; + + public Value(@NonNull String name, @Nullable Object value) { + this.name = name; + this.value = value; + } + } + + @NonNull + public abstract List getValues(); + + @Nullable + public Object getValue(@NonNull String name) { + for (Value value : getValues()) { + if (name.equals(value.name)) { + return value.value; + } + } + return null; + } + + @Nullable + public Object getValue() { + return getValue(ATTR_VALUE); + } + + @NonNull + @Override + public Iterable getAnnotations() { + return Collections.emptyList(); + } + } + + /** + * A package declaration + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @SuppressWarnings("unused") + @Deprecated + public abstract static class ResolvedPackage extends ResolvedNode { + /** Returns the parent package of this package, if any. */ + @Nullable + public abstract ResolvedPackage getParentPackage(); + + @NonNull + @Override + public Iterable getAnnotations() { + return Collections.emptyList(); + } + } + + /** + * A local variable or parameter declaration + * @deprecated Use {@link JavaPsiScanner} APIs instead + */ + @Deprecated + public abstract static class ResolvedVariable extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this variable name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract TypeDescriptor getType(); + } } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaPsiVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaPsiVisitor.java new file mode 100644 index 00000000000..772ca31508b --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaPsiVisitor.java @@ -0,0 +1,1716 @@ +/* + * 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 static com.android.SdkConstants.R_CLASS; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.resources.ResourceType; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; +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; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.psi.ImplicitVariable; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMethod; +import com.intellij.psi.PsiAnnotationParameterList; +import com.intellij.psi.PsiAnonymousClass; +import com.intellij.psi.PsiArrayAccessExpression; +import com.intellij.psi.PsiArrayInitializerExpression; +import com.intellij.psi.PsiArrayInitializerMemberValue; +import com.intellij.psi.PsiAssertStatement; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiBinaryExpression; +import com.intellij.psi.PsiBlockStatement; +import com.intellij.psi.PsiBreakStatement; +import com.intellij.psi.PsiCallExpression; +import com.intellij.psi.PsiCatchSection; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassInitializer; +import com.intellij.psi.PsiClassObjectAccessExpression; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiConditionalExpression; +import com.intellij.psi.PsiContinueStatement; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiDoWhileStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiEmptyStatement; +import com.intellij.psi.PsiEnumConstant; +import com.intellij.psi.PsiEnumConstantInitializer; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiExpressionListStatement; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiForStatement; +import com.intellij.psi.PsiForeachStatement; +import com.intellij.psi.PsiIdentifier; +import com.intellij.psi.PsiIfStatement; +import com.intellij.psi.PsiImportList; +import com.intellij.psi.PsiImportStatement; +import com.intellij.psi.PsiImportStaticReferenceElement; +import com.intellij.psi.PsiImportStaticStatement; +import com.intellij.psi.PsiInstanceOfExpression; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiJavaToken; +import com.intellij.psi.PsiKeyword; +import com.intellij.psi.PsiLabeledStatement; +import com.intellij.psi.PsiLambdaExpression; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiMethodReferenceExpression; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiPackage; +import com.intellij.psi.PsiPackageStatement; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiParenthesizedExpression; +import com.intellij.psi.PsiPolyadicExpression; +import com.intellij.psi.PsiPostfixExpression; +import com.intellij.psi.PsiPrefixExpression; +import com.intellij.psi.PsiReceiverParameter; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiReferenceList; +import com.intellij.psi.PsiReferenceParameterList; +import com.intellij.psi.PsiResourceList; +import com.intellij.psi.PsiResourceVariable; +import com.intellij.psi.PsiReturnStatement; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiSuperExpression; +import com.intellij.psi.PsiSwitchLabelStatement; +import com.intellij.psi.PsiSwitchStatement; +import com.intellij.psi.PsiSynchronizedStatement; +import com.intellij.psi.PsiThisExpression; +import com.intellij.psi.PsiThrowStatement; +import com.intellij.psi.PsiTryStatement; +import com.intellij.psi.PsiTypeCastExpression; +import com.intellij.psi.PsiTypeElement; +import com.intellij.psi.PsiTypeParameter; +import com.intellij.psi.PsiTypeParameterList; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.PsiWhileStatement; +import com.intellij.psi.javadoc.PsiDocComment; +import com.intellij.psi.javadoc.PsiDocTag; +import com.intellij.psi.javadoc.PsiDocTagValue; +import com.intellij.psi.javadoc.PsiDocToken; +import com.intellij.psi.javadoc.PsiInlineDocTag; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Specialized visitor for running detectors on a Java AST. + * It operates in three phases: + *

    + *
  1. First, it computes a set of maps where it generates a map from each + * significant AST attribute (such as method call names) to a list + * of detectors to consult whenever that attribute is encountered. + * Examples of "attributes" are method names, Android resource identifiers, + * and general AST node types such as "cast" nodes etc. These are + * defined on the {@link JavaPsiScanner} interface. + *
  2. Second, it iterates over the document a single time, delegating to + * the detectors found at each relevant AST attribute. + *
  3. Finally, it calls the remaining visitors (those that need to process a + * whole document on their own). + *
+ * It also notifies all the detectors before and after the document is processed + * such that they can do pre- and post-processing. + */ +public class JavaPsiVisitor { + /** Default size of lists holding detectors of the same type for a given node type */ + private static final int SAME_TYPE_COUNT = 8; + + private final Map> mMethodDetectors = + Maps.newHashMapWithExpectedSize(80); + private final Map> mConstructorDetectors = + Maps.newHashMapWithExpectedSize(12); + private final Map> mReferenceDetectors = + Maps.newHashMapWithExpectedSize(10); + private Set mConstructorSimpleNames; + private final List mResourceFieldDetectors = + new ArrayList(); + private final List mAllDetectors; + private final List mFullTreeDetectors; + private final Map, List> mNodePsiTypeDetectors = + new HashMap, List>(16); + private final JavaParser mParser; + private final Map> mSuperClassDetectors = + new HashMap>(); + + /** + * Number of fatal exceptions (internal errors, usually from ECJ) we've + * encountered; we don't log each and every one to avoid massive log spam + * in code which triggers this condition + */ + private static int sExceptionCount; + /** Max number of logs to include */ + private static final int MAX_REPORTED_CRASHES = 20; + + JavaPsiVisitor(@NonNull JavaParser parser, @NonNull List detectors) { + mParser = parser; + mAllDetectors = new ArrayList(detectors.size()); + mFullTreeDetectors = new ArrayList(detectors.size()); + + for (Detector detector : detectors) { + JavaPsiScanner javaPsiScanner = (JavaPsiScanner) detector; + VisitingDetector v = new VisitingDetector(detector, javaPsiScanner); + mAllDetectors.add(v); + + List applicableSuperClasses = detector.applicableSuperClasses(); + if (applicableSuperClasses != null) { + for (String fqn : applicableSuperClasses) { + List list = mSuperClassDetectors.get(fqn); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mSuperClassDetectors.put(fqn, list); + } + list.add(v); + } + continue; + } + + List> nodePsiTypes = detector.getApplicablePsiTypes(); + if (nodePsiTypes != null) { + for (Class type : nodePsiTypes) { + List list = mNodePsiTypeDetectors.get(type); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mNodePsiTypeDetectors.put(type, list); + } + list.add(v); + } + } + + List names = detector.getApplicableMethodNames(); + if (names != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert names != XmlScanner.ALL; + + for (String name : names) { + List list = mMethodDetectors.get(name); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mMethodDetectors.put(name, list); + } + list.add(v); + } + } + + List types = detector.getApplicableConstructorTypes(); + if (types != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert types != XmlScanner.ALL; + if (mConstructorSimpleNames == null) { + mConstructorSimpleNames = Sets.newHashSet(); + } + for (String type : types) { + List list = mConstructorDetectors.get(type); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mConstructorDetectors.put(type, list); + mConstructorSimpleNames.add(type.substring(type.lastIndexOf('.')+1)); + } + list.add(v); + } + } + + List referenceNames = detector.getApplicableReferenceNames(); + if (referenceNames != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert referenceNames != XmlScanner.ALL; + + for (String name : referenceNames) { + List list = mReferenceDetectors.get(name); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mReferenceDetectors.put(name, list); + } + list.add(v); + } + } + + if (detector.appliesToResourceRefs()) { + mResourceFieldDetectors.add(v); + } else if ((referenceNames == null || referenceNames.isEmpty()) + && (nodePsiTypes == null || nodePsiTypes.isEmpty()) + && (types == null || types.isEmpty())) { + mFullTreeDetectors.add(v); + } + } + } + + void visitFile(@NonNull final JavaContext context) { + try { + final PsiJavaFile javaFile = mParser.parseJavaToPsi(context); + if (javaFile == null) { + // No need to log this; the parser should be reporting + // a full warning (such as IssueRegistry#PARSER_ERROR) + // with details, location, etc. + return; + } + try { + context.setJavaFile(javaFile); + + mParser.runReadAction(new Runnable() { + @Override + public void run() { + for (VisitingDetector v : mAllDetectors) { + v.setContext(context); + v.getDetector().beforeCheckFile(context); + } + } + }); + + if (!mSuperClassDetectors.isEmpty()) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + SuperclassPsiVisitor visitor = new SuperclassPsiVisitor(context); + javaFile.accept(visitor); + } + }); + } + + for (final VisitingDetector v : mFullTreeDetectors) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + JavaElementVisitor visitor = v.getVisitor(); + javaFile.accept(visitor); + } + }); + } + + if (!mMethodDetectors.isEmpty() + || !mResourceFieldDetectors.isEmpty() + || !mConstructorDetectors.isEmpty() + || !mReferenceDetectors.isEmpty()) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + // TODO: Do we need to break this one up into finer grain + // locking units + JavaElementVisitor visitor = new DelegatingPsiVisitor(context); + javaFile.accept(visitor); + } + }); + } else { + if (!mNodePsiTypeDetectors.isEmpty()) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + // TODO: Do we need to break this one up into finer grain + // locking units + JavaElementVisitor visitor = new DispatchPsiVisitor(); + javaFile.accept(visitor); + } + }); + } + } + + mParser.runReadAction(new Runnable() { + @Override + public void run() { + for (VisitingDetector v : mAllDetectors) { + v.getDetector().afterCheckFile(context); + } + } + }); + } finally { + mParser.dispose(context, javaFile); + context.setJavaFile(null); + } + } catch (ProcessCanceledException ignore) { + // Cancelling inspections in the IDE + } catch (RuntimeException e) { + if (sExceptionCount++ > MAX_REPORTED_CRASHES) { + // No need to keep spamming the user that a lot of the files + // are tripping up ECJ, they get the picture. + return; + } + + 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; + } + + // Work around ECJ bugs; see https://code.google.com/p/android/issues/detail?id=172268 + // Don't allow lint bugs to take down the whole build. TRY to log this as a + // lint error instead! + StringBuilder sb = new StringBuilder(100); + sb.append("Unexpected failure during lint analysis of "); + sb.append(context.file.getName()); + sb.append(" (this is a bug in lint or one of the libraries it depends on)\n"); + + sb.append(e.getClass().getSimpleName()); + sb.append(':'); + StackTraceElement[] stackTrace = e.getStackTrace(); + int count = 0; + for (StackTraceElement frame : stackTrace) { + if (count > 0) { + sb.append("<-"); + } + + String className = frame.getClassName(); + sb.append(className.substring(className.lastIndexOf('.') + 1)); + sb.append('.').append(frame.getMethodName()); + sb.append('('); + sb.append(frame.getFileName()).append(':').append(frame.getLineNumber()); + sb.append(')'); + count++; + // Only print the top 3-4 frames such that we can identify the bug + if (count == 4) { + break; + } + } + Throwable throwable = null; // NOT e: this makes for very noisy logs + //noinspection ConstantConditions + context.log(throwable, sb.toString()); + } + } + + /** + * 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 contexts) { + mParser.prepareJavaParse(contexts); + } + + public void dispose() { + mParser.dispose(); + } + + @Nullable + private static Set getInterfaceNames( + @Nullable Set addTo, + @NonNull PsiClass cls) { + for (PsiClass resolvedInterface : cls.getInterfaces()) { + String name = resolvedInterface.getQualifiedName(); + 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 JavaElementVisitor mVisitor; + private JavaContext mContext; + public final Detector mDetector; + public final JavaPsiScanner mJavaScanner; + + public VisitingDetector(@NonNull Detector detector, @NonNull JavaPsiScanner javaScanner) { + mDetector = detector; + mJavaScanner = javaScanner; + } + + @NonNull + public Detector getDetector() { + return mDetector; + } + + @Nullable + public JavaPsiScanner getJavaScanner() { + return mJavaScanner; + } + + public void setContext(@NonNull JavaContext context) { + mContext = context; + + // The visitors are one-per-context, so clear them out here and construct + // lazily only if needed + mVisitor = null; + } + + @NonNull + JavaElementVisitor getVisitor() { + if (mVisitor == null) { + mVisitor = mDetector.createPsiVisitor(mContext); + assert !(mVisitor instanceof JavaRecursiveElementVisitor) : + "Your visitor (returned by " + mDetector.getClass().getSimpleName() + + "#createPsiVisitor(...) should *not* extend " + + " JavaRecursiveElementVisitor; use a plain " + + "JavaElementVisitor instead. The lint infrastructure does its own " + + "recursion calling *just* your visit methods specified in " + + "getApplicablePsiTypes"; + if (mVisitor == null) { + mVisitor = new JavaElementVisitor() { + @Override + public void visitElement(PsiElement element) { + // No-op. Workaround for super currently calling + // ProgressIndicatorProvider.checkCanceled(); + } + }; + } + } + return mVisitor; + } + } + + private class SuperclassPsiVisitor extends JavaRecursiveElementVisitor { + private JavaContext mContext; + + public SuperclassPsiVisitor(@NonNull JavaContext context) { + mContext = context; + } + + @Override + public void visitClass(@NonNull PsiClass node) { + super.visitClass(node); + checkClass(node); + } + + private void checkClass(@NonNull PsiClass node) { + if (node instanceof PsiTypeParameter) { + // Not included: explained in javadoc for JavaPsiScanner#checkClass + return; + } + + PsiClass cls = node; + int depth = 0; + while (cls != null) { + List list = mSuperClassDetectors.get(cls.getQualifiedName()); + if (list != null) { + for (VisitingDetector v : list) { + JavaPsiScanner javaPsiScanner = v.getJavaScanner(); + if (javaPsiScanner != null) { + javaPsiScanner.checkClass(mContext, node); + } + } + } + + // Check interfaces too + Set interfaceNames = getInterfaceNames(null, cls); + if (interfaceNames != null) { + for (String name : interfaceNames) { + list = mSuperClassDetectors.get(name); + if (list != null) { + for (VisitingDetector v : list) { + JavaPsiScanner javaPsiScanner = v.getJavaScanner(); + if (javaPsiScanner != null) { + javaPsiScanner.checkClass(mContext, node); + } + } + } + } + } + + 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; + } + } + } + } + + private class DispatchPsiVisitor extends JavaRecursiveElementVisitor { + + @Override + public void visitAnonymousClass(PsiAnonymousClass node) { + List list = mNodePsiTypeDetectors.get(PsiAnonymousClass.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnonymousClass(node); + } + } + super.visitAnonymousClass(node); + } + + @Override + public void visitArrayAccessExpression(PsiArrayAccessExpression node) { + List list = mNodePsiTypeDetectors.get(PsiArrayAccessExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitArrayAccessExpression(node); + } + } + super.visitArrayAccessExpression(node); + } + + @Override + public void visitArrayInitializerExpression(PsiArrayInitializerExpression node) { + List list = mNodePsiTypeDetectors + .get(PsiArrayInitializerExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitArrayInitializerExpression(node); + } + } + super.visitArrayInitializerExpression(node); + } + + @Override + public void visitAssertStatement(PsiAssertStatement node) { + List list = mNodePsiTypeDetectors.get(PsiAssertStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAssertStatement(node); + } + } + super.visitAssertStatement(node); + } + + @Override + public void visitAssignmentExpression(PsiAssignmentExpression node) { + List list = mNodePsiTypeDetectors.get(PsiAssignmentExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAssignmentExpression(node); + } + } + super.visitAssignmentExpression(node); + } + + @Override + public void visitBinaryExpression(PsiBinaryExpression node) { + List list = mNodePsiTypeDetectors.get(PsiBinaryExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBinaryExpression(node); + } + } + super.visitBinaryExpression(node); + } + + @Override + public void visitBlockStatement(PsiBlockStatement node) { + List list = mNodePsiTypeDetectors.get(PsiBlockStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBlockStatement(node); + } + } + super.visitBlockStatement(node); + } + + @Override + public void visitBreakStatement(PsiBreakStatement node) { + List list = mNodePsiTypeDetectors.get(PsiBreakStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBreakStatement(node); + } + } + super.visitBreakStatement(node); + } + + @Override + public void visitClass(PsiClass node) { + List list = mNodePsiTypeDetectors.get(PsiClass.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitClass(node); + } + } + super.visitClass(node); + } + + @Override + public void visitClassInitializer(PsiClassInitializer node) { + List list = mNodePsiTypeDetectors.get(PsiClassInitializer.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitClassInitializer(node); + } + } + super.visitClassInitializer(node); + } + + @Override + public void visitClassObjectAccessExpression(PsiClassObjectAccessExpression node) { + List list = mNodePsiTypeDetectors + .get(PsiClassObjectAccessExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitClassObjectAccessExpression(node); + } + } + super.visitClassObjectAccessExpression(node); + } + + @Override + public void visitCodeBlock(PsiCodeBlock node) { + List list = mNodePsiTypeDetectors.get(PsiCodeBlock.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCodeBlock(node); + } + } + super.visitCodeBlock(node); + } + + @Override + public void visitConditionalExpression(PsiConditionalExpression node) { + List list = mNodePsiTypeDetectors.get(PsiConditionalExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitConditionalExpression(node); + } + } + super.visitConditionalExpression(node); + } + + @Override + public void visitContinueStatement(PsiContinueStatement node) { + List list = mNodePsiTypeDetectors.get(PsiContinueStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitContinueStatement(node); + } + } + super.visitContinueStatement(node); + } + + @Override + public void visitDeclarationStatement(PsiDeclarationStatement node) { + List list = mNodePsiTypeDetectors.get(PsiDeclarationStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDeclarationStatement(node); + } + } + super.visitDeclarationStatement(node); + } + + @Override + public void visitDocComment(PsiDocComment node) { + List list = mNodePsiTypeDetectors.get(PsiDocComment.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDocComment(node); + } + } + super.visitDocComment(node); + } + + @Override + public void visitDocTag(PsiDocTag node) { + List list = mNodePsiTypeDetectors.get(PsiDocTag.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDocTag(node); + } + } + super.visitDocTag(node); + } + + @Override + public void visitDocTagValue(PsiDocTagValue node) { + List list = mNodePsiTypeDetectors.get(PsiDocTagValue.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDocTagValue(node); + } + } + super.visitDocTagValue(node); + } + + @Override + public void visitDoWhileStatement(PsiDoWhileStatement node) { + List list = mNodePsiTypeDetectors.get(PsiDoWhileStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDoWhileStatement(node); + } + } + super.visitDoWhileStatement(node); + } + + @Override + public void visitEmptyStatement(PsiEmptyStatement node) { + List list = mNodePsiTypeDetectors.get(PsiEmptyStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEmptyStatement(node); + } + } + super.visitEmptyStatement(node); + } + + @Override + public void visitExpression(PsiExpression node) { + List list = mNodePsiTypeDetectors.get(PsiExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitExpression(node); + } + } + super.visitExpression(node); + } + + @Override + public void visitExpressionList(PsiExpressionList node) { + List list = mNodePsiTypeDetectors.get(PsiExpressionList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitExpressionList(node); + } + } + super.visitExpressionList(node); + } + + @Override + public void visitExpressionListStatement(PsiExpressionListStatement node) { + List list = mNodePsiTypeDetectors + .get(PsiExpressionListStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitExpressionListStatement(node); + } + } + super.visitExpressionListStatement(node); + } + + @Override + public void visitExpressionStatement(PsiExpressionStatement node) { + List list = mNodePsiTypeDetectors.get(PsiExpressionStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitExpressionStatement(node); + } + } + super.visitExpressionStatement(node); + } + + @Override + public void visitField(PsiField node) { + List list = mNodePsiTypeDetectors.get(PsiField.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitField(node); + } + } + super.visitField(node); + } + + @Override + public void visitForStatement(PsiForStatement node) { + List list = mNodePsiTypeDetectors.get(PsiForStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitForStatement(node); + } + } + super.visitForStatement(node); + } + + @Override + public void visitForeachStatement(PsiForeachStatement node) { + List list = mNodePsiTypeDetectors.get(PsiForeachStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitForeachStatement(node); + } + } + super.visitForeachStatement(node); + } + + @Override + public void visitIdentifier(PsiIdentifier node) { + List list = mNodePsiTypeDetectors.get(PsiIdentifier.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitIdentifier(node); + } + } + super.visitIdentifier(node); + } + + @Override + public void visitIfStatement(PsiIfStatement node) { + List list = mNodePsiTypeDetectors.get(PsiIfStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitIfStatement(node); + } + } + super.visitIfStatement(node); + } + + @Override + public void visitImportList(PsiImportList node) { + List list = mNodePsiTypeDetectors.get(PsiImportList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitImportList(node); + } + } + super.visitImportList(node); + } + + @Override + public void visitImportStatement(PsiImportStatement node) { + List list = mNodePsiTypeDetectors.get(PsiImportStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitImportStatement(node); + } + } + super.visitImportStatement(node); + } + + @Override + public void visitImportStaticStatement(PsiImportStaticStatement node) { + List list = mNodePsiTypeDetectors.get(PsiImportStaticStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitImportStaticStatement(node); + } + } + super.visitImportStaticStatement(node); + } + + @Override + public void visitInlineDocTag(PsiInlineDocTag node) { + List list = mNodePsiTypeDetectors.get(PsiInlineDocTag.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitInlineDocTag(node); + } + } + super.visitInlineDocTag(node); + } + + @Override + public void visitInstanceOfExpression(PsiInstanceOfExpression node) { + List list = mNodePsiTypeDetectors.get(PsiInstanceOfExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitInstanceOfExpression(node); + } + } + super.visitInstanceOfExpression(node); + } + + @Override + public void visitJavaToken(PsiJavaToken node) { + List list = mNodePsiTypeDetectors.get(PsiJavaToken.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitJavaToken(node); + } + } + super.visitJavaToken(node); + } + + @Override + public void visitKeyword(PsiKeyword node) { + List list = mNodePsiTypeDetectors.get(PsiKeyword.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitKeyword(node); + } + } + super.visitKeyword(node); + } + + @Override + public void visitLabeledStatement(PsiLabeledStatement node) { + List list = mNodePsiTypeDetectors.get(PsiLabeledStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLabeledStatement(node); + } + } + super.visitLabeledStatement(node); + } + + @Override + public void visitLiteralExpression(PsiLiteralExpression node) { + List list = mNodePsiTypeDetectors.get(PsiLiteralExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLiteralExpression(node); + } + } + super.visitLiteralExpression(node); + } + + @Override + public void visitLocalVariable(PsiLocalVariable node) { + List list = mNodePsiTypeDetectors.get(PsiLocalVariable.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLocalVariable(node); + } + } + super.visitLocalVariable(node); + } + + @Override + public void visitMethod(PsiMethod node) { + List list = mNodePsiTypeDetectors.get(PsiMethod.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitMethod(node); + } + } + super.visitMethod(node); + } + + @Override + public void visitMethodCallExpression(PsiMethodCallExpression node) { + List list = mNodePsiTypeDetectors.get(PsiMethodCallExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitMethodCallExpression(node); + } + } + super.visitMethodCallExpression(node); + } + + @Override + public void visitCallExpression(PsiCallExpression node) { + List list = mNodePsiTypeDetectors.get(PsiCallExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCallExpression(node); + } + } + super.visitCallExpression(node); + } + + @Override + public void visitModifierList(PsiModifierList node) { + List list = mNodePsiTypeDetectors.get(PsiModifierList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitModifierList(node); + } + } + super.visitModifierList(node); + } + + @Override + public void visitNewExpression(PsiNewExpression node) { + List list = mNodePsiTypeDetectors.get(PsiNewExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitNewExpression(node); + } + } + super.visitNewExpression(node); + } + + @Override + public void visitPackage(PsiPackage node) { + List list = mNodePsiTypeDetectors.get(PsiPackage.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPackage(node); + } + } + super.visitPackage(node); + } + + @Override + public void visitPackageStatement(PsiPackageStatement node) { + List list = mNodePsiTypeDetectors.get(PsiPackageStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPackageStatement(node); + } + } + super.visitPackageStatement(node); + } + + @Override + public void visitParameter(PsiParameter node) { + List list = mNodePsiTypeDetectors.get(PsiParameter.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitParameter(node); + } + } + super.visitParameter(node); + } + + @Override + public void visitReceiverParameter(PsiReceiverParameter node) { + List list = mNodePsiTypeDetectors.get(PsiReceiverParameter.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReceiverParameter(node); + } + } + super.visitReceiverParameter(node); + } + + @Override + public void visitParameterList(PsiParameterList node) { + List list = mNodePsiTypeDetectors.get(PsiParameterList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitParameterList(node); + } + } + super.visitParameterList(node); + } + + @Override + public void visitParenthesizedExpression(PsiParenthesizedExpression node) { + List list = mNodePsiTypeDetectors + .get(PsiParenthesizedExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitParenthesizedExpression(node); + } + } + super.visitParenthesizedExpression(node); + } + + @Override + public void visitPostfixExpression(PsiPostfixExpression node) { + List list = mNodePsiTypeDetectors.get(PsiPostfixExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPostfixExpression(node); + } + } + super.visitPostfixExpression(node); + } + + @Override + public void visitPrefixExpression(PsiPrefixExpression node) { + List list = mNodePsiTypeDetectors.get(PsiPrefixExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPrefixExpression(node); + } + } + super.visitPrefixExpression(node); + } + + @Override + public void visitReferenceElement(PsiJavaCodeReferenceElement node) { + List list = mNodePsiTypeDetectors + .get(PsiJavaCodeReferenceElement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReferenceElement(node); + } + } + super.visitReferenceElement(node); + } + + @Override + public void visitImportStaticReferenceElement(PsiImportStaticReferenceElement node) { + List list = mNodePsiTypeDetectors + .get(PsiImportStaticReferenceElement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitImportStaticReferenceElement(node); + } + } + super.visitImportStaticReferenceElement(node); + } + + @Override + public void visitReferenceExpression(PsiReferenceExpression node) { + List list = mNodePsiTypeDetectors.get(PsiReferenceExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReferenceExpression(node); + } + } + super.visitReferenceExpression(node); + } + + @Override + public void visitMethodReferenceExpression(PsiMethodReferenceExpression node) { + List list = mNodePsiTypeDetectors + .get(PsiMethodReferenceExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitMethodReferenceExpression(node); + } + } + super.visitMethodReferenceExpression(node); + } + + @Override + public void visitReferenceList(PsiReferenceList node) { + List list = mNodePsiTypeDetectors.get(PsiReferenceList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReferenceList(node); + } + } + super.visitReferenceList(node); + } + + @Override + public void visitReferenceParameterList(PsiReferenceParameterList node) { + List list = mNodePsiTypeDetectors + .get(PsiReferenceParameterList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReferenceParameterList(node); + } + } + super.visitReferenceParameterList(node); + } + + @Override + public void visitTypeParameterList(PsiTypeParameterList node) { + List list = mNodePsiTypeDetectors.get(PsiTypeParameterList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeParameterList(node); + } + } + super.visitTypeParameterList(node); + } + + @Override + public void visitReturnStatement(PsiReturnStatement node) { + List list = mNodePsiTypeDetectors.get(PsiReturnStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReturnStatement(node); + } + } + super.visitReturnStatement(node); + } + + @Override + public void visitStatement(PsiStatement node) { + List list = mNodePsiTypeDetectors.get(PsiStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitStatement(node); + } + } + super.visitStatement(node); + } + + @Override + public void visitSuperExpression(PsiSuperExpression node) { + List list = mNodePsiTypeDetectors.get(PsiSuperExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSuperExpression(node); + } + } + super.visitSuperExpression(node); + } + + @Override + public void visitSwitchLabelStatement(PsiSwitchLabelStatement node) { + List list = mNodePsiTypeDetectors.get(PsiSwitchLabelStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSwitchLabelStatement(node); + } + } + super.visitSwitchLabelStatement(node); + } + + @Override + public void visitSwitchStatement(PsiSwitchStatement node) { + List list = mNodePsiTypeDetectors.get(PsiSwitchStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSwitchStatement(node); + } + } + super.visitSwitchStatement(node); + } + + @Override + public void visitSynchronizedStatement(PsiSynchronizedStatement node) { + List list = mNodePsiTypeDetectors.get(PsiSynchronizedStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSynchronizedStatement(node); + } + } + super.visitSynchronizedStatement(node); + } + + @Override + public void visitThisExpression(PsiThisExpression node) { + List list = mNodePsiTypeDetectors.get(PsiThisExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitThisExpression(node); + } + } + super.visitThisExpression(node); + } + + @Override + public void visitThrowStatement(PsiThrowStatement node) { + List list = mNodePsiTypeDetectors.get(PsiThrowStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitThrowStatement(node); + } + } + super.visitThrowStatement(node); + } + + @Override + public void visitTryStatement(PsiTryStatement node) { + List list = mNodePsiTypeDetectors.get(PsiTryStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTryStatement(node); + } + } + super.visitTryStatement(node); + } + + @Override + public void visitCatchSection(PsiCatchSection node) { + List list = mNodePsiTypeDetectors.get(PsiCatchSection.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCatchSection(node); + } + } + super.visitCatchSection(node); + } + + @Override + public void visitResourceList(PsiResourceList node) { + List list = mNodePsiTypeDetectors.get(PsiResourceList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitResourceList(node); + } + } + super.visitResourceList(node); + } + + @Override + public void visitResourceVariable(PsiResourceVariable node) { + List list = mNodePsiTypeDetectors.get(PsiResourceVariable.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitResourceVariable(node); + } + } + super.visitResourceVariable(node); + } + + @Override + public void visitTypeElement(PsiTypeElement node) { + List list = mNodePsiTypeDetectors.get(PsiTypeElement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeElement(node); + } + } + super.visitTypeElement(node); + } + + @Override + public void visitTypeCastExpression(PsiTypeCastExpression node) { + List list = mNodePsiTypeDetectors.get(PsiTypeCastExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeCastExpression(node); + } + } + super.visitTypeCastExpression(node); + } + + @Override + public void visitVariable(PsiVariable node) { + List list = mNodePsiTypeDetectors.get(PsiVariable.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitVariable(node); + } + } + super.visitVariable(node); + } + + @Override + public void visitWhileStatement(PsiWhileStatement node) { + List list = mNodePsiTypeDetectors.get(PsiWhileStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitWhileStatement(node); + } + } + super.visitWhileStatement(node); + } + + @Override + public void visitJavaFile(PsiJavaFile node) { + List list = mNodePsiTypeDetectors.get(PsiJavaFile.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitJavaFile(node); + } + } + super.visitJavaFile(node); + } + + @Override + public void visitImplicitVariable(ImplicitVariable node) { + List list = mNodePsiTypeDetectors.get(ImplicitVariable.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitImplicitVariable(node); + } + } + super.visitImplicitVariable(node); + } + + @Override + public void visitDocToken(PsiDocToken node) { + List list = mNodePsiTypeDetectors.get(PsiDocToken.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDocToken(node); + } + } + super.visitDocToken(node); + } + + @Override + public void visitTypeParameter(PsiTypeParameter node) { + List list = mNodePsiTypeDetectors.get(PsiTypeParameter.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeParameter(node); + } + } + super.visitTypeParameter(node); + } + + @Override + public void visitAnnotation(PsiAnnotation node) { + List list = mNodePsiTypeDetectors.get(PsiAnnotation.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotation(node); + } + } + super.visitAnnotation(node); + } + + @Override + public void visitAnnotationParameterList(PsiAnnotationParameterList node) { + List list = mNodePsiTypeDetectors + .get(PsiAnnotationParameterList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotationParameterList(node); + } + } + super.visitAnnotationParameterList(node); + } + + @Override + public void visitAnnotationArrayInitializer(PsiArrayInitializerMemberValue node) { + List list = mNodePsiTypeDetectors + .get(PsiArrayInitializerMemberValue.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotationArrayInitializer(node); + } + } + super.visitAnnotationArrayInitializer(node); + } + + @Override + public void visitNameValuePair(PsiNameValuePair node) { + List list = mNodePsiTypeDetectors.get(PsiNameValuePair.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitNameValuePair(node); + } + } + super.visitNameValuePair(node); + } + + @Override + public void visitAnnotationMethod(PsiAnnotationMethod node) { + List list = mNodePsiTypeDetectors.get(PsiAnnotationMethod.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotationMethod(node); + } + } + super.visitAnnotationMethod(node); + } + + @Override + public void visitEnumConstant(PsiEnumConstant node) { + List list = mNodePsiTypeDetectors.get(PsiEnumConstant.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEnumConstant(node); + } + } + super.visitEnumConstant(node); + } + + @Override + public void visitEnumConstantInitializer(PsiEnumConstantInitializer node) { + List list = mNodePsiTypeDetectors + .get(PsiEnumConstantInitializer.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEnumConstantInitializer(node); + } + } + super.visitEnumConstantInitializer(node); + } + + @Override + public void visitPolyadicExpression(PsiPolyadicExpression node) { + List list = mNodePsiTypeDetectors.get(PsiPolyadicExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPolyadicExpression(node); + } + } + super.visitPolyadicExpression(node); + } + + @Override + public void visitLambdaExpression(PsiLambdaExpression node) { + List list = mNodePsiTypeDetectors.get(PsiLambdaExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLambdaExpression(node); + } + } + super.visitLambdaExpression(node); + } + } + + /** Performs common AST searches for method calls and R-type-field references. + * Note that this is a specialized form of the {@link DispatchPsiVisitor}. */ + private class DelegatingPsiVisitor extends DispatchPsiVisitor { + private final JavaContext mContext; + private final boolean mVisitResources; + private final boolean mVisitMethods; + private final boolean mVisitConstructors; + private final boolean mVisitReferences; + + public DelegatingPsiVisitor(JavaContext context) { + mContext = context; + + mVisitMethods = !mMethodDetectors.isEmpty(); + mVisitConstructors = !mConstructorDetectors.isEmpty(); + mVisitResources = !mResourceFieldDetectors.isEmpty(); + mVisitReferences = !mReferenceDetectors.isEmpty(); + } + + @Override + public void visitReferenceElement(PsiJavaCodeReferenceElement element) { + if (mVisitReferences) { + String name = element.getReferenceName(); + if (name != null) { + List list = mReferenceDetectors.get(name); + if (list != null) { + PsiElement referenced = element.resolve(); + if (referenced != null) { + for (VisitingDetector v : list) { + JavaPsiScanner javaPsiScanner = v.getJavaScanner(); + if (javaPsiScanner != null) { + javaPsiScanner.visitReference(mContext, v.getVisitor(), + element, referenced); + } + } + } + } + } + } + + super.visitReferenceElement(element); + } + + @Override + public void visitReferenceExpression(PsiReferenceExpression node) { + if (mVisitResources) { + // R.type.name + if (node.getQualifier() instanceof PsiReferenceExpression) { + PsiReferenceExpression select = (PsiReferenceExpression) node.getQualifier(); + if (select.getQualifier() instanceof PsiReferenceExpression) { + PsiReferenceExpression reference = (PsiReferenceExpression) select.getQualifier(); + if (R_CLASS.equals(reference.getReferenceName())) { + String typeName = select.getReferenceName(); + String name = node.getReferenceName(); + + ResourceType type = ResourceType.getEnum(typeName); + if (type != null) { + boolean isFramework = + reference.getQualifier() instanceof PsiReferenceExpression + && ANDROID_PKG.equals(((PsiReferenceExpression)reference. + getQualifier()).getReferenceName()); + + for (VisitingDetector v : mResourceFieldDetectors) { + JavaPsiScanner detector = v.getJavaScanner(); + if (detector != null) { + //noinspection ConstantConditions + detector.visitResourceReference(mContext, v.getVisitor(), + node, type, name, isFramework); + } + } + } + + return; + } + } + } + + // Arbitrary packages -- android.R.type.name, foo.bar.R.type.name + if (R_CLASS.equals(node.getReferenceName())) { + PsiElement parent = node.getParent(); + if (parent instanceof PsiReferenceExpression) { + PsiElement grandParent = parent.getParent(); + if (grandParent instanceof PsiReferenceExpression) { + PsiReferenceExpression select = (PsiReferenceExpression) grandParent; + String name = select.getReferenceName(); + PsiElement typeOperand = select.getQualifier(); + if (name != null && typeOperand instanceof PsiReferenceExpression) { + PsiReferenceExpression typeSelect = + (PsiReferenceExpression) typeOperand; + String typeName = typeSelect.getReferenceName(); + ResourceType type = typeName != null + ? ResourceType.getEnum(typeName) + : null; + if (type != null) { + boolean isFramework = node.getQualifier().getText().equals( + ANDROID_PKG); + for (VisitingDetector v : mResourceFieldDetectors) { + JavaPsiScanner detector = v.getJavaScanner(); + if (detector != null) { + detector.visitResourceReference(mContext, + v.getVisitor(), + node, type, name, isFramework); + } + } + } + + return; + } + } + } + } + } + + super.visitReferenceExpression(node); + } + + @Override + public void visitMethodCallExpression(PsiMethodCallExpression node) { + super.visitMethodCallExpression(node); + + if (mVisitMethods) { + String methodName = node.getMethodExpression().getReferenceName(); + if (methodName != null) { + List list = mMethodDetectors.get(methodName); + if (list != null) { + PsiMethod method = node.resolveMethod(); + if (method != null) { + for (VisitingDetector v : list) { + JavaPsiScanner javaPsiScanner = v.getJavaScanner(); + if (javaPsiScanner != null) { + javaPsiScanner.visitMethod(mContext, v.getVisitor(), node, + method); + } + } + } + } + } + } + } + + @Override + public void visitNewExpression(PsiNewExpression node) { + super.visitNewExpression(node); + + if (mVisitConstructors) { + PsiJavaCodeReferenceElement typeReference = node.getClassReference(); + if (typeReference != null) { + String type = typeReference.getQualifiedName(); + if (type != null) { + List list = mConstructorDetectors.get(type); + if (list != null) { + PsiMethod method = node.resolveMethod(); + if (method != null) { + for (VisitingDetector v : list) { + JavaPsiScanner javaPsiScanner = v.getJavaScanner(); + if (javaPsiScanner != null) { + javaPsiScanner.visitConstructor(mContext, + v.getVisitor(), node, method); + } + } + } + } + } + } + } + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java index e852925bc19..f1584b2e45e 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java @@ -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 contexts) { mParser.prepareJavaParse(contexts); } @@ -330,6 +362,29 @@ public class JavaVisitor { mParser.dispose(); } + @Nullable + private static Set getInterfaceNames( + @Nullable Set addTo, + @NonNull ResolvedClass cls) { + Iterable 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 list = mSuperClassDetectors.get(fqcn); - if (list != null) { - for (VisitingDetector v : list) { - v.getJavaScanner().checkClass(mContext, node, node, resolvedClass); + List list = mSuperClassDetectors.get(cls.getName()); + if (list != null) { + for (VisitingDetector v : list) { + v.getJavaScanner().checkClass(mContext, node, node, resolvedClass); + } + } + + // Check interfaces too + Set 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 list = mSuperClassDetectors.get(fqcn); - if (list != null) { - for (VisitingDetector v : list) { - v.getJavaScanner().checkClass(mContext, null, anonymous, - resolvedClass); + List list = mSuperClassDetectors.get(cls.getName()); + if (list != null) { + for (VisitingDetector v : list) { + v.getJavaScanner().checkClass(mContext, null, anonymous, + resolvedClass); + } + } + + // Check interfaces too + Set 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 list = mConstructorDetectors.get(type); if (list != null) { for (VisitingDetector v : list) { diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java old mode 100755 new mode 100644 index 3b9fc9df8e8..ed72fc36ffb --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java @@ -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. - *

+ *

* 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. */ @@ -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. *

@@ -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 getJavaLibraries(@NonNull Project project) { - return getClassPath(project).getLibraries(); + public List 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 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 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 mClassFolders; private final List mSourceFolders; private final List mLibraries; + private final List mNonProvidedLibraries; private final List mTestFolders; public ClassPathInfo( @NonNull List sourceFolders, @NonNull List classFolders, @NonNull List libraries, + @NonNull List nonProvidedLibraries, @NonNull List testFolders) { mSourceFolders = sourceFolders; mClassFolders = classFolders; mLibraries = libraries; + mNonProvidedLibraries = nonProvidedLibraries; mTestFolders = testFolders; } @@ -422,8 +478,8 @@ public abstract class LintClient { } @NonNull - public List getLibraries() { - return mLibraries; + public List getLibraries(boolean includeProvided) { + return includeProvided ? mLibraries : mNonProvidedLibraries; } public List 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 mDirToProject; + protected Map 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 mProjectDirs = Sets.newHashSet(); + protected Set 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 createSuperClassMap(@NonNull Project project) { - List libraries = project.getJavaLibraries(); + List libraries = project.getJavaLibraries(true); List classFolders = project.getJavaClassFolders(); List 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 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 rules = null; + final Variant variant = project.getCurrentVariant(); + if (variant != null) { + Collection 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); + } } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java index e92054c630e..2ce77af76f7 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java @@ -16,12 +16,33 @@ package com.android.tools.klint.client.api; +import static com.android.SdkConstants.ATTR_IGNORE; +import static com.android.SdkConstants.CLASS_CONSTRUCTOR; +import static com.android.SdkConstants.CONSTRUCTOR_NAME; +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_JAR; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.FD_GRADLE_WRAPPER; +import static com.android.SdkConstants.FN_GRADLE_WRAPPER_PROPERTIES; +import static com.android.SdkConstants.FN_LOCAL_PROPERTIES; +import static com.android.SdkConstants.FQCN_SUPPRESS_LINT; +import static com.android.SdkConstants.RES_FOLDER; +import static com.android.SdkConstants.SUPPRESS_ALL; +import static com.android.SdkConstants.SUPPRESS_LINT; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER; +import static com.android.tools.klint.detector.api.LintUtils.isAnonymousClass; +import static java.io.File.separator; + import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.common.repository.ResourceVisibilityLookup; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.res2.ResourceItem; import com.android.resources.ResourceFolderType; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; +import com.android.tools.klint.client.api.LintListener.EventType; import com.android.tools.klint.detector.api.ClassContext; import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; @@ -37,16 +58,39 @@ import com.android.tools.klint.detector.api.Severity; import com.android.tools.klint.detector.api.TextFormat; import com.android.tools.klint.detector.api.XmlContext; import com.google.common.annotations.Beta; +import com.google.common.base.Joiner; import com.google.common.base.Objects; -import com.google.common.collect.*; -import com.sun.istack.internal.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastChecker; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.UastVisitor; -import org.jetbrains.org.objectweb.asm.ClassReader; -import org.jetbrains.org.objectweb.asm.Opcodes; -import org.jetbrains.org.objectweb.asm.tree.*; +import com.google.common.base.Splitter; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiAnnotationParameterList; +import com.intellij.psi.PsiArrayInitializerExpression; +import com.intellij.psi.PsiArrayInitializerMemberValue; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiLiteral; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.PsiNameValuePair; + +import org.jetbrains.uast.UElement; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; import org.w3c.dom.Attr; import org.w3c.dom.Element; @@ -54,13 +98,40 @@ import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLConnection; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static com.android.SdkConstants.*; -import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER; -import static java.io.File.separator; +import lombok.ast.Annotation; +import lombok.ast.AnnotationElement; +import lombok.ast.AnnotationMethodDeclaration; +import lombok.ast.AnnotationValue; +import lombok.ast.ArrayInitializer; +import lombok.ast.ConstructorDeclaration; +import lombok.ast.Expression; +import lombok.ast.MethodDeclaration; +import lombok.ast.Modifiers; +import lombok.ast.Node; +import lombok.ast.StrictListAccessor; +import lombok.ast.StringLiteral; +import lombok.ast.TypeDeclaration; +import lombok.ast.TypeReference; +import lombok.ast.VariableDefinition; /** * Analyzes Android projects and files @@ -95,6 +166,8 @@ public class LintDriver { private boolean mAbbreviating = true; private boolean mParserErrors; private Map mProperties; + /** Whether we need to look for legacy (old Lombok-based Java API) detectors */ + private boolean mRunCompatChecks = true; /** * Creates a new {@link LintDriver} @@ -357,13 +430,13 @@ public class LintDriver { return; } - registerCustomRules(projects); + registerCustomDetectors(projects); if (mScope == null) { mScope = Scope.infer(projects); } - fireEvent(LintListener.EventType.STARTING, null); + fireEvent(EventType.STARTING, null); for (Project project : projects) { mPhase = 1; @@ -386,10 +459,24 @@ public class LintDriver { runExtraPhases(project, main); } - fireEvent(mCanceled ? LintListener.EventType.CANCELED : LintListener.EventType.COMPLETED, null); + fireEvent(mCanceled ? EventType.CANCELED : EventType.COMPLETED, null); } - private void registerCustomRules(Collection projects) { + @Nullable + private Set myCustomIssues; + + /** + * Returns true if the given issue is an issue that was loaded as a custom rule + * (e.g. a 3rd-party library provided the detector, it's not built in) + * + * @param issue the issue to be looked up + * @return true if this is a custom (non-builtin) check + */ + public boolean isCustomIssue(@NonNull Issue issue) { + return myCustomIssues != null && myCustomIssues.contains(issue); + } + + private void registerCustomDetectors(Collection projects) { // Look at the various projects, and if any of them provide a custom // lint jar, "add" them (this will replace the issue registry with // a CompositeIssueRegistry containing the original issue registry @@ -397,6 +484,9 @@ public class LintDriver { Set jarFiles = Sets.newHashSet(); for (Project project : projects) { jarFiles.addAll(mClient.findRuleJars(project)); + for (Project library : project.getAllLibraries()) { + jarFiles.addAll(mClient.findRuleJars(library)); + } } jarFiles.addAll(mClient.findGlobalRuleJars()); @@ -406,7 +496,15 @@ public class LintDriver { registries.add(mRegistry); for (File jarFile : jarFiles) { try { - registries.add(JarFileIssueRegistry.get(mClient, jarFile)); + JarFileIssueRegistry registry = JarFileIssueRegistry.get(mClient, jarFile); + if (registry.hasLegacyDetectors()) { + mRunCompatChecks = true; + } + if (myCustomIssues == null) { + myCustomIssues = Sets.newHashSet(); + } + myCustomIssues.addAll(registry.getIssues()); + registries.add(registry); } catch (Throwable e) { mClient.log(e, "Could not load custom rule jar file %1$s", jarFile); } @@ -432,7 +530,7 @@ public class LintDriver { do { mPhase++; - fireEvent(LintListener.EventType.NEW_PHASE, + fireEvent(EventType.NEW_PHASE, new Context(this, project, null, project.getDir())); // Narrow the scope down to the set of scopes requested by @@ -493,7 +591,7 @@ public class LintDriver { // and simultaneously build up the detectorToScope map which tracks // the scopes each detector is affected by (this is used to populate // the mScopeDetectors map which is used during iteration). - Configuration configuration = project.getConfiguration(); + Configuration configuration = project.getConfiguration(this); for (Detector detector : detectors) { Class detectorClass = detector.getClass(); Collection detectorIssues = issueMap.get(detectorClass); @@ -550,7 +648,7 @@ public class LintDriver { mCurrentFolderType = null; mCurrentVisitor = null; - Configuration configuration = project.getConfiguration(); + Configuration configuration = project.getConfiguration(this); mScopeDetectors = new EnumMap>(Scope.class); mApplicableDetectors = mRegistry.createDetectors(mClient, configuration, mScope, mScopeDetectors); @@ -567,6 +665,7 @@ public class LintDriver { List resourceFileDetectors = mScopeDetectors.get(Scope.RESOURCE_FILE); if (resourceFileDetectors != null) { for (Detector detector : resourceFileDetectors) { + // This is wrong; it should allow XmlScanner instead of ResourceXmlScanner! assert detector instanceof ResourceXmlDetector : detector; } } @@ -577,16 +676,22 @@ public class LintDriver { assert detector instanceof Detector.XmlScanner : detector; } } - List javaCodeDetectors = mScopeDetectors.get(Scope.ALL_SOURCE_FILES); + List javaCodeDetectors = mScopeDetectors.get(Scope.ALL_JAVA_FILES); if (javaCodeDetectors != null) { for (Detector detector : javaCodeDetectors) { - assert (detector instanceof UastScanner) : detector; + assert detector instanceof Detector.JavaScanner || + // TODO: Migrate all + detector instanceof Detector.UastScanner || + detector instanceof Detector.JavaPsiScanner : detector; } } - List javaFileDetectors = mScopeDetectors.get(Scope.SOURCE_FILE); + List javaFileDetectors = mScopeDetectors.get(Scope.JAVA_FILE); if (javaFileDetectors != null) { for (Detector detector : javaFileDetectors) { - assert (detector instanceof UastScanner) : detector; + assert detector instanceof Detector.JavaScanner || + // TODO: Migrate all + detector instanceof Detector.UastScanner || + detector instanceof Detector.JavaPsiScanner : detector; } } @@ -650,13 +755,15 @@ public class LintDriver { // Ensure that we have absolute paths such that if you lint // "foo bar" in "baz" we can show baz/ as the root - if (files.size() > 1) { - List absolute = new ArrayList(files.size()); - for (File file : files) { - absolute.add(file.getAbsoluteFile()); - } - files = absolute; + List absolute = new ArrayList(files.size()); + for (File file : files) { + absolute.add(file.getAbsoluteFile()); + } + // Always use absoluteFiles so that we can check the file's getParentFile() + // which is null if the file is not absolute. + files = absolute; + if (files.size() > 1) { sharedRoot = LintUtils.getCommonParent(files); if (sharedRoot != null && sharedRoot.getParentFile() == null) { // "/" ? sharedRoot = null; @@ -809,7 +916,7 @@ public class LintDriver { File projectDir = project.getDir(); Context projectContext = new Context(this, project, null, projectDir); - fireEvent(LintListener.EventType.SCANNING_PROJECT, projectContext); + fireEvent(EventType.SCANNING_PROJECT, projectContext); List allLibraries = project.getAllLibraries(); Set allProjects = new HashSet(allLibraries.size() + 1); @@ -833,7 +940,7 @@ public class LintDriver { List libraries = project.getAllLibraries(); for (Project library : libraries) { Context libraryContext = new Context(this, library, project, projectDir); - fireEvent(LintListener.EventType.SCANNING_LIBRARY_PROJECT, libraryContext); + fireEvent(EventType.SCANNING_LIBRARY_PROJECT, libraryContext); mCurrentProject = library; for (Detector check : mApplicableDetectors) { @@ -875,7 +982,7 @@ public class LintDriver { // Must provide an issue since API guarantees that the issue parameter IssueRegistry.CANCELLED, Severity.INFORMATIONAL, - null /*range*/, + Location.create(project.getDir()), "Lint canceled by user", TextFormat.RAW); } @@ -902,7 +1009,7 @@ public class LintDriver { if (detectors != null) { ResourceVisitor v = new ResourceVisitor(parser, detectors, null); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); v.visitFile(context, manifestFile); } } @@ -962,9 +1069,9 @@ public class LintDriver { } } - if (mScope.contains(Scope.SOURCE_FILE) || mScope.contains(Scope.ALL_SOURCE_FILES)) { - List checks = union(mScopeDetectors.get(Scope.SOURCE_FILE), - mScopeDetectors.get(Scope.ALL_SOURCE_FILES)); + if (mScope.contains(Scope.JAVA_FILE) || mScope.contains(Scope.ALL_JAVA_FILES)) { + List checks = union(mScopeDetectors.get(Scope.JAVA_FILE), + mScopeDetectors.get(Scope.ALL_JAVA_FILES)); if (checks != null && !checks.isEmpty()) { List files = project.getSubset(); if (files != null) { @@ -1041,7 +1148,7 @@ public class LintDriver { } for (File file : files) { Context context = new Context(this, project, main, file); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); for (Detector detector : detectors) { if (detector.appliesTo(context, file)) { detector.beforeCheckFile(context); @@ -1059,7 +1166,7 @@ public class LintDriver { List files = project.getProguardFiles(); for (File file : files) { Context context = new Context(this, project, main, file); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); for (Detector detector : detectors) { if (detector.appliesTo(context, file)) { detector.beforeCheckFile(context); @@ -1085,7 +1192,7 @@ public class LintDriver { File file = new File(project.getDir(), relativePath); if (file.exists()) { Context context = new Context(this, project, main, file); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); for (Detector detector : detectors) { if (detector.appliesTo(context, file)) { detector.beforeCheckFile(context); @@ -1180,7 +1287,7 @@ public class LintDriver { // the parent chains (such that for example for a virtual dispatch, we can // also check the super classes). - List libraries = project.getJavaLibraries(); + List libraries = project.getJavaLibraries(false); List libraryEntries = ClassEntry.fromClassPath(mClient, libraries, true); List classFolders = project.getJavaClassFolders(); @@ -1192,7 +1299,7 @@ public class LintDriver { Location location = Location.create(project.getDir()); mClient.report(new Context(this, project, main, project.getDir()), IssueRegistry.LINT_ERROR, - project.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR), + project.getConfiguration(this).getSeverity(IssueRegistry.LINT_ERROR), location, message, TextFormat.RAW); classEntries = Collections.emptyList(); } else { @@ -1397,7 +1504,7 @@ public class LintDriver { } } // Search in the libraries - for (File root : mClient.getJavaLibraries(project)) { + for (File root : mClient.getJavaLibraries(project, true)) { // TODO: Handle .jar files! //if (root.getPath().endsWith(DOT_JAR)) { //} @@ -1424,46 +1531,125 @@ public class LintDriver { @Nullable Project main, @NonNull List sourceFolders, @NonNull List checks) { + JavaParser javaParser = mClient.getJavaParser(project); + if (javaParser == null) { + mClient.log(null, "No java parser provided to lint: not running Java checks"); + return; + } + assert !checks.isEmpty(); // Gather all Java source files in a single pass; more efficient. List sources = new ArrayList(100); for (File folder : sourceFolders) { - gatherKotlinFiles(folder, sources); + gatherJavaFiles(folder, sources); } if (!sources.isEmpty()) { List contexts = Lists.newArrayListWithExpectedSize(sources.size()); for (File file : sources) { - JavaContext context = new JavaContext(this, project, main, file); + JavaContext context = new JavaContext(this, project, main, file, javaParser); contexts.add(context); } + visitJavaFiles(checks, javaParser, contexts); + } + } - com.intellij.openapi.project.Project ideaProject = mClient.getProject(); - if (ideaProject == null) { - return; + private void visitJavaFiles(@NonNull List checks, JavaParser javaParser, + List contexts) { + // Temporary: we still have some builtin checks that aren't migrated to + // PSI. Until that's complete, remove them from the list here + //List scanners = checks; + List scanners = Lists.newArrayListWithCapacity(0); + List uastScanners = Lists.newArrayListWithCapacity(checks.size()); + for (Detector detector : checks) { + if (detector instanceof Detector.JavaPsiScanner) { + scanners.add(detector); + } else if (detector instanceof Detector.UastScanner) { + uastScanners.add(detector); } + } + if (!scanners.isEmpty()) { + JavaPsiVisitor visitor = new JavaPsiVisitor(javaParser, scanners); + visitor.prepare(contexts); for (JavaContext context : contexts) { - fireEvent(LintListener.EventType.SCANNING_FILE, context); - - for (Detector check : checks) { - if (check instanceof UastScanner) { - UastScanner scanner = (UastScanner) check; - UastVisitor customVisitor = scanner.createUastVisitor(context); - if (customVisitor != null) { - UastChecker.INSTANCE.check( - ideaProject, context.file, context, customVisitor); - } else { - UastChecker.INSTANCE.check( - ideaProject, context.file, (UastScanner)check, context); - } - } - } - + fireEvent(EventType.SCANNING_FILE, context); + visitor.visitFile(context); if (mCanceled) { return; } } + + visitor.dispose(); + } + + if (!uastScanners.isEmpty()) { + final UElementVisitor uElementVisitor = new UElementVisitor(javaParser, uastScanners); + uElementVisitor.prepare(contexts); + for (final JavaContext context : contexts) { + fireEvent(EventType.SCANNING_FILE, context); + ApplicationManager.getApplication().runReadAction(new Runnable() { + @Override + public void run() { + uElementVisitor.visitFile(context); + } + }); + if (mCanceled) { + return; + } + } + + uElementVisitor.dispose(); + } + + // Only if the user is using some custom lint rules that haven't been updated + // yet + //noinspection ConstantConditions + if (mRunCompatChecks) { + // Filter the checks to only those that implement JavaScanner + List filtered = Lists.newArrayListWithCapacity(checks.size()); + for (Detector detector : checks) { + if (detector instanceof Detector.JavaScanner) { + filtered.add(detector); + } + } + + if (!filtered.isEmpty()) { + List detectorNames = Lists.newArrayListWithCapacity(filtered.size()); + for (Detector detector : filtered) { + detectorNames.add(detector.getClass().getName()); + } + Collections.sort(detectorNames); + + /* Let's not complain quite yet + String message = String.format("Lint found one or more custom checks using its " + + "older Java API; these checks are still run in compatibility mode, " + + "but this causes duplicated parsing, and in the next version lint " + + "will no longer include this legacy mode. Make sure the following " + + "lint detectors are upgraded to the new API: %1$s", + Joiner.on(", ").join(detectorNames)); + JavaContext first = contexts.get(0); + Project project = first.getProject(); + Location location = Location.create(project.getDir()); + mClient.report(first, + IssueRegistry.LINT_ERROR, + project.getConfiguration(this).getSeverity(IssueRegistry.LINT_ERROR), + location, message, TextFormat.RAW); + */ + + + JavaVisitor oldVisitor = new JavaVisitor(javaParser, filtered); + + oldVisitor.prepare(contexts); + for (JavaContext context : contexts) { + fireEvent(EventType.SCANNING_FILE, context); + oldVisitor.visitFile(context); + if (mCanceled) { + return; + } + } + oldVisitor.dispose(); + } } } @@ -1472,63 +1658,35 @@ public class LintDriver { @Nullable Project main, @NonNull List checks, @NonNull List files) { - List uastDetectors = new ArrayList(); - for (Detector check : checks) { - if (check instanceof UastScanner) { - uastDetectors.add((UastScanner) check); - } - } - - checkWithUastDetectors(project, main, uastDetectors, files); - } - - private void checkWithUastDetectors( - @NotNull Project project, - @Nullable Project main, - @NotNull List detectors, - @NotNull List files) { - com.intellij.openapi.project.Project intellijProject = mClient.getProject(); - if (intellijProject == null || intellijProject.isDisposed()) { + JavaParser javaParser = mClient.getJavaParser(project); + if (javaParser == null) { + mClient.log(null, "No java parser provided to lint: not running Java checks"); return; } - UastChecker checker = UastChecker.INSTANCE; - List plugins = project.getClient().getLanguagePlugins(); - + List contexts = Lists.newArrayListWithExpectedSize(files.size()); for (File file : files) { - if (!file.isFile()) { - continue; - } - - String filename = file.getName(); - // Ignore Java files for now (check only Kotlin files) - if (filename.endsWith(DOT_JAVA) || !UastConverterUtils.isFileSupported(plugins, filename)) { - continue; - } - - JavaContext - context = new JavaContext(this, project, main, file); - - for (UastScanner detector : detectors) { - UastVisitor customHandler = detector.createUastVisitor(context); - if (customHandler != null) { - checker.check(intellijProject, file, context, customHandler); - } else { - checker.check(intellijProject, file, detector, context); - } + if (file.isFile() && file.getPath().endsWith(".kt")) { + contexts.add(new JavaContext(this, project, main, file, javaParser)); } } + + if (contexts.isEmpty()) { + return; + } + + visitJavaFiles(checks, javaParser, contexts); } - private static void gatherKotlinFiles(@NonNull File dir, @NonNull List result) { + private static void gatherJavaFiles(@NonNull File dir, @NonNull List result) { File[] files = dir.listFiles(); if (files != null) { for (File file : files) { - if (file.isFile() && file.getName().endsWith(".kt")) { //$NON-NLS-1$ + if (file.isFile() && file.getName().endsWith(".java")) { //$NON-NLS-1$ result.add(file); } else if (file.isDirectory()) { - gatherKotlinFiles(file, result); + gatherJavaFiles(file, result); } } } @@ -1636,7 +1794,7 @@ public class LintDriver { if (dirChecks != null && !dirChecks.isEmpty()) { ResourceContext context = new ResourceContext(this, project, main, dir, type); String folderName = dir.getName(); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); for (Detector check : dirChecks) { if (check.appliesTo(type)) { check.beforeCheckFile(context); @@ -1663,11 +1821,12 @@ public class LintDriver { if (LintUtils.isXmlFile(file)) { XmlContext context = new XmlContext(this, project, main, file, type, visitor.getParser()); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); - } else if (binaryChecks != null && LintUtils.isBitmapFile(file)) { + } else if (binaryChecks != null && (LintUtils.isBitmapFile(file) || + type == ResourceFolderType.RAW)) { ResourceContext context = new ResourceContext(this, project, main, file, type); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); visitor.visitBinaryResource(context); } if (mCanceled) { @@ -1709,7 +1868,7 @@ public class LintDriver { if (visitor != null) { XmlContext context = new XmlContext(this, project, main, file, type, visitor.getParser()); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); } } @@ -1722,7 +1881,7 @@ public class LintDriver { if (visitor != null) { ResourceContext context = new ResourceContext(this, project, main, file, type); - fireEvent(LintListener.EventType.SCANNING_FILE, context); + fireEvent(EventType.SCANNING_FILE, context); visitor.visitBinaryResource(context); if (mCanceled) { return; @@ -1778,6 +1937,7 @@ public class LintDriver { private final LintClient mDelegate; public LintClientWrapper(@NonNull LintClient delegate) { + super(getClientName()); mDelegate = delegate; } @@ -1786,9 +1946,16 @@ public class LintDriver { @NonNull Context context, @NonNull Issue issue, @NonNull Severity severity, - @Nullable Location location, + @NonNull Location location, @NonNull String message, @NonNull TextFormat format) { + //noinspection ConstantConditions + if (location == null) { + // Misbehaving third-party lint detectors + assert false : issue; + return; + } + assert mCurrentProject != null; if (!mCurrentProject.getReportIssues()) { return; @@ -1818,11 +1985,11 @@ public class LintDriver { @Override @NonNull - public Configuration getConfiguration(@NonNull Project project) { - return mDelegate.getConfiguration(project); + public Configuration getConfiguration(@NonNull Project project, + @Nullable LintDriver driver) { + return mDelegate.getConfiguration(project, driver); } - @Override public void log(@NonNull Severity severity, @Nullable Throwable exception, @Nullable String format, @Nullable Object... args) { @@ -1855,8 +2022,37 @@ public class LintDriver { @NonNull @Override - public List getJavaLibraries(@NonNull Project project) { - return mDelegate.getJavaLibraries(project); + public List getJavaLibraries(@NonNull Project project, boolean includeProvided) { + return mDelegate.getJavaLibraries(project, includeProvided); + } + + @NonNull + @Override + public List getTestSourceFolders(@NonNull Project project) { + return mDelegate.getTestSourceFolders(project); + } + + @Override + public Collection getKnownProjects() { + return mDelegate.getKnownProjects(); + } + + @Nullable + @Override + public BuildToolInfo getBuildTools(@NonNull Project project) { + return mDelegate.getBuildTools(project); + } + + @NonNull + @Override + public Map createSuperClassMap(@NonNull Project project) { + return mDelegate.createSuperClassMap(project); + } + + @NonNull + @Override + public ResourceVisibilityLookup.Provider getResourceVisibilityProvider() { + return mDelegate.getResourceVisibilityProvider(); } @Override @@ -1890,6 +2086,12 @@ public class LintDriver { return mDelegate.getProject(dir, referenceDir); } + @Nullable + @Override + public JavaParser getJavaParser(@Nullable Project project) { + return mDelegate.getJavaParser(project); + } + @Override public File findResource(@NonNull String relativePath) { return mDelegate.findResource(relativePath); @@ -1903,7 +2105,7 @@ public class LintDriver { @Override @NonNull - protected LintClient.ClassPathInfo getClassPath(@NonNull Project project) { + protected ClassPathInfo getClassPath(@NonNull Project project) { return mDelegate.getClassPath(project); } @@ -2000,6 +2202,17 @@ public class LintDriver { return mDelegate.addCustomLintRules(registry); } + @NonNull + @Override + public List getAssetFolders(@NonNull Project project) { + return mDelegate.getAssetFolders(project); + } + + @Override + public ClassLoader createUrlClassLoader(@NonNull URL[] urls, @NonNull ClassLoader parent) { + return mDelegate.createUrlClassLoader(urls, parent); + } + @Override public boolean checkForSuppressComments() { return mDelegate.checkForSuppressComments(); @@ -2033,16 +2246,6 @@ public class LintDriver { public void closeConnection(@NonNull URLConnection connection) throws IOException { mDelegate.closeConnection(connection); } - - @Override - public com.intellij.openapi.project.Project getProject() { - return mDelegate.getProject(); - } - - @Override - public List getLanguagePlugins() { - return mDelegate.getLanguagePlugins(); - } } /** @@ -2114,7 +2317,7 @@ public class LintDriver { return true; } } else if (classNode.outerClass != null && classNode.outerMethod == null - && LintUtils.isAnonymousClass(classNode)) { + && isAnonymousClass(classNode)) { if (isSuppressed(issue, classNode)) { return true; } @@ -2225,7 +2428,7 @@ public class LintDriver { } if (classNode.outerClass != null && classNode.outerMethod == null - && LintUtils.isAnonymousClass(classNode)) { + && isAnonymousClass(classNode)) { ClassNode outer = getOuterClassNode(classNode); if (outer != null) { MethodNode m = findMethod(outer, CONSTRUCTOR_NAME, false); @@ -2311,6 +2514,36 @@ public class LintDriver { return false; } + /** + * Returns true if the given issue is suppressed by the given suppress string; this + * is typically the same as the issue id, but is allowed to not match case sensitively, + * and is allowed to be a comma separated list, and can be the string "all" + * + * @param issue the issue id to match + * @param string the suppress string -- typically the id, or "all", or a comma separated list + * of ids + * @return true if the issue is suppressed by the given string + */ + private static boolean isSuppressed(@NonNull Issue issue, @NonNull String string) { + if (string.isEmpty()) { + return false; + } + + if (string.indexOf(',') == -1) { + if (matches(issue, string)) { + return true; + } + } else { + for (String id : Splitter.on(',').trimResults().split(string)) { + if (matches(issue, id)) { + return true; + } + } + } + + return false; + } + /** * Returns whether the given issue is suppressed in the given parse tree node. * @@ -2321,31 +2554,50 @@ public class LintDriver { * issue in this class */ public boolean isSuppressed(@Nullable JavaContext context, @NonNull Issue issue, - @Nullable UElement scope) { + @Nullable Node scope) { boolean checkComments = mClient.checkForSuppressComments() && - context != null && context.containsCommentSuppress(); + context != null && context.containsCommentSuppress(); while (scope != null) { - if (scope instanceof UVariable) { - UVariable declaration = (UVariable) scope; - if (isUastSuppressed(issue, declaration.getAnnotations())) { + Class 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 == VariableDefinition.class) { + // Variable + VariableDefinition declaration = (VariableDefinition) scope; + if (isSuppressed(issue, declaration.astModifiers())) { return true; } - } else if (scope instanceof UFunction) { - UFunction declaration = (UFunction) scope; - if (isUastSuppressed(issue, declaration.getAnnotations())) { + } else if (type == MethodDeclaration.class) { + // Method + // Look for annotations on the method + MethodDeclaration declaration = (MethodDeclaration) scope; + if (isSuppressed(issue, declaration.astModifiers())) { return true; } - } else if (scope instanceof UClass) { - UClass declaration = (UClass) scope; - if (isUastSuppressed(issue, declaration.getAnnotations())) { + } else if (type == ConstructorDeclaration.class) { + // Constructor + // Look for annotations on the method + ConstructorDeclaration declaration = (ConstructorDeclaration) scope; + if (isSuppressed(issue, declaration.astModifiers())) { + return true; + } + } else if (TypeDeclaration.class.isAssignableFrom(type)) { + // Class, annotation, enum, interface + TypeDeclaration declaration = (TypeDeclaration) scope; + if (isSuppressed(issue, declaration.astModifiers())) { + return true; + } + } else if (type == AnnotationMethodDeclaration.class) { + // Look for annotations on the method + AnnotationMethodDeclaration declaration = (AnnotationMethodDeclaration) scope; + if (isSuppressed(issue, declaration.astModifiers())) { return true; } } - //TODO check comments - //if (checkComments && context.isSuppressedWithComment(scope, issue)) { - // return true; - //} + if (checkComments && context.isSuppressedWithComment(scope, issue)) { + return true; + } scope = scope.getParent(); } @@ -2353,6 +2605,56 @@ public class LintDriver { return false; } + public boolean isSuppressed(@Nullable JavaContext context, @NonNull Issue issue, + @Nullable UElement scope) { + boolean checkComments = mClient.checkForSuppressComments() && + context != null && context.containsCommentSuppress(); + while (scope != null) { + if (scope instanceof PsiModifierListOwner) { + PsiModifierListOwner owner = (PsiModifierListOwner) scope; + if (isSuppressed(issue, owner.getModifierList())) { + return true; + } + } + + if (checkComments && context.isSuppressedWithComment(scope, issue)) { + return true; + } + + scope = scope.getContainingElement(); + if (scope instanceof PsiFile) { + return false; + } + } + + return false; + } + + public boolean isSuppressed(@Nullable JavaContext context, @NonNull Issue issue, + @Nullable PsiElement scope) { + boolean checkComments = mClient.checkForSuppressComments() && + context != null && context.containsCommentSuppress(); + while (scope != null) { + if (scope instanceof PsiModifierListOwner) { + PsiModifierListOwner owner = (PsiModifierListOwner) scope; + if (isSuppressed(issue, owner.getModifierList())) { + return true; + } + } + + if (checkComments && context.isSuppressedWithComment(scope, issue)) { + return true; + } + + scope = scope.getParent(); + if (scope instanceof PsiFile) { + return false; + } + } + + return false; + } + /** * Returns true if the given AST modifier has a suppress annotation for the * given issue (which can be null to check for the "all" annotation) @@ -2362,27 +2664,47 @@ public class LintDriver { * @return true if the issue or all issues should be suppressed for this * modifier */ - private static boolean isUastSuppressed(@Nullable Issue issue, @Nullable List annotations) { - for (UAnnotation annotation : annotations) { - if (annotation.matchesName(SUPPRESS_LINT) - || annotation.matchesName("SuppressWarnings")) { //$NON-NLS-1$ - List values = annotation.getValueArguments(); - for (UNamedExpression element : values) { - UExpression valueNode = element.getExpression(); - String value = UastLiteralUtils.getValueIfStringLiteral(valueNode); - if (value != null) { - if (matches(issue, value)) { - return true; + private static boolean isSuppressed(@Nullable Issue issue, @Nullable Modifiers modifiers) { + if (modifiers == null) { + return false; + } + StrictListAccessor annotations = modifiers.astAnnotations(); + if (annotations == null) { + return false; + } + + for (Annotation annotation : annotations) { + TypeReference t = annotation.astAnnotationTypeReference(); + String typeName = t.getTypeName(); + if (typeName.endsWith(SUPPRESS_LINT) + || typeName.endsWith("SuppressWarnings")) { //$NON-NLS-1$ + StrictListAccessor values = + annotation.astElements(); + if (values != null) { + for (AnnotationElement element : values) { + AnnotationValue valueNode = element.astValue(); + if (valueNode == null) { + continue; } - } else if (valueNode instanceof UCallExpression - && ((UCallExpression)valueNode).getKind() == UastCallKind.ARRAY_INITIALIZER) { - UCallExpression array = (UCallExpression) valueNode; - List expressions = array.getValueArguments(); - for (UExpression arrayElement : expressions) { - String elementValue = UastLiteralUtils.getValueIfStringLiteral(arrayElement); - if (elementValue != null) { - if (matches(issue, elementValue)) { - return true; + if (valueNode instanceof StringLiteral) { + StringLiteral literal = (StringLiteral) valueNode; + String value = literal.astValue(); + if (matches(issue, value)) { + return true; + } + } else if (valueNode instanceof ArrayInitializer) { + ArrayInitializer array = (ArrayInitializer) valueNode; + StrictListAccessor expressions = + array.astExpressions(); + if (expressions == null) { + continue; + } + for (Expression arrayElement : expressions) { + if (arrayElement instanceof StringLiteral) { + String value = ((StringLiteral) arrayElement).astValue(); + if (matches(issue, value)) { + return true; + } } } } @@ -2394,6 +2716,79 @@ public class LintDriver { return false; } + public static final String SUPPRESS_WARNINGS_FQCN = "java.lang.SuppressWarnings"; + + + /** + * Returns true if the given AST modifier has a suppress annotation for the + * given issue (which can be null to check for the "all" annotation) + * + * @param issue the issue to be checked + * @param modifierList the modifier to check + * @return true if the issue or all issues should be suppressed for this + * modifier + */ + public static boolean isSuppressed(@NonNull Issue issue, + @Nullable PsiModifierList modifierList) { + if (modifierList == null) { + return false; + } + + for (PsiAnnotation annotation : modifierList.getAnnotations()) { + String fqcn = annotation.getQualifiedName(); + if (fqcn != null && (fqcn.equals(FQCN_SUPPRESS_LINT) + || fqcn.equals(SUPPRESS_WARNINGS_FQCN) + || fqcn.equals(SUPPRESS_LINT))) { // when missing imports + PsiAnnotationParameterList parameterList = annotation.getParameterList(); + for (PsiNameValuePair pair : parameterList.getAttributes()) { + if (isSuppressed(issue, pair.getValue())) { + return true; + } + } + } + } + + return false; + } + + /** + * Returns true if the annotation member value, assumed to be specified on a a SuppressWarnings + * or SuppressLint annotation, specifies the given id (or "all"). + * + * @param issue the issue to be checked + * @param value the member value to check + * @return true if the issue or all issues should be suppressed for this modifier + */ + public static boolean isSuppressed(@NonNull Issue issue, + @Nullable PsiAnnotationMemberValue value) { + if (value instanceof PsiLiteral) { + PsiLiteral literal = (PsiLiteral)value; + Object literalValue = literal.getValue(); + if (literalValue instanceof String) { + if (isSuppressed(issue, (String) literalValue)) { + return true; + } + } + } else if (value instanceof PsiArrayInitializerMemberValue) { + PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue)value; + for (PsiAnnotationMemberValue mmv : mv.getInitializers()) { + if (isSuppressed(issue, mmv)) { + return true; + } + } + } else if (value instanceof PsiArrayInitializerExpression) { + PsiArrayInitializerExpression expression = (PsiArrayInitializerExpression) value; + PsiExpression[] initializers = expression.getInitializers(); + for (PsiExpression e : initializers) { + if (isSuppressed(issue, e)) { + return true; + } + } + } + + return false; + } + /** * Returns whether the given issue is suppressed in the given XML DOM node. * @@ -2414,16 +2809,8 @@ public class LintDriver { Element element = (Element) node; if (element.hasAttributeNS(TOOLS_URI, ATTR_IGNORE)) { String ignore = element.getAttributeNS(TOOLS_URI, ATTR_IGNORE); - if (ignore.indexOf(',') == -1) { - if (matches(issue, ignore)) { - return true; - } - } else { - for (String id : ignore.split(",")) { //$NON-NLS-1$ - if (matches(issue, id)) { - return true; - } - } + if (isSuppressed(issue, ignore)) { + return true; } } else if (checkComments && context.isSuppressedWithComment(node, issue)) { return true; diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintLanguageExtension.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintLanguageExtension.java deleted file mode 100644 index ecaa04efe7d..00000000000 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintLanguageExtension.java +++ /dev/null @@ -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 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 getPlugins(@Nullable Project project) { - if (project == null) { - return Collections.emptyList(); - } - - LintLanguageExtension[] languageExtensions = project.getExtensions(EP_NAME); - List converters = new ArrayList(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); - } -} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java index 28b4ad22d89..f812591dabe 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java @@ -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; diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java index 7b64e46323f..f6a42f24698 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java @@ -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 files = new ArrayList(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 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); } } } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java index 7e257f9090c..5ea1008cfb2 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java @@ -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, diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/UElementVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/UElementVisitor.java new file mode 100644 index 00000000000..a5ad21d1450 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/UElementVisitor.java @@ -0,0 +1,1085 @@ +/* + * 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 static com.android.SdkConstants.R_CLASS; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.resources.ResourceType; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; +import com.android.tools.klint.detector.api.Detector.UastScanner; +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; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.StandardFileSystems; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiTypeParameter; + +import org.jetbrains.uast.*; +import org.jetbrains.uast.expressions.UTypeReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Specialized visitor for running detectors on a Java AST. + * It operates in three phases: + *

    + *
  1. First, it computes a set of maps where it generates a map from each + * significant AST attribute (such as method call names) to a list + * of detectors to consult whenever that attribute is encountered. + * Examples of "attributes" are method names, Android resource identifiers, + * and general AST node types such as "cast" nodes etc. These are + * defined on the {@link JavaPsiScanner} interface. + *
  2. Second, it iterates over the document a single time, delegating to + * the detectors found at each relevant AST attribute. + *
  3. Finally, it calls the remaining visitors (those that need to process a + * whole document on their own). + *
+ * It also notifies all the detectors before and after the document is processed + * such that they can do pre- and post-processing. + */ +public class UElementVisitor { + /** Default size of lists holding detectors of the same type for a given node type */ + private static final int SAME_TYPE_COUNT = 8; + + private final Map> mMethodDetectors = + Maps.newHashMapWithExpectedSize(80); + private final Map> mConstructorDetectors = + Maps.newHashMapWithExpectedSize(12); + private final Map> mReferenceDetectors = + Maps.newHashMapWithExpectedSize(10); + private Set mConstructorSimpleNames; + private final List mResourceFieldDetectors = + new ArrayList(); + private final List mAllDetectors; + private final List mFullTreeDetectors; + private final Map, List> mNodePsiTypeDetectors = + new HashMap, List>(16); + private final JavaParser mParser; + private final Map> mSuperClassDetectors = + new HashMap>(); + + /** + * Number of fatal exceptions (internal errors, usually from ECJ) we've + * encountered; we don't log each and every one to avoid massive log spam + * in code which triggers this condition + */ + private static int sExceptionCount; + /** Max number of logs to include */ + private static final int MAX_REPORTED_CRASHES = 20; + + UElementVisitor(@NonNull JavaParser parser, @NonNull List detectors) { + mParser = parser; + mAllDetectors = new ArrayList(detectors.size()); + mFullTreeDetectors = new ArrayList(detectors.size()); + + for (Detector detector : detectors) { + UastScanner uastScanner = (UastScanner) detector; + VisitingDetector v = new VisitingDetector(detector, uastScanner); + mAllDetectors.add(v); + + List names = detector.getApplicableMethodNames(); + if (names != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert names != XmlScanner.ALL; + + for (String name : names) { + List list = mMethodDetectors.get(name); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mMethodDetectors.put(name, list); + } + list.add(v); + } + } + + List applicableSuperClasses = detector.applicableSuperClasses(); + if (applicableSuperClasses != null) { + for (String fqn : applicableSuperClasses) { + List list = mSuperClassDetectors.get(fqn); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mSuperClassDetectors.put(fqn, list); + } + list.add(v); + } + continue; + } + + List> nodePsiTypes = detector.getApplicableUastTypes(); + if (nodePsiTypes != null) { + for (Class type : nodePsiTypes) { + List list = mNodePsiTypeDetectors.get(type); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mNodePsiTypeDetectors.put(type, list); + } + list.add(v); + } + } + + List types = detector.getApplicableConstructorTypes(); + if (types != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert types != XmlScanner.ALL; + if (mConstructorSimpleNames == null) { + mConstructorSimpleNames = Sets.newHashSet(); + } + for (String type : types) { + List list = mConstructorDetectors.get(type); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mConstructorDetectors.put(type, list); + mConstructorSimpleNames.add(type.substring(type.lastIndexOf('.')+1)); + } + list.add(v); + } + } + + List referenceNames = detector.getApplicableReferenceNames(); + if (referenceNames != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert referenceNames != XmlScanner.ALL; + + for (String name : referenceNames) { + List list = mReferenceDetectors.get(name); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mReferenceDetectors.put(name, list); + } + list.add(v); + } + } + + if (detector.appliesToResourceRefs()) { + mResourceFieldDetectors.add(v); + } else if ((referenceNames == null || referenceNames.isEmpty()) + && (nodePsiTypes == null || nodePsiTypes.isEmpty()) + && (types == null || types.isEmpty())) { + mFullTreeDetectors.add(v); + } + } + } + + void visitFile(@NonNull final JavaContext context) { + try { + Project ideaProject = context.getParser().getIdeaProject(); + if (ideaProject == null) { + return; + } + + VirtualFile virtualFile = StandardFileSystems.local() + .findFileByPath(context.file.getAbsolutePath()); + if (virtualFile == null) { + return; + } + + PsiFile psiFile = PsiManager.getInstance(ideaProject).findFile(virtualFile); + if (psiFile == null) { + return; + } + + UElement uElement = context.getUastContext().convertElementWithParent(psiFile, UFile.class); + if (!(uElement instanceof UFile)) { + // No need to log this; the parser should be reporting + // a full warning (such as IssueRegistry#PARSER_ERROR) + // with details, location, etc. + return; + } + + final UFile uFile = (UFile) uElement; + + try { + context.setUFile(uFile); + + mParser.runReadAction(new Runnable() { + @Override + public void run() { + for (VisitingDetector v : mAllDetectors) { + v.setContext(context); + v.getDetector().beforeCheckFile(context); + } + } + }); + + if (!mSuperClassDetectors.isEmpty()) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + SuperclassPsiVisitor visitor = new SuperclassPsiVisitor(context); + uFile.accept(visitor); + } + }); + } + + for (final VisitingDetector v : mFullTreeDetectors) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + UastVisitor visitor = v.getVisitor(); + uFile.accept(visitor); + } + }); + } + + if (!mMethodDetectors.isEmpty() + || !mResourceFieldDetectors.isEmpty() + || !mConstructorDetectors.isEmpty() + || !mReferenceDetectors.isEmpty()) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + // TODO: Do we need to break this one up into finer grain + // locking units + UastVisitor visitor = new DelegatingPsiVisitor(context); + uFile.accept(visitor); + } + }); + } else { + if (!mNodePsiTypeDetectors.isEmpty()) { + mParser.runReadAction(new Runnable() { + @Override + public void run() { + // TODO: Do we need to break this one up into finer grain + // locking units + UastVisitor visitor = new DispatchPsiVisitor(); + uFile.accept(visitor); + } + }); + } + } + + mParser.runReadAction(new Runnable() { + @Override + public void run() { + for (VisitingDetector v : mAllDetectors) { + v.getDetector().afterCheckFile(context); + } + } + }); + } finally { + mParser.dispose(context, uFile); + context.setUFile(null); + } + } catch (ProcessCanceledException ignore) { + // Cancelling inspections in the IDE + } catch (RuntimeException e) { + if (sExceptionCount++ > MAX_REPORTED_CRASHES) { + // No need to keep spamming the user that a lot of the files + // are tripping up ECJ, they get the picture. + return; + } + + 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; + } + + // Work around ECJ bugs; see https://code.google.com/p/android/issues/detail?id=172268 + // Don't allow lint bugs to take down the whole build. TRY to log this as a + // lint error instead! + StringBuilder sb = new StringBuilder(100); + sb.append("Unexpected failure during lint analysis of "); + sb.append(context.file.getName()); + sb.append(" (this is a bug in lint or one of the libraries it depends on)\n"); + + sb.append(e.getClass().getSimpleName()); + sb.append(':'); + StackTraceElement[] stackTrace = e.getStackTrace(); + int count = 0; + for (StackTraceElement frame : stackTrace) { + if (count > 0) { + sb.append("<-"); + } + + String className = frame.getClassName(); + sb.append(className.substring(className.lastIndexOf('.') + 1)); + sb.append('.').append(frame.getMethodName()); + sb.append('('); + sb.append(frame.getFileName()).append(':').append(frame.getLineNumber()); + sb.append(')'); + count++; + // Only print the top 3-4 frames such that we can identify the bug + if (count == 4) { + break; + } + } + Throwable throwable = null; // NOT e: this makes for very noisy logs + //noinspection ConstantConditions + context.log(throwable, sb.toString()); + } + } + + /** + * 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 contexts) { + mParser.prepareJavaParse(contexts); + } + + public void dispose() { + mParser.dispose(); + } + + @Nullable + private static Set getInterfaceNames( + @Nullable Set addTo, + @NonNull PsiClass cls) { + for (PsiClass resolvedInterface : cls.getInterfaces()) { + String name = resolvedInterface.getQualifiedName(); + 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 UastVisitor mVisitor; + private JavaContext mContext; + public final Detector mDetector; + public final UastScanner mUastScanner; + + public VisitingDetector(@NonNull Detector detector, @NonNull UastScanner uastScanner) { + mDetector = detector; + mUastScanner = uastScanner; + } + + @NonNull + public Detector getDetector() { + return mDetector; + } + + @Nullable + public UastScanner getUastScanner() { + return mUastScanner; + } + + public void setContext(@NonNull JavaContext context) { + mContext = context; + + // The visitors are one-per-context, so clear them out here and construct + // lazily only if needed + mVisitor = null; + } + + @NonNull + UastVisitor getVisitor() { + if (mVisitor == null) { + mVisitor = mDetector.createUastVisitor(mContext); + if (mVisitor == null) { + mVisitor = new AbstractUastVisitor() {}; + } + } + return mVisitor; + } + } + + private class SuperclassPsiVisitor extends AbstractUastVisitor { + private JavaContext mContext; + + public SuperclassPsiVisitor(@NonNull JavaContext context) { + mContext = context; + } + + @Override + public boolean visitClass(UClass node) { + boolean result = super.visitClass(node); + checkClass(node); + return result; + } + + private void checkClass(@NonNull UClass node) { + if (node instanceof PsiTypeParameter) { + // Not included: explained in javadoc for JavaPsiScanner#checkClass + return; + } + + UClass cls = node; + int depth = 0; + while (cls != null) { + List list = mSuperClassDetectors.get(cls.getQualifiedName()); + if (list != null) { + for (VisitingDetector v : list) { + UastScanner uastScanner = v.getUastScanner(); + if (uastScanner != null) { + uastScanner.checkClass(mContext, node); + } + } + } + + // Check interfaces too + Set interfaceNames = getInterfaceNames(null, cls); + if (interfaceNames != null) { + for (String name : interfaceNames) { + list = mSuperClassDetectors.get(name); + if (list != null) { + for (VisitingDetector v : list) { + UastScanner javaPsiScanner = v.getUastScanner(); + if (javaPsiScanner != null) { + javaPsiScanner.checkClass(mContext, node); + } + } + } + } + } + + cls = cls.getUastSuperClass(); + 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; + } + } + } + } + + private class DispatchPsiVisitor extends AbstractUastVisitor { + + @Override + public boolean visitCatchClause(UCatchClause node) { + List list = mNodePsiTypeDetectors.get(UCatchClause.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCatchClause(node); + } + } + return super.visitCatchClause(node); + } + + @Override + public boolean visitMethod(UMethod node) { + List list = mNodePsiTypeDetectors.get(UMethod.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitMethod(node); + } + } + return super.visitMethod(node); + } + + @Override + public boolean visitVariable(UVariable node) { + List list = mNodePsiTypeDetectors.get(UVariable.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitVariable(node); + } + } + return super.visitVariable(node); + } + + @Override + public boolean visitFile(UFile node) { + List list = mNodePsiTypeDetectors.get(UFile.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitFile(node); + } + } + return super.visitFile(node); + } + + @Override + public boolean visitImportStatement(UImportStatement node) { + List list = mNodePsiTypeDetectors.get(UImportStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitImportStatement(node); + } + } + return super.visitImportStatement(node); + } + + @Override + public boolean visitElement(UElement node) { + List list = mNodePsiTypeDetectors.get(UElement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitElement(node); + } + } + return super.visitElement(node); + } + + @Override + public boolean visitClass(UClass node) { + List list = mNodePsiTypeDetectors.get(UClass.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitClass(node); + } + } + return super.visitClass(node); + } + + @Override + public boolean visitInitializer(UClassInitializer node) { + List list = mNodePsiTypeDetectors.get(UClassInitializer.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitInitializer(node); + } + } + return super.visitInitializer(node); + } + + @Override + public boolean visitLabeledExpression(ULabeledExpression node) { + List list = mNodePsiTypeDetectors.get(ULabeledExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLabeledExpression(node); + } + } + return super.visitLabeledExpression(node); + } + + @Override + public boolean visitBlockExpression(UBlockExpression node) { + List list = mNodePsiTypeDetectors.get(UBlockExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBlockExpression(node); + } + } + return super.visitBlockExpression(node); + } + + @Override + public boolean visitCallExpression(UCallExpression node) { + List list = mNodePsiTypeDetectors.get(UCallExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCallExpression(node); + } + } + return super.visitCallExpression(node); + } + + @Override + public boolean visitBinaryExpression(UBinaryExpression node) { + List list = mNodePsiTypeDetectors.get(UBinaryExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBinaryExpression(node); + } + } + return super.visitBinaryExpression(node); + } + + @Override + public boolean visitBinaryExpressionWithType(UBinaryExpressionWithType node) { + List list = mNodePsiTypeDetectors.get(UBinaryExpressionWithType.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBinaryExpressionWithType(node); + } + } + return super.visitBinaryExpressionWithType(node); + } + + @Override + public boolean visitParenthesizedExpression(UParenthesizedExpression node) { + List list = mNodePsiTypeDetectors.get(UParenthesizedExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitParenthesizedExpression(node); + } + } + return super.visitParenthesizedExpression(node); + } + + @Override + public boolean visitUnaryExpression(UUnaryExpression node) { + List list = mNodePsiTypeDetectors.get(UUnaryExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitUnaryExpression(node); + } + } + return super.visitUnaryExpression(node); + } + + @Override + public boolean visitPrefixExpression(UPrefixExpression node) { + List list = mNodePsiTypeDetectors.get(UPrefixExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPrefixExpression(node); + } + } + return super.visitPrefixExpression(node); + } + + @Override + public boolean visitPostfixExpression(UPostfixExpression node) { + List list = mNodePsiTypeDetectors.get(UPostfixExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPostfixExpression(node); + } + } + return super.visitPostfixExpression(node); + } + + @Override + public boolean visitIfExpression(UIfExpression node) { + List list = mNodePsiTypeDetectors.get(UIfExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitIfExpression(node); + } + } + return super.visitIfExpression(node); + } + + @Override + public boolean visitSwitchExpression(USwitchExpression node) { + List list = mNodePsiTypeDetectors.get(USwitchExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSwitchExpression(node); + } + } + return super.visitSwitchExpression(node); + } + + @Override + public boolean visitSwitchClauseExpression(USwitchClauseExpression node) { + List list = mNodePsiTypeDetectors.get(USwitchClauseExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSwitchClauseExpression(node); + } + } + return super.visitSwitchClauseExpression(node); + } + + @Override + public boolean visitWhileExpression(UWhileExpression node) { + List list = mNodePsiTypeDetectors.get(UWhileExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitWhileExpression(node); + } + } + return super.visitWhileExpression(node); + } + + @Override + public boolean visitDoWhileExpression(UDoWhileExpression node) { + List list = mNodePsiTypeDetectors.get(UDoWhileExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDoWhileExpression(node); + } + } + return super.visitDoWhileExpression(node); + } + + @Override + public boolean visitForExpression(UForExpression node) { + List list = mNodePsiTypeDetectors.get(UForExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitForExpression(node); + } + } + return super.visitForExpression(node); + } + + @Override + public boolean visitForEachExpression(UForEachExpression node) { + List list = mNodePsiTypeDetectors.get(UForEachExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitForEachExpression(node); + } + } + return super.visitForEachExpression(node); + } + + @Override + public boolean visitTryExpression(UTryExpression node) { + List list = mNodePsiTypeDetectors.get(UTryExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTryExpression(node); + } + } + return super.visitTryExpression(node); + } + + @Override + public boolean visitLiteralExpression(ULiteralExpression node) { + List list = mNodePsiTypeDetectors.get(ULiteralExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLiteralExpression(node); + } + } + return super.visitLiteralExpression(node); + } + + @Override + public boolean visitThisExpression(UThisExpression node) { + List list = mNodePsiTypeDetectors.get(UThisExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitThisExpression(node); + } + } + return super.visitThisExpression(node); + } + + @Override + public boolean visitSuperExpression(USuperExpression node) { + List list = mNodePsiTypeDetectors.get(USuperExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSuperExpression(node); + } + } + return super.visitSuperExpression(node); + } + + @Override + public boolean visitReturnExpression(UReturnExpression node) { + List list = mNodePsiTypeDetectors.get(UReturnExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReturnExpression(node); + } + } + return super.visitReturnExpression(node); + } + + @Override + public boolean visitBreakExpression(UBreakExpression node) { + List list = mNodePsiTypeDetectors.get(UBreakExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBreakExpression(node); + } + } + return super.visitBreakExpression(node); + } + + @Override + public boolean visitContinueExpression(UContinueExpression node) { + List list = mNodePsiTypeDetectors.get(UContinueExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitContinueExpression(node); + } + } + return super.visitContinueExpression(node); + } + + @Override + public boolean visitThrowExpression(UThrowExpression node) { + List list = mNodePsiTypeDetectors.get(UThrowExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitThrowExpression(node); + } + } + return super.visitThrowExpression(node); + } + + @Override + public boolean visitArrayAccessExpression(UArrayAccessExpression node) { + List list = mNodePsiTypeDetectors.get(UArrayAccessExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitArrayAccessExpression(node); + } + } + return super.visitArrayAccessExpression(node); + } + + @Override + public boolean visitCallableReferenceExpression(UCallableReferenceExpression node) { + List list = mNodePsiTypeDetectors.get(UCallableReferenceExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCallableReferenceExpression(node); + } + } + return super.visitCallableReferenceExpression(node); + } + + @Override + public boolean visitClassLiteralExpression(UClassLiteralExpression node) { + List list = mNodePsiTypeDetectors.get(UClassLiteralExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitClassLiteralExpression(node); + } + } + return super.visitClassLiteralExpression(node); + } + + @Override + public boolean visitLambdaExpression(ULambdaExpression node) { + List list = mNodePsiTypeDetectors.get(ULambdaExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLambdaExpression(node); + } + } + return super.visitLambdaExpression(node); + } + + @Override + public boolean visitObjectLiteralExpression(UObjectLiteralExpression node) { + List list = mNodePsiTypeDetectors.get(UObjectLiteralExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitObjectLiteralExpression(node); + } + } + return super.visitObjectLiteralExpression(node); + } + + @Override + public boolean visitExpressionList(UExpressionList node) { + List list = mNodePsiTypeDetectors.get(UExpressionList.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitExpressionList(node); + } + } + return super.visitExpressionList(node); + } + + @Override + public boolean visitTypeReferenceExpression(UTypeReferenceExpression node) { + List list = mNodePsiTypeDetectors.get(UTypeReferenceExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeReferenceExpression(node); + } + } + return super.visitTypeReferenceExpression(node); + } + + @Override + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + List list = mNodePsiTypeDetectors.get(USimpleNameReferenceExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSimpleNameReferenceExpression(node); + } + } + return super.visitSimpleNameReferenceExpression(node); + } + + @Override + public boolean visitQualifiedReferenceExpression(UQualifiedReferenceExpression node) { + List list = mNodePsiTypeDetectors.get(UQualifiedReferenceExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitQualifiedReferenceExpression(node); + } + } + return super.visitQualifiedReferenceExpression(node); + } + + @Override + public boolean visitDeclarationsExpression(UVariableDeclarationsExpression node) { + List list = mNodePsiTypeDetectors.get(UVariableDeclarationsExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDeclarationsExpression(node); + } + } + return super.visitDeclarationsExpression(node); + } + } + + /** Performs common AST searches for method calls and R-type-field references. + * Note that this is a specialized form of the {@link DispatchPsiVisitor}. */ + private class DelegatingPsiVisitor extends DispatchPsiVisitor { + private final JavaContext mContext; + private final boolean mVisitResources; + private final boolean mVisitMethods; + private final boolean mVisitConstructors; + private final boolean mVisitReferences; + + DelegatingPsiVisitor(JavaContext context) { + mContext = context; + + mVisitMethods = !mMethodDetectors.isEmpty(); + mVisitConstructors = !mConstructorDetectors.isEmpty(); + mVisitResources = !mResourceFieldDetectors.isEmpty(); + mVisitReferences = !mReferenceDetectors.isEmpty(); + } + + @Override + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + if (mVisitReferences) { + List list = mReferenceDetectors.get(node.getIdentifier()); + if (list != null) { + PsiElement referenced = node.resolve(); + if (referenced != null) { + for (VisitingDetector v : list) { + UastScanner uastScanner = v.getUastScanner(); + if (uastScanner != null) { + uastScanner.visitReference(mContext, v.getVisitor(), + node, referenced); + } + } + } + } + } + + if (mVisitResources) { + AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(node); + if (androidReference != null) { + for (VisitingDetector v : mResourceFieldDetectors) { + UastScanner uastScanner = v.getUastScanner(); + if (uastScanner != null) { + uastScanner.visitResourceReference(mContext, v.getVisitor(), + androidReference.node, + androidReference.getType(), + androidReference.getName(), + androidReference.getPackage().equals(ANDROID_PKG)); + } + } + } + } + + return super.visitSimpleNameReferenceExpression(node); + } + + @Override + public boolean visitCallExpression(UCallExpression node) { + boolean result = super.visitCallExpression(node); + + if (UastExpressionUtils.isMethodCall(node)) { + visitMethodCallExpression(node); + } else if (UastExpressionUtils.isConstructorCall(node)) { + visitNewExpression(node); + } + + return result; + } + + private void visitMethodCallExpression(UCallExpression node) { + if (mVisitMethods) { + String methodName = node.getMethodName(); + if (methodName != null) { + List list = mMethodDetectors.get(methodName); + if (list != null) { + PsiMethod function = node.resolve(); + if (function != null) { + for (VisitingDetector v : list) { + UastScanner scanner = v.getUastScanner(); + if (scanner != null) { + scanner.visitMethod(mContext, v.getVisitor(), node, + mContext.getUastContext().getMethod(function)); + } + } + } + } + } + } + } + + private void visitNewExpression(UCallExpression node) { + if (mVisitConstructors) { + PsiMethod resolvedConstructor = node.resolve(); + if (resolvedConstructor == null) { + return; + } + + PsiClass resolvedClass = resolvedConstructor.getContainingClass(); + if (resolvedClass != null) { + List list = mConstructorDetectors.get( + resolvedClass.getQualifiedName()); + if (list != null) { + for (VisitingDetector v : list) { + UastScanner javaPsiScanner = v.getUastScanner(); + if (javaPsiScanner != null) { + javaPsiScanner.visitConstructor(mContext, + v.getVisitor(), node, + mContext.getUastContext().getMethod(resolvedConstructor)); + } + } + } + } + } + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/UastLintUtils.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/UastLintUtils.java index c0d0bba54b4..32b8010bd47 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/UastLintUtils.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/UastLintUtils.java @@ -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 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; + } } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java index b1ec10e57f6..4a50c67d571 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java @@ -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. - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java index df780538370..8bcb56a39ff 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java @@ -22,7 +22,7 @@ import com.google.common.annotations.Beta; /** * A category is a container for related issues. - *

+ *

* NOTE: This is not a public or final API; if you rely on this be prepared * to adjust your code for the next tools release. */ @@ -107,7 +107,36 @@ public final class Category implements Comparable { } @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 { 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 { /** 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 { /** 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); } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java index 14cecdbf612..1465c5bb3a7 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java @@ -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. - *

+ *

* 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. */ @@ -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()); diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ConstantEvaluator.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ConstantEvaluator.java new file mode 100644 index 00000000000..270fabec730 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ConstantEvaluator.java @@ -0,0 +1,1941 @@ +/* + * 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_BYTE; +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_OBJECT; +import static com.android.tools.klint.client.api.JavaParser.TYPE_SHORT; +import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; +import static com.android.tools.klint.detector.api.JavaContext.getParentOfType; +import static org.jetbrains.uast.UastBinaryExpressionWithTypeKind.TYPE_CAST; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaParser.ResolvedField; +import com.android.tools.klint.client.api.JavaParser.ResolvedNode; +import com.android.tools.klint.client.api.UastLintUtils; +import com.google.common.collect.Lists; +import com.intellij.psi.JavaTokenType; +import com.intellij.psi.PsiArrayInitializerExpression; +import com.intellij.psi.PsiArrayType; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiBinaryExpression; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +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.PsiLiteral; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiParenthesizedExpression; +import com.intellij.psi.PsiPrefixExpression; +import com.intellij.psi.PsiPrimitiveType; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiTypeCastExpression; +import com.intellij.psi.PsiTypeElement; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; + +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UBinaryExpressionWithType; +import org.jetbrains.uast.UBlockExpression; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UParenthesizedExpression; +import org.jetbrains.uast.UPrefixExpression; +import org.jetbrains.uast.UResolvable; +import org.jetbrains.uast.UVariable; +import org.jetbrains.uast.UastBinaryOperator; +import org.jetbrains.uast.UastContext; +import org.jetbrains.uast.UastPrefixOperator; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import lombok.ast.ArrayCreation; +import lombok.ast.ArrayInitializer; +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.Node; +import lombok.ast.NullLiteral; +import lombok.ast.Select; +import lombok.ast.Statement; +import lombok.ast.StrictListAccessor; +import lombok.ast.StringLiteral; +import lombok.ast.TypeReference; +import lombok.ast.UnaryExpression; +import lombok.ast.UnaryOperator; +import lombok.ast.VariableDeclaration; +import lombok.ast.VariableDefinition; +import lombok.ast.VariableDefinitionEntry; +import lombok.ast.VariableReference; + +/** Evaluates constant expressions */ +public class ConstantEvaluator { + private final JavaContext mContext; + private boolean mAllowUnknown; + + /** + * Creates a new constant evaluator + * + * @param context the context to use to resolve field references, if any + */ + public ConstantEvaluator(@Nullable JavaContext context) { + mContext = context; + } + + /** + * Whether we allow computing values where some terms are unknown. For example, the expression + * {@code "foo" + x + "bar"} would return {@code null} without and {@code "foobar"} with. + * + * @return this for constructor chaining + */ + public ConstantEvaluator allowUnknowns() { + mAllowUnknown = true; + return this; + } + + /** + * Evaluates the given node and returns the constant value it resolves to, if any + * + * @param node the node to compute the constant value for + * @return the corresponding constant value - a String, an Integer, a Float, and so on + * @deprecated Use {@link #evaluate(PsiElement)} instead + */ + @Deprecated + @Nullable + public Object evaluate(@NonNull Node node) { + if (node instanceof NullLiteral) { + return null; + } else if (node instanceof BooleanLiteral) { + return ((BooleanLiteral)node).astValue(); + } else if (node instanceof StringLiteral) { + StringLiteral string = (StringLiteral) node; + return string.astValue(); + } else if (node instanceof CharLiteral) { + return ((CharLiteral)node).astValue(); + } 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 literal.astLongValue(); + } else { + return literal.astIntValue(); + } + } 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 literal.astFloatValue(); + } else { + return literal.astDoubleValue(); + } + } else if (node instanceof UnaryExpression) { + UnaryOperator operator = ((UnaryExpression) node).astOperator(); + Object operand = evaluate(((UnaryExpression) node).astOperand()); + if (operand == null) { + return null; + } + switch (operator) { + case LOGICAL_NOT: + if (operand instanceof Boolean) { + return !(Boolean) operand; + } + break; + case UNARY_PLUS: + return operand; + case BINARY_NOT: + if (operand instanceof Integer) { + return ~(Integer) operand; + } else if (operand instanceof Long) { + return ~(Long) operand; + } else if (operand instanceof Short) { + return ~(Short) operand; + } else if (operand instanceof Character) { + return ~(Character) operand; + } else if (operand instanceof Byte) { + return ~(Byte) operand; + } + break; + case UNARY_MINUS: + if (operand instanceof Integer) { + return -(Integer) operand; + } else if (operand instanceof Long) { + return -(Long) operand; + } else if (operand instanceof Double) { + return -(Double) operand; + } else if (operand instanceof Float) { + return -(Float) operand; + } else if (operand instanceof Short) { + return -(Short) operand; + } else if (operand instanceof Character) { + return -(Character) operand; + } else if (operand instanceof Byte) { + return -(Byte) operand; + } + break; + } + } else if (node instanceof InlineIfExpression) { + InlineIfExpression expression = (InlineIfExpression) node; + Object known = evaluate(expression.astCondition()); + if (known == Boolean.TRUE && expression.astIfTrue() != null) { + return evaluate(expression.astIfTrue()); + } else if (known == Boolean.FALSE && expression.astIfFalse() != null) { + return evaluate(expression.astIfFalse()); + } + } else if (node instanceof BinaryExpression) { + BinaryOperator operator = ((BinaryExpression) node).astOperator(); + Object operandLeft = evaluate(((BinaryExpression) node).astLeft()); + Object operandRight = evaluate(((BinaryExpression) node).astRight()); + if (operandLeft == null || operandRight == null) { + if (mAllowUnknown) { + if (operandLeft == null) { + return operandRight; + } else { + return operandLeft; + } + } + return null; + } + if (operandLeft instanceof String && operandRight instanceof String) { + if (operator == BinaryOperator.PLUS) { + return operandLeft.toString() + operandRight.toString(); + } + return null; + } else if (operandLeft instanceof Boolean && operandRight instanceof Boolean) { + boolean left = (Boolean) operandLeft; + boolean right = (Boolean) operandRight; + switch (operator) { + case LOGICAL_OR: + return left || right; + case LOGICAL_AND: + return left && right; + case BITWISE_OR: + return left | right; + case BITWISE_XOR: + return left ^ right; + case BITWISE_AND: + return left & right; + case EQUALS: + return left == right; + case NOT_EQUALS: + return left != right; + } + } else if (operandLeft instanceof Number && operandRight instanceof Number) { + Number left = (Number) operandLeft; + Number right = (Number) operandRight; + boolean isInteger = + !(left instanceof Float || left instanceof Double + || right instanceof Float || right instanceof Double); + boolean isWide = + isInteger ? (left instanceof Long || right instanceof Long) + : (left instanceof Double || right instanceof Double); + + switch (operator) { + case BITWISE_OR: + if (isWide) { + return left.longValue() | right.longValue(); + } else { + return left.intValue() | right.intValue(); + } + case BITWISE_XOR: + if (isWide) { + return left.longValue() ^ right.longValue(); + } else { + return left.intValue() ^ right.intValue(); + } + case BITWISE_AND: + if (isWide) { + return left.longValue() & right.longValue(); + } else { + return left.intValue() & right.intValue(); + } + case EQUALS: + if (isInteger) { + return left.longValue() == right.longValue(); + } else { + return left.doubleValue() == right.doubleValue(); + } + case NOT_EQUALS: + if (isInteger) { + return left.longValue() != right.longValue(); + } else { + return left.doubleValue() != right.doubleValue(); + } + case GREATER: + if (isInteger) { + return left.longValue() > right.longValue(); + } else { + return left.doubleValue() > right.doubleValue(); + } + case GREATER_OR_EQUAL: + if (isInteger) { + return left.longValue() >= right.longValue(); + } else { + return left.doubleValue() >= right.doubleValue(); + } + case LESS: + if (isInteger) { + return left.longValue() < right.longValue(); + } else { + return left.doubleValue() < right.doubleValue(); + } + case LESS_OR_EQUAL: + if (isInteger) { + return left.longValue() <= right.longValue(); + } else { + return left.doubleValue() <= right.doubleValue(); + } + case SHIFT_LEFT: + if (isWide) { + return left.longValue() << right.intValue(); + } else { + return left.intValue() << right.intValue(); + } + case SHIFT_RIGHT: + if (isWide) { + return left.longValue() >> right.intValue(); + } else { + return left.intValue() >> right.intValue(); + } + case BITWISE_SHIFT_RIGHT: + if (isWide) { + return left.longValue() >>> right.intValue(); + } else { + return left.intValue() >>> right.intValue(); + } + case PLUS: + if (isInteger) { + if (isWide) { + return left.longValue() + right.longValue(); + } else { + return left.intValue() + right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() + right.doubleValue(); + } else { + return left.floatValue() + right.floatValue(); + } + } + case MINUS: + if (isInteger) { + if (isWide) { + return left.longValue() - right.longValue(); + } else { + return left.intValue() - right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() - right.doubleValue(); + } else { + return left.floatValue() - right.floatValue(); + } + } + case MULTIPLY: + if (isInteger) { + if (isWide) { + return left.longValue() * right.longValue(); + } else { + return left.intValue() * right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() * right.doubleValue(); + } else { + return left.floatValue() * right.floatValue(); + } + } + case DIVIDE: + if (isInteger) { + if (isWide) { + return left.longValue() / right.longValue(); + } else { + return left.intValue() / right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() / right.doubleValue(); + } else { + return left.floatValue() / right.floatValue(); + } + } + case REMAINDER: + if (isInteger) { + if (isWide) { + return left.longValue() % right.longValue(); + } else { + return left.intValue() % right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() % right.doubleValue(); + } else { + return left.floatValue() % right.floatValue(); + } + } + default: + return null; + } + } + } else if (node instanceof Cast) { + Cast cast = (Cast)node; + Object operandValue = evaluate(cast.astOperand()); + if (operandValue instanceof Number) { + Number number = (Number)operandValue; + String typeName = cast.astTypeReference().getTypeName(); + if (typeName.equals("float")) { + return number.floatValue(); + } else if (typeName.equals("double")) { + return number.doubleValue(); + } else if (typeName.equals("int")) { + return number.intValue(); + } else if (typeName.equals("long")) { + return number.longValue(); + } else if (typeName.equals("short")) { + return number.shortValue(); + } else if (typeName.equals("byte")) { + return number.byteValue(); + } + } + return operandValue; + } else if (mContext != null && (node instanceof VariableReference || + node instanceof Select)) { + ResolvedNode resolved = mContext.resolve(node); + if (resolved instanceof ResolvedField) { + ResolvedField field = (ResolvedField) resolved; + Object value = field.getValue(); + if (value != null) { + return value; + } + Node astNode = field.findAstNode(); + if (astNode instanceof VariableDeclaration) { + VariableDeclaration declaration = (VariableDeclaration) astNode; + VariableDefinition definition = declaration.astDefinition(); + if (definition != null && definition.astModifiers().isFinal()) { + StrictListAccessor variables = + definition.astVariables(); + if (variables.size() == 1) { + VariableDefinitionEntry first = variables.first(); + if (first.astInitializer() != null) { + return evaluate(first.astInitializer()); + } + } + } + } + return null; + } else if (node instanceof VariableReference) { + Statement statement = getParentOfType(node, Statement.class, false); + if (statement != null) { + ListIterator 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 ArrayCreation) { + ArrayCreation creation = (ArrayCreation) node; + ArrayInitializer initializer = creation.astInitializer(); + if (initializer != null) { + TypeReference typeReference = creation.astComponentTypeReference(); + StrictListAccessor expressions = initializer + .astExpressions(); + List values = Lists.newArrayListWithExpectedSize(expressions.size()); + Class commonType = null; + for (Expression expression : expressions) { + Object value = evaluate(expression); + if (value != null) { + values.add(value); + if (commonType == null) { + commonType = value.getClass(); + } else { + while (!commonType.isAssignableFrom(value.getClass())) { + commonType = commonType.getSuperclass(); + } + } + } else if (!mAllowUnknown) { + // Inconclusive + return null; + } + } + if (!values.isEmpty()) { + Object o = Array.newInstance(commonType, values.size()); + return values.toArray((Object[]) o); + } else if (mContext != null) { + ResolvedNode type = mContext.resolve(typeReference); + System.out.println(type); + // TODO: return new array of this type + } + } else { + // something like "new byte[3]" but with no initializer. + String type = creation.astComponentTypeReference().toString(); + // TODO: Look up the size and only if small, use it. E.g. if it was byte[3] + // we could return a byte[3] array, but if it's say byte[1024*1024] we don't + // want to do that. + int size = 0; + if (TYPE_BYTE.equals(type)) { + return new byte[size]; + } + if (TYPE_BOOLEAN.equals(type)) { + return new boolean[size]; + } + if (TYPE_INT.equals(type)) { + return new int[size]; + } + if (TYPE_LONG.equals(type)) { + return new long[size]; + } + if (TYPE_CHAR.equals(type)) { + return new char[size]; + } + if (TYPE_FLOAT.equals(type)) { + return new float[size]; + } + if (TYPE_DOUBLE.equals(type)) { + return new double[size]; + } + if (TYPE_STRING.equals(type)) { + //noinspection SSBasedInspection + return new String[size]; + } + if (TYPE_SHORT.equals(type)) { + return new short[size]; + } + if (TYPE_OBJECT.equals(type)) { + //noinspection SSBasedInspection + return new Object[size]; + } + } + } + + // TODO: Check for MethodInvocation and perform some common operations - + // Math.* methods, String utility methods like notNullize, etc + + return null; + } + + /** + * Evaluates the given node and returns the constant value it resolves to, if any + * + * @param node the node to compute the constant value for + * @return the corresponding constant value - a String, an Integer, a Float, and so on + */ + @Nullable + public Object evaluate(@Nullable UElement node) { + if (node == null) { + return null; + } + + if (node instanceof ULiteralExpression) { + return ((ULiteralExpression) node).getValue(); + } else if (node instanceof UPrefixExpression) { + UastPrefixOperator operator = ((UPrefixExpression) node).getOperator(); + Object operand = evaluate(((UPrefixExpression) node).getOperand()); + if (operand == null) { + return null; + } + if (operator == UastPrefixOperator.LOGICAL_NOT) { + if (operand instanceof Boolean) { + return !(Boolean) operand; + } + } else if (operator == UastPrefixOperator.UNARY_PLUS) { + return operand; + } else if (operator == UastPrefixOperator.BITWISE_NOT) { + if (operand instanceof Integer) { + return ~(Integer) operand; + } else if (operand instanceof Long) { + return ~(Long) operand; + } else if (operand instanceof Short) { + return ~(Short) operand; + } else if (operand instanceof Character) { + return ~(Character) operand; + } else if (operand instanceof Byte) { + return ~(Byte) operand; + } + } else if (operator == UastPrefixOperator.UNARY_MINUS) { + if (operand instanceof Integer) { + return -(Integer) operand; + } else if (operand instanceof Long) { + return -(Long) operand; + } else if (operand instanceof Double) { + return -(Double) operand; + } else if (operand instanceof Float) { + return -(Float) operand; + } else if (operand instanceof Short) { + return -(Short) operand; + } else if (operand instanceof Character) { + return -(Character) operand; + } else if (operand instanceof Byte) { + return -(Byte) operand; + } + } + } else if (node instanceof UIfExpression + && ((UIfExpression) node).getExpressionType() != null) { + UIfExpression expression = (UIfExpression) node; + Object known = evaluate(expression.getCondition()); + if (known == Boolean.TRUE && expression.getThenExpression() != null) { + return evaluate(expression.getThenExpression()); + } else if (known == Boolean.FALSE && expression.getElseExpression() != null) { + return evaluate(expression.getElseExpression()); + } + } else if (node instanceof UParenthesizedExpression) { + UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) node; + UExpression expression = parenthesizedExpression.getExpression(); + return evaluate(expression); + } else if (node instanceof UBinaryExpression) { + UastBinaryOperator operator = ((UBinaryExpression) node).getOperator(); + Object operandLeft = evaluate(((UBinaryExpression) node).getLeftOperand()); + Object operandRight = evaluate(((UBinaryExpression) node).getRightOperand()); + if (operandLeft == null || operandRight == null) { + if (mAllowUnknown) { + if (operandLeft == null) { + return operandRight; + } else { + return operandLeft; + } + } + return null; + } + if (operandLeft instanceof String && operandRight instanceof String) { + if (operator == UastBinaryOperator.PLUS) { + return operandLeft.toString() + operandRight.toString(); + } + return null; + } else if (operandLeft instanceof Boolean && operandRight instanceof Boolean) { + boolean left = (Boolean) operandLeft; + boolean right = (Boolean) operandRight; + if (operator == UastBinaryOperator.LOGICAL_OR) { + return left || right; + } else if (operator == UastBinaryOperator.LOGICAL_AND) { + return left && right; + } else if (operator == UastBinaryOperator.BITWISE_OR) { + return left | right; + } else if (operator == UastBinaryOperator.BITWISE_XOR) { + return left ^ right; + } else if (operator == UastBinaryOperator.BITWISE_AND) { + return left & right; + } else if (operator == UastBinaryOperator.IDENTITY_EQUALS + || operator == UastBinaryOperator.EQUALS) { + return left == right; + } else if (operator == UastBinaryOperator.IDENTITY_NOT_EQUALS + || operator == UastBinaryOperator.NOT_EQUALS) { + return left != right; + } + } else if (operandLeft instanceof Number && operandRight instanceof Number) { + Number left = (Number) operandLeft; + Number right = (Number) operandRight; + boolean isInteger = + !(left instanceof Float || left instanceof Double + || right instanceof Float || right instanceof Double); + boolean isWide = + isInteger ? (left instanceof Long || right instanceof Long) + : (left instanceof Double || right instanceof Double); + + if (operator == UastBinaryOperator.BITWISE_OR) { + if (isWide) { + return left.longValue() | right.longValue(); + } else { + return left.intValue() | right.intValue(); + } + } else if (operator == UastBinaryOperator.BITWISE_XOR) { + if (isWide) { + return left.longValue() ^ right.longValue(); + } else { + return left.intValue() ^ right.intValue(); + } + } else if (operator == UastBinaryOperator.BITWISE_AND) { + if (isWide) { + return left.longValue() & right.longValue(); + } else { + return left.intValue() & right.intValue(); + } + } else if (operator == UastBinaryOperator.EQUALS + || operator == UastBinaryOperator.IDENTITY_EQUALS) { + if (isInteger) { + return left.longValue() == right.longValue(); + } else { + return left.doubleValue() == right.doubleValue(); + } + } else if (operator == UastBinaryOperator.NOT_EQUALS + || operator == UastBinaryOperator.IDENTITY_NOT_EQUALS) { + if (isInteger) { + return left.longValue() != right.longValue(); + } else { + return left.doubleValue() != right.doubleValue(); + } + } else if (operator == UastBinaryOperator.GREATER) { + if (isInteger) { + return left.longValue() > right.longValue(); + } else { + return left.doubleValue() > right.doubleValue(); + } + } else if (operator == UastBinaryOperator.GREATER_OR_EQUAL) { + if (isInteger) { + return left.longValue() >= right.longValue(); + } else { + return left.doubleValue() >= right.doubleValue(); + } + } else if (operator == UastBinaryOperator.LESS) { + if (isInteger) { + return left.longValue() < right.longValue(); + } else { + return left.doubleValue() < right.doubleValue(); + } + } else if (operator == UastBinaryOperator.LESS_OR_EQUAL) { + if (isInteger) { + return left.longValue() <= right.longValue(); + } else { + return left.doubleValue() <= right.doubleValue(); + } + } else if (operator == UastBinaryOperator.SHIFT_LEFT) { + if (isWide) { + return left.longValue() << right.intValue(); + } else { + return left.intValue() << right.intValue(); + } + } else if (operator == UastBinaryOperator.SHIFT_RIGHT) { + if (isWide) { + return left.longValue() >> right.intValue(); + } else { + return left.intValue() >> right.intValue(); + } + } else if (operator == UastBinaryOperator.UNSIGNED_SHIFT_RIGHT) { + if (isWide) { + return left.longValue() >>> right.intValue(); + } else { + return left.intValue() >>> right.intValue(); + } + } else if (operator == UastBinaryOperator.PLUS) { + if (isInteger) { + if (isWide) { + return left.longValue() + right.longValue(); + } else { + return left.intValue() + right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() + right.doubleValue(); + } else { + return left.floatValue() + right.floatValue(); + } + } + } else if (operator == UastBinaryOperator.MINUS) { + if (isInteger) { + if (isWide) { + return left.longValue() - right.longValue(); + } else { + return left.intValue() - right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() - right.doubleValue(); + } else { + return left.floatValue() - right.floatValue(); + } + } + } else if (operator == UastBinaryOperator.MULTIPLY) { + if (isInteger) { + if (isWide) { + return left.longValue() * right.longValue(); + } else { + return left.intValue() * right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() * right.doubleValue(); + } else { + return left.floatValue() * right.floatValue(); + } + } + } else if (operator == UastBinaryOperator.DIV) { + if (isInteger) { + if (isWide) { + return left.longValue() / right.longValue(); + } else { + return left.intValue() / right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() / right.doubleValue(); + } else { + return left.floatValue() / right.floatValue(); + } + } + } else if (operator == UastBinaryOperator.MOD) { + if (isInteger) { + if (isWide) { + return left.longValue() % right.longValue(); + } else { + return left.intValue() % right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() % right.doubleValue(); + } else { + return left.floatValue() % right.floatValue(); + } + } + } else { + return null; + } + } + } else if (node instanceof UBinaryExpressionWithType && + ((UBinaryExpressionWithType) node).getOperationKind() == TYPE_CAST) { + UBinaryExpressionWithType cast = (UBinaryExpressionWithType) node; + Object operandValue = evaluate(cast.getOperand()); + if (operandValue instanceof Number) { + Number number = (Number) operandValue; + PsiType type = cast.getType(); + if (PsiType.FLOAT.equals(type)) { + return number.floatValue(); + } else if (PsiType.DOUBLE.equals(type)) { + return number.doubleValue(); + } else if (PsiType.INT.equals(type)) { + return number.intValue(); + } else if (PsiType.LONG.equals(type)) { + return number.longValue(); + } else if (PsiType.SHORT.equals(type)) { + return number.shortValue(); + } else if (PsiType.BYTE.equals(type)) { + return number.byteValue(); + } + } + return operandValue; + } else if (node instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) node).resolve(); + if (resolved instanceof PsiVariable) { + PsiVariable variable = (PsiVariable) resolved; + Object value = UastLintUtils.findLastValue(variable, node, mContext, this); + + if (value != null) { + return value; + } + if (variable.getInitializer() != null) { + return evaluate(variable.getInitializer()); + } + return null; + } + } else if (UastExpressionUtils.isNewArrayWithDimensions((UExpression) node)) { + UCallExpression call = (UCallExpression) node; + PsiType arrayType = call.getExpressionType(); + if (arrayType instanceof PsiArrayType) { + PsiType componentType = ((PsiArrayType) arrayType).getComponentType(); + // Single-dimension array + if (!(componentType instanceof PsiArrayType) + && call.getValueArgumentCount() == 1) { + Object lengthObj = evaluate(call.getValueArguments().get(0)); + if (lengthObj instanceof Number) { + int length = ((Number) lengthObj).intValue(); + if (length > 30) { + length = 30; + } + if (componentType == PsiType.BOOLEAN) { + return new boolean[length]; + } else if (isObjectType(componentType)) { + return new Object[length]; + } else if (componentType == PsiType.CHAR) { + return new char[length]; + } else if (componentType == PsiType.BYTE) { + return new byte[length]; + } else if (componentType == PsiType.DOUBLE) { + return new double[length]; + } else if (componentType == PsiType.FLOAT) { + return new float[length]; + } else if (componentType == PsiType.INT) { + return new int[length]; + } else if (componentType == PsiType.SHORT) { + return new short[length]; + } else if (componentType == PsiType.LONG) { + return new long[length]; + } else if (isStringType(componentType)) { + return new String[length]; + } + } + } + } + } else if (UastExpressionUtils.isNewArrayWithInitializer(node)) { + UCallExpression call = (UCallExpression) node; + PsiType arrayType = call.getExpressionType(); + if (arrayType instanceof PsiArrayType) { + PsiType componentType = ((PsiArrayType) arrayType).getComponentType(); + // Single-dimension array + if (!(componentType instanceof PsiArrayType)) { + int length = call.getValueArgumentCount(); + List evaluatedArgs = new ArrayList(length); + for (UExpression arg : call.getValueArguments()) { + Object evaluatedArg = evaluate(arg); + if (!mAllowUnknown && evaluatedArg == null) { + // Inconclusive + return null; + } + evaluatedArgs.add(evaluatedArg); + } + + if (componentType == PsiType.BOOLEAN) { + boolean[] arr = new boolean[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Boolean) { + arr[i] = (Boolean) o; + } + } + return arr; + } else if (isObjectType(componentType)) { + Object[] arr = new Object[length]; + for (int i = 0; i < length; ++i) { + arr[i] = evaluatedArgs.get(i); + } + return arr; + } else if (componentType.equals(PsiType.CHAR)) { + char[] arr = new char[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Character) { + arr[i] = (Character) o; + } + } + return arr; + } else if (componentType.equals(PsiType.BYTE)) { + byte[] arr = new byte[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Byte) { + arr[i] = (Byte) o; + } + } + return arr; + } else if (componentType.equals(PsiType.DOUBLE)) { + double[] arr = new double[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Double) { + arr[i] = (Double) o; + } + } + return arr; + } else if (componentType.equals(PsiType.FLOAT)) { + float[] arr = new float[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Float) { + arr[i] = (Float) o; + } + } + return arr; + } else if (componentType.equals(PsiType.INT)) { + int[] arr = new int[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Integer) { + arr[i] = (Integer) o; + } + } + return arr; + } else if (componentType.equals(PsiType.SHORT)) { + short[] arr = new short[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Short) { + arr[i] = (Short) o; + } + } + return arr; + } else if (componentType.equals(PsiType.LONG)) { + long[] arr = new long[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof Long) { + arr[i] = (Long) o; + } + } + return arr; + } else if (isStringType(componentType)) { + String[] arr = new String[length]; + for (int i = 0; i < length; ++i) { + Object o = evaluatedArgs.get(i); + if (o instanceof String) { + arr[i] = (String) o; + } + } + return arr; + } + } + } + } + + if (node instanceof UExpression) { + Object evaluated = ((UExpression) node).evaluate(); + if (evaluated != null) { + return evaluated; + } + } + + // TODO: Check for MethodInvocation and perform some common operations - + // Math.* methods, String utility methods like notNullize, etc + + return null; + } + + private static boolean isStringType(PsiType type) { + if (!(type instanceof PsiClassType)) { + return false; + } + + PsiClass resolvedClass = ((PsiClassType) type).resolve(); + return resolvedClass != null && TYPE_STRING.equals(resolvedClass.getQualifiedName()); + } + + private static boolean isObjectType(PsiType type) { + if (!(type instanceof PsiClassType)) { + return false; + } + + PsiClass resolvedClass = ((PsiClassType) type).resolve(); + return resolvedClass != null && TYPE_OBJECT.equals(resolvedClass.getQualifiedName()); + } + + public static class LastAssignmentFinder extends AbstractUastVisitor { + private final PsiVariable mVariable; + private final UElement mEndAt; + + private final JavaContext mContext; + private final ConstantEvaluator mConstantEvaluator; + + private boolean mDone = false; + private int mCurrentLevel = 0; + private int mVariableLevel = -1; + private Object mCurrentValue; + private UElement mLastAssignment; + + public LastAssignmentFinder( + @NonNull PsiVariable variable, + @NonNull UElement endAt, + @NonNull JavaContext context, + @Nullable ConstantEvaluator constantEvaluator, + int variableLevel) { + mVariable = variable; + mEndAt = endAt; + UExpression initializer = context.getUastContext().getInitializerBody(variable); + mLastAssignment = initializer; + mContext = context; + mConstantEvaluator = constantEvaluator; + if (initializer != null && constantEvaluator != null) { + mCurrentValue = constantEvaluator.evaluate(initializer); + } + this.mVariableLevel = variableLevel; + } + + @Nullable + public Object getCurrentValue() { + return mCurrentValue; + } + + @Nullable + public UElement getLastAssignment() { + return mLastAssignment; + } + + @Override + public boolean visitElement(UElement node) { + if (!(node instanceof UBlockExpression)) { + mCurrentLevel++; + } + if (node.equals(mEndAt)) { + mDone = true; + } + return mDone || super.visitElement(node); + } + + @Override + public boolean visitVariable(UVariable node) { + if (mVariableLevel < 0 && node.equals(mVariable)) { + mVariableLevel = mCurrentLevel; + } + + return super.visitVariable(node); + } + + @Override + public void afterVisitBinaryExpression(UBinaryExpression node) { + if (!mDone + && node.getOperator() instanceof UastBinaryOperator.AssignOperator + && mVariableLevel >= 0) { + UExpression leftOperand = node.getLeftOperand(); + UastBinaryOperator operator = node.getOperator(); + + if (!(operator instanceof UastBinaryOperator.AssignOperator) + || !(leftOperand instanceof UResolvable)) { + return; + } + + PsiElement resolved = ((UResolvable) leftOperand).resolve(); + if (!mVariable.equals(resolved)) { + return; + } + + // Stop search if we see an assignment inside some conditional or loop statement. + if (mCurrentLevel > mVariableLevel) { + mLastAssignment = null; + mCurrentValue = null; + mDone = true; + } + + UExpression rightOperand = node.getRightOperand(); + ConstantEvaluator constantEvaluator = mConstantEvaluator; + + Object newExpression = (constantEvaluator != null) + ? constantEvaluator.evaluate(rightOperand) + : null; + + //TODO implement other assign operators + if (node.getOperator() == UastBinaryOperator.ASSIGN) { + mCurrentValue = newExpression; + mLastAssignment = rightOperand; + } else { + mCurrentValue = newExpression; + // Technically wrong, just reflect the old behaviour for now + mLastAssignment = rightOperand; + } + } + } + + @Override + public void afterVisitElement(UElement node) { + if (!(node instanceof UBlockExpression)) { + mCurrentLevel--; + } + super.afterVisitElement(node); + } + } + + /** + * Evaluates the given node and returns the constant value it resolves to, if any + * + * @param node the node to compute the constant value for + * @return the corresponding constant value - a String, an Integer, a Float, and so on + */ + @Nullable + public Object evaluate(@Nullable PsiElement node) { + if (node == null) { + return null; + } + if (node instanceof PsiLiteral) { + return ((PsiLiteral)node).getValue(); + } else if (node instanceof PsiPrefixExpression) { + IElementType operator = ((PsiPrefixExpression) node).getOperationTokenType(); + Object operand = evaluate(((PsiPrefixExpression) node).getOperand()); + if (operand == null) { + return null; + } + if (operator == JavaTokenType.EXCL) { + if (operand instanceof Boolean) { + return !(Boolean) operand; + } + } else if (operator == JavaTokenType.PLUS) { + return operand; + } else if (operator == JavaTokenType.TILDE) { + if (operand instanceof Integer) { + return ~(Integer) operand; + } else if (operand instanceof Long) { + return ~(Long) operand; + } else if (operand instanceof Short) { + return ~(Short) operand; + } else if (operand instanceof Character) { + return ~(Character) operand; + } else if (operand instanceof Byte) { + return ~(Byte) operand; + } + } else if (operator == JavaTokenType.MINUS) { + if (operand instanceof Integer) { + return -(Integer) operand; + } else if (operand instanceof Long) { + return -(Long) operand; + } else if (operand instanceof Double) { + return -(Double) operand; + } else if (operand instanceof Float) { + return -(Float) operand; + } else if (operand instanceof Short) { + return -(Short) operand; + } else if (operand instanceof Character) { + return -(Character) operand; + } else if (operand instanceof Byte) { + return -(Byte) operand; + } + } + } else if (node instanceof PsiConditionalExpression) { + PsiConditionalExpression expression = (PsiConditionalExpression) node; + Object known = evaluate(expression.getCondition()); + if (known == Boolean.TRUE && expression.getThenExpression() != null) { + return evaluate(expression.getThenExpression()); + } else if (known == Boolean.FALSE && expression.getElseExpression() != null) { + return evaluate(expression.getElseExpression()); + } + } else if (node instanceof PsiParenthesizedExpression) { + PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) node; + PsiExpression expression = parenthesizedExpression.getExpression(); + if (expression != null) { + return evaluate(expression); + } + } else if (node instanceof PsiBinaryExpression) { + IElementType operator = ((PsiBinaryExpression) node).getOperationTokenType(); + Object operandLeft = evaluate(((PsiBinaryExpression) node).getLOperand()); + Object operandRight = evaluate(((PsiBinaryExpression) node).getROperand()); + if (operandLeft == null || operandRight == null) { + if (mAllowUnknown) { + if (operandLeft == null) { + return operandRight; + } else { + return operandLeft; + } + } + return null; + } + if (operandLeft instanceof String && operandRight instanceof String) { + if (operator == JavaTokenType.PLUS) { + return operandLeft.toString() + operandRight.toString(); + } + return null; + } else if (operandLeft instanceof Boolean && operandRight instanceof Boolean) { + boolean left = (Boolean) operandLeft; + boolean right = (Boolean) operandRight; + if (operator == JavaTokenType.OROR) { + return left || right; + } else if (operator == JavaTokenType.ANDAND) { + return left && right; + } else if (operator == JavaTokenType.OR) { + return left | right; + } else if (operator == JavaTokenType.XOR) { + return left ^ right; + } else if (operator == JavaTokenType.AND) { + return left & right; + } else if (operator == JavaTokenType.EQEQ) { + return left == right; + } else if (operator == JavaTokenType.NE) { + return left != right; + } + } else if (operandLeft instanceof Number && operandRight instanceof Number) { + Number left = (Number) operandLeft; + Number right = (Number) operandRight; + boolean isInteger = + !(left instanceof Float || left instanceof Double + || right instanceof Float || right instanceof Double); + boolean isWide = + isInteger ? (left instanceof Long || right instanceof Long) + : (left instanceof Double || right instanceof Double); + + if (operator == JavaTokenType.OR) { + if (isWide) { + return left.longValue() | right.longValue(); + } else { + return left.intValue() | right.intValue(); + } + } else if (operator == JavaTokenType.XOR) { + if (isWide) { + return left.longValue() ^ right.longValue(); + } else { + return left.intValue() ^ right.intValue(); + } + } else if (operator == JavaTokenType.AND) { + if (isWide) { + return left.longValue() & right.longValue(); + } else { + return left.intValue() & right.intValue(); + } + } else if (operator == JavaTokenType.EQEQ) { + if (isInteger) { + return left.longValue() == right.longValue(); + } else { + return left.doubleValue() == right.doubleValue(); + } + } else if (operator == JavaTokenType.NE) { + if (isInteger) { + return left.longValue() != right.longValue(); + } else { + return left.doubleValue() != right.doubleValue(); + } + } else if (operator == JavaTokenType.GT) { + if (isInteger) { + return left.longValue() > right.longValue(); + } else { + return left.doubleValue() > right.doubleValue(); + } + } else if (operator == JavaTokenType.GE) { + if (isInteger) { + return left.longValue() >= right.longValue(); + } else { + return left.doubleValue() >= right.doubleValue(); + } + } else if (operator == JavaTokenType.LT) { + if (isInteger) { + return left.longValue() < right.longValue(); + } else { + return left.doubleValue() < right.doubleValue(); + } + } else if (operator == JavaTokenType.LE) { + if (isInteger) { + return left.longValue() <= right.longValue(); + } else { + return left.doubleValue() <= right.doubleValue(); + } + } else if (operator == JavaTokenType.LTLT) { + if (isWide) { + return left.longValue() << right.intValue(); + } else { + return left.intValue() << right.intValue(); + } + } else if (operator == JavaTokenType.GTGT) { + if (isWide) { + return left.longValue() >> right.intValue(); + } else { + return left.intValue() >> right.intValue(); + } + } else if (operator == JavaTokenType.GTGTGT) { + if (isWide) { + return left.longValue() >>> right.intValue(); + } else { + return left.intValue() >>> right.intValue(); + } + } else if (operator == JavaTokenType.PLUS) { + if (isInteger) { + if (isWide) { + return left.longValue() + right.longValue(); + } else { + return left.intValue() + right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() + right.doubleValue(); + } else { + return left.floatValue() + right.floatValue(); + } + } + } else if (operator == JavaTokenType.MINUS) { + if (isInteger) { + if (isWide) { + return left.longValue() - right.longValue(); + } else { + return left.intValue() - right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() - right.doubleValue(); + } else { + return left.floatValue() - right.floatValue(); + } + } + } else if (operator == JavaTokenType.ASTERISK) { + if (isInteger) { + if (isWide) { + return left.longValue() * right.longValue(); + } else { + return left.intValue() * right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() * right.doubleValue(); + } else { + return left.floatValue() * right.floatValue(); + } + } + } else if (operator == JavaTokenType.DIV) { + if (isInteger) { + if (isWide) { + return left.longValue() / right.longValue(); + } else { + return left.intValue() / right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() / right.doubleValue(); + } else { + return left.floatValue() / right.floatValue(); + } + } + } else if (operator == JavaTokenType.PERC) { + if (isInteger) { + if (isWide) { + return left.longValue() % right.longValue(); + } else { + return left.intValue() % right.intValue(); + } + } else { + if (isWide) { + return left.doubleValue() % right.doubleValue(); + } else { + return left.floatValue() % right.floatValue(); + } + } + } else { + return null; + } + } + } else if (node instanceof PsiTypeCastExpression) { + PsiTypeCastExpression cast = (PsiTypeCastExpression) node; + Object operandValue = evaluate(cast.getOperand()); + if (operandValue instanceof Number) { + Number number = (Number) operandValue; + PsiTypeElement typeElement = cast.getCastType(); + if (typeElement != null) { + PsiType type = typeElement.getType(); + if (PsiType.FLOAT.equals(type)) { + return number.floatValue(); + } else if (PsiType.DOUBLE.equals(type)) { + return number.doubleValue(); + } else if (PsiType.INT.equals(type)) { + return number.intValue(); + } else if (PsiType.LONG.equals(type)) { + return number.longValue(); + } else if (PsiType.SHORT.equals(type)) { + return number.shortValue(); + } else if (PsiType.BYTE.equals(type)) { + return number.byteValue(); + } + } + } + return operandValue; + } else if (node instanceof PsiReference) { + PsiElement resolved = ((PsiReference) node).resolve(); + if (resolved instanceof PsiField) { + PsiField field = (PsiField) resolved; + Object value = field.computeConstantValue(); + if (value != null) { + return value; + } + if (field.getInitializer() != null) { + return evaluate(field.getInitializer()); + } + return null; + } 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); + } + } + } + } else if (node instanceof PsiNewExpression) { + PsiNewExpression creation = (PsiNewExpression) node; + PsiArrayInitializerExpression initializer = creation.getArrayInitializer(); + PsiType type = creation.getType(); + if (type instanceof PsiArrayType) { + if (initializer != null) { + PsiExpression[] initializers = initializer.getInitializers(); + Class commonType = null; + List values = Lists.newArrayListWithExpectedSize(initializers.length); + int count = 0; + for (PsiExpression expression : initializers) { + Object value = evaluate(expression); + if (value != null) { + values.add(value); + if (commonType == null) { + commonType = value.getClass(); + } else { + while (!commonType.isAssignableFrom(value.getClass())) { + commonType = commonType.getSuperclass(); + } + } + } else if (!mAllowUnknown) { + // Inconclusive + return null; + } + count++; + if (count == 20) { // avoid large initializers + break; + } + } + type = type.getDeepComponentType(); + if (type == PsiType.INT) { + if (!values.isEmpty()) { + int[] array = new int[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Integer) { + array[i] = (Integer) o; + } + } + return array; + } + return new int[0]; + } else if (type == PsiType.BOOLEAN) { + if (!values.isEmpty()) { + boolean[] array = new boolean[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Boolean) { + array[i] = (Boolean) o; + } + } + return array; + } + return new boolean[0]; + } else if (type == PsiType.DOUBLE) { + if (!values.isEmpty()) { + double[] array = new double[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Double) { + array[i] = (Double) o; + } + } + return array; + } + return new double[0]; + } else if (type == PsiType.LONG) { + if (!values.isEmpty()) { + long[] array = new long[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Long) { + array[i] = (Long) o; + } + } + return array; + } + return new long[0]; + } else if (type == PsiType.FLOAT) { + if (!values.isEmpty()) { + float[] array = new float[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Float) { + array[i] = (Float) o; + } + } + return array; + } + return new float[0]; + } else if (type == PsiType.CHAR) { + if (!values.isEmpty()) { + char[] array = new char[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Character) { + array[i] = (Character) o; + } + } + return array; + } + return new char[0]; + } else if (type == PsiType.BYTE) { + if (!values.isEmpty()) { + byte[] array = new byte[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Byte) { + array[i] = (Byte) o; + } + } + return array; + } + return new byte[0]; + } else if (type == PsiType.SHORT) { + if (!values.isEmpty()) { + short[] array = new short[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object o = values.get(i); + if (o instanceof Short) { + array[i] = (Short) o; + } + } + return array; + } + return new short[0]; + } else { + if (!values.isEmpty()) { + Object o = Array.newInstance(commonType, values.size()); + return values.toArray((Object[]) o); + } + return null; + } + } else { + // something like "new byte[3]" but with no initializer. + // Look up the size and only if small, use it. E.g. if it was byte[3] + // we return a byte[3] array, but if it's say byte[1024*1024] we don't + // want to do that. + PsiExpression[] arrayDimensions = creation.getArrayDimensions(); + int size = 0; + if (arrayDimensions.length == 1) { + Object fixedSize = evaluate(arrayDimensions[0]); + if (fixedSize instanceof Number) { + size = ((Number)fixedSize).intValue(); + if (size > 30) { + size = 30; + } + } + } + type = type.getDeepComponentType(); + if (type instanceof PsiPrimitiveType) { + if (PsiType.BYTE.equals(type)) { + return new byte[size]; + } + if (PsiType.BOOLEAN.equals(type)) { + return new boolean[size]; + } + if (PsiType.INT.equals(type)) { + return new int[size]; + } + if (PsiType.LONG.equals(type)) { + return new long[size]; + } + if (PsiType.CHAR.equals(type)) { + return new char[size]; + } + if (PsiType.FLOAT.equals(type)) { + return new float[size]; + } + if (PsiType.DOUBLE.equals(type)) { + return new double[size]; + } + if (PsiType.SHORT.equals(type)) { + return new short[size]; + } + } else if (type instanceof PsiClassType) { + String className = type.getCanonicalText(); + if (TYPE_STRING.equals(className)) { + //noinspection SSBasedInspection + return new String[size]; + } + if (TYPE_OBJECT.equals(className)) { + //noinspection SSBasedInspection + return new Object[size]; + } + } + } + } + } + + // TODO: Check for MethodInvocation and perform some common operations - + // Math.* methods, String utility methods like notNullize, etc + + return null; + } + + /** + * Returns true if the node is pointing to a an array literal + */ + public static boolean isArrayLiteral(@Nullable UElement node, @NonNull JavaContext context) { + if (node instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) node).resolve(); + if (resolved instanceof PsiVariable) { + PsiVariable variable = (PsiVariable) resolved; + UExpression lastAssignment = + UastLintUtils.findLastAssignment(variable, node, context); + + if (lastAssignment != null) { + return isArrayLiteral(lastAssignment, context); + } + } + } else if (UastExpressionUtils.isNewArrayWithDimensions(node)) { + return true; + } else if (UastExpressionUtils.isNewArrayWithInitializer(node)) { + return true; + } else if (node instanceof UParenthesizedExpression) { + UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) node; + UExpression expression = parenthesizedExpression.getExpression(); + return isArrayLiteral(expression, context); + } else if (UastExpressionUtils.isTypeCast(node)) { + UBinaryExpressionWithType castExpression = (UBinaryExpressionWithType) node; + assert castExpression != null; + UExpression operand = castExpression.getOperand(); + return isArrayLiteral(operand, context); + } + + return false; + } + + /** + * Returns true if the node is pointing to a an array literal + */ + public static boolean isArrayLiteral(@Nullable PsiElement node) { + if (node instanceof PsiReference) { + PsiElement resolved = ((PsiReference) node).resolve(); + if (resolved instanceof PsiField) { + PsiField field = (PsiField) resolved; + if (field.getInitializer() != null) { + return isArrayLiteral(field.getInitializer()); + } + } 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 false; + } + while (prev != null) { + if (prev instanceof PsiDeclarationStatement) { + for (PsiElement element : ((PsiDeclarationStatement) prev) + .getDeclaredElements()) { + if (variable.equals(element)) { + return isArrayLiteral(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 isArrayLiteral(assign.getRExpression()); + } + } + } + } + prev = PsiTreeUtil.getPrevSiblingOfType(prev, + PsiStatement.class); + } + } + } + } else if (node instanceof PsiNewExpression) { + PsiNewExpression creation = (PsiNewExpression) node; + if (creation.getArrayInitializer() != null) { + return true; + } + PsiType type = creation.getType(); + if (type instanceof PsiArrayType) { + return true; + } + } else if (node instanceof PsiParenthesizedExpression) { + PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) node; + PsiExpression expression = parenthesizedExpression.getExpression(); + if (expression != null) { + return isArrayLiteral(expression); + } + } else if (node instanceof PsiTypeCastExpression) { + PsiTypeCastExpression castExpression = (PsiTypeCastExpression) node; + PsiExpression operand = castExpression.getOperand(); + if (operand != null) { + return isArrayLiteral(operand); + } + } + + return false; + } + + /** + * Evaluates the given node and returns the constant value it resolves to, if any. Convenience + * wrapper which creates a new {@linkplain ConstantEvaluator}, 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 constant value for + * @return the corresponding constant value - a String, an Integer, a Float, and so on + * @deprecated Use {@link #evaluate(JavaContext, PsiElement)} instead + */ + @Deprecated + @Nullable + public static Object evaluate(@NonNull JavaContext context, @NonNull Node node) { + return new ConstantEvaluator(context).evaluate(node); + } + + /** + * Evaluates the given node and returns the constant string it resolves to, if any. Convenience + * wrapper which creates a new {@linkplain ConstantEvaluator}, evaluates the node and returns + * the result if the result is a string. + * + * @param context the context to use to resolve field references, if any + * @param node the node to compute the constant value for + * @param allowUnknown whether we should construct the string even if some parts of it are + * unknown + * @return the corresponding string, if any + * @deprecated Use {@link #evaluateString(JavaContext, PsiElement, boolean)} instead + */ + @Deprecated + @Nullable + public static String evaluateString(@NonNull JavaContext context, @NonNull Node node, + boolean allowUnknown) { + ConstantEvaluator evaluator = new ConstantEvaluator(context); + if (allowUnknown) { + evaluator.allowUnknowns(); + } + Object value = evaluator.evaluate(node); + return value instanceof String ? (String) value : null; + } + + /** + * Evaluates the given node and returns the constant value it resolves to, if any. Convenience + * wrapper which creates a new {@linkplain ConstantEvaluator}, 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 constant value for + * @return the corresponding constant value - a String, an Integer, a Float, and so on + */ + @Nullable + public static Object evaluate(@Nullable JavaContext context, @NonNull PsiElement node) { + return new ConstantEvaluator(context).evaluate(node); + } + + /** + * Evaluates the given node and returns the constant value it resolves to, if any. Convenience + * wrapper which creates a new {@linkplain ConstantEvaluator}, 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 constant value for + * @return the corresponding constant value - a String, an Integer, a Float, and so on + */ + @Nullable + public static Object evaluate(@Nullable JavaContext context, @NonNull UElement node) { + return new ConstantEvaluator(context).evaluate(node); + } + + /** + * Evaluates the given node and returns the constant string it resolves to, if any. Convenience + * wrapper which creates a new {@linkplain ConstantEvaluator}, evaluates the node and returns + * the result if the result is a string. + * + * @param context the context to use to resolve field references, if any + * @param node the node to compute the constant value for + * @param allowUnknown whether we should construct the string even if some parts of it are + * unknown + * @return the corresponding string, if any + */ + @Nullable + public static String evaluateString(@Nullable JavaContext context, @NonNull PsiElement node, + boolean allowUnknown) { + ConstantEvaluator evaluator = new ConstantEvaluator(context); + if (allowUnknown) { + evaluator.allowUnknowns(); + } + Object value = evaluator.evaluate(node); + return value instanceof String ? (String) value : null; + } + + /** + * Evaluates the given node and returns the constant string it resolves to, if any. Convenience + * wrapper which creates a new {@linkplain ConstantEvaluator}, evaluates the node and returns + * the result if the result is a string. + * + * @param context the context to use to resolve field references, if any + * @param node the node to compute the constant value for + * @param allowUnknown whether we should construct the string even if some parts of it are + * unknown + * @return the corresponding string, if any + */ + @Nullable + public static String evaluateString(@Nullable JavaContext context, @NonNull UElement node, + boolean allowUnknown) { + ConstantEvaluator evaluator = new ConstantEvaluator(context); + if (allowUnknown) { + evaluator.allowUnknowns(); + } + Object value = evaluator.evaluate(node); + return value instanceof String ? (String) value : null; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java index b977415d60d..9d87faef96f 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java @@ -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; diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java index 1ffd3f62b70..b2d55c5ef8e 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java @@ -19,24 +19,48 @@ package com.android.tools.klint.detector.api; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +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.LintDriver; +import com.android.tools.klint.client.api.UElementVisitor; import com.google.common.annotations.Beta; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiNewExpression; + +import com.intellij.psi.PsiTypeParameter; + import org.jetbrains.uast.UCallExpression; import org.jetbrains.uast.UClass; import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UFunction; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.expressions.UReferenceExpression; import org.jetbrains.uast.visitor.UastVisitor; -import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode; -import org.jetbrains.org.objectweb.asm.tree.ClassNode; -import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode; -import org.jetbrains.org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import java.io.File; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import lombok.ast.AstVisitor; +import lombok.ast.ClassDeclaration; +import lombok.ast.ConstructorInvocation; +import lombok.ast.MethodInvocation; +import lombok.ast.Node; /** * A detector is able to find a particular problem (or a set of related problems). @@ -63,6 +87,811 @@ import java.util.*; */ @Beta public abstract class Detector { + /** + * Specialized interface for detectors that scan Java source file parse trees + * @deprecated Use {@link JavaPsiScanner} instead + */ + @Deprecated @SuppressWarnings("unused") // Still here for third-party rules + public interface JavaScanner { + /** + * Create a parse tree visitor to process the parse tree. All + * {@link JavaScanner} detectors must provide a visitor, unless they + * either return true from {@link #appliesToResourceRefs()} or return + * non null from {@link #getApplicableMethodNames()}. + *

+ * If you return specific AST node types from + * {@link #getApplicableNodeTypes()}, then the visitor will only + * be called for the specific requested node types. This is more + * efficient, since it allows many detectors that apply to only a small + * part of the AST (such as method call nodes) to share iteration of the + * majority of the parse tree. + *

+ * If you return null from {@link #getApplicableNodeTypes()}, then your + * visitor will be called from the top and all node types visited. + *

+ * Note that a new visitor is created for each separate compilation + * unit, so you can store per file state in the visitor. + * + * @param context the {@link Context} for the file being analyzed + * @return a visitor, or null. + */ + @Nullable + AstVisitor createJavaVisitor(@NonNull JavaContext context); + + /** + * Return the types of AST nodes that the visitor returned from + * {@link #createJavaVisitor(JavaContext)} should visit. See the + * documentation for {@link #createJavaVisitor(JavaContext)} for details + * on how the shared visitor is used. + *

+ * If you return null from this method, then the visitor will process + * the full tree instead. + *

+ * Note that for the shared visitor, the return codes from the visit + * methods are ignored: returning true will not prune iteration + * of the subtree, since there may be other node types interested in the + * children. If you need to ensure that your visitor only processes a + * part of the tree, use a full visitor instead. See the + * OverdrawDetector implementation for an example of this. + * + * @return the list of applicable node types (AST node classes), or null + */ + @Nullable + List> getApplicableNodeTypes(); + + /** + * Return the list of method names this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a method call in the list will be passed to the + * {@link #visitMethod(JavaContext, AstVisitor, MethodInvocation)} + * method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed calls. + * For example, the StringFormatDetector uses this mechanism to look for + * "format" calls, and when found it looks around (using the AST's + * {@link Node#getParent()} method) to see if it's called on + * a String class instance, and if so do its normal processing. Note + * that since it doesn't need to do any other AST processing, that + * detector does not actually supply a visitor. + * + * @return a set of applicable method names, or null. + */ + @Nullable + List getApplicableMethodNames(); + + /** + * Method invoked for any method calls found that matches any names + * returned by {@link #getApplicableMethodNames()}. This also passes + * back the visitor that was created by + * {@link #createJavaVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createJavaVisitor(JavaContext)}, or null + * @param node the {@link MethodInvocation} node for the invoked method + */ + void visitMethod( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull MethodInvocation node); + + /** + * Return the list of constructor types this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a constructor call in the list will be passed to the + * {@link #visitConstructor(JavaContext, AstVisitor, ConstructorInvocation, ResolvedMethod)} + * method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed constructors. + * + * @return a set of applicable fully qualified types, or null. + */ + @Nullable + List getApplicableConstructorTypes(); + + /** + * Method invoked for any constructor calls found that matches any names + * returned by {@link #getApplicableConstructorTypes()}. This also passes + * back the visitor that was created by + * {@link #createJavaVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createJavaVisitor(JavaContext)}, or null + * @param node the {@link ConstructorInvocation} node for the invoked method + * @param constructor the resolved constructor method with type information + */ + void visitConstructor( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull ConstructorInvocation node, + @NonNull ResolvedMethod constructor); + + /** + * Returns whether this detector cares about Android resource references + * (such as {@code R.layout.main} or {@code R.string.app_name}). If it + * does, then the visitor will look for these patterns, and if found, it + * will invoke {@link #visitResourceReference} passing the resource type + * and resource name. It also passes the visitor, if any, that was + * created by {@link #createJavaVisitor(JavaContext)}, such that a + * detector can do more than just look for resources. + * + * @return true if this detector wants to be notified of R resource + * identifiers found in the code. + */ + boolean appliesToResourceRefs(); + + /** + * Called for any resource references (such as {@code R.layout.main} + * found in Java code, provided this detector returned {@code true} from + * {@link #appliesToResourceRefs()}. + * + * @param context the lint scanning context + * @param visitor the visitor created from + * {@link #createJavaVisitor(JavaContext)}, or null + * @param node the variable reference for the resource + * @param type the resource type, such as "layout" or "string" + * @param name the resource name, such as "main" from + * {@code R.layout.main} + * @param isFramework whether the resource is a framework resource + * (android.R) or a local project resource (R) + */ + void visitResourceReference( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull Node node, + @NonNull String type, + @NonNull String name, + boolean isFramework); + + /** + * Returns a list of fully qualified names for super classes that this + * detector cares about. If not null, this detector will *only* be called + * if the current class is a subclass of one of the specified superclasses. + * + * @return a list of fully qualified names + */ + @Nullable + List applicableSuperClasses(); + + /** + * Called for each class that extends one of the super classes specified with + * {@link #applicableSuperClasses()} + * + * @param context the lint scanning context + * @param declaration the class declaration node, or null for anonymous classes + * @param node the class declaration node or the anonymous class construction node + * @param resolvedClass the resolved class + */ + // TODO: Change signature to pass in the NormalTypeBody instead of the plain Node? + void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration, + @NonNull Node node, @NonNull ResolvedClass resolvedClass); + } + + /** + Interface to be implemented by lint detectors that want to analyze + Java source files. +

+ The Lint Java API sits on top of IntelliJ IDEA's "PSI" API, an API + which exposes the underlying abstract syntax tree as well as providing + core services like resolving symbols. +

+ This new API replaces the older Lombok AST API that was used for Java + source checks. Migrating a check from the Lombok APIs to the new PSI + based APIs is relatively straightforward. +

+ First, replace "implements JavaScanner" with "implements + JavaPsiScanner" in your detector signature, and then locate all the + JavaScanner methods your detector was overriding and replace them with + the new corresponding signatures. +

+ For example, replace +

+     {@code List> getApplicableNodeTypes();}
+     
+ with +
+     {@code List> getApplicablePsiTypes();}
+     
+ and replace +
+     void visitMethod(
+     @NonNull JavaContext context,
+     @Nullable AstVisitor visitor,
+     @NonNull MethodInvocation node);
+     
+ with +
+     void visitMethod(
+     @NonNull JavaContext context,
+     @Nullable JavaElementVisitor visitor,
+     @NonNull PsiMethodCallExpression call,
+     @NonNull PsiMethod method);
+     
+ and so on. +

+ Finally, replace the various Lombok iteration code with PSI based + code. Both Lombok and PSI used class names that closely resemble the + Java language specification, so guessing the corresponding names is + straightforward; here are some examples: +

+     ClassDeclaration ⇒ PsiClass
+     MethodDeclaration ⇒ PsiMethod
+     MethodInvocation ⇒ PsiMethodCallExpression
+     ConstructorInvocation ⇒ PsiNewExpression
+     If ⇒ PsiIfStatement
+     For ⇒ PsiForStatement
+     Continue ⇒ PsiContinueStatement
+     StringLiteral ⇒ PsiLiteral
+     IntegralLiteral ⇒ PsiLiteral
+     ... etc
+     
+ Lombok AST had no support for symbol and type resolution, so lint + added its own separate API to support (the "ResolvedNode" + hierarchy). This is no longer needed since PSI supports it directly + (for example, on a PsiMethodCallExpression you just call + "resolveMethod" to get the PsiMethod the method calls, and on a + PsiExpression you just call getType() to get the corresponding +

+ The old ResolvedNode interface was written for lint so it made certain + kinds of common checks very easy. To help make porting lint rules from + the old API easier, and to make writing future lint checks easier + too), there is a new helper class, "JavaEvaluator" (which you can + obtain from JavaContext). This lets you for example quickly check + whether a given method is a member of a subclass of a given class, or + whether a method has a certain set of parameters, etc. It also makes + it easy to check whether a given method is private, abstract or + static, and so on. (And most of its parameters are nullable which + makes it simpler to use; you don't have to null check resolve results + before calling into it.) +

+ Some further porting tips: +

    +
  • Make sure you don't call toString() on nodes to get their + contents. In Lombok, toString returned the underlying source + text. In PSI, call getText() instead, since toString() is meant for + debugging and includes node types etc. + +
  • ResolvedClass#getName() used to return *qualified* name. In PSI, + PsiClass#getName() returns just the simple name, so call + #getQualifiedName() instead if that's what your code needs! Node + also that PsiClassType#getClassName() returns the simple name; if + you want the fully qualified name, call PsiType#getCanonicalText(). + +
  • Lombok didn't distinguish between a local variable declaration, a + parameter and a field declaration. These are all different in PSI, + so when writing visitors, make sure you replace a single + visitVariableDeclaration with visitField, visitLocalVariable and + visitParameter methods as applicable. + +
  • Note that when lint runs in the IDE, there may be extra PSI nodes in + the hierarchy representing whitespace as well as parentheses. Watch + out for this when calling getParent, getPrevSibling or + getNextSibling - don't just go one level up and check instanceof + {@code }; instead, use LintUtils.skipParentheses (or the + corresponding methods to skip whitespace left and right.) Note that + when you write lint unit tests, the infrastructure will run your + tests twice, one with a normal AST and once where it has inserted + whitespace and parentheses everywhere, and it asserts that you come + up with the same analysis results. (This caught 16 failing tests + across 7 different detectors.) + +
  • Annotation handling is a bit different. In ResolvedAnnotations I had + (for convenience) inlined things like annotations on the class; you + now have to resolve the annotation name reference to the + corresponding annotation class and look there. +
+ + Some additional conversion examples: replace +
+     @Override
+     public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
+     @NonNull MethodInvocation node) {
+         ResolvedNode resolved = context.resolve(node);
+         if (resolved instanceof ResolvedMethod) {
+             ResolvedMethod method = (ResolvedMethod) resolved;
+             if (method.getContainingClass().matches("android.os.Parcel")) {
+                 ...
+     
+ with +
+     @Override
+     public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
+             @NonNull PsiCall node) {
+         if (method != null && method.getContainingClass() != null &&
+             "android.os.Parcel".equals(method.getContainingClass().getQualifiedName())) {
+             ....
+     
+ + Similarly: +
+     if (method.getArgumentCount() != 2
+             || !method.getArgumentType(0).matchesName(TYPE_OBJECT)
+             || !method.getArgumentType(1).matchesName(TYPE_STRING)) {
+         return;
+     }
+     
+ can now be written as +
+     JavaEvaluator resolver = context.getEvaluator();
+     if (!resolver.methodMatches(method, WEB_VIEW, true, TYPE_OBJECT, TYPE_STRING)) {
+         return;
+     }
+     
+ Finally, note that many deprecated methods in lint itself point to the replacement + methods, see for example {@link JavaContext#findSurroundingMethod(Node)}. + */ + public interface JavaPsiScanner { + /** + * Create a parse tree visitor to process the parse tree. All + * {@link JavaScanner} detectors must provide a visitor, unless they + * either return true from {@link #appliesToResourceRefs()} or return + * non null from {@link #getApplicableMethodNames()}. + *

+ * If you return specific AST node types from + * {@link #getApplicablePsiTypes()}, then the visitor will only + * be called for the specific requested node types. This is more + * efficient, since it allows many detectors that apply to only a small + * part of the AST (such as method call nodes) to share iteration of the + * majority of the parse tree. + *

+ * If you return null from {@link #getApplicablePsiTypes()}, then your + * visitor will be called from the top and all node types visited. + *

+ * Note that a new visitor is created for each separate compilation + * unit, so you can store per file state in the visitor. + *

+ * + * NOTE: Your visitor should NOT extend JavaRecursiveElementVisitor. + * Your visitor should only visit the current node type; the infrastructure + * will do the recursion. (Lint's unit test infrastructure will check and + * enforce this restriction.) + * + * + * @param context the {@link Context} for the file being analyzed + * @return a visitor, or null. + */ + @Nullable + JavaElementVisitor createPsiVisitor(@NonNull JavaContext context); + + /** + * Return the types of AST nodes that the visitor returned from + * {@link #createJavaVisitor(JavaContext)} should visit. See the + * documentation for {@link #createJavaVisitor(JavaContext)} for details + * on how the shared visitor is used. + *

+ * If you return null from this method, then the visitor will process + * the full tree instead. + *

+ * Note that for the shared visitor, the return codes from the visit + * methods are ignored: returning true will not prune iteration + * of the subtree, since there may be other node types interested in the + * children. If you need to ensure that your visitor only processes a + * part of the tree, use a full visitor instead. See the + * OverdrawDetector implementation for an example of this. + * + * @return the list of applicable node types (AST node classes), or null + */ + @Nullable + List> getApplicablePsiTypes(); + + /** + * Return the list of method names this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a method call in the list will be passed to the + * {@link #visitMethod(JavaContext, JavaElementVisitor, PsiMethodCallExpression, PsiMethod)} + * method for processing. The visitor created by + * {@link #createPsiVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed calls. + * For example, the StringFormatDetector uses this mechanism to look for + * "format" calls, and when found it looks around (using the AST's + * {@link PsiElement#getParent()} method) to see if it's called on + * a String class instance, and if so do its normal processing. Note + * that since it doesn't need to do any other AST processing, that + * detector does not actually supply a visitor. + * + * @return a set of applicable method names, or null. + */ + @Nullable + List getApplicableMethodNames(); + + /** + * Method invoked for any method calls found that matches any names + * returned by {@link #getApplicableMethodNames()}. This also passes + * back the visitor that was created by + * {@link #createJavaVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createPsiVisitor(JavaContext)}, or null + * @param call the {@link PsiMethodCallExpression} node for the invoked method + * @param method the {@link PsiMethod} being called + */ + void visitMethod( + @NonNull JavaContext context, + @Nullable JavaElementVisitor visitor, + @NonNull PsiMethodCallExpression call, + @NonNull PsiMethod method); + + /** + * Return the list of constructor types this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a constructor call in the list will be passed to the + * {@link #visitConstructor(JavaContext, JavaElementVisitor, PsiNewExpression, PsiMethod)} + * method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed constructors. + * + * @return a set of applicable fully qualified types, or null. + */ + @Nullable + List getApplicableConstructorTypes(); + + /** + * Method invoked for any constructor calls found that matches any names + * returned by {@link #getApplicableConstructorTypes()}. This also passes + * back the visitor that was created by + * {@link #createPsiVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createPsiVisitor(JavaContext)}, or null + * @param node the {@link PsiNewExpression} node for the invoked method + * @param constructor the called constructor method + */ + void visitConstructor( + @NonNull JavaContext context, + @Nullable JavaElementVisitor visitor, + @NonNull PsiNewExpression node, + @NonNull PsiMethod constructor); + + /** + * Return the list of reference names types this detector is interested in, or null. If this + * method returns non-null, then any AST elements that match a reference in the list will be + * passed to the {@link #visitReference(JavaContext, JavaElementVisitor, + * PsiJavaCodeReferenceElement, PsiElement)} method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that method, although it can be + * null.

This makes it easy to write detectors that focus on some fixed references. + * + * @return a set of applicable reference names, or null. + */ + @Nullable + List getApplicableReferenceNames(); + + /** + * Method invoked for any references found that matches any names returned by {@link + * #getApplicableReferenceNames()}. This also passes back the visitor that was created by + * {@link #createPsiVisitor(JavaContext)}, but a visitor is not required. It is intended for + * detectors that need to do additional AST processing, but also want the convenience of not + * having to look for method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from {@link #createPsiVisitor(JavaContext)}, or + * null + * @param reference the {@link PsiJavaCodeReferenceElement} element + * @param referenced the referenced element + */ + void visitReference( + @NonNull JavaContext context, + @Nullable JavaElementVisitor visitor, + @NonNull PsiJavaCodeReferenceElement reference, + @NonNull PsiElement referenced); + + /** + * Returns whether this detector cares about Android resource references + * (such as {@code R.layout.main} or {@code R.string.app_name}). If it + * does, then the visitor will look for these patterns, and if found, it + * will invoke {@link #visitResourceReference} passing the resource type + * and resource name. It also passes the visitor, if any, that was + * created by {@link #createJavaVisitor(JavaContext)}, such that a + * detector can do more than just look for resources. + * + * @return true if this detector wants to be notified of R resource + * identifiers found in the code. + */ + boolean appliesToResourceRefs(); + + /** + * Called for any resource references (such as {@code R.layout.main} + * found in Java code, provided this detector returned {@code true} from + * {@link #appliesToResourceRefs()}. + * + * @param context the lint scanning context + * @param visitor the visitor created from + * {@link #createPsiVisitor(JavaContext)}, or null + * @param node the variable reference for the resource + * @param type the resource type, such as "layout" or "string" + * @param name the resource name, such as "main" from + * {@code R.layout.main} + * @param isFramework whether the resource is a framework resource + * (android.R) or a local project resource (R) + */ + void visitResourceReference( + @NonNull JavaContext context, + @Nullable JavaElementVisitor visitor, + @NonNull PsiElement node, + @NonNull ResourceType type, + @NonNull String name, + boolean isFramework); + + /** + * Returns a list of fully qualified names for super classes that this + * detector cares about. If not null, this detector will only be called + * if the current class is a subclass of one of the specified superclasses. + * + * @return a list of fully qualified names + */ + @Nullable + List applicableSuperClasses(); + + /** + * Called for each class that extends one of the super classes specified with + * {@link #applicableSuperClasses()}. + *

+ * Note: This method will not be called for {@link PsiTypeParameter} classes. These + * aren't really classes in the sense most lint detectors think of them, so these + * are excluded to avoid having lint checks that don't defensively code for these + * accidentally report errors on type parameters. If you really need to check these, + * use {@link #getApplicablePsiTypes} with {@code PsiTypeParameter.class} instead. + * + * @param context the lint scanning context + * @param declaration the class declaration node, or null for anonymous classes + */ + void checkClass(@NonNull JavaContext context, @NonNull PsiClass declaration); + } + + public interface UastScanner { + /** + * Create a parse tree visitor to process the parse tree. All + * {@link JavaScanner} detectors must provide a visitor, unless they + * either return true from {@link #appliesToResourceRefs()} or return + * non null from {@link #getApplicableMethodNames()}. + *

+ * If you return specific AST node types from + * {@link #getApplicablePsiTypes()}, then the visitor will only + * be called for the specific requested node types. This is more + * efficient, since it allows many detectors that apply to only a small + * part of the AST (such as method call nodes) to share iteration of the + * majority of the parse tree. + *

+ * If you return null from {@link #getApplicablePsiTypes()}, then your + * visitor will be called from the top and all node types visited. + *

+ * Note that a new visitor is created for each separate compilation + * unit, so you can store per file state in the visitor. + *

+ * + * NOTE: Your visitor should NOT extend JavaRecursiveElementVisitor. + * Your visitor should only visit the current node type; the infrastructure + * will do the recursion. (Lint's unit test infrastructure will check and + * enforce this restriction.) + * + * + * @param context the {@link Context} for the file being analyzed + * @return a visitor, or null. + */ + @Nullable + UastVisitor createUastVisitor(@NonNull JavaContext context); + + /** + * Return the types of AST nodes that the visitor returned from + * {@link #createJavaVisitor(JavaContext)} should visit. See the + * documentation for {@link #createJavaVisitor(JavaContext)} for details + * on how the shared visitor is used. + *

+ * If you return null from this method, then the visitor will process + * the full tree instead. + *

+ * Note that for the shared visitor, the return codes from the visit + * methods are ignored: returning true will not prune iteration + * of the subtree, since there may be other node types interested in the + * children. If you need to ensure that your visitor only processes a + * part of the tree, use a full visitor instead. See the + * OverdrawDetector implementation for an example of this. + * + * @return the list of applicable node types (AST node classes), or null + */ + @Nullable + List> getApplicableUastTypes(); + + @Nullable + List> getApplicablePsiTypes(); + + /** + * Return the list of method names this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a method call in the list will be passed to the + * {@link #visitMethod(JavaContext, JavaElementVisitor, PsiMethodCallExpression, PsiMethod)} + * method for processing. The visitor created by + * {@link #createPsiVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed calls. + * For example, the StringFormatDetector uses this mechanism to look for + * "format" calls, and when found it looks around (using the AST's + * {@link PsiElement#getParent()} method) to see if it's called on + * a String class instance, and if so do its normal processing. Note + * that since it doesn't need to do any other AST processing, that + * detector does not actually supply a visitor. + * + * @return a set of applicable method names, or null. + */ + @Nullable + List getApplicableMethodNames(); + + /** + * Method invoked for any method calls found that matches any names + * returned by {@link #getApplicableMethodNames()}. This also passes + * back the visitor that was created by + * {@link #createJavaVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createPsiVisitor(JavaContext)}, or null + * @param node the {@link PsiMethodCallExpression} node for the invoked method + * @param method the {@link PsiMethod} being called + */ + void visitMethod( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UCallExpression node, + @NonNull UMethod method); + + /** + * Return the list of constructor types this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a constructor call in the list will be passed to the + * {@link #visitConstructor(JavaContext, JavaElementVisitor, PsiNewExpression, PsiMethod)} + * method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed constructors. + * + * @return a set of applicable fully qualified types, or null. + */ + @Nullable + List getApplicableConstructorTypes(); + + /** + * Method invoked for any constructor calls found that matches any names + * returned by {@link #getApplicableConstructorTypes()}. This also passes + * back the visitor that was created by + * {@link #createPsiVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createPsiVisitor(JavaContext)}, or null + * @param node the {@link PsiNewExpression} node for the invoked method + * @param constructor the called constructor method + */ + void visitConstructor( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UCallExpression node, + @NonNull UMethod constructor); + + /** + * Return the list of reference names types this detector is interested in, or null. If this + * method returns non-null, then any AST elements that match a reference in the list will be + * passed to the {@link #visitReference(JavaContext, JavaElementVisitor, + * PsiJavaCodeReferenceElement, PsiElement)} method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that method, although it can be + * null.

This makes it easy to write detectors that focus on some fixed references. + * + * @return a set of applicable reference names, or null. + */ + @Nullable + List getApplicableReferenceNames(); + + /** + * Method invoked for any references found that matches any names returned by {@link + * #getApplicableReferenceNames()}. This also passes back the visitor that was created by + * {@link #createPsiVisitor(JavaContext)}, but a visitor is not required. It is intended for + * detectors that need to do additional AST processing, but also want the convenience of not + * having to look for method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from {@link #createPsiVisitor(JavaContext)}, or + * null + * @param reference the {@link PsiJavaCodeReferenceElement} element + * @param referenced the referenced element + */ + void visitReference( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UReferenceExpression reference, + @NonNull PsiElement referenced); + + /** + * Returns whether this detector cares about Android resource references + * (such as {@code R.layout.main} or {@code R.string.app_name}). If it + * does, then the visitor will look for these patterns, and if found, it + * will invoke {@link #visitResourceReference} passing the resource type + * and resource name. It also passes the visitor, if any, that was + * created by {@link #createJavaVisitor(JavaContext)}, such that a + * detector can do more than just look for resources. + * + * @return true if this detector wants to be notified of R resource + * identifiers found in the code. + */ + boolean appliesToResourceRefs(); + + /** + * Called for any resource references (such as {@code R.layout.main} + * found in Java code, provided this detector returned {@code true} from + * {@link #appliesToResourceRefs()}. + * + * @param context the lint scanning context + * @param visitor the visitor created from + * {@link #createPsiVisitor(JavaContext)}, or null + * @param node the variable reference for the resource + * @param type the resource type, such as "layout" or "string" + * @param name the resource name, such as "main" from + * {@code R.layout.main} + * @param isFramework whether the resource is a framework resource + * (android.R) or a local project resource (R) + */ + void visitResourceReference( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UElement node, + @NonNull ResourceType type, + @NonNull String name, + boolean isFramework); + + /** + * Returns a list of fully qualified names for super classes that this + * detector cares about. If not null, this detector will only be called + * if the current class is a subclass of one of the specified superclasses. + * + * @return a list of fully qualified names + */ + @Nullable + List applicableSuperClasses(); + + /** + * Called for each class that extends one of the super classes specified with + * {@link #applicableSuperClasses()}. + *

+ * Note: This method will not be called for {@link PsiTypeParameter} classes. These + * aren't really classes in the sense most lint detectors think of them, so these + * are excluded to avoid having lint checks that don't defensively code for these + * accidentally report errors on type parameters. If you really need to check these, + * use {@link #getApplicablePsiTypes} with {@code PsiTypeParameter.class} instead. + * + * @param context the lint scanning context + * @param declaration the class declaration node, or null for anonymous classes + */ + void checkClass(@NonNull JavaContext context, @NonNull UClass declaration); + } + /** Specialized interface for detectors that scan Java class files */ public interface ClassScanner { /** @@ -100,7 +929,7 @@ public abstract class Detector { @NonNull MethodNode method, @NonNull AbstractInsnNode instruction); /** - * Return the list of method call names (in VM format, e.g. "" for + * Return the list of method call names (in VM format, e.g. {@code ""} for * constructors, etc) for method calls this detector is interested in, * or null. T his will be used to dispatch calls to * {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)} @@ -167,7 +996,10 @@ public abstract class Detector { @NonNull MethodNode method, @NonNull MethodInsnNode call); } - /** Specialized interface for detectors that scan binary resource files */ + /** + * Specialized interface for detectors that scan binary resource files + * (typically bitmaps but also files in res/raw) + */ public interface BinaryResourceScanner { /** * Called for each resource folder @@ -326,6 +1158,7 @@ public abstract class Detector { * @param file the file in the context to check * @return true if this detector applies to the given context and file */ + @Deprecated // Slated for removal in lint 2.0 public boolean appliesTo(@NonNull Context context, @NonNull File file) { return false; } @@ -404,6 +1237,7 @@ public abstract class Detector { * @return the expected speed of this detector */ @NonNull + @Deprecated // Slated for removal in Lint 2.0 public Speed getSpeed() { return Speed.NORMAL; } @@ -419,6 +1253,7 @@ public abstract class Detector { * @return the expected speed of this detector */ @NonNull + @Deprecated // Slated for removal in Lint 2.0 public Speed getSpeed(@SuppressWarnings("UnusedParameters") @NonNull Issue issue) { // If not overridden, this detector does not distinguish speed by issue type return getSpeed(); @@ -426,7 +1261,7 @@ public abstract class Detector { // ---- Dummy implementations to make implementing XmlScanner easier: ---- - @SuppressWarnings("javadoc") + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { // This method must be overridden if your detector does // not return something from getApplicableElements or @@ -434,18 +1269,18 @@ public abstract class Detector { assert false; } - @SuppressWarnings("javadoc") + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) public void visitElement(@NonNull XmlContext context, @NonNull Element element) { // This method must be overridden if your detector returns // tag names from getApplicableElements assert false; } - @SuppressWarnings("javadoc") + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) { } - @SuppressWarnings("javadoc") + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { // This method must be overridden if your detector returns // attribute names from getApplicableAttributes @@ -466,53 +1301,38 @@ public abstract class Detector { // ---- Dummy implementations to make implementing JavaScanner easier: ---- - @Nullable @SuppressWarnings("javadoc") - public List getApplicableMethodNames() { + @Deprecated @Nullable @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public AstVisitor createJavaVisitor(@NonNull JavaContext context) { return null; } - @SuppressWarnings("javadoc") - public boolean appliesToResourceRefs() { - return false; - } - - @SuppressWarnings("javadoc") - public void visitResourceReference( - UastAndroidContext context, - UElement element, - String type, - String name, - boolean isFramework - ) {} - - @Nullable @SuppressWarnings("javadoc") - public List getApplicableConstructorTypes() { + @Deprecated @Nullable@SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public List> getApplicableNodeTypes() { return null; } - public List getApplicableSuperClasses() { - return null; + @Deprecated @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, + @NonNull MethodInvocation node) { } - public void visitClass(UastAndroidContext context, UClass node) { - + @Deprecated @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor, + @NonNull Node node, @NonNull String type, @NonNull String name, + boolean isFramework) { } - public List getApplicableFunctionNames() { - return null; + @Deprecated @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration, + @NonNull Node node, @NonNull ResolvedClass resolvedClass) { } - public void visitConstructor(UastAndroidContext context, UCallExpression functionCall, - UFunction constructor) { - - } - - public UastVisitor createUastVisitor(UastAndroidContext context) { - return null; - } - - public void visitCall(UastAndroidContext context, UCallExpression node) { - + @Deprecated @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void visitConstructor( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull ConstructorInvocation node, + @NonNull ResolvedMethod constructor) { } // ---- Dummy implementations to make implementing a ClassScanner easier: ---- @@ -551,6 +1371,7 @@ public abstract class Detector { // ---- Dummy implementations to make implementing an OtherFileScanner easier: ---- + @SuppressWarnings({"UnusedParameters", "unused"}) public boolean appliesToFolder(@NonNull Scope scope, @Nullable ResourceFolderType folderType) { return false; } @@ -578,4 +1399,117 @@ public abstract class Detector { public boolean appliesTo(@NonNull ResourceFolderType folderType) { return true; } + + // ---- Dummy implementation to make implementing UastScanner easier: ---- + + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + } + + public void visitReference( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UReferenceExpression reference, + @NonNull PsiElement referenced) { + } + + public void visitConstructor( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UCallExpression node, + @NonNull UMethod constructor) { + } + + public void visitMethod( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UCallExpression node, + @NonNull UMethod method) { + } + + @Nullable + public UastVisitor createUastVisitor(@NonNull JavaContext context) { + return null; + } + + @Nullable + public List> getApplicableUastTypes() { + return null; + } + + public void visitResourceReference( + @NonNull JavaContext context, + @Nullable UastVisitor visitor, + @NonNull UElement node, + @NonNull ResourceType type, + @NonNull String name, + boolean isFramework) { + } + + // ---- Dummy implementation to make implementing JavaPsiScanner easier: ---- + + @Nullable + public List getApplicableMethodNames() { + return null; + } + + @Nullable @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public List getApplicableConstructorTypes() { + return null; + } + + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public boolean appliesToResourceRefs() { + return false; + } + + @Nullable @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public List applicableSuperClasses() { + return null; + } + + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor, + @NonNull PsiMethodCallExpression call, @NonNull PsiMethod method) { + } + + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void visitConstructor( + @NonNull JavaContext context, + @Nullable JavaElementVisitor visitor, + @NonNull PsiNewExpression node, + @NonNull PsiMethod constructor) { + } + + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void visitResourceReference(@NonNull JavaContext context, + @Nullable JavaElementVisitor visitor, @NonNull PsiElement node, + @NonNull ResourceType type, @NonNull String name, boolean isFramework) { + } + + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void checkClass(@NonNull JavaContext context, @NonNull PsiClass declaration) { + } + + @Nullable @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) { + return null; + } + + @Nullable @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public List> getApplicablePsiTypes() { + return null; + } + + @Nullable @SuppressWarnings({"unused", "javadoc"}) + public List getApplicableReferenceNames() { + return null; + } + + @SuppressWarnings({"UnusedParameters", "unused", "javadoc"}) + public void visitReference( + @NonNull JavaContext context, + @Nullable JavaElementVisitor visitor, + @NonNull PsiJavaCodeReferenceElement reference, + @NonNull PsiElement referenced) { + } } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java index d312bb1ea4a..eb5358c1737 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java @@ -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. - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java index b3f6abce962..24271325471 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java @@ -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. - *

+ *

* 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. */ @@ -139,7 +142,7 @@ public final class Issue implements Comparable { */ @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 { */ @NonNull public String getExplanation(@NonNull TextFormat format) { - return TextFormat.RAW.convertTo(mExplanation, format); + return RAW.convertTo(mExplanation, format); } /** diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java index 5b37cca166f..5b69351aa9d 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java @@ -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. - *

+ *

* 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. */ -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 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 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 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 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 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 getParentOfType( + @Nullable Node element, + @NonNull Class clz) { + return getParentOfType(element, clz, true); } - @NotNull - @Override - public List 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 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 getParentOfType( + @Nullable Node element, + @NonNull Class 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 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 getParentOfType(@Nullable Node element, + @NonNull Class clz, + boolean strict, + @NonNull Class... 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 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 getNextSiblingOfType(@Nullable Node sibling, + @NonNull Class clz) { + if (sibling == null) { + return null; + } + Node parent = sibling.getParent(); + if (parent == null) { + return null; + } + + Iterator 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)); } } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java index 83fd6e7fdfb..ce1b8b2b5ab 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java @@ -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. - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java old mode 100755 new mode 100644 index 03996e935c9..4afc6410558 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java @@ -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; + } } diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java index dcb63d825ab..bb80f41329f 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java @@ -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 - *

+ *

* 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. */ @@ -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, } /** diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java index 749f6b41051..02756d7bbda 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java @@ -20,7 +20,7 @@ import com.google.common.annotations.Beta; /** * Information about a position in a file/document. - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java old mode 100755 new mode 100644 index 569ff7434ea..933c54cff6a --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java @@ -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 mManifestFiles; protected List mJavaSourceFolders; protected List mJavaClassFolders; + protected List mNonProvidedJavaLibraries; protected List mJavaLibraries; protected List mTestSourceFolders; protected List mResourceFolders; + protected List mAssetFolders; protected List mDirectLibraries; protected List mAllLibraries; protected boolean mReportIssues = true; @@ -104,6 +138,7 @@ public class Project { protected Boolean mAppCompat; private Map 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 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 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 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 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 getResourceFolders() { if (mResourceFolders == null) { List 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 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 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; diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java index 3bc6d49d316..33fd6849d25 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java @@ -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}.) - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceEvaluator.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceEvaluator.java new file mode 100644 index 00000000000..15828370286 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceEvaluator.java @@ -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 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 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 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 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 left = getResourceTypes( + expression.getThenExpression()); + EnumSet right = getResourceTypes( + expression.getElseExpression()); + if (left == null) { + return right; + } else if (right == null) { + return left; + } else { + EnumSet 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 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 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 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 left = getResourceTypes( + expression.getThenExpression()); + EnumSet right = getResourceTypes( + expression.getElseExpression()); + if (left == null) { + return right; + } else if (right == null) { + return left; + } else { + EnumSet 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 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 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 getAnyRes() { + EnumSet types = EnumSet.allOf(ResourceType.class); + types.remove(ResourceEvaluator.COLOR_INT_MARKER_TYPE); + types.remove(ResourceEvaluator.PX_MARKER_TYPE); + return types; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java index 61d67f9d5fa..85fdcfcd72f 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java @@ -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. - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java index c3320a25694..e8201cf3405 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java @@ -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 all the Java source files together. *

- * 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 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 SOURCE_FILE_SCOPE = EnumSet.of(SOURCE_FILE); + public static final EnumSet 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 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 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 SOURCE_AND_RESOURCE_FILES = - EnumSet.of(RESOURCE_FILE, SOURCE_FILE); + public static final EnumSet 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 CLASS_AND_ALL_RESOURCE_FILES = EnumSet.of(ALL_RESOURCE_FILES, CLASS_FILE); diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java index a2d3c56b945..c1de962099a 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java @@ -22,7 +22,7 @@ import com.google.common.annotations.Beta; /** * Severity of an issue found by lint - *

+ *

* 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. */ diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java index 0855b10f817..c0815831741 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java @@ -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 } tags) */ - HTML; + HTML, + + /** + * HTML formatted output (note: does not include surrounding {@code } tags). + * This is like {@link #HTML}, but it does not escape unicode characters with entities. + *

+ * (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("
", i) || - html.startsWith("
", i) || - html.startsWith("
", i) || - html.startsWith("
", i)) { - sb.append('\n'); - } else if (html.startsWith("", i)); + // Strip comments + if (html.startsWith("", 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); + if (i == -1) { + // Unclosed tag + break; + } + int end = i; + if (html.charAt(i - 1) == '/') { + end--; + isEndTag = true; + } + // TODO: Handle

 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 
'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("
\n"); } else { - if (c > 255) { + if (c > 255 && escapeUnicode) { sb.append("&#"); //$NON-NLS-1$ sb.append(Integer.toString(c)); sb.append(';'); diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TypeEvaluator.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TypeEvaluator.java new file mode 100644 index 00000000000..4a9f5a64e17 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TypeEvaluator.java @@ -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: + *
+ *     Object o = new StringBuilder();
+ *     Object var = o;
+ * 
+ * it will return "java.lang.StringBuilder". + *

+ * NOTE: 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 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); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java index 7dd2e6bf9d9..ee16bab79bb 100644 --- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java @@ -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. - *

+ *

* 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. */ @@ -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 diff --git a/plugins/lint/lint-checks/lint-checks.iml b/plugins/lint/lint-checks/lint-checks.iml index 2297ebf6bac..7f00be7406e 100644 --- a/plugins/lint/lint-checks/lint-checks.iml +++ b/plugins/lint/lint-checks/lint-checks.iml @@ -12,7 +12,6 @@ - diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AddJavascriptInterfaceDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AddJavascriptInterfaceDetector.java index 4be34a872a7..2be12a36734 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AddJavascriptInterfaceDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AddJavascriptInterfaceDetector.java @@ -22,25 +22,27 @@ import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; import com.android.annotations.NonNull; import com.android.annotations.Nullable; - -import java.util.Collections; -import java.util.List; - +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Collections; +import java.util.List; /** * Ensures that addJavascriptInterface is not called for API levels below 17. */ -public class AddJavascriptInterfaceDetector extends Detector implements UastScanner { +public class AddJavascriptInterfaceDetector extends Detector implements Detector.UastScanner { public static final Issue ISSUE = Issue.create( "AddJavascriptInterface", //$NON-NLS-1$ "addJavascriptInterface Called", @@ -53,49 +55,40 @@ public class AddJavascriptInterfaceDetector extends Detector implements UastScan Severity.WARNING, new Implementation( AddJavascriptInterfaceDetector.class, - Scope.SOURCE_FILE_SCOPE)). + Scope.JAVA_FILE_SCOPE)). addMoreInfo( "https://labs.mwrinfosecurity.com/blog/2013/09/24/webview-addjavascriptinterface-remote-code-execution/"); private static final String WEB_VIEW = "android.webkit.WebView"; //$NON-NLS-1$ private static final String ADD_JAVASCRIPT_INTERFACE = "addJavascriptInterface"; //$NON-NLS-1$ - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- @Nullable @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { // Ignore the issue if we never build for any API less than 17. - if (context.getLintContext().getMainProject().getMinSdk() >= 17) { + if (context.getMainProject().getMinSdk() >= 17) { return; } - // Ignore if the method doesn't fit our description. - UFunction resolvedFunction = node.resolveOrEmpty(context); - UClass containingClass = UastUtils.getContainingClassOrEmpty(resolvedFunction); - if (!containingClass.isSubclassOf(WEB_VIEW)) { - return; - } - List valueParameters = resolvedFunction.getValueParameters(); - if (node.getValueArgumentCount() != 2 - || !valueParameters.get(0).getType().matchesFqName(TYPE_OBJECT) - || !valueParameters.get(1).getType().matchesFqName(TYPE_STRING)) { + JavaEvaluator evaluator = context.getEvaluator(); + if (!evaluator.methodMatches(method, WEB_VIEW, true, TYPE_OBJECT, TYPE_STRING)) { return; } String message = "`WebView.addJavascriptInterface` should not be called with minSdkVersion < 17 for security reasons: " + - "JavaScript can use reflection to manipulate application"; - context.report(ISSUE, node, context.getLocation(node), message); + "JavaScript can use reflection to manipulate application"; + UElement reportElement = call.getMethodIdentifier(); + if (reportElement == null) { + reportElement = call; + } + context.reportUast(ISSUE, reportElement, context.getUastNameLocation(reportElement), message); } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlarmDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlarmDetector.java index fbc3d29b70a..823b4087ef8 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlarmDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlarmDetector.java @@ -17,31 +17,33 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; -import java.io.File; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + import java.util.Collections; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Makes sure that alarms are handled correctly */ -public class AlarmDetector extends Detector implements UastScanner { +public class AlarmDetector extends Detector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION = new Implementation( AlarmDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Alarm set too soon/frequently */ public static final Issue ISSUE = Issue.create( @@ -65,49 +67,39 @@ public class AlarmDetector extends Detector implements UastScanner { public AlarmDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements UastScanner ---- + // ---- Implements JavaScanner ---- @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("setRepeating"); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - UFunction setRepeatingFunction = node.resolve(context); - UClass containingClass = UastUtils.getContainingClassOrEmpty(setRepeatingFunction); - - if (containingClass.matchesFqName("android.app.AlarmManager") - && node.getValueArgumentCount() == 4) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.isMemberInClass(method, "android.app.AlarmManager") && + evaluator.getParameterCount(method) == 4) { ensureAtLeast(context, node, 1, 5000L); ensureAtLeast(context, node, 2, 60000L); } } - private static void ensureAtLeast(@NonNull UastAndroidContext context, + private static void ensureAtLeast(@NonNull JavaContext context, @NonNull UCallExpression node, int parameter, long min) { - UExpression arg = node.getValueArguments().get(parameter); - long value = getLongValue(arg); + UExpression argument = node.getValueArguments().get(parameter); + long value = getLongValue(context, argument); if (value < min) { - String message = String.format("Value will be forced up to %d as of Android 5.1; " + String message = String.format("Value will be forced up to %1$d as of Android 5.1; " + "don't rely on this to be exact", min); - context.report(ISSUE, arg, context.getLocation(arg), message); + context.report(ISSUE, argument, context.getUastLocation(argument), message); } } - private static long getLongValue(@NonNull UExpression argument) { - Object value = argument.evaluate(); + private static long getLongValue( + @NonNull JavaContext context, + @NonNull UExpression argument) { + Object value = ConstantEvaluator.evaluate(context, argument); if (value instanceof Number) { return ((Number)value).longValue(); } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AllowAllHostnameVerifierDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AllowAllHostnameVerifierDetector.java new file mode 100644 index 00000000000..6fe09fa96a2 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AllowAllHostnameVerifierDetector.java @@ -0,0 +1,105 @@ +/* + * 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.checks; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class AllowAllHostnameVerifierDetector extends Detector implements Detector.UastScanner { + + @SuppressWarnings("unchecked") + private static final Implementation IMPLEMENTATION = + new Implementation(AllowAllHostnameVerifierDetector.class, + Scope.JAVA_FILE_SCOPE); + + public static final Issue ISSUE = Issue.create("AllowAllHostnameVerifier", + "Insecure HostnameVerifier", + "This check looks for use of HostnameVerifier implementations " + + "whose `verify` method always returns true (thus trusting any hostname) " + + "which could result in insecure network traffic caused by trusting arbitrary " + + "hostnames in TLS/SSL certificates presented by peers.", + Category.SECURITY, + 6, + Severity.WARNING, + IMPLEMENTATION); + + // ---- Implements JavaScanner ---- + + @Override + @Nullable @SuppressWarnings("javadoc") + public List getApplicableConstructorTypes() { + return Collections.singletonList("org.apache.http.conn.ssl.AllowAllHostnameVerifier"); + } + + @Override + public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod constructor) { + Location location = context.getUastLocation(node); + context.report(ISSUE, node, location, + "Using the AllowAllHostnameVerifier HostnameVerifier is unsafe " + + "because it always returns true, which could cause insecure network " + + "traffic due to trusting TLS/SSL server certificates for wrong " + + "hostnames"); + } + + @Override + public List getApplicableMethodNames() { + return Arrays.asList("setHostnameVerifier", "setDefaultHostnameVerifier"); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.methodMatches(method, null, false, "javax.net.ssl.HostnameVerifier")) { + UExpression argument = node.getValueArguments().get(0); + PsiElement resolvedArgument = UastUtils.tryResolve(argument); + if (resolvedArgument instanceof PsiField) { + PsiField field = (PsiField) resolvedArgument; + if ("ALLOW_ALL_HOSTNAME_VERIFIER".equals(field.getName())) { + Location location = context.getUastLocation(argument); + String message = "Using the ALLOW_ALL_HOSTNAME_VERIFIER HostnameVerifier " + + "is unsafe because it always returns true, which could cause " + + "insecure network traffic due to trusting TLS/SSL server " + + "certificates for wrong hostnames"; + context.report(ISSUE, argument, location, message); + } + } + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlwaysShowActionDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlwaysShowActionDetector.java index 4b64d61cbf3..5bcd54adb1c 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlwaysShowActionDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlwaysShowActionDetector.java @@ -21,9 +21,12 @@ import static com.android.SdkConstants.VALUE_ALWAYS; import static com.android.SdkConstants.VALUE_IF_ROOM; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.resources.ResourceFolderType; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.JavaContext; @@ -31,21 +34,16 @@ import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.ResourceXmlDetector; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UQualifiedExpression; -import org.jetbrains.uast.USimpleReferenceExpression; -import org.jetbrains.uast.UastUtils; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.expressions.UReferenceExpression; import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -55,7 +53,8 @@ import java.util.List; * MenuItem.SHOW_AS_ACTION_ALWAYS in code), which is usually a style guide violation. * (Use ifRoom instead). */ -public class AlwaysShowActionDetector extends ResourceXmlDetector implements UastScanner { +public class AlwaysShowActionDetector extends ResourceXmlDetector implements + Detector.UastScanner { /** The main issue discovered by this detector */ @SuppressWarnings("unchecked") @@ -81,7 +80,7 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas Severity.WARNING, new Implementation( AlwaysShowActionDetector.class, - Scope.SOURCE_AND_RESOURCE_FILES, + Scope.JAVA_AND_RESOURCE_FILES, Scope.RESOURCE_FILE_SCOPE)) .addMoreInfo("http://developer.android.com/design/patterns/actionbar.html"); //$NON-NLS-1$ @@ -103,12 +102,6 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas return folderType == ResourceFolderType.MENU; } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - @Override public Collection getApplicableAttributes() { return Collections.singletonList(ATTR_SHOW_AS_ACTION); @@ -164,8 +157,10 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas location.setSecondary(next); } } - context.report(ISSUE, location, - "Prefer \"`ifRoom`\" instead of \"`always`\""); + if (location != null) { + context.report(ISSUE, location, + "Prefer \"`ifRoom`\" instead of \"`always`\""); + } } } } @@ -198,45 +193,29 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas // ---- Implements UastScanner ---- + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { - return new FieldAccessChecker(context); + public List getApplicableReferenceNames() { + return Arrays.asList("SHOW_AS_ACTION_IF_ROOM", "SHOW_AS_ACTION_ALWAYS"); } - private class FieldAccessChecker extends AbstractUastVisitor { - private final UastAndroidContext mContext; - - public FieldAccessChecker(UastAndroidContext context) { - mContext = context; - } - - @Override - public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) { - UElement selector = node.getSelector(); - if (!(selector instanceof USimpleReferenceExpression)) { - return false; - } - - String description = ((USimpleReferenceExpression)selector).getIdentifier(); - boolean isIfRoom = description.equals("SHOW_AS_ACTION_IF_ROOM"); //$NON-NLS-1$ - boolean isAlways = description.equals("SHOW_AS_ACTION_ALWAYS"); //$NON-NLS-1$ - if ((isIfRoom || isAlways) - && UastUtils.endsWithQualified(node.getReceiver(), "MenuItem")) { //$NON-NLS-1$ - if (isAlways) { - JavaContext lintContext = mContext.getLintContext(); - if (lintContext.getDriver().isSuppressed(lintContext, ISSUE, node)) { - return super.visitQualifiedExpression(node); - } - if (mAlwaysFields == null) { - mAlwaysFields = new ArrayList(); - } - mAlwaysFields.add(mContext.getLocation(node)); - } else { - mHasIfRoomRefs = true; + @Override + public void visitReference(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UReferenceExpression reference, @NonNull PsiElement resolved) { + if (resolved instanceof PsiField + && JavaEvaluator.isMemberInClass((PsiField) resolved, + "android.view.MenuItem")) { + if ("SHOW_AS_ACTION_ALWAYS".equals(((PsiField) resolved).getName())) { + if (context.getDriver().isSuppressed(context, ISSUE, reference)) { + return; } + if (mAlwaysFields == null) { + mAlwaysFields = new ArrayList(); + } + mAlwaysFields.add(context.getUastLocation(reference)); + } else { + mHasIfRoomRefs = true; } - - return super.visitQualifiedExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AndroidAutoDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AndroidAutoDetector.java new file mode 100644 index 00000000000..6a7cca0899b --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AndroidAutoDetector.java @@ -0,0 +1,399 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.TAG_INTENT_FILTER; +import static com.android.SdkConstants.TAG_SERVICE; +import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; +import static com.android.xml.AndroidManifest.NODE_ACTION; +import static com.android.xml.AndroidManifest.NODE_APPLICATION; +import static com.android.xml.AndroidManifest.NODE_METADATA; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceFolderType; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.XmlScanner; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LintUtils; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.ResourceXmlDetector; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.XmlContext; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; + +/** + * Detector for Android Auto issues. + *

Uses a {@code } tag with a {@code name="com.google.android.gms.car.application"} + * as a trigger for validating Automotive specific issues. + */ +public class AndroidAutoDetector extends ResourceXmlDetector + implements XmlScanner, Detector.UastScanner { + + @SuppressWarnings("unchecked") + public static final Implementation IMPL = new Implementation( + AndroidAutoDetector.class, + EnumSet.of(Scope.RESOURCE_FILE, Scope.MANIFEST, Scope.JAVA_FILE), + Scope.RESOURCE_FILE_SCOPE); + + /** Invalid attribute for uses tag.*/ + public static final Issue INVALID_USES_TAG_ISSUE = Issue.create( + "InvalidUsesTagAttribute", //$NON-NLS-1$ + "Invalid `name` attribute for `uses` element.", + "The element in `` should contain a " + + "valid value for the `name` attribute.\n" + + "Valid values are `media` or `notification`.", + Category.CORRECTNESS, + 6, + Severity.ERROR, + IMPL).addMoreInfo( + "https://developer.android.com/training/auto/start/index.html#auto-metadata"); + + /** Missing MediaBrowserService action */ + public static final Issue MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE = Issue.create( + "MissingMediaBrowserServiceIntentFilter", //$NON-NLS-1$ + "Missing intent-filter with action `android.media.browse.MediaBrowserService`.", + "An Automotive Media App requires an exported service that extends " + + "`android.service.media.MediaBrowserService` with an " + + "`intent-filter` for the action `android.media.browse.MediaBrowserService` " + + "to be able to browse and play media.\n" + + "To do this, add\n" + + "``\n" + + " ``\n" + + "``\n to the service that extends " + + "`android.service.media.MediaBrowserService`", + Category.CORRECTNESS, + 6, + Severity.ERROR, + IMPL).addMoreInfo( + "https://developer.android.com/training/auto/audio/index.html#config_manifest"); + + /** Missing intent-filter for Media Search. */ + public static final Issue MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH = Issue.create( + "MissingIntentFilterForMediaSearch", //$NON-NLS-1$ + "Missing intent-filter with action `android.media.action.MEDIA_PLAY_FROM_SEARCH`", + "To support voice searches on Android Auto, you should also register an " + + "`intent-filter` for the action `android.media.action.MEDIA_PLAY_FROM_SEARCH`" + + ".\nTo do this, add\n" + + "``\n" + + " ``\n" + + "``\n" + + "to your `` or ``.", + Category.CORRECTNESS, + 6, + Severity.ERROR, + IMPL).addMoreInfo( + "https://developer.android.com/training/auto/audio/index.html#support_voice"); + + /** Missing implementation of MediaSession.Callback#onPlayFromSearch*/ + public static final Issue MISSING_ON_PLAY_FROM_SEARCH = Issue.create( + "MissingOnPlayFromSearch", //$NON-NLS-1$ + "Missing `onPlayFromSearch`.", + "To support voice searches on Android Auto, in addition to adding an " + + "`intent-filter` for the action `onPlayFromSearch`," + + " you also need to override and implement " + + "`onPlayFromSearch(String query, Bundle bundle)`", + Category.CORRECTNESS, + 6, + Severity.ERROR, + IMPL).addMoreInfo( + "https://developer.android.com/training/auto/audio/index.html#support_voice"); + + private static final String CAR_APPLICATION_METADATA_NAME = + "com.google.android.gms.car.application"; //$NON-NLS-1$ + private static final String VAL_NAME_MEDIA = "media"; //$NON-NLS-1$ + private static final String VAL_NAME_NOTIFICATION = "notification"; //$NON-NLS-1$ + private static final String TAG_AUTOMOTIVE_APP = "automotiveApp"; //$NON-NLS-1$ + private static final String ATTR_RESOURCE = "resource"; //$NON-NLS-1$ + private static final String TAG_USES = "uses"; //$NON-NLS-1$ + private static final String ACTION_MEDIA_BROWSER_SERVICE = + "android.media.browse.MediaBrowserService"; //$NON-NLS-1$ + private static final String ACTION_MEDIA_PLAY_FROM_SEARCH = + "android.media.action.MEDIA_PLAY_FROM_SEARCH"; //$NON-NLS-1$ + private static final String CLASS_MEDIA_SESSION_CALLBACK = + "android.media.session.MediaSession.Callback"; //$NON-NLS-1$ + private static final String CLASS_V4MEDIA_SESSION_COMPAT_CALLBACK = + "android.support.v4.media.session.MediaSessionCompat.Callback"; //$NON-NLS-1$ + private static final String METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH = + "onPlayFromSearch"; //$NON-NLS-1$ + private static final String BUNDLE_ARG = "android.os.Bundle"; //$NON-NLS-1$ + + /** + * Indicates whether we identified that the current app is an automotive app and + * that we should validate all the automotive specific issues. + */ + private boolean mDoAutomotiveAppCheck; + + /** Indicates that a {@link #ACTION_MEDIA_BROWSER_SERVICE} intent-filter action was found. */ + private boolean mMediaIntentFilterFound; + + /** Indicates that a {@link #ACTION_MEDIA_PLAY_FROM_SEARCH} intent-filter action was found. */ + private boolean mMediaSearchIntentFilterFound; + + /** The resource file name deduced by the meta-data resource value */ + private String mAutomotiveResourceFileName; + + /** Indicates whether this app is an automotive Media App. */ + private boolean mIsAutomotiveMediaApp; + + /** {@link Location.Handle} to the application element */ + private Location.Handle mMainApplicationHandle; + + /** Constructs a new {@link AndroidAutoDetector} check */ + public AndroidAutoDetector() { + } + + @Override + public boolean appliesTo(@NonNull ResourceFolderType folderType) { + // We only need to check the meta data resource file in res/xml if any. + return folderType == ResourceFolderType.XML; + } + + @Override + public Collection getApplicableElements() { + return Arrays.asList( + TAG_AUTOMOTIVE_APP, // Root element of a declared automotive descriptor. + NODE_METADATA, // meta-data from AndroidManifest.xml + TAG_SERVICE, // service from AndroidManifest.xml + TAG_INTENT_FILTER, // Any declared intent-filter from AndroidManifest.xml + NODE_APPLICATION // Used for storing the application element/location. + ); + } + + @Override + public void beforeCheckProject(@NonNull Context context) { + mIsAutomotiveMediaApp = false; + mAutomotiveResourceFileName = null; + mMediaIntentFilterFound = false; + mMediaSearchIntentFilterFound = false; + } + + @Override + public void visitElement(@NonNull XmlContext context, @NonNull Element element) { + String tagName = element.getTagName(); + if (NODE_METADATA.equals(tagName) && !mDoAutomotiveAppCheck) { + checkAutoMetadataTag(element); + } else if (TAG_AUTOMOTIVE_APP.equals(tagName)) { + checkAutomotiveAppElement(context, element); + } else if (NODE_APPLICATION.equals(tagName)) { + // Disable reporting the error if the Issue was suppressed at + // the application level. + if (context.getMainProject() == context.getProject() + && !context.getProject().isLibrary()) { + mMainApplicationHandle = context.createLocationHandle(element); + mMainApplicationHandle.setClientData(element); + } + } else if (TAG_SERVICE.equals(tagName)) { + checkServiceForBrowserServiceIntentFilter(element); + } else if (TAG_INTENT_FILTER.equals(tagName)) { + checkForMediaSearchIntentFilter(element); + } + } + + private void checkAutoMetadataTag(Element element) { + String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); + + if (CAR_APPLICATION_METADATA_NAME.equals(name)) { + String autoFileName = element.getAttributeNS(ANDROID_URI, ATTR_RESOURCE); + + if (autoFileName != null && autoFileName.startsWith("@xml/")) { //$NON-NLS-1$ + // Store the fact that we need to check all the auto issues. + mDoAutomotiveAppCheck = true; + mAutomotiveResourceFileName = + autoFileName.substring("@xml/".length()) + DOT_XML; //$NON-NLS-1$ + } + } + } + + private void checkAutomotiveAppElement(XmlContext context, Element element) { + // Indicates whether the current file matches the resource that was registered + // in AndroidManifest.xml. + boolean isMetadataResource = + mAutomotiveResourceFileName != null + && mAutomotiveResourceFileName.equals(context.file.getName()); + + for (Element child : LintUtils.getChildren(element)) { + + if (TAG_USES.equals(child.getTagName())) { + String attrValue = child.getAttribute(ATTR_NAME); + if (VAL_NAME_MEDIA.equals(attrValue)) { + mIsAutomotiveMediaApp |= isMetadataResource; + } else if (!VAL_NAME_NOTIFICATION.equals(attrValue) + && context.isEnabled(INVALID_USES_TAG_ISSUE)) { + // Error invalid value for attribute. + Attr node = child.getAttributeNode(ATTR_NAME); + if (node == null) { + // no name specified + continue; + } + context.report(INVALID_USES_TAG_ISSUE, node, + context.getLocation(node), + "Expecting one of `" + VAL_NAME_MEDIA + "` or `" + + VAL_NAME_NOTIFICATION + "` for the name " + + "attribute in " + TAG_USES + " tag."); + } + } + } + // Report any errors that we have collected that can be shown to the user + // once we determine that this is an Automotive Media App. + if (mIsAutomotiveMediaApp + && !context.getProject().isLibrary() + && mMainApplicationHandle != null + && mDoAutomotiveAppCheck) { + + Element node = (Element) mMainApplicationHandle.getClientData(); + + if (!mMediaIntentFilterFound + && context.isEnabled(MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE)) { + context.report(MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE, node, + mMainApplicationHandle.resolve(), + "Missing `intent-filter` for action " + + "`android.media.browse.MediaBrowserService` that is required for " + + "android auto support"); + } + if (!mMediaSearchIntentFilterFound + && context.isEnabled(MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH)) { + context.report(MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH, node, + mMainApplicationHandle.resolve(), + "Missing `intent-filter` for action " + + "`android.media.action.MEDIA_PLAY_FROM_SEARCH`."); + } + } + } + + private void checkServiceForBrowserServiceIntentFilter(Element element) { + if (TAG_SERVICE.equals(element.getTagName()) + && !mMediaIntentFilterFound) { + + for (Element child : LintUtils.getChildren(element)) { + String tagName = child.getTagName(); + if (TAG_INTENT_FILTER.equals(tagName)) { + for (Element filterChild : LintUtils.getChildren(child)) { + if (NODE_ACTION.equals(filterChild.getTagName())) { + String actionValue = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME); + if (ACTION_MEDIA_BROWSER_SERVICE.equals(actionValue)) { + mMediaIntentFilterFound = true; + return; + } + } + } + } + } + } + } + + private void checkForMediaSearchIntentFilter(Element element) { + if (!mMediaSearchIntentFilterFound) { + + for (Element filterChild : LintUtils.getChildren(element)) { + if (NODE_ACTION.equals(filterChild.getTagName())) { + String actionValue = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME); + if (ACTION_MEDIA_PLAY_FROM_SEARCH.equals(actionValue)) { + mMediaSearchIntentFilterFound = true; + break; + } + } + } + } + } + + // Implementation of the UastScanner + + @Override + @Nullable + public List applicableSuperClasses() { + // We currently enable scanning only for media apps. + return mIsAutomotiveMediaApp ? + Arrays.asList(CLASS_MEDIA_SESSION_CALLBACK, + CLASS_V4MEDIA_SESSION_COMPAT_CALLBACK) + : null; + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + // Only check classes that are not declared abstract. + if (!context.getEvaluator().isAbstract(declaration)) { + MediaSessionCallbackVisitor visitor = new MediaSessionCallbackVisitor(context); + declaration.accept(visitor); + if (!visitor.isPlayFromSearchMethodFound() + && context.isEnabled(MISSING_ON_PLAY_FROM_SEARCH)) { + + context.reportUast(MISSING_ON_PLAY_FROM_SEARCH, declaration, + context.getUastNameLocation(declaration), + "This class does not override `" + + METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH + "` from `MediaSession.Callback`" + + " The method should be overridden and implemented to support " + + "Voice search on Android Auto."); + } + } + } + + /** + * A Visitor class to search for {@code MediaSession.Callback#onPlayFromSearch(..)} + * method declaration. + */ + private static class MediaSessionCallbackVisitor extends JavaRecursiveElementVisitor { + + private final JavaContext mContext; + + private boolean mOnPlayFromSearchFound; + + public MediaSessionCallbackVisitor(JavaContext context) { + this.mContext = context; + } + + public boolean isPlayFromSearchMethodFound() { + return mOnPlayFromSearchFound; + } + + @Override + public void visitMethod(PsiMethod method) { + super.visitMethod(method); + if (METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH.equals(method.getName()) + && mContext.getEvaluator().parametersMatch(method, TYPE_STRING, + BUNDLE_ARG)) { + mOnPlayFromSearchFound = true; + } + } + } + + // Used by the IDE to show errors. + @SuppressWarnings("unused") + @NonNull + public static String[] getAllowedAutomotiveAppTypes() { + return new String[]{VAL_NAME_MEDIA, VAL_NAME_NOTIFICATION}; + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AnnotationDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AnnotationDetector.java index 218c069c2cd..dce520e5d3f 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AnnotationDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AnnotationDetector.java @@ -16,35 +16,115 @@ package com.android.tools.klint.checks; +import static com.android.SdkConstants.ATTR_VALUE; import static com.android.SdkConstants.FQCN_SUPPRESS_LINT; -import static com.android.SdkConstants.SUPPRESS_LINT; +import static com.android.SdkConstants.INT_DEF_ANNOTATION; +import static com.android.SdkConstants.STRING_DEF_ANNOTATION; +import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX; +import static com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE; +import static com.android.tools.klint.checks.PermissionRequirement.getAnnotationBooleanValue; +import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_ALL_OF; +import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_ANY_OF; +import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_FROM; +import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_MAX; +import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_MIN; +import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_MULTIPLE; +import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_TO; +import static com.android.tools.klint.checks.SupportAnnotationDetector.CHECK_RESULT_ANNOTATION; +import static com.android.tools.klint.checks.SupportAnnotationDetector.FLOAT_RANGE_ANNOTATION; +import static com.android.tools.klint.checks.SupportAnnotationDetector.INT_RANGE_ANNOTATION; +import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION; +import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_READ; +import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_WRITE; +import static com.android.tools.klint.checks.SupportAnnotationDetector.SIZE_ANNOTATION; +import static com.android.tools.klint.checks.SupportAnnotationDetector.filterRelevantAnnotations; +import static com.android.tools.klint.checks.SupportAnnotationDetector.getDoubleAttribute; +import static com.android.tools.klint.checks.SupportAnnotationDetector.getLongAttribute; +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.LintUtils.findSubstring; +import static com.android.tools.klint.detector.api.LintUtils.getAutoBoxedType; +import static com.android.tools.klint.detector.api.ResourceEvaluator.COLOR_INT_ANNOTATION; +import static com.android.tools.klint.detector.api.ResourceEvaluator.PX_ANNOTATION; +import static com.android.tools.klint.detector.api.ResourceEvaluator.RES_SUFFIX; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.client.api.IssueRegistry; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.android.tools.klint.detector.api.TextFormat; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiAnnotationOwner; +import com.intellij.psi.PsiArrayInitializerMemberValue; +import com.intellij.psi.PsiArrayType; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiCodeBlock; +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.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiLiteral; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.PsiNameValuePair; +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.PsiSwitchLabelStatement; +import com.intellij.psi.PsiSwitchStatement; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiTypeCastExpression; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.util.PsiTreeUtil; -import java.io.File; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; -import org.jetbrains.uast.visitor.UastVisitor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; /** * Checks annotations to make sure they are valid */ -public class AnnotationDetector extends Detector implements UastScanner { +public class AnnotationDetector extends Detector implements JavaPsiScanner { + + public static final Implementation IMPLEMENTATION = new Implementation( + AnnotationDetector.class, + Scope.JAVA_FILE_SCOPE); + /** Placing SuppressLint on a local variable doesn't work for class-file based checks */ - public static final Issue ISSUE = Issue.create( + public static final Issue INSIDE_METHOD = Issue.create( "LocalSuppress", //$NON-NLS-1$ "@SuppressLint on invalid element", @@ -59,105 +139,710 @@ public class AnnotationDetector extends Detector implements UastScanner { Category.CORRECTNESS, 3, Severity.ERROR, - new Implementation( - AnnotationDetector.class, - Scope.SOURCE_FILE_SCOPE)); + IMPLEMENTATION); + + /** Incorrectly using a support annotation */ + @SuppressWarnings("WeakerAccess") + public static final Issue ANNOTATION_USAGE = Issue.create( + "SupportAnnotationUsage", //$NON-NLS-1$ + "Incorrect support annotation usage", + + "This lint check makes sure that the support annotations (such as " + + "`@IntDef` and `@ColorInt`) are used correctly. For example, it's an " + + "error to specify an `@IntRange` where the `from` value is higher than " + + "the `to` value.", + + Category.CORRECTNESS, + 2, + Severity.ERROR, + IMPLEMENTATION); + + /** IntDef annotations should be unique */ + public static final Issue UNIQUE = Issue.create( + "UniqueConstants", //$NON-NLS-1$ + "Overlapping Enumeration Constants", + + "The `@IntDef` annotation allows you to " + + "create a light-weight \"enum\" or type definition. However, it's possible to " + + "accidentally specify the same value for two or more of the values, which can " + + "lead to hard-to-detect bugs. This check looks for this scenario and flags any " + + "repeated constants.\n" + + "\n" + + "In some cases, the repeated constant is intentional (for example, renaming a " + + "constant to a more intuitive name, and leaving the old name in place for " + + "compatibility purposes.) In that case, simply suppress this check by adding a " + + "`@SuppressLint(\"UniqueConstants\")` annotation.", + + Category.CORRECTNESS, + 3, + Severity.ERROR, + IMPLEMENTATION); + + /** Flags should typically be specified as bit shifts */ + public static final Issue FLAG_STYLE = Issue.create( + "ShiftFlags", //$NON-NLS-1$ + "Dangerous Flag Constant Declaration", + + "When defining multiple constants for use in flags, the recommended style is " + + "to use the form `1 << 2`, `1 << 3`, `1 << 4` and so on to ensure that the " + + "constants are unique and non-overlapping.", + + Category.CORRECTNESS, + 3, + Severity.WARNING, + IMPLEMENTATION); + + /** All IntDef constants should be included in switch */ + public static final Issue SWITCH_TYPE_DEF = Issue.create( + "SwitchIntDef", //$NON-NLS-1$ + "Missing @IntDef in Switch", + + "This check warns if a `switch` statement does not explicitly include all " + + "the values declared by the typedef `@IntDef` declaration.", + + Category.CORRECTNESS, + 3, + Severity.WARNING, + IMPLEMENTATION); /** Constructs a new {@link AnnotationDetector} check */ public AnnotationDetector() { } + // ---- Implements JavaScanner ---- + + /** + * Set of fields we've already warned about {@link #FLAG_STYLE} for; these can + * be referenced multiple times, so we should only flag them once + */ + private Set mWarnedFlags; + @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; + public List> getApplicablePsiTypes() { + List> types = new ArrayList>(2); + types.add(PsiAnnotation.class); + types.add(PsiSwitchStatement.class); + return types; } - @NonNull + @Nullable @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements UastScanner ---- - - @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) { return new AnnotationChecker(context); } - private static class AnnotationChecker extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private class AnnotationChecker extends JavaElementVisitor { + private final JavaContext mContext; - public AnnotationChecker(UastAndroidContext context) { + public AnnotationChecker(JavaContext context) { mContext = context; } @Override - public boolean visitAnnotation(@NotNull UAnnotation node) { - String type = node.getName(); - if (SUPPRESS_LINT.equals(type) || FQCN_SUPPRESS_LINT.equals(type)) { - UElement parent = node.getParent(); - if (parent instanceof UVariable) { - for (UNamedExpression element : node.getValueArguments()) { - UExpression valueNode = element.getExpression(); - if (UastLiteralUtils.isStringLiteral(valueNode)) { - ULiteralExpression literal = (ULiteralExpression) valueNode; - String id = ((String)literal.getValue()); - if (!checkId(node, id)) { - return super.visitAnnotation(node); - } - } else if (valueNode instanceof UCallExpression && - ((UCallExpression) valueNode).getKind() == UastCallKind.ARRAY_INITIALIZER) { - UCallExpression array = (UCallExpression) valueNode; - if (array.getValueArgumentCount() == 0) { - continue; - } - for (UExpression arrayElement : array.getValueArguments()) { - if (UastLiteralUtils.isStringLiteral(arrayElement)) { - String id = ((String)((ULiteralExpression)arrayElement).getValue()); - if (!checkId(node, id)) { - return super.visitAnnotation(node); + public void visitAnnotation(PsiAnnotation annotation) { + String type = annotation.getQualifiedName(); + if (type == null || type.startsWith("java.lang.")) { + return; + } + + if (FQCN_SUPPRESS_LINT.equals(type)) { + PsiAnnotationOwner owner = annotation.getOwner(); + if (owner == null) { + return; + } + if (owner instanceof PsiModifierList) { + PsiElement parent = ((PsiModifierList) owner).getParent(); + // Only flag local variables and parameters (not classes, fields and methods) + if (!(parent instanceof PsiDeclarationStatement + || parent instanceof PsiLocalVariable + || parent instanceof PsiParameter)) { + return; + } + } else { + return; + } + PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes(); + if (attributes.length == 1) { + PsiNameValuePair attribute = attributes[0]; + PsiAnnotationMemberValue value = attribute.getValue(); + if (value instanceof PsiLiteral) { + Object v = ((PsiLiteral) value).getValue(); + if (v instanceof String) { + String id = (String) v; + checkSuppressLint(annotation, id); + } + } else if (value instanceof PsiArrayInitializerMemberValue) { + PsiArrayInitializerMemberValue initializer = + (PsiArrayInitializerMemberValue) value; + for (PsiAnnotationMemberValue expression : initializer.getInitializers()) { + if (expression instanceof PsiLiteral) { + Object v = ((PsiLiteral) expression).getValue(); + if (v instanceof String) { + String id = (String) v; + if (!checkSuppressLint(annotation, id)) { + return; } } } } } } - } + } else if (type.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) { + if (CHECK_RESULT_ANNOTATION.equals(type)) { + // Check that the return type of this method is not void! + if (annotation.getParent() instanceof PsiModifierList + && annotation.getParent().getParent() instanceof PsiMethod) { + PsiMethod method = (PsiMethod) annotation.getParent().getParent(); + if (!method.isConstructor() + && PsiType.VOID.equals(method.getReturnType())) { + mContext.report(ANNOTATION_USAGE, annotation, + mContext.getLocation(annotation), + "@CheckResult should not be specified on `void` methods"); + } + } + } else if (INT_RANGE_ANNOTATION.equals(type) + || FLOAT_RANGE_ANNOTATION.equals(type)) { + // Check that the annotated element's type is int or long. + // Also make sure that from <= to. + boolean invalid; + if (INT_RANGE_ANNOTATION.equals(type)) { + checkTargetType(annotation, TYPE_INT, TYPE_LONG, true); - return super.visitAnnotation(node); + long from = getLongAttribute(annotation, ATTR_FROM, Long.MIN_VALUE); + long to = getLongAttribute(annotation, ATTR_TO, Long.MAX_VALUE); + invalid = from > to; + } else { + checkTargetType(annotation, TYPE_FLOAT, TYPE_DOUBLE, true); + + double from = getDoubleAttribute(annotation, ATTR_FROM, + Double.NEGATIVE_INFINITY); + double to = getDoubleAttribute(annotation, ATTR_TO, + Double.POSITIVE_INFINITY); + invalid = from > to; + } + if (invalid) { + mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation), + "Invalid range: the `from` attribute must be less than " + + "the `to` attribute"); + } + } else if (SIZE_ANNOTATION.equals(type)) { + // Check that the annotated element's type is an array, or a collection + // (or at least not an int or long; if so, suggest IntRange) + // Make sure the size and the modulo is not negative. + int unset = -42; + long exact = getLongAttribute(annotation, ATTR_VALUE, unset); + long min = getLongAttribute(annotation, ATTR_MIN, Long.MIN_VALUE); + long max = getLongAttribute(annotation, ATTR_MAX, Long.MAX_VALUE); + long multiple = getLongAttribute(annotation, ATTR_MULTIPLE, 1); + if (min > max) { + mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation), + "Invalid size range: the `min` attribute must be less than " + + "the `max` attribute"); + } else if (multiple < 1) { + mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation), + "The size multiple must be at least 1"); + + } else if (exact < 0 && exact != unset || min < 0 && min != Long.MIN_VALUE) { + mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation), + "The size can't be negative"); + } + } else if (COLOR_INT_ANNOTATION.equals(type) || (PX_ANNOTATION.equals(type))) { + // Check that ColorInt applies to the right type + checkTargetType(annotation, TYPE_INT, TYPE_LONG, true); + } else if (INT_DEF_ANNOTATION.equals(type)) { + // Make sure IntDef constants are unique + ensureUniqueValues(annotation); + } else if (PERMISSION_ANNOTATION.equals(type) || + PERMISSION_ANNOTATION_READ.equals(type) || + PERMISSION_ANNOTATION_WRITE.equals(type)) { + // Check that if there are no arguments, this is specified on a parameter, + // and conversely, on methods and fields there is a valid argument. + if (annotation.getParent() instanceof PsiModifierList + && annotation.getParent().getParent() instanceof PsiMethod) { + String value = PermissionRequirement.getAnnotationStringValue(annotation, ATTR_VALUE); + String[] anyOf = PermissionRequirement.getAnnotationStringValues(annotation, ATTR_ANY_OF); + String[] allOf = PermissionRequirement.getAnnotationStringValues(annotation, ATTR_ALL_OF); + + int set = 0; + //noinspection VariableNotUsedInsideIf + if (value != null) { + set++; + } + //noinspection VariableNotUsedInsideIf + if (allOf != null) { + set++; + } + //noinspection VariableNotUsedInsideIf + if (anyOf != null) { + set++; + } + + if (set == 0) { + mContext.report(ANNOTATION_USAGE, annotation, + mContext.getLocation(annotation), + "For methods, permission annotation should specify one " + + "of `value`, `anyOf` or `allOf`"); + } else if (set > 1) { + mContext.report(ANNOTATION_USAGE, annotation, + mContext.getLocation(annotation), + "Only specify one of `value`, `anyOf` or `allOf`"); + } + } + + } else if (type.endsWith(RES_SUFFIX)) { + // Check that resource type annotations are on ints + checkTargetType(annotation, TYPE_INT, TYPE_LONG, true); + } + } else { + // Look for typedefs (and make sure they're specified on the right type) + PsiJavaCodeReferenceElement referenceElement = annotation + .getNameReferenceElement(); + if (referenceElement != null) { + PsiElement resolved = referenceElement.resolve(); + if (resolved instanceof PsiClass) { + PsiClass cls = (PsiClass) resolved; + if (cls.isAnnotationType() && cls.getModifierList() != null) { + for (PsiAnnotation a : cls.getModifierList().getAnnotations()) { + String name = a.getQualifiedName(); + if (INT_DEF_ANNOTATION.equals(name)) { + checkTargetType(annotation, TYPE_INT, TYPE_LONG, true); + } else if (STRING_DEF_ANNOTATION.equals(type)) { + checkTargetType(annotation, TYPE_STRING, null, true); + } + } + } + } + } + } } - private boolean checkId(UAnnotation node, String id) { - IssueRegistry registry = mContext.getLintContext().getDriver().getRegistry(); + private void checkTargetType(@NonNull PsiAnnotation node, @NonNull String type1, + @Nullable String type2, boolean allowCollection) { + PsiAnnotationOwner owner = node.getOwner(); + if (owner instanceof PsiModifierList) { + PsiElement parent = ((PsiModifierList) owner).getParent(); + PsiType type; + if (parent instanceof PsiDeclarationStatement) { + PsiElement[] elements = ((PsiDeclarationStatement) parent).getDeclaredElements(); + if (elements.length > 0) { + PsiElement element = elements[0]; + if (element instanceof PsiLocalVariable) { + type = ((PsiLocalVariable)element).getType(); + } else { + return; + } + } else { + return; + } + } else if (parent instanceof PsiMethod) { + PsiMethod method = (PsiMethod) parent; + type = method.isConstructor() + ? mContext.getEvaluator().getClassType(method.getContainingClass()) + : method.getReturnType(); + } else if (parent instanceof PsiVariable) { + // Field or local variable or parameter + type = ((PsiVariable)parent).getType(); + } else { + return; + } + if (type == null) { + return; + } + + if (allowCollection) { + if (type instanceof PsiArrayType) { + // For example, int[] + type = type.getDeepComponentType(); + } else if (type instanceof PsiClassType) { + // For example, List + PsiClassType classType = (PsiClassType)type; + if (classType.getParameters().length == 1) { + PsiClass resolved = classType.resolve(); + if (resolved != null && + mContext.getEvaluator().implementsInterface(resolved, + "java.util.Collection", false)) { + type = classType.getParameters()[0]; + } + } + } + } + + String typeName = type.getCanonicalText(); + if (!typeName.equals(type1) + && (type2 == null || !typeName.equals(type2))) { + // Autoboxing? You can put @DrawableRes on a java.lang.Integer for example + if (typeName.equals(getAutoBoxedType(type1)) + || type2 != null && typeName.equals(getAutoBoxedType(type2))) { + return; + } + + String expectedTypes = type2 == null ? type1 : type1 + " or " + type2; + if (typeName.equals(TYPE_STRING)) { + typeName = "String"; + } + String message = String.format( + "This annotation does not apply for type %1$s; expected %2$s", + typeName, expectedTypes); + Location location = mContext.getLocation(node); + mContext.report(ANNOTATION_USAGE, node, location, message); + } + } + } + + @Override + public void visitSwitchStatement(PsiSwitchStatement statement) { + PsiExpression condition = statement.getExpression(); + if (condition != null && PsiType.INT.equals(condition.getType())) { + PsiAnnotation annotation = findIntDef(condition); + if (annotation != null) { + checkSwitch(statement, annotation); + } + } + } + + /** + * Searches for the corresponding @IntDef annotation definition associated + * with a given node + */ + @Nullable + private PsiAnnotation findIntDef(@NonNull PsiElement node) { + if (node instanceof PsiReferenceExpression) { + PsiElement resolved = ((PsiReference) node).resolve(); + if (resolved instanceof PsiModifierListOwner) { + PsiAnnotation[] annotations = mContext.getEvaluator().getAllAnnotations( + (PsiModifierListOwner)resolved, true); + PsiAnnotation annotation = SupportAnnotationDetector.findIntDef( + filterRelevantAnnotations(annotations)); + if (annotation != null) { + return annotation; + } + } + + 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)) { + PsiExpression initializer = variable.getInitializer(); + if (initializer != null) { + return findIntDef(initializer); + } + break; + } + } + } 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) { + PsiExpression rExpression = assign.getRExpression(); + if (rExpression != null) { + return findIntDef(rExpression); + } + break; + } + } + } + } + prev = PsiTreeUtil.getPrevSiblingOfType(prev, + PsiStatement.class); + } + } + + } + } else if (node instanceof PsiMethodCallExpression) { + PsiMethod method = ((PsiMethodCallExpression) node).resolveMethod(); + if (method != null) { + PsiAnnotation[] annotations = mContext.getEvaluator().getAllAnnotations(method, true); + PsiAnnotation annotation = SupportAnnotationDetector.findIntDef( + filterRelevantAnnotations(annotations)); + if (annotation != null) { + return annotation; + } + } + } else if (node instanceof PsiConditionalExpression) { + PsiConditionalExpression expression = (PsiConditionalExpression) node; + if (expression.getThenExpression() != null) { + PsiAnnotation result = findIntDef(expression.getThenExpression()); + if (result != null) { + return result; + } + } + if (expression.getElseExpression() != null) { + PsiAnnotation result = findIntDef(expression.getElseExpression()); + if (result != null) { + return result; + } + } + } else if (node instanceof PsiTypeCastExpression) { + PsiTypeCastExpression cast = (PsiTypeCastExpression) node; + if (cast.getOperand() != null) { + return findIntDef(cast.getOperand()); + } + } else if (node instanceof PsiParenthesizedExpression) { + PsiParenthesizedExpression expression = (PsiParenthesizedExpression) node; + if (expression.getExpression() != null) { + return findIntDef(expression.getExpression()); + } + } + + return null; + } + + private void checkSwitch(@NonNull PsiSwitchStatement node, @NonNull PsiAnnotation annotation) { + PsiCodeBlock block = node.getBody(); + if (block == null) { + return; + } + + PsiAnnotationMemberValue value = annotation.findDeclaredAttributeValue(ATTR_VALUE); + if (value == null) { + value = annotation.findDeclaredAttributeValue(null); + } + if (value == null) { + return; + } + + if (!(value instanceof PsiArrayInitializerMemberValue)) { + return; + } + + PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue)value; + PsiAnnotationMemberValue[] allowedValues = array.getInitializers(); + + List fields = Lists.newArrayListWithCapacity(allowedValues.length); + for (PsiAnnotationMemberValue allowedValue : allowedValues) { + if (allowedValue instanceof PsiReferenceExpression) { + PsiElement resolved = ((PsiReferenceExpression) allowedValue).resolve(); + if (resolved != null) { + fields.add(resolved); + } + } else if (allowedValue instanceof PsiLiteral) { + fields.add(allowedValue); + } + } + + + // Empty switch: arguably we could skip these (since the IDE already warns about + // empty switches) but it's useful since the quickfix will kick in and offer all + // the missing ones when you're editing. + // if (block.getStatements().length == 0) { return; } + + for (PsiStatement statement : block.getStatements()) { + if (statement instanceof PsiSwitchLabelStatement) { + PsiSwitchLabelStatement caseStatement = (PsiSwitchLabelStatement) statement; + PsiExpression expression = caseStatement.getCaseValue(); + if (expression instanceof PsiLiteral) { + // Report warnings if you specify hardcoded constants. + // It's the wrong thing to do. + List list = computeFieldNames(node, Arrays.asList(allowedValues)); + // Keep error message in sync with {@link #getMissingCases} + String message = "Don't use a constant here; expected one of: " + Joiner + .on(", ").join(list); + mContext.report(SWITCH_TYPE_DEF, expression, + mContext.getLocation(expression), message); + return; // Don't look for other missing typedef constants since you might + // have aliased with value + } else if (expression instanceof PsiReferenceExpression) { // default case can have null expression + PsiElement resolved = ((PsiReferenceExpression) expression).resolve(); + if (resolved == null) { + // If there are compilation issues (e.g. user is editing code) we + // can't be certain, so don't flag anything. + return; + } + if (resolved instanceof PsiField) { + // We can't just do + // fields.remove(resolved); + // since the fields list contains instances of potentially + // different types with different hash codes (due to the + // external annotations, which are not of the same type as + // for example the ECJ based ones. + // + // The equals method on external field class deliberately handles + // this (but it can't make its hash code match what + // the ECJ fields do, which is tied to the ECJ binding hash code.) + // So instead, manually check for equals. These lists tend to + // be very short anyway. + boolean found = false; + ListIterator iterator = fields.listIterator(); + while (iterator.hasNext()) { + PsiElement field = iterator.next(); + if (field.isEquivalentTo(resolved)) { + iterator.remove(); + found = true; + break; + } + } + if (!found) { + // Look for local alias + PsiExpression initializer = ((PsiField) resolved).getInitializer(); + if (initializer instanceof PsiReferenceExpression) { + resolved = ((PsiReferenceExpression) expression).resolve(); + if (resolved instanceof PsiField) { + iterator = fields.listIterator(); + while (iterator.hasNext()) { + PsiElement field = iterator.next(); + if (field.equals(initializer)) { + iterator.remove(); + found = true; + break; + } + } + } + } + } + + if (!found) { + List list = computeFieldNames(node, Arrays.asList(allowedValues)); + // Keep error message in sync with {@link #getMissingCases} + String message = "Unexpected constant; expected one of: " + Joiner + .on(", ").join(list); + Location location = mContext.getNameLocation(expression); + mContext.report(SWITCH_TYPE_DEF, expression, location, message); + } + } + } + } + } + if (!fields.isEmpty()) { + List list = computeFieldNames(node, fields); + // Keep error message in sync with {@link #getMissingCases} + String message = "Switch statement on an `int` with known associated constant " + + "missing case " + Joiner.on(", ").join(list); + Location location = mContext.getNameLocation(node); + mContext.report(SWITCH_TYPE_DEF, node, location, message); + } + } + + private void ensureUniqueValues(@NonNull PsiAnnotation node) { + PsiAnnotationMemberValue value = node.findAttributeValue(ATTR_VALUE); + if (value == null) { + value = node.findAttributeValue(null); + } + if (value == null) { + return; + } + + if (!(value instanceof PsiArrayInitializerMemberValue)) { + return; + } + + PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue) value; + PsiAnnotationMemberValue[] initializers = array.getInitializers(); + Map valueToIndex = + Maps.newHashMapWithExpectedSize(initializers.length); + + boolean flag = getAnnotationBooleanValue(node, TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE; + if (flag) { + ensureUsingFlagStyle(initializers); + } + + ConstantEvaluator constantEvaluator = new ConstantEvaluator(mContext); + for (int index = 0; index < initializers.length; index++) { + PsiAnnotationMemberValue expression = initializers[index]; + Object o = constantEvaluator.evaluate(expression); + if (o instanceof Number) { + Number number = (Number) o; + if (valueToIndex.containsKey(number)) { + @SuppressWarnings("UnnecessaryLocalVariable") + Number repeatedValue = number; + + Location location; + String message; + int prevIndex = valueToIndex.get(number); + PsiElement prevConstant = initializers[prevIndex]; + message = String.format( + "Constants `%1$s` and `%2$s` specify the same exact " + + "value (%3$s); this is usually a cut & paste or " + + "merge error", + expression.getText(), prevConstant.getText(), + repeatedValue.toString()); + location = mContext.getLocation(expression); + Location secondary = mContext.getLocation(prevConstant); + secondary.setMessage("Previous same value"); + location.setSecondary(secondary); + PsiElement scope = getAnnotationScope(node); + mContext.report(UNIQUE, scope, location, message); + break; + } + valueToIndex.put(number, index); + } + } + } + + private void ensureUsingFlagStyle(@NonNull PsiAnnotationMemberValue[] constants) { + if (constants.length < 3) { + return; + } + + for (PsiAnnotationMemberValue constant : constants) { + if (constant instanceof PsiReferenceExpression) { + PsiElement resolved = ((PsiReferenceExpression) constant).resolve(); + if (resolved instanceof PsiField) { + PsiExpression initializer = ((PsiField) resolved).getInitializer(); + if (initializer instanceof PsiLiteral) { + PsiLiteral literal = (PsiLiteral) initializer; + Object o = literal.getValue(); + if (!(o instanceof Number)) { + continue; + } + long value = ((Number)o).longValue(); + // Allow -1, 0 and 1. You can write 1 as "1 << 0" but IntelliJ for + // example warns that that's a redundant shift. + if (Math.abs(value) <= 1) { + continue; + } + // Only warn if we're setting a specific bit + if (Long.bitCount(value) != 1) { + continue; + } + int shift = Long.numberOfTrailingZeros(value); + if (mWarnedFlags == null) { + mWarnedFlags = Sets.newHashSet(); + } + if (!mWarnedFlags.add(resolved)) { + return; + } + String message = String.format( + "Consider declaring this constant using 1 << %1$d instead", + shift); + Location location = mContext.getLocation(initializer); + mContext.report(FLAG_STYLE, initializer, location, message); + } + } + } + } + } + + private boolean checkSuppressLint(@NonNull PsiAnnotation node, @NonNull String id) { + IssueRegistry registry = mContext.getDriver().getRegistry(); Issue issue = registry.getIssue(id); // Special-case the ApiDetector issue, since it does both source file analysis // only on field references, and class file analysis on the rest, so we allow // annotations outside of methods only on fields - if (issue != null && !issue.getImplementation().getScope().contains(Scope.SOURCE_FILE) + if (issue != null && !issue.getImplementation().getScope().contains(Scope.JAVA_FILE) || issue == ApiDetector.UNSUPPORTED) { - // Ensure that this isn't a field - UElement parent = node.getParent(); - while (parent != null) { - if (parent instanceof UFunction || parent instanceof UBlockExpression) { - break; - } else if (issue == ApiDetector.UNSUPPORTED && parent instanceof UDeclarationsExpression) { - UDeclarationsExpression declarations = (UDeclarationsExpression) parent; - for (UVariable var : declarations.getVariables()) { - if (var.getKind() != UastVariableKind.MEMBER && var.getInitializer() instanceof UQualifiedExpression) { - return true; - } - } - } - parent = parent.getParent(); - if (parent == null) { - return true; - } - } - // This issue doesn't have AST access: annotations are not // available for local variables or parameters - mContext.report(ISSUE, node, mContext.getLocation(node), String.format( + PsiElement scope = getAnnotationScope(node); + mContext.report(INSIDE_METHOD, scope, mContext.getLocation(node), String.format( "The `@SuppressLint` annotation cannot be used on a local " + "variable with the lint check '%1$s': move out to the " + "surrounding method", id)); @@ -167,4 +852,82 @@ public class AnnotationDetector extends Detector implements UastScanner { return true; } } + + @NonNull + private static List computeFieldNames(@NonNull PsiSwitchStatement node, + Iterable allowedValues) { + List list = Lists.newArrayList(); + for (Object o : allowedValues) { + if (o instanceof PsiReferenceExpression) { + PsiElement resolved = ((PsiReferenceExpression) o).resolve(); + if (resolved != null) { + o = resolved; + } + } else if (o instanceof PsiLiteral) { + list.add("`" + ((PsiLiteral) o).getValue() + '`'); + continue; + } + + if (o instanceof PsiField) { + PsiField field = (PsiField) o; + // Only include class name if necessary + String name = field.getName(); + PsiClass clz = PsiTreeUtil.getParentOfType(node, PsiClass.class, true); + if (clz != null) { + PsiClass containingClass = field.getContainingClass(); + if (containingClass != null && !containingClass.equals(clz)) { + + //if (Objects.equal(containingClass.getPackage(), + // ((ResolvedClass) resolved).getPackage())) { + // name = containingClass.getSimpleName() + '.' + field.getName(); + //} else { + name = containingClass.getName() + '.' + field.getName(); + //} + } + } + list.add('`' + name + '`'); + } + } + Collections.sort(list); + return list; + } + + /** + * Given an error message produced by this lint detector for the {@link #SWITCH_TYPE_DEF} issue + * type, returns the list of missing enum cases.

Intended for IDE quickfix implementations. + * + * @param errorMessage the error message associated with the error + * @param format the format of the error message + * @return the list of enum cases, or null if not recognized + */ + @Nullable + public static List getMissingCases(@NonNull String errorMessage, + @NonNull TextFormat format) { + errorMessage = format.toText(errorMessage); + + String substring = findSubstring(errorMessage, " missing case ", null); + if (substring == null) { + substring = findSubstring(errorMessage, "expected one of: ", null); + } + if (substring != null) { + return Splitter.on(",").trimResults().splitToList(substring); + } + + return null; + } + + /** + * Returns the node to use as the scope for the given annotation node. + * You can't annotate an annotation itself (with {@code @SuppressLint}), but + * you should be able to place an annotation next to it, as a sibling, to only + * suppress the error on this annotated element, not the whole surrounding class. + */ + @NonNull + private static PsiElement getAnnotationScope(@NonNull PsiAnnotation node) { + PsiElement scope = PsiTreeUtil.getParentOfType(node, PsiAnnotation.class, true); + if (scope == null) { + scope = node; + } + return scope; + } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/Api.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/Api.java index fee8695bcb7..191b172adb8 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/Api.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/Api.java @@ -17,11 +17,14 @@ package com.android.tools.klint.checks; +import com.android.annotations.NonNull; + import org.xml.sax.SAXException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -44,14 +47,25 @@ public class Api { * @return a new ApiInfo */ public static Api parseApi(File apiFile) { - FileInputStream fileInputStream = null; + InputStream inputStream = null; try { - fileInputStream = new FileInputStream(apiFile); + inputStream = new FileInputStream(apiFile); SAXParserFactory parserFactory = SAXParserFactory.newInstance(); SAXParser parser = parserFactory.newSAXParser(); ApiParser apiParser = new ApiParser(); - parser.parse(fileInputStream, apiParser); - return new Api(apiParser.getClasses()); + parser.parse(inputStream, apiParser); + inputStream.close(); + + // Also read in API (unless regenerating the map for newer libraries) + //noinspection PointlessBooleanExpression,TestOnlyProblems + if (!ApiLookup.DEBUG_FORCE_REGENERATE_BINARY) { + inputStream = Api.class.getResourceAsStream("api-versions-support-library.xml"); + if (inputStream != null) { + parser.parse(inputStream, apiParser); + } + } + + return new Api(apiParser.getClasses(), apiParser.getPackages()); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (SAXException e) { @@ -59,9 +73,9 @@ public class Api { } catch (IOException e) { e.printStackTrace(); } finally { - if (fileInputStream != null) { + if (inputStream != null) { try { - fileInputStream.close(); + inputStream.close(); } catch (IOException e) { // ignore } @@ -72,9 +86,13 @@ public class Api { } private final Map mClasses; + private final Map mPackages; - private Api(Map classes) { + private Api( + @NonNull Map classes, + @NonNull Map packages) { mClasses = new HashMap(classes); + mPackages = new HashMap(packages); } ApiClass getClass(String fqcn) { @@ -84,4 +102,8 @@ public class Api { Map getClasses() { return Collections.unmodifiableMap(mClasses); } + + Map getPackages() { + return Collections.unmodifiableMap(mPackages); + } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiClass.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiClass.java index b23c5b466d0..80d1771a3d3 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiClass.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiClass.java @@ -18,6 +18,7 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.CONSTRUCTOR_NAME; +import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.utils.Pair; import com.google.common.collect.Lists; @@ -36,20 +37,31 @@ import java.util.Set; * {@link #getMethod} returns when the method was introduced. * {@link #getField} returns when the field was introduced. */ -public class ApiClass { - +public class ApiClass implements Comparable { private final String mName; private final int mSince; + private final int mDeprecatedIn; private final List> mSuperClasses = Lists.newArrayList(); private final List> mInterfaces = Lists.newArrayList(); private final Map mFields = new HashMap(); private final Map mMethods = new HashMap(); + private final Map mDeprecatedMembersIn = new HashMap(); - ApiClass(String name, int since) { + // Persistence data: Used when writing out binary data in ApiLookup + List members; + int index; // class number, e.g. entry in index where the pointer can be found + int indexOffset; // offset of the class entry + int memberOffsetBegin; // offset of the first member entry in the class + int memberOffsetEnd; // offset after the last member entry in the class + int memberIndexStart; // entry in index for first member + int memberIndexLength; // number of entries + + ApiClass(String name, int since, int deprecatedIn) { mName = name; mSince = since; + mDeprecatedIn = deprecatedIn; } /** @@ -69,11 +81,20 @@ public class ApiClass { } /** - * Returns when a field was added, or null if it doesn't exist. + * Returns the API level a method was deprecated in, or 0 if the method is not deprecated + * + * @return the API level a method was deprecated in, or 0 if the method is not deprecated + */ + int getDeprecatedIn() { + return mDeprecatedIn; + } + + /** + * Returns when a field was added, or Integer.MAX_VALUE if it doesn't exist. * @param name the name of the field. * @param info the corresponding info */ - Integer getField(String name, Api info) { + int getField(String name, Api info) { // The field can come from this class or from a super class or an interface // The value can never be lower than this introduction of this class. // When looking at super classes and interfaces, it can never be lower than when the @@ -98,11 +119,9 @@ public class ApiClass { ApiClass superClass = info.getClass(superClassPair.getFirst()); if (superClass != null) { i = superClass.getField(name, info); - if (i != null) { - int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; - if (tmp < min) { - min = tmp; - } + int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; + if (tmp < min) { + min = tmp; } } } @@ -112,11 +131,9 @@ public class ApiClass { ApiClass superClass = info.getClass(superClassPair.getFirst()); if (superClass != null) { i = superClass.getField(name, info); - if (i != null) { - int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; - if (tmp < min) { - min = tmp; - } + int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; + if (tmp < min) { + min = tmp; } } } @@ -125,8 +142,55 @@ public class ApiClass { } /** - * Returns when a method was added, or null if it doesn't exist. This goes through the super - * class to find method only present there. + * Returns when a field was deprecated, or 0 if it's not deprecated + * + * @param name the name of the field. + * @param info the corresponding info + */ + int getMemberDeprecatedIn(String name, Api info) { + int deprecatedIn = findMemberDeprecatedIn(name, info); + return deprecatedIn < Integer.MAX_VALUE ? deprecatedIn : 0; + } + + private int findMemberDeprecatedIn(String name, Api info) { + // This follows the same logic as getField/getMethod. + // However, it also incorporates deprecation versions from the class. + int min = Integer.MAX_VALUE; + Integer i = mDeprecatedMembersIn.get(name); + if (i != null) { + min = i; + } + + // now look at the super classes + for (Pair superClassPair : mSuperClasses) { + ApiClass superClass = info.getClass(superClassPair.getFirst()); + if (superClass != null) { + i = superClass.findMemberDeprecatedIn(name, info); + int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; + if (tmp < min) { + min = tmp; + } + } + } + + // now look at the interfaces + for (Pair superClassPair : mInterfaces) { + ApiClass superClass = info.getClass(superClassPair.getFirst()); + if (superClass != null) { + i = superClass.findMemberDeprecatedIn(name, info); + int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; + if (tmp < min) { + min = tmp; + } + } + } + + return min; + } + + /** + * Returns when a method was added, or Integer.MAX_VALUE if it doesn't exist. + * This goes through the super class to find method only present there. * @param methodSignature the method signature */ int getMethod(String methodSignature, Api info) { @@ -159,11 +223,9 @@ public class ApiClass { ApiClass superClass = info.getClass(superClassPair.getFirst()); if (superClass != null) { i = superClass.getMethod(methodSignature, info); - if (i != null) { - int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; - if (tmp < min) { - min = tmp; - } + int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; + if (tmp < min) { + min = tmp; } } } @@ -173,11 +235,9 @@ public class ApiClass { ApiClass superClass = info.getClass(interfacePair.getFirst()); if (superClass != null) { i = superClass.getMethod(methodSignature, info); - if (i != null) { - int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i; - if (tmp < min) { - min = tmp; - } + int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i; + if (tmp < min) { + min = tmp; } } } @@ -185,14 +245,16 @@ public class ApiClass { return min; } - void addField(String name, int since) { + void addField(String name, int since, int deprecatedIn) { Integer i = mFields.get(name); - if (i == null || i.intValue() > since) { - mFields.put(name, Integer.valueOf(since)); + assert i == null; + mFields.put(name, since); + if (deprecatedIn > 0) { + mDeprecatedMembersIn.put(name, deprecatedIn); } } - void addMethod(String name, int since) { + void addMethod(String name, int since, int deprecatedIn) { // Strip off the method type at the end to ensure that the code which // produces inherited methods doesn't get confused and end up multiple entries. // For example, java/nio/Buffer has the method "array()Ljava/lang/Object;", @@ -205,8 +267,10 @@ public class ApiClass { } Integer i = mMethods.get(name); - if (i == null || i.intValue() > since) { - mMethods.put(name, Integer.valueOf(since)); + assert i == null || i == since : i; + mMethods.put(name, since); + if (deprecatedIn > 0) { + mDeprecatedMembersIn.put(name, deprecatedIn); } } @@ -222,11 +286,12 @@ public class ApiClass { // check if we already have that name (at a lower level) for (Pair pair : list) { if (name.equals(pair.getFirst())) { + assert false; return; } } - list.add(Pair.of(name, Integer.valueOf(value))); + list.add(Pair.of(name, value)); } @@ -240,6 +305,16 @@ public class ApiClass { return null; } + @NonNull + public String getSimpleName() { + int index = mName.lastIndexOf('/'); + if (index != -1) { + return mName.substring(index + 1); + } + + return mName; + } + @Override public String toString() { return mName; @@ -259,6 +334,14 @@ public class ApiClass { return members; } + List> getInterfaces() { + return mInterfaces; + } + + List> getSuperClasses() { + return mSuperClasses; + } + private void addAllMethods(Api info, Set set, boolean includeConstructors) { if (!includeConstructors) { for (String method : mMethods.keySet()) { @@ -274,7 +357,6 @@ public class ApiClass { for (Pair superClass : mSuperClasses) { ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); if (clz != null) { clz.addAllMethods(info, set, false); } @@ -283,7 +365,6 @@ public class ApiClass { // Get methods from implemented interfaces as well; for (Pair superClass : mInterfaces) { ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); if (clz != null) { clz.addAllMethods(info, set, false); } @@ -312,21 +393,22 @@ public class ApiClass { for (Pair superClass : mSuperClasses) { ApiClass clz = info.getClass(superClass.getFirst()); assert clz != null : superClass.getSecond(); - if (clz != null) { - clz.addAllFields(info, set); - } + clz.addAllFields(info, set); } // Get methods from implemented interfaces as well; for (Pair superClass : mInterfaces) { ApiClass clz = info.getClass(superClass.getFirst()); assert clz != null : superClass.getSecond(); - if (clz != null) { - clz.addAllFields(info, set); - } + clz.addAllFields(info, set); } } + @Override + public int compareTo(@NonNull ApiClass other) { + return mName.compareTo(other.mName); + } + /* This code can be used to scan through all the fields and look for fields that have moved to a higher class: Field android/view/MotionEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9 diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java new file mode 100644 index 00000000000..23b1b2f70be --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java @@ -0,0 +1,2610 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_THEME_PREFIX; +import static com.android.SdkConstants.ATTR_CLASS; +import static com.android.SdkConstants.ATTR_FULL_BACKUP_CONTENT; +import static com.android.SdkConstants.ATTR_LABEL_FOR; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_PARENT; +import static com.android.SdkConstants.ATTR_TARGET_API; +import static com.android.SdkConstants.ATTR_TEXT_IS_SELECTABLE; +import static com.android.SdkConstants.ATTR_VALUE; +import static com.android.SdkConstants.BUTTON; +import static com.android.SdkConstants.CHECK_BOX; +import static com.android.SdkConstants.CLASS_CONSTRUCTOR; +import static com.android.SdkConstants.CONSTRUCTOR_NAME; +import static com.android.SdkConstants.FQCN_TARGET_API; +import static com.android.SdkConstants.PREFIX_ANDROID; +import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX; +import static com.android.SdkConstants.SWITCH; +import static com.android.SdkConstants.TAG; +import static com.android.SdkConstants.TAG_ITEM; +import static com.android.SdkConstants.TAG_STYLE; +import static com.android.SdkConstants.TARGET_API; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VIEW_TAG; +import static com.android.tools.klint.detector.api.ClassContext.getFqcn; +import static com.android.tools.klint.detector.api.LintUtils.getNextInstruction; +import static com.android.tools.klint.detector.api.LintUtils.skipParentheses; +import static com.android.tools.klint.detector.api.Location.SearchDirection.BACKWARD; +import static com.android.tools.klint.detector.api.Location.SearchDirection.EOL_NEAREST; +import static com.android.tools.klint.detector.api.Location.SearchDirection.FORWARD; +import static com.android.tools.klint.detector.api.Location.SearchDirection.NEAREST; +import static com.android.utils.SdkUtils.getResourceFieldName; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SdkVersionInfo; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.client.api.LintDriver; +import com.android.tools.klint.client.api.UastLintUtils; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ClassContext; +import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.ClassScanner; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LintUtils; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Location.SearchHints; +import com.android.tools.klint.detector.api.ResourceXmlDetector; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.TextFormat; +import com.android.tools.klint.detector.api.XmlContext; +import com.google.common.collect.Lists; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiAnnotationParameterList; +import com.intellij.psi.PsiArrayInitializerMemberValue; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiLiteral; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiPrimitiveType; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiResourceListElement; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UBinaryExpressionWithType; +import org.jetbrains.uast.UBlockExpression; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UCatchClause; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UFile; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.UImportStatement; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.ULocalVariable; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UReturnExpression; +import org.jetbrains.uast.USimpleNameReferenceExpression; +import org.jetbrains.uast.USwitchClauseExpression; +import org.jetbrains.uast.UTryExpression; +import org.jetbrains.uast.UVariable; +import org.jetbrains.uast.UastBinaryOperator; +import org.jetbrains.uast.UastOperator; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.expressions.UTypeReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.LookupSwitchInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TryCatchBlockNode; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Looks for usages of APIs that are not supported in all the versions targeted + * by this application (according to its minimum API requirement in the manifest). + */ +public class ApiDetector extends ResourceXmlDetector + implements ClassScanner, Detector.UastScanner { + + /** + * Whether we flag variable, field, parameter and return type declarations of a type + * not yet available. It appears Dalvik is very forgiving and doesn't try to preload + * classes until actually needed, so there is no need to flag these, and in fact, + * patterns used for supporting new and old versions sometimes declares these methods + * and only conditionally end up actually accessing methods and fields, so only check + * method and field accesses. + */ + private static final boolean CHECK_DECLARATIONS = false; + + private static final boolean AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null; //$NON-NLS-1$ + + public static final String REQUIRES_API_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "RequiresApi"; //$NON-NLS-1$ + + /** Accessing an unsupported API */ + @SuppressWarnings("unchecked") + public static final Issue UNSUPPORTED = Issue.create( + "NewApi", //$NON-NLS-1$ + "Calling new methods on older versions", + + "This check scans through all the Android API calls in the application and " + + "warns about any calls that are not available on *all* versions targeted " + + "by this application (according to its minimum SDK attribute in the manifest).\n" + + "\n" + + "If you really want to use this API and don't need to support older devices just " + + "set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files.\n" + + "\n" + + "If your code is *deliberately* accessing newer APIs, and you have ensured " + + "(e.g. with conditional execution) that this code will only ever be called on a " + + "supported platform, then you can annotate your class or method with the " + + "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + + "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " + + "file's minimum SDK as the required API level.\n" + + "\n" + + "If you are deliberately setting `android:` attributes in style definitions, " + + "make sure you place this in a `values-vNN` folder in order to avoid running " + + "into runtime conflicts on certain devices where manufacturers have added " + + "custom attributes whose ids conflict with the new ones on later platforms.\n" + + "\n" + + "Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " + + "the element will only be inflated in an adequate context.", + Category.CORRECTNESS, + 6, + Severity.ERROR, + new Implementation( + ApiDetector.class, + EnumSet.of(Scope.CLASS_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST), + Scope.RESOURCE_FILE_SCOPE, + Scope.CLASS_FILE_SCOPE, + Scope.MANIFEST_SCOPE)); + + /** Accessing an inlined API on older platforms */ + public static final Issue INLINED = Issue.create( + "InlinedApi", //$NON-NLS-1$ + "Using inlined constants on older versions", + + "This check scans through all the Android API field references in the application " + + "and flags certain constants, such as static final integers and Strings, " + + "which were introduced in later versions. These will actually be copied " + + "into the class files rather than being referenced, which means that " + + "the value is available even when running on older devices. In some " + + "cases that's fine, and in other cases it can result in a runtime " + + "crash or incorrect behavior. It depends on the context, so consider " + + "the code carefully and device whether it's safe and can be suppressed " + + "or whether the code needs tbe guarded.\n" + + "\n" + + "If you really want to use this API and don't need to support older devices just " + + "set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files." + + "\n" + + "If your code is *deliberately* accessing newer APIs, and you have ensured " + + "(e.g. with conditional execution) that this code will only ever be called on a " + + "supported platform, then you can annotate your class or method with the " + + "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + + "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " + + "file's minimum SDK as the required API level.\n", + Category.CORRECTNESS, + 6, + Severity.WARNING, + new Implementation( + ApiDetector.class, + Scope.JAVA_FILE_SCOPE)); + + /** Method conflicts with new inherited method */ + public static final Issue OVERRIDE = Issue.create( + "Override", //$NON-NLS-1$ + "Method conflicts with new inherited method", + + "Suppose you are building against Android API 8, and you've subclassed Activity. " + + "In your subclass you add a new method called `isDestroyed`(). At some later point, " + + "a method of the same name and signature is added to Android. Your method will " + + "now override the Android method, and possibly break its contract. Your method " + + "is not calling `super.isDestroyed()`, since your compilation target doesn't " + + "know about the method.\n" + + "\n" + + "The above scenario is what this lint detector looks for. The above example is " + + "real, since `isDestroyed()` was added in API 17, but it will be true for *any* " + + "method you have added to a subclass of an Android class where your build target " + + "is lower than the version the method was introduced in.\n" + + "\n" + + "To fix this, either rename your method, or if you are really trying to augment " + + "the builtin method if available, switch to a higher build target where you can " + + "deliberately add `@Override` on your overriding method, and call `super` if " + + "appropriate etc.\n", + Category.CORRECTNESS, + 6, + Severity.ERROR, + new Implementation( + ApiDetector.class, + Scope.CLASS_FILE_SCOPE)); + + /** Attribute unused on older versions */ + public static final Issue UNUSED = Issue.create( + "UnusedAttribute", //$NON-NLS-1$ + "Attribute unused on older versions", + + "This check finds attributes set in XML files that were introduced in a version " + + "newer than the oldest version targeted by your application (with the " + + "`minSdkVersion` attribute).\n" + + "\n" + + "This is not an error; the application will simply ignore the attribute. However, " + + "if the attribute is important to the appearance of functionality of your " + + "application, you should consider finding an alternative way to achieve the " + + "same result with only available attributes, and then you can optionally create " + + "a copy of the layout in a layout-vNN folder which will be used on API NN or " + + "higher where you can take advantage of the newer attribute.\n" + + "\n" + + "Note: This check does not only apply to attributes. For example, some tags can be " + + "unused too, such as the new `` element in layouts introduced in API 21.", + Category.CORRECTNESS, + 6, + Severity.WARNING, + new Implementation( + ApiDetector.class, + Scope.RESOURCE_FILE_SCOPE)); + + private static final String TARGET_API_VMSIG = '/' + TARGET_API + ';'; + private static final String SWITCH_TABLE_PREFIX = "$SWITCH_TABLE$"; //$NON-NLS-1$ + private static final String ORDINAL_METHOD = "ordinal"; //$NON-NLS-1$ + public static final String ENUM_SWITCH_PREFIX = "$SwitchMap$"; //$NON-NLS-1$ + + private static final String TAG_RIPPLE = "ripple"; + private static final String TAG_VECTOR = "vector"; + private static final String TAG_ANIMATED_VECTOR = "animated-vector"; + private static final String TAG_ANIMATED_SELECTOR = "animated-selector"; + + private static final String SDK_INT = "SDK_INT"; + private static final String ANDROID_OS_BUILD_VERSION = "android/os/Build$VERSION"; + + protected ApiLookup mApiDatabase; + private boolean mWarnedMissingDb; + private int mMinApi = -1; + + /** Constructs a new API check */ + public ApiDetector() { + } + + @Override + public void beforeCheckProject(@NonNull Context context) { + if (mApiDatabase == null) { + //mApiDatabase = ApiLookup.get(context.getClient()); + //// We can't look up the minimum API required by the project here: + //// The manifest file hasn't been processed yet in the -before- project hook. + //// For now it's initialized lazily in getMinSdk(Context), but the + //// lint infrastructure should be fixed to parse manifest file up front. + // + //if (mApiDatabase == null && !mWarnedMissingDb) { + // mWarnedMissingDb = true; + // context.report(IssueRegistry.LINT_ERROR, Location.create(context.file), + // "Can't find API database; API check not performed"); + //} else { + // // See if you don't have at least version 23.0.1 of platform tools installed + // AndroidSdkHandler sdk = context.getClient().getSdk(); + // if (sdk == null) { + // return; + // } + // LocalPackage pkgInfo = sdk.getLocalPackage(SdkConstants.FD_PLATFORM_TOOLS, + // context.getClient().getRepositoryLogger()); + // if (pkgInfo == null) { + // return; + // } + // Revision revision = pkgInfo.getVersion(); + // + // // The platform tools must be at at least the same revision + // // as the compileSdkVersion! + // // And as a special case, for 23, they must be at 23.0.1 + // // because 23.0.0 accidentally shipped without Android M APIs. + // int compileSdkVersion = context.getProject().getBuildSdk(); + // if (compileSdkVersion == 23) { + // if (revision.getMajor() > 23 || revision.getMajor() == 23 + // && (revision.getMinor() > 0 || revision.getMicro() > 0)) { + // return; + // } + // } else if (compileSdkVersion <= revision.getMajor()) { + // return; + // } + // + // // Pick a location: when incrementally linting in the IDE, tie + // // it to the current file + // List currentFiles = context.getProject().getSubset(); + // Location location; + // if (currentFiles != null && currentFiles.size() == 1) { + // File file = currentFiles.get(0); + // String contents = context.getClient().readFile(file); + // int firstLineEnd = contents.indexOf('\n'); + // if (firstLineEnd == -1) { + // firstLineEnd = contents.length(); + // } + // location = Location.create(file, + // new DefaultPosition(0, 0, 0), new + // DefaultPosition(0, firstLineEnd, firstLineEnd)); + // } else { + // location = Location.create(context.file); + // } + // context.report(UNSUPPORTED, + // location, + // String.format("The SDK platform-tools version (%1$s) is too old " + // + " to check APIs compiled with API %2$d; please update", + // revision.toShortString(), + // compileSdkVersion)); + //} + } + } + + // ---- Implements XmlScanner ---- + + @Override + public boolean appliesTo(@NonNull ResourceFolderType folderType) { + return true; + } + + @Override + public Collection getApplicableElements() { + return ALL; + } + + @Override + public Collection getApplicableAttributes() { + return ALL; + } + + @Override + public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { + if (mApiDatabase == null) { + return; + } + + int attributeApiLevel = -1; + //if (ANDROID_URI.equals(attribute.getNamespaceURI())) { + // String name = attribute.getLocalName(); + // if (!(name.equals(ATTR_LAYOUT_WIDTH) && !(name.equals(ATTR_LAYOUT_HEIGHT)) && + // !(name.equals(ATTR_ID)))) { + // String owner = "android/R$attr"; //$NON-NLS-1$ + // attributeApiLevel = mApiDatabase.getFieldVersion(owner, name); + // int minSdk = getMinSdk(context); + // if (attributeApiLevel > minSdk && attributeApiLevel > context.getFolderVersion() + // && attributeApiLevel > getLocalMinSdk(attribute.getOwnerElement()) + // && !isBenignUnusedAttribute(name) + // && !isAlreadyWarnedDrawableFile(context, attribute, attributeApiLevel)) { + // if (RtlDetector.isRtlAttributeName(name) || ATTR_SUPPORTS_RTL.equals(name)) { + // // No need to warn for example that + // // "layout_alignParentEnd will only be used in API level 17 and higher" + // // since we have a dedicated RTL lint rule dealing with those attributes + // + // // However, paddingStart in particular is known to cause crashes + // // when used on TextViews (and subclasses of TextViews), on some + // // devices, because vendor specific attributes conflict with the + // // later-added framework resources, and these are apparently read + // // by the text views. + // // + // // However, as of build tools 23.0.1 aapt works around this by packaging + // // the resources differently. + // if (name.equals(ATTR_PADDING_START)) { + // BuildToolInfo buildToolInfo = context.getProject().getBuildTools(); + // Revision buildTools = buildToolInfo != null + // ? buildToolInfo.getRevision() : null; + // boolean isOldBuildTools = buildTools != null && + // (buildTools.getMajor() < 23 || buildTools.getMajor() == 23 + // && buildTools.getMinor() == 0 && buildTools.getMicro() == 0); + // if ((buildTools == null || isOldBuildTools) && + // viewMayExtendTextView(attribute.getOwnerElement())) { + // Location location = context.getLocation(attribute); + // String message = String.format( + // "Attribute `%1$s` referenced here can result in a crash on " + // + "some specific devices older than API %2$d " + // + "(current min is %3$d)", + // attribute.getLocalName(), attributeApiLevel, minSdk); + // //noinspection VariableNotUsedInsideIf + // if (buildTools != null) { + // message = String.format("Upgrade `buildToolsVersion` from " + // + "`%1$s` to at least `23.0.1`; if not, ", + // buildTools.toShortString()) + // + Character.toLowerCase(message.charAt(0)) + // + message.substring(1); + // } + // context.report(UNSUPPORTED, attribute, location, message); + // } + // } + // } else { + // Location location = context.getLocation(attribute); + // String message = String.format( + // "Attribute `%1$s` is only used in API level %2$d and higher " + // + "(current min is %3$d)", + // attribute.getLocalName(), attributeApiLevel, minSdk); + // context.report(UNUSED, attribute, location, message); + // } + // } + // } + // + // // Special case: + // // the dividers attribute is present in API 1, but it won't be read on older + // // versions, so don't flag the common pattern + // // android:divider="?android:attr/dividerHorizontal" + // // since this will work just fine. See issue 67440 for more. + // if (name.equals("divider")) { + // return; + // } + //} + + String value = attribute.getValue(); + String owner = null; + String name = null; + String prefix; + if (value.startsWith(ANDROID_PREFIX)) { + prefix = ANDROID_PREFIX; + } else if (value.startsWith(ANDROID_THEME_PREFIX)) { + prefix = ANDROID_THEME_PREFIX; + if (context.getResourceFolderType() == ResourceFolderType.DRAWABLE) { + int api = 21; + int minSdk = getMinSdk(context); + if (api > minSdk && api > context.getFolderVersion() + && api > getLocalMinSdk(attribute.getOwnerElement())) { + Location location = context.getLocation(attribute); + String message; + message = String.format( + "Using theme references in XML drawables requires API level %1$d " + + "(current min is %2$d)", api, + minSdk); + context.report(UNSUPPORTED, attribute, location, message); + // Don't flag individual theme attribute requirements here, e.g. once + // we've told you that you need at least v21 to reference themes, we don't + // need to also tell you that ?android:selectableItemBackground requires + // API level 11 + return; + } + } + } else if (value.startsWith(PREFIX_ANDROID) && ATTR_NAME.equals(attribute.getName()) + && TAG_ITEM.equals(attribute.getOwnerElement().getTagName()) + && attribute.getOwnerElement().getParentNode() != null + && TAG_STYLE.equals(attribute.getOwnerElement().getParentNode().getNodeName())) { + owner = "android/R$attr"; //$NON-NLS-1$ + name = value.substring(PREFIX_ANDROID.length()); + prefix = null; + } else if (value.startsWith(PREFIX_ANDROID) && ATTR_PARENT.equals(attribute.getName()) + && TAG_STYLE.equals(attribute.getOwnerElement().getTagName())) { + owner = "android/R$style"; //$NON-NLS-1$ + name = getResourceFieldName(value.substring(PREFIX_ANDROID.length())); + prefix = null; + } else { + return; + } + + if (owner == null) { + // Convert @android:type/foo into android/R$type and "foo" + int index = value.indexOf('/', prefix.length()); + if (index != -1) { + owner = "android/R$" //$NON-NLS-1$ + + value.substring(prefix.length(), index); + name = getResourceFieldName(value.substring(index + 1)); + } else if (value.startsWith(ANDROID_THEME_PREFIX)) { + owner = "android/R$attr"; //$NON-NLS-1$ + name = value.substring(ANDROID_THEME_PREFIX.length()); + } else { + return; + } + } + int api = mApiDatabase.getFieldVersion(owner, name); + int minSdk = getMinSdk(context); + if (api > minSdk && api > context.getFolderVersion() + && api > getLocalMinSdk(attribute.getOwnerElement())) { + // Don't complain about resource references in the tools namespace, + // such as for example "tools:layout="@android:layout/list_content", + // used only for designtime previews + if (TOOLS_URI.equals(attribute.getNamespaceURI())) { + return; + } + + //noinspection StatementWithEmptyBody + if (attributeApiLevel >= api) { + // The attribute will only be *read* on platforms >= attributeApiLevel. + // If this isn't lower than the attribute reference's API level, it + // won't be a problem + } else if (attributeApiLevel > minSdk) { + String attributeName = attribute.getLocalName(); + Location location = context.getLocation(attribute); + String message = String.format( + "`%1$s` requires API level %2$d (current min is %3$d), but note " + + "that attribute `%4$s` is only used in API level %5$d " + + "and higher", + name, api, minSdk, attributeName, attributeApiLevel); + context.report(UNSUPPORTED, attribute, location, message); + } else { + Location location = context.getLocation(attribute); + String message = String.format( + "`%1$s` requires API level %2$d (current min is %3$d)", + value, api, minSdk); + context.report(UNSUPPORTED, attribute, location, message); + } + } + } + + /** + * Returns true if the view tag is possibly a text view. It may not be certain, + * but will err on the side of caution (for example, any custom view is considered + * to be a potential text view.) + */ + private static boolean viewMayExtendTextView(@NonNull Element element) { + String tag = element.getTagName(); + if (tag.equals(VIEW_TAG)) { + tag = element.getAttribute(ATTR_CLASS); + if (tag == null || tag.isEmpty()) { + return false; + } + } + + //noinspection SimplifiableIfStatement + if (tag.indexOf('.') != -1) { + // Custom views: not sure. Err on the side of caution. + return true; + + } + + return tag.contains("Text") // TextView, EditText, etc + || tag.contains(BUTTON) // Button, ToggleButton, etc + || tag.equals("DigitalClock") + || tag.equals("Chronometer") + || tag.equals(CHECK_BOX) + || tag.equals(SWITCH); + } + + /** + * Returns true if this attribute is in a drawable document with one of the + * root tags that require API 21 + */ + private static boolean isAlreadyWarnedDrawableFile(@NonNull XmlContext context, + @NonNull Attr attribute, int attributeApiLevel) { + // Don't complain if it's in a drawable file where we've already + // flagged the root drawable type as being unsupported + if (context.getResourceFolderType() == ResourceFolderType.DRAWABLE + && attributeApiLevel == 21) { + String root = attribute.getOwnerDocument().getDocumentElement().getTagName(); + if (TAG_RIPPLE.equals(root) + || TAG_VECTOR.equals(root) + || TAG_ANIMATED_VECTOR.equals(root) + || TAG_ANIMATED_SELECTOR.equals(root)) { + return true; + } + } + + return false; + } + + /** + * Is the given attribute a "benign" unused attribute, one we probably don't need to + * flag to the user as not applicable on all versions? These are typically attributes + * which add some nice platform behavior when available, but that are not critical + * and developers would not typically need to be aware of to try to implement workarounds + * on older platforms. + */ + public static boolean isBenignUnusedAttribute(@NonNull String name) { + return ATTR_LABEL_FOR.equals(name) + || ATTR_TEXT_IS_SELECTABLE.equals(name) + || ATTR_FULL_BACKUP_CONTENT.equals(name); + } + + @Override + public void visitElement(@NonNull XmlContext context, @NonNull Element element) { + if (mApiDatabase == null) { + return; + } + + String tag = element.getTagName(); + + ResourceFolderType folderType = context.getResourceFolderType(); + if (folderType != ResourceFolderType.LAYOUT) { + if (folderType == ResourceFolderType.DRAWABLE) { + checkElement(context, element, TAG_VECTOR, 21, "1.4", UNSUPPORTED); + checkElement(context, element, TAG_RIPPLE, 21, null, UNSUPPORTED); + checkElement(context, element, TAG_ANIMATED_SELECTOR, 21, null, UNSUPPORTED); + checkElement(context, element, TAG_ANIMATED_VECTOR, 21, null, UNSUPPORTED); + } + if (element.getParentNode().getNodeType() != Node.ELEMENT_NODE) { + // Root node + return; + } + NodeList childNodes = element.getChildNodes(); + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node textNode = childNodes.item(i); + if (textNode.getNodeType() == Node.TEXT_NODE) { + String text = textNode.getNodeValue(); + if (text.contains(ANDROID_PREFIX)) { + text = text.trim(); + // Convert @android:type/foo into android/R$type and "foo" + int index = text.indexOf('/', ANDROID_PREFIX.length()); + if (index != -1) { + String typeString = text.substring(ANDROID_PREFIX.length(), index); + if (ResourceType.getEnum(typeString) != null) { + String owner = "android/R$" + typeString; + String name = getResourceFieldName(text.substring(index + 1)); + int api = mApiDatabase.getFieldVersion(owner, name); + int minSdk = getMinSdk(context); + if (api > minSdk && api > context.getFolderVersion() + && api > getLocalMinSdk(element)) { + Location location = context.getLocation(textNode); + String message = String.format( + "`%1$s` requires API level %2$d (current min is %3$d)", + text, api, minSdk); + context.report(UNSUPPORTED, element, location, message); + } + } + } + } + } + } + } else { + if (VIEW_TAG.equals(tag)) { + tag = element.getAttribute(ATTR_CLASS); + if (tag == null || tag.isEmpty()) { + return; + } + } else { + // TODO: Complain if is used at the root level! + checkElement(context, element, TAG, 21, null, UNUSED); + } + + // Check widgets to make sure they're available in this version of the SDK. + if (tag.indexOf('.') != -1) { + // Custom views aren't in the index + return; + } + String fqn = "android/widget/" + tag; //$NON-NLS-1$ + if (tag.equals("TextureView")) { //$NON-NLS-1$ + fqn = "android/view/TextureView"; //$NON-NLS-1$ + } + // TODO: Consider other widgets outside of android.widget.* + int api = mApiDatabase.getClassVersion(fqn); + int minSdk = getMinSdk(context); + if (api > minSdk && api > context.getFolderVersion() + && api > getLocalMinSdk(element)) { + Location location = context.getLocation(element); + String message = String.format( + "View requires API level %1$d (current min is %2$d): `<%3$s>`", + api, minSdk, tag); + context.report(UNSUPPORTED, element, location, message); + } + } + } + + /** Checks whether the given element is the given tag, and if so, whether it satisfied + * the minimum version that the given tag is supported in */ + private void checkElement(@NonNull XmlContext context, @NonNull Element element, + @NonNull String tag, int api, @Nullable String gradleVersion, @NonNull Issue issue) { + if (tag.equals(element.getTagName())) { + int minSdk = getMinSdk(context); + if (api > minSdk + && api > context.getFolderVersion() + && api > getLocalMinSdk(element)) { + Location location = context.getLocation(element); + String message; + if (issue == UNSUPPORTED) { + message = String.format( + "`<%1$s>` requires API level %2$d (current min is %3$d)", tag, api, + minSdk); + if (gradleVersion != null) { + message += String.format( + " or building with Android Gradle plugin %1$s or higher", + gradleVersion); + } + } else { + assert issue == UNUSED : issue; + message = String.format( + "`<%1$s>` is only used in API level %2$d and higher " + + "(current min is %3$d)", tag, api, minSdk); + } + context.report(issue, element, location, message); + } + } + } + + protected int getMinSdk(Context context) { + if (mMinApi == -1) { + AndroidVersion minSdkVersion = context.getMainProject().getMinSdkVersion(); + mMinApi = minSdkVersion.getFeatureLevel(); + } + + return mMinApi; + } + + // ---- Implements ClassScanner ---- + + @SuppressWarnings("rawtypes") // ASM API + @Override + public void checkClass(@NonNull final ClassContext context, @NonNull ClassNode classNode) { + if (mApiDatabase == null) { + return; + } + + if (AOSP_BUILD && classNode.name.startsWith("android/support/")) { //$NON-NLS-1$ + return; + } + + // Requires util package (add prebuilts/tools/common/asm-tools/asm-debug-all-4.0.jar) + //classNode.accept(new TraceClassVisitor(new PrintWriter(System.out))); + + int classMinSdk = getClassMinSdk(context, classNode); + if (classMinSdk == -1) { + classMinSdk = getMinSdk(context); + } + + List methodList = classNode.methods; + if (methodList.isEmpty()) { + return; + } + + boolean checkCalls = context.isEnabled(UNSUPPORTED) + || context.isEnabled(INLINED); + boolean checkMethods = context.isEnabled(OVERRIDE) + && context.getMainProject().getBuildSdk() >= 1; + String frameworkParent = null; + if (checkMethods) { + LintDriver driver = context.getDriver(); + String owner = classNode.superName; + while (owner != null) { + // For virtual dispatch, walk up the inheritance chain checking + // each inherited method + if ((owner.startsWith("android/") //$NON-NLS-1$ + && !owner.startsWith("android/support/")) //$NON-NLS-1$ + || owner.startsWith("java/") //$NON-NLS-1$ + || owner.startsWith("javax/")) { //$NON-NLS-1$ + frameworkParent = owner; + break; + } + owner = driver.getSuperClass(owner); + } + if (frameworkParent == null) { + checkMethods = false; + } + } + + if (checkCalls) { // Check implements/extends + if (classNode.superName != null) { + String signature = classNode.superName; + checkExtendsClass(context, classNode, classMinSdk, signature); + } + if (classNode.interfaces != null) { + @SuppressWarnings("unchecked") // ASM API + List interfaceList = classNode.interfaces; + for (String signature : interfaceList) { + checkExtendsClass(context, classNode, classMinSdk, signature); + } + } + } + + for (Object m : methodList) { + MethodNode method = (MethodNode) m; + + int minSdk = getLocalMinSdk(method.invisibleAnnotations); + if (minSdk == -1) { + minSdk = classMinSdk; + } + + InsnList nodes = method.instructions; + + if (checkMethods && Character.isJavaIdentifierStart(method.name.charAt(0))) { + int buildSdk = context.getMainProject().getBuildSdk(); + String name = method.name; + assert frameworkParent != null; + int api = mApiDatabase.getCallVersion(frameworkParent, name, method.desc); + if (api > buildSdk && buildSdk != -1) { + // TODO: Don't complain if it's annotated with @Override; that means + // somehow the build target isn't correct. + String fqcn; + String owner = classNode.name; + if (CONSTRUCTOR_NAME.equals(name)) { + fqcn = "new " + getFqcn(owner); //$NON-NLS-1$ + } else { + fqcn = getFqcn(owner) + '#' + name; + } + String message = String.format( + "This method is not overriding anything with the current build " + + "target, but will in API level %1$d (current target is %2$d): `%3$s`", + api, buildSdk, fqcn); + + Location location = context.getLocation(method, classNode); + context.report(OVERRIDE, method, null, location, message); + } + } + + if (!checkCalls) { + continue; + } + + List tryCatchBlocks = method.tryCatchBlocks; + // single-catch blocks are already handled by an AST level check in ApiVisitor + if (tryCatchBlocks.size() > 1) { + List checked = Lists.newArrayList(); + for (Object o : tryCatchBlocks) { + TryCatchBlockNode tryCatchBlock = (TryCatchBlockNode) o; + String className = tryCatchBlock.type; + if (className == null || checked.contains(className)) { + continue; + } + + int api = mApiDatabase.getClassVersion(className); + if (api > minSdk) { + // Find instruction node + LabelNode label = tryCatchBlock.handler; + String fqcn = getFqcn(className); + String message = String.format( + "Class requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + report(context, message, label, method, + className.substring(className.lastIndexOf('/') + 1), null, + SearchHints.create(EOL_NEAREST).matchJavaSymbol()); + } + } + } + + + if (CHECK_DECLARATIONS) { + // Check types in parameter list and types of local variables + List localVariables = method.localVariables; + if (localVariables != null) { + for (Object v : localVariables) { + LocalVariableNode var = (LocalVariableNode) v; + String desc = var.desc; + if (desc.charAt(0) == 'L') { + // "Lpackage/Class;" ⇒ "package/Bar" + String className = desc.substring(1, desc.length() - 1); + int api = mApiDatabase.getClassVersion(className); + if (api > minSdk) { + String fqcn = getFqcn(className); + String message = String.format( + "Class requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + report(context, message, var.start, method, + className.substring(className.lastIndexOf('/') + 1), null, + SearchHints.create(NEAREST).matchJavaSymbol()); + } + } + } + } + + // Check return type + // The parameter types are already handled as local variables so we can skip + // right to the return type. + // Check types in parameter list + String signature = method.desc; + if (signature != null) { + int args = signature.indexOf(')'); + if (args != -1 && signature.charAt(args + 1) == 'L') { + String type = signature.substring(args + 2, signature.length() - 1); + int api = mApiDatabase.getClassVersion(type); + if (api > minSdk) { + String fqcn = getFqcn(type); + String message = String.format( + "Class requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + AbstractInsnNode first = nodes.size() > 0 ? nodes.get(0) : null; + report(context, message, first, method, method.name, null, + SearchHints.create(BACKWARD).matchJavaSymbol()); + } + } + } + } + + for (int i = 0, n = nodes.size(); i < n; i++) { + AbstractInsnNode instruction = nodes.get(i); + int type = instruction.getType(); + if (type == AbstractInsnNode.METHOD_INSN) { + MethodInsnNode node = (MethodInsnNode) instruction; + String name = node.name; + String owner = node.owner; + String desc = node.desc; + + // No need to check methods in this local class; we know they + // won't be an API match + if (node.getOpcode() == Opcodes.INVOKEVIRTUAL + && owner.equals(classNode.name)) { + owner = classNode.superName; + } + + boolean checkingSuperClass = false; + while (owner != null) { + int api = mApiDatabase.getCallVersion(owner, name, desc); + if (api > minSdk) { + if (method.name.startsWith(SWITCH_TABLE_PREFIX)) { + // We're in a compiler-generated method to generate an + // array indexed by enum ordinal values to enum values. The enum + // itself must be requiring a higher API number than is + // currently used, but the call site for the switch statement + // will also be referencing it, so no need to report these + // calls. + break; + } + + if (!checkingSuperClass + && node.getOpcode() == Opcodes.INVOKEVIRTUAL + && methodDefinedLocally(classNode, name, desc)) { + break; + } + + String fqcn; + if (CONSTRUCTOR_NAME.equals(name)) { + fqcn = "new " + getFqcn(owner); //$NON-NLS-1$ + } else { + fqcn = getFqcn(owner) + '#' + name; + } + String message = String.format( + "Call requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + + if (name.equals(ORDINAL_METHOD) + && instruction.getNext() != null + && instruction.getNext().getNext() != null + && instruction.getNext().getOpcode() == Opcodes.IALOAD + && instruction.getNext().getNext().getOpcode() + == Opcodes.TABLESWITCH) { + message = String.format( + "Enum for switch requires API level %1$d " + + "(current min is %2$d): `%3$s`", + api, minSdk, getFqcn(owner)); + } + + // If you're simply calling super.X from method X, even if method X + // is in a higher API level than the minSdk, we're generally safe; + // that method should only be called by the framework on the right + // API levels. (There is a danger of somebody calling that method + // locally in other contexts, but this is hopefully unlikely.) + if (instruction.getOpcode() == Opcodes.INVOKESPECIAL && + name.equals(method.name) && desc.equals(method.desc) && + // We specifically exclude constructors from this check, + // because we do want to flag constructors requiring the + // new API level; it's highly likely that the constructor + // is called by local code so you should specifically + // investigate this as a developer + !name.equals(CONSTRUCTOR_NAME)) { + break; + } + + if (isWithinSdkConditional(context, classNode, method, instruction, + api)) { + break; + } + + if (api == 19 + && owner.equals("java/lang/ReflectiveOperationException") + && !method.tryCatchBlocks.isEmpty()) { + boolean direct = false; + for (Object o : method.tryCatchBlocks) { + if (((TryCatchBlockNode)o).type.equals("java/lang/ReflectiveOperationException")) { + direct = true; + break; + } + } + if (!direct) { + message = String.format("Multi-catch with these reflection " + + "exceptions requires API level 19 (current min is" + + " %2$d) because they get compiled to the common but " + + "new super type `ReflectiveOperationException`. " + + "As a workaround either create individual catch " + + "statements, or catch `Exception`.", + api, minSdk); + } + } + + report(context, message, node, method, name, null, + SearchHints.create(FORWARD).matchJavaSymbol()); + break; + } + + // For virtual dispatch, walk up the inheritance chain checking + // each inherited method + if (owner.startsWith("android/") //$NON-NLS-1$ + || owner.startsWith("javax/")) { //$NON-NLS-1$ + // The API map has already inlined all inherited methods + // so no need to keep checking up the chain + // -- unless it's the support library which is also in + // the android/ namespace: + if (owner.startsWith("android/support/") && api == -1) { //$NON-NLS-1$ + owner = context.getDriver().getSuperClass(owner); + } else { + owner = null; + } + } else if (owner.startsWith("java/")) { //$NON-NLS-1$ + if (owner.equals("java/text/SimpleDateFormat")) { + checkSimpleDateFormat(context, method, node, minSdk); + } + // Already inlined; see comment above + owner = null; + } else if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) { + owner = context.getDriver().getSuperClass(owner); + } else if (node.getOpcode() == Opcodes.INVOKESTATIC && api == -1) { + // Inherit through static classes as well + owner = context.getDriver().getSuperClass(owner); + } else { + owner = null; + } + + checkingSuperClass = true; + } + } else if (type == AbstractInsnNode.FIELD_INSN) { + FieldInsnNode node = (FieldInsnNode) instruction; + String name = node.name; + String owner = node.owner; + int api = mApiDatabase.getFieldVersion(owner, name); + if (api > minSdk) { + if (method.name.startsWith(SWITCH_TABLE_PREFIX)) { + checkSwitchBlock(context, classNode, node, method, name, owner, + api, minSdk); + continue; + } + + if (isSkippedEnumSwitch(context, classNode, method, node, owner, api)) { + continue; + } + + if (isWithinSdkConditional(context, classNode, method, instruction, api)) { + continue; + } + + String fqcn = getFqcn(owner) + '#' + name; + String message = String.format( + "Field requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + report(context, message, node, method, name, null, + SearchHints.create(FORWARD).matchJavaSymbol()); + } + } else if (type == AbstractInsnNode.LDC_INSN) { + LdcInsnNode node = (LdcInsnNode) instruction; + if (node.cst instanceof Type) { + Type t = (Type) node.cst; + String className = t.getInternalName(); + + int api = mApiDatabase.getClassVersion(className); + if (api > minSdk) { + String fqcn = getFqcn(className); + String message = String.format( + "Class requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + report(context, message, node, method, + className.substring(className.lastIndexOf('/') + 1), null, + SearchHints.create(FORWARD).matchJavaSymbol()); + } + } + } + } + } + } + + private void checkExtendsClass(ClassContext context, ClassNode classNode, int classMinSdk, + String signature) { + int api = mApiDatabase.getClassVersion(signature); + if (api > classMinSdk) { + String fqcn = getFqcn(signature); + String message = String.format( + "Class requires API level %1$d (current min is %2$d): `%3$s`", + api, classMinSdk, fqcn); + + String name = signature.substring(signature.lastIndexOf('/') + 1); + name = name.substring(name.lastIndexOf('$') + 1); + SearchHints hints = SearchHints.create(BACKWARD).matchJavaSymbol(); + int lineNumber = ClassContext.findLineNumber(classNode); + Location location = context.getLocationForLine(lineNumber, name, null, + hints); + context.report(UNSUPPORTED, location, message); + } + } + + private static void checkSimpleDateFormat(ClassContext context, MethodNode method, + MethodInsnNode node, int minSdk) { + if (minSdk >= 9) { + // Already OK + return; + } + if (node.name.equals(CONSTRUCTOR_NAME) && !node.desc.equals("()V")) { //$NON-NLS-1$ + // Check first argument + AbstractInsnNode prev = LintUtils.getPrevInstruction(node); + if (prev != null && !node.desc.equals("(Ljava/lang/String;)V")) { //$NON-NLS-1$ + prev = LintUtils.getPrevInstruction(prev); + } + if (prev != null && prev.getOpcode() == Opcodes.LDC) { + LdcInsnNode ldc = (LdcInsnNode) prev; + Object cst = ldc.cst; + if (cst instanceof String) { + String pattern = (String) cst; + boolean isEscaped = false; + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == '\'') { + isEscaped = !isEscaped; + } else if (!isEscaped && (c == 'L' || c == 'c')) { + String message = String.format( + "The pattern character '%1$c' requires API level 9 (current " + + "min is %2$d) : \"`%3$s`\"", c, minSdk, pattern); + report(context, message, node, method, pattern, null, + SearchHints.create(FORWARD)); + return; + } + } + } + } + } + } + + @SuppressWarnings("rawtypes") // ASM API + private static boolean methodDefinedLocally(ClassNode classNode, String name, String desc) { + List methodList = classNode.methods; + for (Object m : methodList) { + MethodNode method = (MethodNode) m; + if (name.equals(method.name) && desc.equals(method.desc)) { + return true; + } + } + + return false; + } + + @SuppressWarnings("rawtypes") // ASM API + private static void checkSwitchBlock(ClassContext context, ClassNode classNode, + FieldInsnNode field, MethodNode method, String name, String owner, int api, + int minSdk) { + // Switch statements on enums are tricky. The compiler will generate a method + // which returns an array of the enum constants, indexed by their ordinal() values. + // However, we only want to complain if the code is actually referencing one of + // the non-available enum fields. + // + // For the android.graphics.PorterDuff.Mode enum for example, the first few items + // in the array are populated like this: + // + // L0 + // ALOAD 0 + // GETSTATIC android/graphics/PorterDuff$Mode.ADD : Landroid/graphics/PorterDuff$Mode; + // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I + // ICONST_1 + // IASTORE + // L1 + // GOTO L3 + // L2 + // FRAME FULL [[I] [java/lang/NoSuchFieldError] + // POP + // L3 + // FRAME SAME + // ALOAD 0 + // GETSTATIC android/graphics/PorterDuff$Mode.CLEAR : Landroid/graphics/PorterDuff$Mode; + // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I + // ICONST_2 + // IASTORE + // ... + // So if we for example find that the "ADD" field isn't accessible, since it requires + // API 11, we need to + // (1) First find out what its ordinal number is. We can look at the following + // instructions to discover this; it's the "ICONST_1" and "IASTORE" instructions. + // (After ICONST_5 it moves on to BIPUSH 6, BIPUSH 7, etc.) + // (2) Find the corresponding *usage* of this switch method. For the above enum, + // the switch ordinal lookup method will be called + // "$SWITCH_TABLE$android$graphics$PorterDuff$Mode" with desc "()[I". + // This means we will be looking for an invocation in some other method which looks + // like this: + // INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I + // (obviously, it can be invoked more than once) + // Note that it can be used more than once in this class and all sites should be + // checked! + // (3) Look up the corresponding table switch, which should look something like this: + // INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I + // ALOAD 0 + // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I + // IALOAD + // LOOKUPSWITCH + // 2: L1 + // 11: L2 + // default: L3 + // Here we need to see if the LOOKUPSWITCH instruction is referencing our target + // case. Above we were looking for the "ADD" case which had ordinal 1. Since this + // isn't explicitly referenced, we can ignore this field reference. + AbstractInsnNode next = field.getNext(); + if (next == null || next.getOpcode() != Opcodes.INVOKEVIRTUAL) { + return; + } + next = next.getNext(); + if (next == null) { + return; + } + int ordinal; + switch (next.getOpcode()) { + case Opcodes.ICONST_0: ordinal = 0; break; + case Opcodes.ICONST_1: ordinal = 1; break; + case Opcodes.ICONST_2: ordinal = 2; break; + case Opcodes.ICONST_3: ordinal = 3; break; + case Opcodes.ICONST_4: ordinal = 4; break; + case Opcodes.ICONST_5: ordinal = 5; break; + case Opcodes.BIPUSH: { + IntInsnNode iin = (IntInsnNode) next; + ordinal = iin.operand; + break; + } + default: + return; + } + + // Find usages of this call site + List methodList = classNode.methods; + for (Object m : methodList) { + InsnList nodes = ((MethodNode) m).instructions; + for (int i = 0, n = nodes.size(); i < n; i++) { + AbstractInsnNode instruction = nodes.get(i); + if (instruction.getOpcode() != Opcodes.INVOKESTATIC){ + continue; + } + MethodInsnNode node = (MethodInsnNode) instruction; + if (node.name.equals(method.name) + && node.desc.equals(method.desc) + && node.owner.equals(classNode.name)) { + // Find lookup switch + AbstractInsnNode target = getNextInstruction(node); + while (target != null) { + if (target.getOpcode() == Opcodes.LOOKUPSWITCH) { + LookupSwitchInsnNode lookup = (LookupSwitchInsnNode) target; + @SuppressWarnings("unchecked") // ASM API + List keys = lookup.keys; + if (keys != null && keys.contains(ordinal)) { + String fqcn = getFqcn(owner) + '#' + name; + String message = String.format( + "Enum value requires API level %1$d " + + "(current min is %2$d): `%3$s`", + api, minSdk, fqcn); + report(context, message, lookup, (MethodNode) m, name, null, + SearchHints.create(FORWARD).matchJavaSymbol()); + + // Break out of the inner target search only; the switch + // statement could be used in other places in this class as + // well and we want to report all problematic usages. + break; + } + } + target = getNextInstruction(target); + } + } + } + } + } + + private static boolean isEnumSwitchInitializer(ClassNode classNode) { + @SuppressWarnings("rawtypes") // ASM API + List fieldList = classNode.fields; + for (Object f : fieldList) { + FieldNode field = (FieldNode) f; + if (field.name.startsWith(ENUM_SWITCH_PREFIX)) { + return true; + } + } + return false; + } + + private static MethodNode findEnumSwitchUsage(ClassNode classNode, String owner) { + String target = ENUM_SWITCH_PREFIX + owner.replace('/', '$'); + @SuppressWarnings("rawtypes") // ASM API + List methodList = classNode.methods; + for (Object f : methodList) { + MethodNode method = (MethodNode) f; + InsnList nodes = method.instructions; + for (int i = 0, n = nodes.size(); i < n; i++) { + AbstractInsnNode instruction = nodes.get(i); + if (instruction.getOpcode() == Opcodes.GETSTATIC) { + FieldInsnNode field = (FieldInsnNode) instruction; + if (field.name.equals(target)) { + return method; + } + } + } + } + return null; + } + + private static boolean isSkippedEnumSwitch(ClassContext context, ClassNode classNode, + MethodNode method, FieldInsnNode node, String owner, int api) { + // Enum-style switches are handled in a different way: it generates + // an innerclass where the class initializer creates a mapping from + // the ordinals to the corresponding values. + // Here we need to check to see if the call site which *used* the + // table switch had a suppress node on it (or up that node's parent + // chain + AbstractInsnNode next = getNextInstruction(node); + if (next != null && next.getOpcode() == Opcodes.INVOKEVIRTUAL + && CLASS_CONSTRUCTOR.equals(method.name) + && ORDINAL_METHOD.equals(((MethodInsnNode) next).name) + && classNode.outerClass != null + && isEnumSwitchInitializer(classNode)) { + LintDriver driver = context.getDriver(); + ClassNode outer = driver.getOuterClassNode(classNode); + if (outer != null) { + MethodNode switchUser = findEnumSwitchUsage(outer, owner); + if (switchUser != null) { + // Is the API check suppressed at the call site? + if (driver.isSuppressed(UNSUPPORTED, outer, switchUser, + null)) { + return true; + } + // Is there a @TargetAPI annotation on the method or + // class referencing this switch map class? + if (getLocalMinSdk(switchUser.invisibleAnnotations) >= api + || getLocalMinSdk(outer.invisibleAnnotations) >= api) { + return true; + } + } + } + } + return false; + } + + /** + * Return the {@code @TargetApi} level to use for the given {@code classNode}; + * this will be the {@code @TargetApi} annotation on the class, or any outer + * methods (for anonymous inner classes) or outer classes (for inner classes) + * of the given class. + */ + private static int getClassMinSdk(ClassContext context, ClassNode classNode) { + int classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations); + if (classMinSdk != -1) { + return classMinSdk; + } + + LintDriver driver = context.getDriver(); + while (classNode != null) { + ClassNode prev = classNode; + classNode = driver.getOuterClassNode(classNode); + if (classNode != null) { + // TODO: Should this be "curr" instead? + if (prev.outerMethod != null) { + @SuppressWarnings("rawtypes") // ASM API + List methods = classNode.methods; + for (Object m : methods) { + MethodNode method = (MethodNode) m; + if (method.name.equals(prev.outerMethod) + && method.desc.equals(prev.outerMethodDesc)) { + // Found the outer method for this anonymous class; check method + // annotations on it, then continue up the class hierarchy + int methodMinSdk = getLocalMinSdk(method.invisibleAnnotations); + if (methodMinSdk != -1) { + return methodMinSdk; + } + + break; + } + } + } + + classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations); + if (classMinSdk != -1) { + return classMinSdk; + } + } + } + + return -1; + } + + /** + * Returns the minimum SDK to use according to the given annotation list, or + * -1 if no annotation was found. + * + * @param annotations a list of annotation nodes from ASM + * @return the API level to use for this node, or -1 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static int getLocalMinSdk(List annotations) { + if (annotations != null) { + for (AnnotationNode annotation : (List)annotations) { + String desc = annotation.desc; + if (desc.endsWith(TARGET_API_VMSIG)) { + if (annotation.values != null) { + for (int i = 0, n = annotation.values.size(); i < n; i += 2) { + String key = (String) annotation.values.get(i); + if (key.equals("value")) { //$NON-NLS-1$ + Object value = annotation.values.get(i + 1); + if (value instanceof Integer) { + return (Integer) value; + } + } + } + } + } + } + } + + return -1; + } + + /** + * Returns the minimum SDK to use in the given element context, or -1 if no + * {@code tools:targetApi} attribute was found. + * + * @param element the element to look at, including parents + * @return the API level to use for this element, or -1 + */ + private static int getLocalMinSdk(@NonNull Element element) { + //noinspection ConstantConditions + while (element != null) { + String targetApi = element.getAttributeNS(TOOLS_URI, ATTR_TARGET_API); + if (targetApi != null && !targetApi.isEmpty()) { + if (Character.isDigit(targetApi.charAt(0))) { + try { + return Integer.parseInt(targetApi); + } catch (NumberFormatException e) { + break; + } + } else { + return SdkVersionInfo.getApiByBuildCode(targetApi, true); + } + } + + Node parent = element.getParentNode(); + if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) { + element = (Element) parent; + } else { + break; + } + } + + return -1; + } + + private static void report(final ClassContext context, String message, AbstractInsnNode node, + MethodNode method, String patternStart, String patternEnd, SearchHints hints) { + int lineNumber = node != null ? ClassContext.findLineNumber(node) : -1; + + // If looking for a constructor, the string we'll see in the source is not the + // method name () but the class name + if (patternStart != null && patternStart.equals(CONSTRUCTOR_NAME) + && node instanceof MethodInsnNode) { + if (hints != null) { + hints = hints.matchConstructor(); + } + patternStart = ((MethodInsnNode) node).owner; + } + + if (patternStart != null) { + int index = patternStart.lastIndexOf('$'); + if (index != -1) { + patternStart = patternStart.substring(index + 1); + } + index = patternStart.lastIndexOf('/'); + if (index != -1) { + patternStart = patternStart.substring(index + 1); + } + } + + Location location = context.getLocationForLine(lineNumber, patternStart, patternEnd, + hints); + context.report(UNSUPPORTED, method, node, location, message); + } + + // ---- Implements UastScanner ---- + + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { + if (mApiDatabase == null) { + return new AbstractUastVisitor() { + @Override + public boolean visitElement(UElement element) { + // No-op. Workaround for super currently calling + // ProgressIndicatorProvider.checkCanceled(); + return super.visitElement(element); + } + }; + } + return new ApiVisitor(context); + } + + @Nullable + @Override + public List> getApplicableUastTypes() { + List> types = new ArrayList>(9); + types.add(UImportStatement.class); + types.add(USimpleNameReferenceExpression.class); + types.add(UVariable.class); + types.add(UTryExpression.class); + types.add(UBinaryExpressionWithType.class); + types.add(UBinaryExpression.class); + types.add(UCallExpression.class); + types.add(UClass.class); + types.add(UMethod.class); + return types; + } + + /** + * Checks whether the given instruction is a benign usage of a constant defined in + * a later version of Android than the application's {@code minSdkVersion}. + * + * @param node the instruction to check + * @param name the name of the constant + * @param owner the field owner + * @return true if the given usage is safe on older versions than the introduction + * level of the constant + */ + public static boolean isBenignConstantUsage( + @Nullable UElement node, + @NonNull String name, + @NonNull String owner) { + if (owner.equals("android/os/Build$VERSION_CODES")) { //$NON-NLS-1$ + // These constants are required for compilation, not execution + // and valid code checks it even on older platforms + return true; + } + if (owner.equals("android/view/ViewGroup$LayoutParams") //$NON-NLS-1$ + && name.equals("MATCH_PARENT")) { //$NON-NLS-1$ + return true; + } + if (owner.equals("android/widget/AbsListView") //$NON-NLS-1$ + && ((name.equals("CHOICE_MODE_NONE") //$NON-NLS-1$ + || name.equals("CHOICE_MODE_MULTIPLE") //$NON-NLS-1$ + || name.equals("CHOICE_MODE_SINGLE")))) { //$NON-NLS-1$ + // android.widget.ListView#CHOICE_MODE_MULTIPLE and friends have API=1, + // but in API 11 it was moved up to the parent class AbsListView. + // Referencing AbsListView#CHOICE_MODE_MULTIPLE technically requires API 11, + // but the constant is the same as the older version, so accept this without + // warning. + return true; + } + + // Gravity#START and Gravity#END are okay; these were specifically written to + // be backwards compatible (by using the same lower bits for START as LEFT and + // for END as RIGHT) + if ("android/view/Gravity".equals(owner) //$NON-NLS-1$ + && ("START".equals(name) || "END".equals(name))) { //$NON-NLS-1$ //$NON-NLS-2$ + return true; + } + + if (node == null) { + return false; + } + + // It's okay to reference the constant as a case constant (since that + // code path won't be taken) or in a condition of an if statement + UElement curr = node.getContainingElement(); + while (curr != null) { + if (curr instanceof USwitchClauseExpression) { + List caseValues = ((USwitchClauseExpression) curr).getCaseValues(); + if (caseValues != null) { + for (UExpression condition : caseValues) { + if (condition != null && UastUtils.isChildOf(node, condition, false)) { + return true; + } + } + } + return false; + } else if (curr instanceof UIfExpression) { + UExpression condition = ((UIfExpression) curr).getCondition(); + return UastUtils.isChildOf(node, condition, false); + } else if (curr instanceof UMethod || curr instanceof UClass) { + break; + } + curr = curr.getContainingElement(); + } + + return false; + } + + private final class ApiVisitor extends AbstractUastVisitor { + private final JavaContext mContext; + + private ApiVisitor(JavaContext context) { + mContext = context; + } + + @Override + public boolean visitImportStatement(UImportStatement statement) { + if (!statement.isOnDemand()) { + PsiElement resolved = statement.resolve(); + if (resolved instanceof PsiField) { + checkField(statement, (PsiField)resolved); + } + } + + return super.visitImportStatement(statement); + } + + @Override + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + PsiElement resolved = node.resolve(); + if (resolved instanceof PsiField) { + checkField(node, (PsiField)resolved); + } + + return super.visitSimpleNameReferenceExpression(node); + } + + @Override + public boolean visitBinaryExpressionWithType(UBinaryExpressionWithType node) { + if (UastExpressionUtils.isTypeCast(node)) { + visitTypeCastExpression(node); + } + + return super.visitBinaryExpressionWithType(node); + } + + private void visitTypeCastExpression(UBinaryExpressionWithType expression) { + UExpression operand = expression.getOperand(); + + PsiType operandType = operand.getExpressionType(); + PsiType castType = expression.getType(); + + if (castType.equals(operandType)) { + return; + } + if (!(operandType instanceof PsiClassType)) { + return; + } + if (!(castType instanceof PsiClassType)) { + return; + } + PsiClassType classType = (PsiClassType)operandType; + PsiClassType interfaceType = (PsiClassType)castType; + checkCast(expression, classType, interfaceType); + } + + private void checkCast(@NonNull UElement node, @NonNull PsiClassType classType, + @NonNull PsiClassType interfaceType) { + if (classType.equals(interfaceType)) { + return; + } + JavaEvaluator evaluator = mContext.getEvaluator(); + String classTypeInternal = evaluator.getInternalName(classType); + String interfaceTypeInternal = evaluator.getInternalName(interfaceType); + if ("java/lang/Object".equals(interfaceTypeInternal)) { + return; + } + + int api = mApiDatabase.getValidCastVersion(classTypeInternal, interfaceTypeInternal); + if (api == -1) { + return; + } + + int minSdk = getMinSdk(mContext); + if (api <= minSdk) { + return; + } + + if (isSuppressed(api, node, minSdk, mContext)) { + return; + } + + Location location = mContext.getUastLocation(node); + String message = String.format("Cast from %1$s to %2$s requires API level %3$d (current min is %4$d)", + UastLintUtils.getClassName(classType), + UastLintUtils.getClassName(interfaceType), api, minSdk); + mContext.report(UNSUPPORTED, location, message); + } + + @Override + public boolean visitMethod(UMethod method) { + // API check for default methods + if (method.getModifierList().hasExplicitModifier(PsiModifier.DEFAULT)) { + int api = 24; // minSdk for default methods + int minSdk = getMinSdk(mContext); + + if (!isSuppressed(api, method, minSdk, mContext)) { + Location location = mContext.getLocation((PsiElement) method); + String message = String.format("Default method requires API level %1$d " + + "(current min is %2$d)", api, minSdk); + mContext.reportUast(UNSUPPORTED, method, location, message); + } + } + + return super.visitMethod(method); + } + + @Override + public boolean visitClass(UClass aClass) { + // Check for repeatable annotations + if (aClass.isAnnotationType()) { + PsiModifierList modifierList = aClass.getModifierList(); + if (modifierList != null) { + for (PsiAnnotation annotation : modifierList.getAnnotations()) { + String name = annotation.getQualifiedName(); + if ("java.lang.annotation.Repeatable".equals(name)) { + int api = 24; // minSdk for repeatable annotations + int minSdk = getMinSdk(mContext); + if (!isSuppressed(api, aClass, minSdk, mContext)) { + Location location = mContext.getLocation(annotation); + String message = String.format("Repeatable annotation requires " + + "API level %1$d (current min is %2$d)", api, minSdk); + mContext.report(UNSUPPORTED, annotation, location, message); + } + } else if ("java.lang.annotation.Target".equals(name)) { + PsiNameValuePair[] attributes = annotation.getParameterList() + .getAttributes(); + for (PsiNameValuePair pair : attributes) { + PsiAnnotationMemberValue value = pair.getValue(); + if (value instanceof PsiArrayInitializerMemberValue) { + PsiArrayInitializerMemberValue array + = (PsiArrayInitializerMemberValue) value; + for (PsiAnnotationMemberValue t : array.getInitializers()) { + checkAnnotationTarget(t, modifierList); + } + } else if (value != null) { + checkAnnotationTarget(value, modifierList); + } + } + } + } + } + } + + return super.visitClass(aClass); + } + + private void checkAnnotationTarget(@NonNull PsiAnnotationMemberValue element, + PsiModifierList modifierList) { + if (element instanceof UReferenceExpression) { + UReferenceExpression ref = (UReferenceExpression) element; + String referenceName = UastLintUtils.getReferenceName(ref); + if ("TYPE_PARAMETER".equals(referenceName) + || "TYPE_USE".equals(referenceName)) { + PsiAnnotation retention = modifierList + .findAnnotation("java.lang.annotation.Retention"); + if (retention == null || + retention.getText().contains("RUNTIME")) { + Location location = mContext.getLocation(element); + String message = String.format("Type annotations are not " + + "supported in Android: %1$s", referenceName); + mContext.report(UNSUPPORTED, element, location, message); + } + } + } + } + + @Override + public boolean visitCallExpression(UCallExpression expression) { + PsiMethod method = expression.resolve(); + if (method != null) { + PsiParameterList parameterList = method.getParameterList(); + if (parameterList.getParametersCount() > 0) { + PsiParameter[] parameters = parameterList.getParameters(); + List arguments = expression.getValueArguments(); + for (int i = 0; i < parameters.length; i++) { + PsiType parameterType = parameters[i].getType(); + if (parameterType instanceof PsiClassType) { + if (i >= arguments.size()) { + // We can end up with more arguments than parameters when + // there is a varargs call. + break; + } + UExpression argument = arguments.get(i); + PsiType argumentType = argument.getExpressionType(); + if (argumentType == null || parameterType.equals(argumentType) + || !(argumentType instanceof PsiClassType)) { + continue; + } + checkCast(argument, (PsiClassType) argumentType, + (PsiClassType) parameterType); + } + } + } + + PsiModifierList modifierList = method.getModifierList(); + if (!checkRequiresApi(expression, method, modifierList)) { + PsiClass containingClass = method.getContainingClass(); + if (containingClass != null) { + modifierList = containingClass.getModifierList(); + if (modifierList != null) { + checkRequiresApi(expression, method, modifierList); + } + } + } + } + + return super.visitCallExpression(expression); + } + + // Look for @RequiresApi in modifier lists + private boolean checkRequiresApi(UCallExpression expression, PsiMethod method, + PsiModifierList modifierList) { + for (PsiAnnotation annotation : modifierList.getAnnotations()) { + if (REQUIRES_API_ANNOTATION.equals(annotation.getQualifiedName())) { + int api = (int) SupportAnnotationDetector.getLongAttribute(annotation, + ATTR_VALUE, -1); + int minSdk = getMinSdk(mContext); + if (api > minSdk) { + int target = getTargetApi(expression); + if (target == -1 || api > target) { + Location location; + location = mContext.getUastLocation(expression); + String fqcn = method.getName(); + String message = String.format( + "Call requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + mContext.report(UNSUPPORTED, location, message); + } + } + + return true; + } + } + + return false; + } + + @Override + public boolean visitVariable(UVariable node) { + if (node instanceof ULocalVariable) { + visitLocalVariable((ULocalVariable) node); + } + return super.visitVariable(node); + } + + private void visitLocalVariable(ULocalVariable variable) { + UExpression initializer = variable.getUastInitializer(); + if (initializer == null) { + return; + } + + PsiType initializerType = initializer.getExpressionType(); + if (!(initializerType instanceof PsiClassType)) { + return; + } + + PsiType interfaceType = variable.getType(); + if (initializerType.equals(interfaceType)) { + return; + } + + if (!(interfaceType instanceof PsiClassType)) { + return; + } + + checkCast(initializer, (PsiClassType)initializerType, (PsiClassType)interfaceType); + } + + @Override + public boolean visitBinaryExpression(UBinaryExpression node) { + if (UastExpressionUtils.isAssignment(node)) { + visitAssignmentExpression(node); + } + + return super.visitBinaryExpression(node); + } + + private void visitAssignmentExpression(UBinaryExpression expression) { + UExpression rExpression = expression.getRightOperand(); + PsiType rhsType = rExpression.getExpressionType(); + if (!(rhsType instanceof PsiClassType)) { + return; + } + + PsiType interfaceType = expression.getLeftOperand().getExpressionType(); + if (rhsType.equals(interfaceType)) { + return; + } + + if (!(interfaceType instanceof PsiClassType)) { + return; + } + + checkCast(rExpression, (PsiClassType)rhsType, (PsiClassType)interfaceType); + } + + @Override + public boolean visitTryExpression(UTryExpression statement) { + List resourceList = statement.getResources(); + //noinspection VariableNotUsedInsideIf + if (!resourceList.isEmpty()) { + int api = 19; // minSdk for try with resources + int minSdk = getMinSdk(mContext); + + if (api > minSdk && api > getTargetApi(statement)) { + Location location = mContext.getUastLocation(statement); + String message = String.format("Try-with-resources requires " + + "API level %1$d (current min is %2$d)", api, minSdk); + LintDriver driver = mContext.getDriver(); + if (!driver.isSuppressed(mContext, UNSUPPORTED, statement)) { + mContext.report(UNSUPPORTED, statement, location, message); + } + } + } + + for (UCatchClause catchClause : statement.getCatchClauses()) { + for (UTypeReferenceExpression typeReference : catchClause.getTypeReferences()) { + checkCatchTypeElement(statement, typeReference, typeReference.getType()); + } + } + + return super.visitTryExpression(statement); + } + + private void checkCatchTypeElement(@NonNull UTryExpression statement, + @NonNull UTypeReferenceExpression typeReference, + @Nullable PsiType type) { + PsiClass resolved = null; + if (type instanceof PsiClassType) { + resolved = ((PsiClassType) type).resolve(); + } + if (resolved != null) { + String signature = mContext.getEvaluator().getInternalName(resolved); + int api = mApiDatabase.getClassVersion(signature); + if (api == -1) { + return; + } + int minSdk = getMinSdk(mContext); + if (api <= minSdk) { + return; + } + int target = getTargetApi(statement); + if (target != -1 && api <= target) { + return; + } + + Location location; + location = mContext.getUastLocation(typeReference); + String fqcn = resolved.getQualifiedName(); + String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn); + + // Special case reflective operation exception which can be implicitly used + // with multi-catches: see issue 153406 + if (api == 19 && "ReflectiveOperationException".equals(fqcn)) { + message = String.format("Multi-catch with these reflection exceptions requires API level 19 (current min is %2$d) " + + "because they get compiled to the common but new super type `ReflectiveOperationException`. " + + "As a workaround either create individual catch statements, or catch `Exception`.", + api, minSdk); + } + mContext.report(UNSUPPORTED, location, message); + } + } + + /** + * Checks a Java source field reference. Returns true if the field is known + * regardless of whether it's an invalid field or not + */ + private boolean checkField(@NonNull UElement node, @NonNull PsiField field) { + PsiType type = field.getType(); + // Only look for compile time constants. See JLS 15.28 and JLS 13.4.9. + if (!(type instanceof PsiPrimitiveType) && !LintUtils.isString(type)) { + return false; + } + String name = field.getName(); + PsiClass containingClass = field.getContainingClass(); + if (containingClass == null || name == null) { + return false; + } + String owner = mContext.getEvaluator().getInternalName(containingClass); + int api = mApiDatabase.getFieldVersion(owner, name); + if (api != -1) { + int minSdk = getMinSdk(mContext); + if (api > minSdk + && api > getTargetApi(node)) { + if (isBenignConstantUsage(node, name, owner)) { + return true; + } + + String fqcn = getFqcn(owner) + '#' + name; + + // For import statements, place the underlines only under the + // reference, not the import and static keywords + if (node instanceof UImportStatement) { + UElement reference = ((UImportStatement) node).getImportReference(); + if (reference != null) { + node = reference; + } + } + + LintDriver driver = mContext.getDriver(); + if (driver.isSuppressed(mContext, INLINED, node)) { + return true; + } + + // backwards compatibility: lint used to use this issue type so respect + // older suppress annotations + if (driver.isSuppressed(mContext, UNSUPPORTED, node)) { + return true; + } + if (isWithinVersionCheckConditional(node, api, mContext)) { + return true; + } + if (isPrecededByVersionCheckExit(node, api, mContext)) { + return true; + } + + String message = String.format( + "Field requires API level %1$d (current min is %2$d): `%3$s`", + api, minSdk, fqcn); + + Location location = mContext.getUastLocation(node); + mContext.report(INLINED, node, location, message); + } + + return true; + } + + return false; + } + } + + private static boolean isSuppressed( + int api, UElement element, int minSdk, JavaContext context) { + if (api <= minSdk) { + return true; + } + //if (mySeenTargetApi) { + int target = getTargetApi(element); + if (target != -1) { + if (api <= target) { + return true; + } + } + //} +// TODO: This MUST BE RESTORED +// if (context.getDriver().isSuppressed(UNSUPPORTED, element)) +// if (/*mySeenSuppress &&*/ +// (IntellijLintUtils.isSuppressed(element, myFile, UNSUPPORTED) || IntellijLintUtils.isSuppressed(element, myFile, INLINED))) { +// return true; +// } + + if (isWithinVersionCheckConditional(element, api, context)) { + return true; + } + if (isPrecededByVersionCheckExit(element, api, context)) { + return true; + } + + return false; + } + + private static int getTargetApi(@Nullable UElement scope) { + while (scope != null) { + if (scope instanceof PsiModifierListOwner) { + PsiModifierList modifierList = ((PsiModifierListOwner) scope).getModifierList(); + int targetApi = getTargetApi(modifierList); + if (targetApi != -1) { + return targetApi; + } + } + scope = scope.getContainingElement(); + if (scope instanceof PsiFile) { + break; + } + } + + return -1; + } + + /** + * Returns the API level for the given AST node if specified with + * an {@code @TargetApi} annotation. + * + * @param modifierList the modifier list to check + * @return the target API level, or -1 if not specified + */ + public static int getTargetApi(@Nullable PsiModifierList modifierList) { + if (modifierList == null) { + return -1; + } + + for (PsiAnnotation annotation : modifierList.getAnnotations()) { + String fqcn = annotation.getQualifiedName(); + if (fqcn != null && (fqcn.equals(FQCN_TARGET_API) || fqcn.equals(REQUIRES_API_ANNOTATION) + || fqcn.equals(TARGET_API))) { // when missing imports + PsiAnnotationParameterList parameterList = annotation.getParameterList(); + for (PsiNameValuePair pair : parameterList.getAttributes()) { + PsiAnnotationMemberValue v = pair.getValue(); + if (v instanceof PsiLiteral) { + PsiLiteral literal = (PsiLiteral)v; + Object value = literal.getValue(); + if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof String) { + return codeNameToApi((String) value); + } + } else if (v instanceof PsiArrayInitializerMemberValue) { + PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue)v; + for (PsiAnnotationMemberValue mmv : mv.getInitializers()) { + if (mmv instanceof PsiLiteral) { + PsiLiteral literal = (PsiLiteral)mmv; + Object value = literal.getValue(); + if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof String) { + return codeNameToApi((String) value); + } + } + } + } else if (v instanceof PsiExpression) { + // PsiExpression nodes are not present in light classes, so + // we can use Java PSI api to get the qualified name + if (v instanceof PsiReferenceExpression) { + String name = ((PsiReferenceExpression)v).getQualifiedName(); + return codeNameToApi(name); + } else { + return codeNameToApi(v.getText()); + } + } + } + } + } + + return -1; + } + + public static int codeNameToApi(@NonNull String text) { + int dotIndex = text.lastIndexOf('.'); + if (dotIndex != -1) { + text = text.substring(dotIndex + 1); + } + + return SdkVersionInfo.getApiByBuildCode(text, true); + } + + public static int getRequiredVersion(@NonNull Issue issue, @NonNull String errorMessage, + @NonNull TextFormat format) { + errorMessage = format.toText(errorMessage); + + if (issue == UNSUPPORTED || issue == INLINED) { + Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$ + Matcher matcher = pattern.matcher(errorMessage); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + } + + return -1; + } + + private static boolean isWithinSdkConditional( + @NonNull ClassContext context, + @NonNull ClassNode classNode, + @NonNull MethodNode method, + @NonNull AbstractInsnNode call, + int requiredApi) { + assert requiredApi != -1; + + if (!containsSimpleSdkCheck(method)) { + return false; + } + + try { + // Search in the control graph, from beginning, up to the target call + // node, to see if it's reachable. The call graph is constructed in a + // special way: we include all control flow edges, *except* those that + // are satisfied by a SDK_INT version check (where the operand is a version + // that is at least as high as the one needed for the given call). + // + // If we can reach the call, that means that there is a way this call + // can be reached on some versions, and lint should flag the call/field lookup. + // + // + // Let's say you have code like this: + // if (SDK_INT >= LOLLIPOP) { + // // Call + // return property.hasAdjacentMapping(); + // } + // ... + // + // The compiler will turn this into the following byte code: + // + // 0: getstatic #3; //Field android/os/Build$VERSION.SDK_INT:I + // 3: bipush 21 + // 5: if_icmple 17 + // 8: aload_1 + // 9: invokeinterface #4, 1; //InterfaceMethod + // android/view/ViewDebug$ExportedProperty.hasAdjacentMapping:()Z + // 14: ifeq 17 + // 17: ... code after if loop + // + // When the call graph is constructed, for an if branch we're called twice; once + // where the target is the next instruction (the one taken if byte code check is false) + // and one to the jump label (the one taken if the byte code condition is true). + // + // Notice how at the byte code level, the logic is reversed: the >= instruction + // is turned into "<" and we jump to the code *after* the if clause; otherwise + // it will just fall through. Therefore, if we take a byte code branch, that means + // that the SDK check was *not* satisfied, and conversely, the target call is reachable + // if we don't take the branch. + // + // Therefore, when we build the call graph, we will add call graph nodes for an + // if check if : + // (1) it is some other comparison than <, <= or !=. + // (2) if the byte code comparison check is *not* satisfied, this means that the the + // SDK check was successful and that the call graph should only include + // the jump edge + // (3) all other edges are added + // + // With a flow control graph like that, we can determine whether a target call + // is guarded by a given SDK check: that will be the case if we cannot reach + // the target call in the call graph + + ApiCheckGraph graph = new ApiCheckGraph(requiredApi); + ControlFlowGraph.create(graph, classNode, method); + + // Note: To debug unit tests, you may want to for example do + // ControlFlowGraph.Node callNode = graph.getNode(call); + // Set highlight = Sets.newHashSet(callNode); + // Files.write(graph.toDot(highlight), new File("/tmp/graph.gv"), Charsets.UTF_8); + // This will generate a graphviz file you can visualize with the "dot" utility + AbstractInsnNode first = method.instructions.get(0); + return !graph.isConnected(first, call); + } catch (AnalyzerException e) { + context.log(e, null); + } + + return false; + } + + private static boolean containsSimpleSdkCheck(@NonNull MethodNode method) { + // Look for a compiled version of "if (Build.VERSION.SDK_INT op N) {" + InsnList nodes = method.instructions; + for (int i = 0, n = nodes.size(); i < n; i++) { + AbstractInsnNode instruction = nodes.get(i); + if (isSdkVersionLookup(instruction)) { + AbstractInsnNode bipush = getNextInstruction(instruction); + if (bipush != null && bipush.getOpcode() == Opcodes.BIPUSH) { + AbstractInsnNode ifNode = getNextInstruction(bipush); + if (ifNode != null && ifNode.getType() == AbstractInsnNode.JUMP_INSN) { + return true; + } + } + } + } + + return false; + } + + private static boolean isSdkVersionLookup(@NonNull AbstractInsnNode instruction) { + if (instruction.getOpcode() == Opcodes.GETSTATIC) { + FieldInsnNode fieldNode = (FieldInsnNode) instruction; + return (SDK_INT.equals(fieldNode.name) + && ANDROID_OS_BUILD_VERSION.equals(fieldNode.owner)); + } + return false; + } + + /** + * Control flow graph which skips control flow edges that check + * a given SDK_VERSION requirement that is not met by a given call + */ + private static class ApiCheckGraph extends ControlFlowGraph { + private final int mRequiredApi; + + public ApiCheckGraph(int requiredApi) { + mRequiredApi = requiredApi; + } + + @Override + protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) { + if (from.getType() == AbstractInsnNode.JUMP_INSN && + from.getPrevious() != null && + from.getPrevious().getType() == AbstractInsnNode.INT_INSN) { + IntInsnNode intNode = (IntInsnNode) from.getPrevious(); + if (intNode.getPrevious() != null && isSdkVersionLookup(intNode.getPrevious())) { + JumpInsnNode jumpNode = (JumpInsnNode) from; + int api = intNode.operand; + boolean isJumpEdge = to == jumpNode.label; + boolean includeEdge; + switch (from.getOpcode()) { + case Opcodes.IF_ICMPNE: + includeEdge = api < mRequiredApi || isJumpEdge; + break; + case Opcodes.IF_ICMPLE: + includeEdge = api < mRequiredApi - 1 || isJumpEdge; + break; + case Opcodes.IF_ICMPLT: + includeEdge = api < mRequiredApi || isJumpEdge; + break; + + case Opcodes.IF_ICMPGE: + includeEdge = api < mRequiredApi || !isJumpEdge; + break; + case Opcodes.IF_ICMPGT: + includeEdge = api < mRequiredApi - 1 || !isJumpEdge; + break; + default: + // unexpected comparison for int API level + includeEdge = true; + } + if (!includeEdge) { + return; + } + } + } + + super.add(from, to); + } + } + + private static boolean isPrecededByVersionCheckExit( + UElement element, int api, JavaContext context) { + UElement current = UastUtils.getParentOfType(element, UExpression.class); + if (current != null) { + UElement prev = getPreviousStatement(current); + if (prev == null) { + //noinspection unchecked + current = UastUtils.getParentOfType(current, UExpression.class, true, + UMethod.class, UClass.class); + } else { + current = prev; + } + } + while (current != null) { + if (current instanceof UIfExpression) { + UIfExpression ifStatement = (UIfExpression)current; + UExpression thenBranch = ifStatement.getThenExpression(); + UExpression elseBranch = ifStatement.getElseExpression(); + if (thenBranch != null) { + Boolean level = isVersionCheckConditional(api, thenBranch, ifStatement, context); + //noinspection VariableNotUsedInsideIf + if (level != null) { + // See if the body does an immediate return + if (isUnconditionalReturn(thenBranch)) { + return true; + } + } + } + if (elseBranch != null) { + Boolean level = isVersionCheckConditional(api, elseBranch, ifStatement, context); + //noinspection VariableNotUsedInsideIf + if (level != null) { + if (isUnconditionalReturn(elseBranch)) { + return true; + } + } + } + } + UElement prev = getPreviousStatement(current); + if (prev == null) { + //noinspection unchecked + current = UastUtils.getParentOfType(current, UExpression.class, true, + UMethod.class, UClass.class); + if (current == null) { + return false; + } + } else { + current = prev; + } + } + + return false; + } + + private static boolean isUnconditionalReturn(UExpression statement) { + if (statement instanceof UBlockExpression) { + List expressions = ((UBlockExpression) statement).getExpressions(); + if (expressions.size() == 1 && expressions.get(0) instanceof UReturnExpression) { + return true; + } + } + return statement instanceof UReturnExpression; + } + + + @Nullable + public static UElement getPreviousStatement(UElement element) { + //TODO + return null; + //final PsiElement prevStatement = PsiTreeUtil.skipSiblingsBackward(element, + // PsiWhiteSpace.class, PsiComment.class); + //return prevStatement instanceof PsiStatement ? (PsiStatement)prevStatement : null; + } + + public static boolean isWithinVersionCheckConditional( + UElement element, int api, JavaContext context) { + UElement current = element.getContainingElement(); + UElement prev = element; + while (current != null) { + if (current instanceof UIfExpression) { + UIfExpression ifStatement = (UIfExpression) current; + Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement, context); + if (isConditional != null) { + return isConditional; + } + } else if (current instanceof UMethod || current instanceof UFile) { + return false; + } + prev = current; + current = current.getContainingElement(); + } + + return false; + } + + @Nullable + private static Boolean isVersionCheckConditional( + int api, + UElement prev, + UIfExpression ifStatement, + @NonNull JavaContext context) { + UExpression condition = ifStatement.getCondition(); + if (condition != prev && condition instanceof UBinaryExpression) { + Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement, (UBinaryExpression) condition); + if (isConditional != null) { + return isConditional; + } + } else if (condition instanceof UCallExpression) { + UCallExpression call = (UCallExpression) condition; + PsiMethod method = call.resolve(); + if (method != null && !method.hasModifierProperty(PsiModifier.ABSTRACT)) { + UExpression body = context.getUastContext().getMethodBody(method); + List expressions; + if (body instanceof UBlockExpression) { + expressions = ((UBlockExpression) body).getExpressions(); + } else { + expressions = Collections.singletonList(body); + } + + if (expressions.size() == 1) { + UExpression statement = expressions.get(0); + if (statement instanceof UReturnExpression) { + UReturnExpression returnStatement = (UReturnExpression) statement; + UExpression returnValue = returnStatement.getReturnExpression(); + if (returnValue instanceof UBinaryExpression) { + Boolean isConditional = isVersionCheckConditional(api, null, null, + (UBinaryExpression) returnValue); + if (isConditional != null) { + return isConditional; + } + } + } + } + } + } + return null; + } + + @Nullable + private static Boolean isVersionCheckConditional(int api, + @Nullable UElement prev, + @Nullable UIfExpression ifStatement, + @NonNull UBinaryExpression binary) { + UastBinaryOperator tokenType = binary.getOperator(); + if (tokenType == UastBinaryOperator.GREATER || tokenType == UastBinaryOperator.GREATER_OR_EQUAL || + tokenType == UastBinaryOperator.LESS_OR_EQUAL || tokenType == UastBinaryOperator.LESS || + tokenType == UastBinaryOperator.EQUALS || tokenType == UastBinaryOperator.IDENTITY_EQUALS) { + UExpression left = binary.getLeftOperand(); + if (left instanceof UReferenceExpression) { + UReferenceExpression ref = (UReferenceExpression)left; + if (SDK_INT.equals(ref.getResolvedName())) { + UExpression right = binary.getRightOperand(); + int level = -1; + if (right instanceof UReferenceExpression) { + UReferenceExpression ref2 = (UReferenceExpression)right; + String codeName = ref2.getResolvedName(); + if (codeName == null) { + return false; + } + level = SdkVersionInfo.getApiByBuildCode(codeName, true); + } else if (right instanceof ULiteralExpression) { + ULiteralExpression lit = (ULiteralExpression)right; + Object value = lit.getValue(); + if (value instanceof Integer) { + level = (Integer) value; + } + } + if (level != -1) { + boolean fromThen = ifStatement == null || prev == ifStatement.getThenExpression(); + boolean fromElse = ifStatement != null && prev == ifStatement.getElseExpression(); + assert fromThen == !fromElse; + if (tokenType == UastBinaryOperator.GREATER_OR_EQUAL) { + // if (SDK_INT >= ICE_CREAM_SANDWICH) { } else { ... } + return level >= api && fromThen; + } + else if (tokenType == UastBinaryOperator.GREATER) { + // if (SDK_INT > ICE_CREAM_SANDWICH) { } else { ... } + return level >= api - 1 && fromThen; + } + else if (tokenType == UastBinaryOperator.LESS_OR_EQUAL) { + // if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { } + return level >= api - 1 && fromElse; + } + else if (tokenType == UastBinaryOperator.LESS) { + // if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { } + return level >= api && fromElse; + } + else if (tokenType == UastBinaryOperator.EQUALS + || tokenType == UastBinaryOperator.IDENTITY_EQUALS) { + // if (SDK_INT == ICE_CREAM_SANDWICH) { } else { } + return level >= api && fromThen; + } else { + assert false : tokenType; + } + } + } + } + } else if (tokenType == UastBinaryOperator.LOGICAL_AND + && (ifStatement != null && prev == ifStatement.getThenExpression())) { + if (isAndedWithConditional(ifStatement.getCondition(), api, prev)) { + return true; + } + } + return null; + } + + private static boolean isAndedWithConditional(UElement element, int api, @Nullable UElement before) { + if (element instanceof UBinaryExpression) { + UBinaryExpression inner = (UBinaryExpression) element; + if (inner.getOperator() == UastBinaryOperator.LOGICAL_AND) { + return isAndedWithConditional(inner.getLeftOperand(), api, before) || + inner.getRightOperand() != before && isAndedWithConditional(inner.getRightOperand(), api, before); + } else if (inner.getLeftOperand() instanceof UReferenceExpression && + SDK_INT.equals(((UReferenceExpression)inner.getLeftOperand()).getResolvedName())) { + int level = -1; + UastOperator tokenType = inner.getOperator(); + UExpression right = inner.getRightOperand(); + if (right instanceof UReferenceExpression) { + UReferenceExpression ref2 = (UReferenceExpression)right; + String codeName = ref2.getResolvedName(); + if (codeName == null) { + return false; + } + level = SdkVersionInfo.getApiByBuildCode(codeName, true); + } else if (right instanceof ULiteralExpression) { + ULiteralExpression lit = (ULiteralExpression)right; + Object value = lit.getValue(); + if (value instanceof Integer) { + level = ((Integer)value).intValue(); + } + } + if (level != -1) { + if (tokenType == UastBinaryOperator.GREATER_OR_EQUAL) { + // if (SDK_INT >= ICE_CREAM_SANDWICH && + return level >= api; + } + else if (tokenType == UastBinaryOperator.GREATER) { + // if (SDK_INT > ICE_CREAM_SANDWICH) && + return level >= api - 1; + } + else if (tokenType == UastBinaryOperator.EQUALS + || tokenType == UastBinaryOperator.IDENTITY_EQUALS) { + // if (SDK_INT == ICE_CREAM_SANDWICH) && + return level >= api; + } + } + } + } + + return false; + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.kt b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.kt deleted file mode 100755 index 8dfb03dc513..00000000000 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.kt +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tools.klint.checks - -import com.android.SdkConstants.TARGET_API -import com.android.annotations.NonNull -import com.android.sdklib.SdkVersionInfo -import com.android.tools.klint.client.api.IssueRegistry -import com.android.tools.klint.detector.api.* -import org.jetbrains.uast.* -import org.jetbrains.uast.check.UastAndroidContext -import org.jetbrains.uast.check.UastScanner -import org.jetbrains.uast.java.JavaUVariable -import org.jetbrains.uast.visitor.AbstractUastVisitor -import org.jetbrains.uast.visitor.UastVisitor -import java.util.* - -/** - * Looks for usages of APIs that are not supported in all the versions targeted - * by this application (according to its minimum API requirement in the manifest). - */ -open class ApiDetector : Detector(), UastScanner { - - protected var mApiDatabase: ApiLookup? = null - private var mWarnedMissingDb: Boolean = false - - @NonNull - override fun getSpeed(): Speed { - return Speed.SLOW - } - - override fun beforeCheckProject(@NonNull context: Context) { - mApiDatabase = ApiLookup.get(context.client) - - if (mApiDatabase == null && !mWarnedMissingDb) { - mWarnedMissingDb = true - context.report(IssueRegistry.LINT_ERROR, Location.create(context.file), - "Can't find API database; API check not performed") - } - } - - override fun createUastVisitor(context: UastAndroidContext): UastVisitor { - return ApiVersionVisitor(context) - } - - private inner class ApiVersionVisitor(val context: UastAndroidContext) : AbstractUastVisitor() { - private var mMinApi = -1 - - override fun visitCallExpression(node: UCallExpression): Boolean { - when (node.kind) { - UastCallKind.FUNCTION_CALL -> checkVersion(context, node, node.functionReference?.resolve(context)) - UastCallKind.CONSTRUCTOR_CALL -> checkVersion(context, node, node.classReference?.resolve(context)) - } - - return super.visitCallExpression(node) - } - - override fun visitBinaryExpressionWithType(node: UBinaryExpressionWithType): Boolean { - val typeRef = node.typeReference - if (typeRef != null) { - checkVersion(context, typeRef, node.type.resolve(context)) - } - - return super.visitBinaryExpressionWithType(node) - } - - override fun visitSimpleReferenceExpression(node: USimpleReferenceExpression): Boolean { - checkVersion(context, node, node.resolve(context) as? UVariable) - return super.visitSimpleReferenceExpression(node) - } - - override fun visitClass(node: UClass): Boolean { - val nameElement = node.nameElement - - if (nameElement != null) { - for (type in node.superTypes) { - checkVersion(context, nameElement, type.resolveClass(context)) - } - } - - return super.visitClass(node) - } - - override fun visitClassLiteralExpression(node: UClassLiteralExpression): Boolean { - val clazz = node.type?.resolve(context) - if (clazz != null) { - checkVersion(context, node, clazz) - } - return super.visitClassLiteralExpression(node) - } - - override fun visitFunction(node: UFunction): Boolean { - if (!node.hasModifier(UastModifier.OVERRIDE)) { - val parentClass = node.parent as? UClass ?: return false - - val db = mApiDatabase ?: return false - val desc = node.bytecodeDescriptor ?: return false - - val buildSdk = context.lintContext.getMainProject().getBuildSdk() - if (buildSdk == -1) return false - - for (type in parentClass.superTypes) { - val clazz = type.resolve(context) ?: continue - if (!isSdkClass(clazz)) continue - val internalName = clazz.internalName ?: continue - - val methodSdkLevel = db.getCallVersion(internalName, node.name, desc) - if (methodSdkLevel != -1 && methodSdkLevel > buildSdk) { - val message = "This method is not overriding anything with the current build " + - "target, but will in API level $methodSdkLevel (current target is $buildSdk): `${node.name}`" - context.report(OVERRIDE, node, context.getLocation(node.nameElement), message) - } - } - } - - return false - } - - private fun checkIfSpecialCase(declaration: UDeclaration, owner: UClass): Boolean { - if (declaration is UVariable && owner.fqName == "android.os.Build.VERSION_CODES") return true - if (declaration.name == "MATCH_PARENT" && owner.fqName == "android.view.ViewGroup.LayoutParams") return true - if ((declaration.name == "CHOICE_MODE_NONE" || declaration.name == "CHOICE_MODE_MULTIPLE" - || declaration.name == "CHOICE_MODE_SINGLE") && owner.fqName == "android.widget.AbsListView") return true - if ((declaration.name == "START" || declaration.name == "END") && owner.fqName == "android.view.Gravity") return true - - return false - } - - private fun checkVersion(context: UastAndroidContext, node: UElement, declaration: UDeclaration?) { - if (declaration == null) return - val db = mApiDatabase ?: return - - val projectMinSdk = getMinSdk(context) - if (projectMinSdk == -1) return - - fun check(declarationSdkLevel: Int, parentClass: UClass?) { - if (declarationSdkLevel == -1) return - if (parentClass != null && checkIfSpecialCase(declaration, parentClass)) return - if (isCheckedExplicitly(context, declarationSdkLevel, node)) return - - if (projectMinSdk < declarationSdkLevel) { - val subject = when (declaration) { - is UFunction -> "Call" - is UVariable -> "Field" - is UClass -> "Class" - else -> "Call" - } - val message = "$subject requires API level $declarationSdkLevel" + - " (current min is $projectMinSdk): `${declaration.name}`" - context.report(UNSUPPORTED, node, context.getLocation(node), message) - return - } - } - - fun checkAosp(clazz: UClass) = AOSP_BUILD && clazz.fqName?.startsWith("android.support.") ?: false - - if (declaration is UClass) { - if (!isSdkClass(declaration) || checkAosp(declaration)) return - val internalName = declaration.internalName ?: return - check(db.getClassVersion(internalName), null) - } - - val parentClass = declaration.parent as? UClass ?: return - if (!isSdkClass(parentClass) || checkAosp(parentClass)) return - - var parentInternalName = parentClass.internalName - - if (node is UCallExpression) { - val clazz = node.receiverType?.resolveClass(context)?.let { getSupertypeFromAndroidSdk(context, it) } - val internalName = clazz?.internalName - if (internalName != null) { - parentInternalName = internalName - } - } - - if (parentInternalName == null) return - - when (declaration) { - is UFunction -> { - val descriptor = declaration.bytecodeDescriptor ?: return - check(db.getCallVersion(parentInternalName, declaration.name, descriptor), parentClass) - } - is UVariable -> { - if (declaration.kind != UastVariableKind.MEMBER) return - if (declaration is JavaUVariable - && declaration.hasModifier(UastModifier.IMMUTABLE) - && declaration.hasModifier(UastModifier.STATIC) - && declaration.visibility == UastVisibility.PUBLIC - && declaration.initializer.evaluate() != null) { - val type = declaration.type - // Kotlin inlines Java field values with the primitive types, so we don't need to check its version. - if (type.isPrimitive || type.isString) { - return - } - } - check(db.getFieldVersion(parentInternalName, declaration.name), parentClass) - } - } - } - - private fun isSdkClass(clazz: UClass): Boolean { - val fqName = clazz.fqName ?: return false - return fqName.startsWith("android.") - || fqName.startsWith("java.") - || fqName.startsWith("javax.") - || fqName.startsWith("dalvik.") - } - - private fun getMinSdk(context: UastAndroidContext): Int { - if (mMinApi == -1) { - val minSdkVersion = context.lintContext.mainProject.minSdkVersion - mMinApi = minSdkVersion.featureLevel - } - - return mMinApi - } - - private fun getSupertypeFromAndroidSdk(context: UastContext, clazz: UClass): UClass? { - tailrec fun getSuperclassFromAndroidSdk(clazz: UClass): UClass? { - if (clazz.fqName?.startsWith("android.") ?: false) return clazz - return getSuperclassFromAndroidSdk(clazz.getSuperClass(context) ?: return null) - } - - val superClass = getSuperclassFromAndroidSdk(clazz) - if (superClass != null) return superClass - - return clazz.superTypes.firstOrNull { it.fqName?.startsWith("android.") ?: false }?.resolve(context) - } - } - - companion object { - private val AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null - private val SDK_INT_CONTAINING = "android.os.Build.VERSION" - private val SDK_INT = "SDK_INT" - - tailrec fun getLocalMinSdk(scope: UElement?): Int { - if (scope == null) return -1 - - if (scope is UAnnotated) { - val targetApi = getTargetApi(scope.annotations) - if (targetApi != -1) return targetApi - } - - return getLocalMinSdk(scope.parent) - } - - fun getTargetApi(annotations: List): Int { - for (annotation in annotations) { - if (annotation.matchesName(TARGET_API)) { - for (element in annotation.valueArguments) { - val valueNode = element.expression - if (valueNode.isIntegralLiteral()) { - return (valueNode as ULiteralExpression).getLongValue().toInt() - } else if (valueNode.isStringLiteral()) { - val value = (valueNode as ULiteralExpression).value as String - return SdkVersionInfo.getApiByBuildCode(value, true) - } else if (valueNode is UQualifiedExpression) { - val codename = valueNode.getSelectorAsIdentifier() ?: return -1 - return SdkVersionInfo.getApiByBuildCode(codename, true) - } else if (valueNode is USimpleReferenceExpression) { - val codename = valueNode.identifier - return SdkVersionInfo.getApiByBuildCode(codename, true) - } - } - } - } - - return -1 - } - - fun isCheckedExplicitly(context: UastAndroidContext, requiredVersion: Int, node: UElement): Boolean { - tailrec fun UExpression.isSdkIntReference(): Boolean = when (this) { - is UParenthesizedExpression -> expression.isSdkIntReference() - is USimpleReferenceExpression -> resolve(context)?.matchesNameWithContaining(SDK_INT_CONTAINING, SDK_INT) ?: false - is UQualifiedExpression -> resolve(context)?.matchesNameWithContaining(SDK_INT_CONTAINING, SDK_INT) ?: false - else -> false - } - - fun checkCondition(node: UExpression, invertCondition: Boolean = false): Boolean? { - if (node is UBinaryExpression && node.operator is UastBinaryOperator.ComparationOperator) { - var invert: Boolean = invertCondition - - val value: UExpression - if (node.leftOperand.isSdkIntReference()) { - value = node.rightOperand - } - else if (node.rightOperand.isSdkIntReference()) { - invert = !invert - value = node.leftOperand - } - else return false - fun inv(cond: Boolean) = if (invert) !cond else cond - - tailrec fun evaluateValue(value: UExpression?): Int? { - if (value == null) return null - - return (value.evaluate() as? Number)?.toInt() ?: if (value is UResolvable) { - val declaration = value.resolve(context) ?: return null - if (declaration is UVariable) - evaluateValue(declaration.initializer) - else - null - } else null - } - - val sdkLevel = evaluateValue(value) ?: return null - - return when (node.operator) { - UastBinaryOperator.GREATER -> inv(sdkLevel > requiredVersion) - UastBinaryOperator.GREATER_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel > requiredVersion) - UastBinaryOperator.LESS -> inv(sdkLevel < requiredVersion) - UastBinaryOperator.LESS_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel < requiredVersion) - UastBinaryOperator.EQUALS -> requiredVersion == sdkLevel - else -> null - } - } - - return when (node) { - is UBinaryExpression -> if (node.operator is UastBinaryOperator.LogicalOperator) { - checkCondition(node.leftOperand) ?: checkCondition(node.rightOperand) - } else null - is UUnaryExpression -> if (node.operator == UastPrefixOperator.LOGICAL_NOT) { - checkCondition(node.operand, true) - } else null - is UParenthesizedExpression -> checkCondition(node.expression) - else -> null - } - } - - tailrec fun check(node: UElement?, prev: UElement?, context: UastAndroidContext): Boolean { - return when (node) { - null -> false - is UIfExpression -> { - val cond = checkCondition(node.condition) ?: return false - if ((cond && prev == node.thenBranch) || (!cond && prev == node.elseBranch)) - true - else - check(node.parent, node, context) - } - is USwitchClauseExpression -> { - if (node.caseValues?.any { checkCondition(it) == true } ?: false) - true - else - check(node.parent, node, context) - } - is UBinaryExpression -> { - if (prev == node.rightOperand - && node.operator is UastBinaryOperator.LogicalOperator - && checkCondition(node.leftOperand) == true) - true - else - check(node.parent, node, context) - } - is UFunction -> false - is UVariable -> if (node.kind == UastVariableKind.MEMBER) false else check(node.parent, node, context) - is UClass -> false - else -> check(node.parent, node, context) - } - } - - val minSdk = getLocalMinSdk(node) - if (minSdk != -1 && minSdk >= requiredVersion) return true - - return check(node, null, context) - } - - /** Accessing an unsupported API */ - @SuppressWarnings("unchecked") - @JvmField - val UNSUPPORTED = Issue.create( - "NewApi", //$NON-NLS-1$ - "Calling new methods on older versions", - - "This check scans through all the Android API calls in the application and " + - "warns about any calls that are not available on *all* versions targeted " + - "by this application (according to its minimum SDK attribute in the manifest).\n" + - "\n" + - "If you really want to use this API and don't need to support older devices just " + - "set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files.\n" + - "\n" + - "If your code is *deliberately* accessing newer APIs, and you have ensured " + - "(e.g. with conditional execution) that this code will only ever be called on a " + - "supported platform, then you can annotate your class or method with the " + - "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + - "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " + - "file's minimum SDK as the required API level.\n" + - "\n" + - "If you are deliberately setting `android:` attributes in style definitions, " + - "make sure you place this in a `values-vNN` folder in order to avoid running " + - "into runtime conflicts on certain devices where manufacturers have added " + - "custom attributes whose ids conflict with the new ones on later platforms.\n" + - "\n" + - "Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " + - "the element will only be inflated in an adequate context.", - Category.CORRECTNESS, - 6, - Severity.ERROR, - Implementation( - ApiDetector::class.java, - Scope.SOURCE_FILE_SCOPE)) - - /** Accessing an inlined API on older platforms */ - @JvmField - val INLINED = Issue.create( - "InlinedApi", //$NON-NLS-1$ - "Using inlined constants on older versions", - - "This check scans through all the Android API field references in the application " + - "and flags certain constants, such as static final integers and Strings, " + - "which were introduced in later versions. These will actually be copied " + - "into the class files rather than being referenced, which means that " + - "the value is available even when running on older devices. In some " + - "cases that's fine, and in other cases it can result in a runtime " + - "crash or incorrect behavior. It depends on the context, so consider " + - "the code carefully and device whether it's safe and can be suppressed " + - "or whether the code needs tbe guarded.\n" + - "\n" + - "If you really want to use this API and don't need to support older devices just " + - "set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files." + - "\n" + - "If your code is *deliberately* accessing newer APIs, and you have ensured " + - "(e.g. with conditional execution) that this code will only ever be called on a " + - "supported platform, then you can annotate your class or method with the " + - "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + - "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " + - "file's minimum SDK as the required API level.\n", - Category.CORRECTNESS, - 6, - Severity.WARNING, - Implementation( - ApiDetector::class.java, - Scope.SOURCE_FILE_SCOPE)) - - /** Accessing an unsupported API */ - @JvmField - val OVERRIDE = Issue.create( - "Override", //$NON-NLS-1$ - "Method conflicts with new inherited method", - - "Suppose you are building against Android API 8, and you've subclassed Activity. " + - "In your subclass you add a new method called `isDestroyed`(). At some later point, " + - "a method of the same name and signature is added to Android. Your method will " + - "now override the Android method, and possibly break its contract. Your method " + - "is not calling `super.isDestroyed()`, since your compilation target doesn't " + - "know about the method.\n" + - "\n" + - "The above scenario is what this lint detector looks for. The above example is " + - "real, since `isDestroyed()` was added in API 17, but it will be true for *any* " + - "method you have added to a subclass of an Android class where your build target " + - "is lower than the version the method was introduced in.\n" + - "\n" + - "To fix this, either rename your method, or if you are really trying to augment " + - "the builtin method if available, switch to a higher build target where you can " + - "deliberately add `@Override` on your overriding method, and call `super` if " + - "appropriate etc.\n", - Category.CORRECTNESS, - 6, - Severity.ERROR, - Implementation( - ApiDetector::class.java, - Scope.SOURCE_FILE_SCOPE)) - } -} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiLookup.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiLookup.java index cf8588b25f0..46c18bdc691 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiLookup.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiLookup.java @@ -18,17 +18,18 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.ANDROID_PKG; import static com.android.SdkConstants.DOT_XML; +import static com.android.tools.klint.detector.api.LintUtils.assertionsEnabled; +import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.tools.klint.client.api.LintClient; import com.android.tools.klint.client.api.SdkWrapper; import com.android.tools.klint.detector.api.LintUtils; +import com.android.utils.Pair; import com.google.common.base.Charsets; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import com.google.common.io.ByteSink; import com.google.common.io.Files; import com.google.common.primitives.UnsignedBytes; @@ -38,8 +39,6 @@ import java.io.IOException; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel.MapMode; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -78,21 +77,27 @@ public class ApiLookup { /** Relative path to the api-versions.xml database file within the Lint installation */ private static final String XML_FILE_PATH = "platform-tools/api/api-versions.xml"; //$NON-NLS-1$ private static final String FILE_HEADER = "API database used by Android lint\000"; - private static final int BINARY_FORMAT_VERSION = 6; - private static final boolean DEBUG_FORCE_REGENERATE_BINARY = false; + private static final int BINARY_FORMAT_VERSION = 8; private static final boolean DEBUG_SEARCH = false; private static final boolean WRITE_STATS = false; - /** Default size to reserve for each API entry when creating byte buffer to build up data */ - private static final int BYTES_PER_ENTRY = 36; + + private static final int CLASS_HEADER_MEMBER_OFFSETS = 1; + private static final int CLASS_HEADER_API = 2; + private static final int CLASS_HEADER_DEPRECATED = 3; + private static final int CLASS_HEADER_INTERFACES = 4; + private static final int HAS_DEPRECATION_BYTE_FLAG = 1 << 7; + private static final int API_MASK = ~HAS_DEPRECATION_BYTE_FLAG; + + @VisibleForTesting + static final boolean DEBUG_FORCE_REGENERATE_BINARY = false; private final Api mInfo; private byte[] mData; private int[] mIndices; - private int mClassCount; - private String[] mJavaPackages; - private static WeakReference sInstance = - new WeakReference(null); + private static WeakReference sInstance = new WeakReference(null); + + private int mPackageCount; /** * Returns an instance of the API database @@ -139,7 +144,6 @@ public class ApiLookup { if (sdk != null) { return sdk.getPlatformVersion(); } - return null; } @@ -251,34 +255,63 @@ public class ApiLookup { /** * Database format: *

+     * (Note: all numbers are big endian; the format uses 1, 2, 3 and 4 byte integers.)
+     *
+     *
      * 1. A file header, which is the exact contents of {@link #FILE_HEADER} encoded
      *     as ASCII characters. The purpose of the header is to identify what the file
      *     is for, for anyone attempting to open the file.
      * 2. A file version number. If the binary file does not match the reader's expected
      *     version, it can ignore it (and regenerate the cache from XML).
-     * 3. The number of classes [1 int]
-     * 4. The number of members (across all classes) [1 int].
-     * 5. The number of java/javax packages [1 int]
-     * 6. The java/javax package name table. Each item consists of a byte count for
-     *    the package string (as 1 byte) followed by the UTF-8 encoded bytes for each package.
-     *    These are in sorted order.
-     * 7. Class offset table (one integer per class, pointing to the byte offset in the
-     *      file (relative to the beginning of the file) where each class begins.
-     *      The classes are always sorted alphabetically by fully qualified name.
-     * 8. Member offset table (one integer per member, pointing to the byte offset in the
-     *      file (relative to the beginning of the file) where each member entry begins.
-     *      The members are always sorted alphabetically.
-     * 9. Class entry table. Each class entry consists of the fully qualified class name,
-     *       in JVM format (using / instead of . in package names and $ for inner classes),
-     *       followed by the byte 0 as a terminator, followed by the API version as a byte.
-     * 10. Member entry table. Each member entry consists of the class number (as a short),
-     *      followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte
-     *      signature terminator, followed by the API level as a byte.
-     * 

- * TODO: Pack the offsets: They increase by a small amount for each entry, so no need - * to spend 4 bytes on each. These will need to be processed when read back in anyway, - * so consider storing the offset -deltas- as single bytes and adding them up cumulatively - * in readData(). + * + * 3. The index table. When the data file is read, this is used to initialize the + * {@link #mIndices} array. The index table is built up like this: + * a. The number of index entries (e.g. number of elements in the {@link #mIndices} array) + * [1 4-byte int] + * b. The number of java/javax packages [1 4 byte int] + * c. Offsets to the package entries, one for each package, and each offset is 4 bytes. + * d. Offsets to the class entries, one for each class, and each offset is 4 bytes. + * e. Offsets to the member entries, one for each member, and each offset is 4 bytes. + * + * 4. The member entries -- one for each member. A given class entry will point to the + * first and last members in the index table above, and the offset of a given member + * is pointing to the offset of these entries. + * a. The name and description (except for the return value) of the member, in JVM format + * (e.g. for toLowerCase(char) we'd have "toLowerCase(C)". This is converted into + * UTF_8 representation as bytes [n bytes, the length of the byte representation of + * the description). + * b. A terminating 0 byte [1 byte]. + * c. The API level the member was introduced in [1 byte], BUT with the + * top bit ({@link #HAS_DEPRECATION_BYTE_FLAG}) set if the member is deprecated. + * d. IF the member is deprecated, the API level the member was deprecated in [1 byte]. + * Note that this byte does not appear if the bit indicated in (c) is not set. + * + * 5. The class entries -- one for each class. + * a. The index within this class entry where the metadata (other than the name) + * can be found. [1 byte]. This means that if you know a class by its number, + * you can quickly jump to its metadata without scanning through the string to + * find the end of it, by just adding this byte to the current offset and + * then you're at the data described below for (d). + * b. The name of the class (just the base name, not the package), as encoded as a + * UTF-8 string. [n bytes] + * c. A terminating 0 [1 byte]. + * d. The index in the index table (3) of the first member in the class [a 3 byte integer.] + * e. The number of members in the class [a 2 byte integer]. + * f. The API level the class was introduced in [1 byte], BUT with the + * top bit ({@link #HAS_DEPRECATION_BYTE_FLAG}) set if the class is deprecated. + * g. IF the class is deprecated, the API level the class was deprecated in [1 byte]. + * Note that this byte does not appear if the bit indicated in (f) is not set. + * h. The number of new super classes and interfaces [1 byte]. This counts only + * super classes and interfaces added after the original API level of the class. + * i. For each super class or interface counted in h, + * I. The index of the class [a 3 byte integer] + * II. The API level the class/interface was added [1 byte] + * + * 6. The package entries -- one for each package. + * a. The name of the package as encoded as a UTF-8 string. [n bytes] + * b. A terminating 0 [1 byte]. + * c. The index in the index table (3) of the first class in the package [a 3 byte integer.] + * d. The number of classes in the package [a 2 byte integer]. *

*/ private void readData(@NonNull LintClient client, @NonNull File xmlFile, @@ -289,14 +322,13 @@ public class ApiLookup { } long start = System.currentTimeMillis(); try { - MappedByteBuffer buffer = Files.map(binaryFile, MapMode.READ_ONLY); - assert buffer.order() == ByteOrder.BIG_ENDIAN; + byte[] b = Files.toByteArray(binaryFile); // First skip the header + int offset = 0; byte[] expectedHeader = FILE_HEADER.getBytes(Charsets.US_ASCII); - buffer.rewind(); - for (int offset = 0; offset < expectedHeader.length; offset++) { - if (expectedHeader[offset] != buffer.get()) { + for (byte anExpectedHeader : expectedHeader) { + if (anExpectedHeader != b[offset++]) { client.log(null, "Incorrect file header: not an API database cache " + "file, or a corrupt cache file"); return; @@ -304,7 +336,7 @@ public class ApiLookup { } // Read in the format number - if (buffer.get() != BINARY_FORMAT_VERSION) { + if (b[offset++] != BINARY_FORMAT_VERSION) { // Force regeneration of new binary data with up to date format if (createCache(client, xmlFile, binaryFile)) { readData(client, xmlFile, binaryFile); // Recurse @@ -313,39 +345,21 @@ public class ApiLookup { return; } - mClassCount = buffer.getInt(); - int methodCount = buffer.getInt(); + int indexCount = get4ByteInt(b, offset); + offset += 4; + mPackageCount = get4ByteInt(b, offset); + offset += 4; - int javaPackageCount = buffer.getInt(); - // Read in the Java packages - mJavaPackages = new String[javaPackageCount]; - for (int i = 0; i < javaPackageCount; i++) { - int count = UnsignedBytes.toInt(buffer.get()); - byte[] bytes = new byte[count]; - buffer.get(bytes, 0, count); - mJavaPackages[i] = new String(bytes, Charsets.UTF_8); + mIndices = new int[indexCount]; + for (int i = 0; i < indexCount; i++) { + // TODO: Pack the offsets: They increase by a small amount for each entry, so + // no need to spend 4 bytes on each. These will need to be processed when read + // back in anyway, so consider storing the offset -deltas- as single bytes and + // adding them up cumulatively in readData(). + mIndices[i] = get4ByteInt(b, offset); + offset += 4; } - - // Read in the class table indices; - int count = mClassCount + methodCount; - int[] offsets = new int[count]; - - // Another idea: I can just store the DELTAS in the file (and add them up - // when reading back in) such that it takes just ONE byte instead of four! - - for (int i = 0; i < count; i++) { - offsets[i] = buffer.getInt(); - } - - // No need to read in the rest -- we'll just keep the whole byte array in memory - // TODO: Make this code smarter/more efficient. - int size = buffer.limit(); - byte[] b = new byte[size]; - buffer.rewind(); - buffer.get(b); mData = b; - mIndices = offsets; - // TODO: We only need to keep the data portion here since we've initialized // the offset array separately. // TODO: Investigate (profile) accessing the byte buffer directly instead of @@ -367,213 +381,295 @@ public class ApiLookup { /** See the {@link #readData(LintClient,File,File)} for documentation on the data format. */ private static void writeDatabase(File file, Api info) throws IOException { - /* - * 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded - * as ASCII characters. The purpose of the header is to identify what the file - * is for, for anyone attempting to open the file. - * 2. A file version number. If the binary file does not match the reader's expected - * version, it can ignore it (and regenerate the cache from XML). - */ Map classMap = info.getClasses(); - // Write the class table - List classes = new ArrayList(classMap.size()); - Map> memberMap = - Maps.newHashMapWithExpectedSize(classMap.size()); - int memberCount = 0; - Set javaPackageSet = Sets.newHashSetWithExpectedSize(70); - for (Map.Entry entry : classMap.entrySet()) { - String className = entry.getKey(); - ApiClass apiClass = entry.getValue(); + List packages = Lists.newArrayList(info.getPackages().values()); + Collections.sort(packages); - if (className.startsWith("java/") //$NON-NLS-1$ - || className.startsWith("javax/")) { //$NON-NLS-1$ - String pkg = apiClass.getPackage(); - javaPackageSet.add(pkg); + // Compute members of each class that must be included in the database; we can + // skip those that have the same since-level as the containing class. And we + // also need to keep those entries that are marked deprecated. + int estimatedSize = 0; + for (ApiPackage pkg : packages) { + estimatedSize += 4; // offset entry + estimatedSize += pkg.getName().length() + 20; // package entry + + if (assertionsEnabled() && !isRelevantOwner(pkg.getName() + "/") && + !pkg.getName().startsWith("android/support")) { + System.out.println("Warning: isRelevantOwner fails for " + pkg.getName() + "/"); } - if (!isRelevantOwner(className)) { - System.out.println("Warning: The isRelevantOwner method does not pass " - + className); - } + for (ApiClass apiClass : pkg.getClasses()) { + estimatedSize += 4; // offset entry + estimatedSize += apiClass.getName().length() + 20; // class entry - Set allMethods = apiClass.getAllMethods(info); - Set allFields = apiClass.getAllFields(info); - - // Strip out all members that have been supported since version 1. - // This makes the database *much* leaner (down from about 4M to about - // 1.7M), and this just fills the table with entries that ultimately - // don't help the API checker since it just needs to know if something - // requires a version *higher* than the minimum. If in the future the - // database needs to answer queries about whether a method is public - // or not, then we'd need to put this data back in. - List members = new ArrayList(allMethods.size() + allFields.size()); - for (String member : allMethods) { - - Integer since = apiClass.getMethod(member, info); - if (since == null) { - assert false : className + ':' + member; - since = 1; - } - if (since != 1) { - members.add(member); - } - } - - // Strip out all members that have been supported since version 1. - // This makes the database *much* leaner (down from about 4M to about - // 1.7M), and this just fills the table with entries that ultimately - // don't help the API checker since it just needs to know if something - // requires a version *higher* than the minimum. If in the future the - // database needs to answer queries about whether a method is public - // or not, then we'd need to put this data back in. - for (String member : allFields) { - Integer since = apiClass.getField(member, info); - if (since == null) { - assert false : className + ':' + member; - since = 1; - } - if (since != 1) { - members.add(member); - } - } - - // Only include classes that have one or more members requiring version 2 or higher: - if (!members.isEmpty()) { - classes.add(className); - memberMap.put(apiClass, members); - memberCount += members.size(); - } - } - Collections.sort(classes); - - List javaPackages = Lists.newArrayList(javaPackageSet); - Collections.sort(javaPackages); - int javaPackageCount = javaPackages.size(); - - int entryCount = classMap.size() + memberCount; - int capacity = entryCount * BYTES_PER_ENTRY; - ByteBuffer buffer = ByteBuffer.allocate(capacity); - buffer.order(ByteOrder.BIG_ENDIAN); - // 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded - // as ASCII characters. The purpose of the header is to identify what the file - // is for, for anyone attempting to open the file. - - buffer.put(FILE_HEADER.getBytes(Charsets.US_ASCII)); - - // 2. A file version number. If the binary file does not match the reader's expected - // version, it can ignore it (and regenerate the cache from XML). - buffer.put((byte) BINARY_FORMAT_VERSION); - - // 3. The number of classes [1 int] - buffer.putInt(classes.size()); - - // 4. The number of members (across all classes) [1 int]. - buffer.putInt(memberCount); - - // 5. The number of Java packages [1 int]. - buffer.putInt(javaPackageCount); - - // 6. The Java package table. There are javaPackage.size() entries, where each entry - // consists of a string length, as a byte, followed by the bytes in the package. - // There is no terminating 0. - for (String pkg : javaPackages) { - byte[] bytes = pkg.getBytes(Charsets.UTF_8); - assert bytes.length < 255 : pkg; - buffer.put((byte) bytes.length); - buffer.put(bytes); - } - - // 7. Class offset table (one integer per class, pointing to the byte offset in the - // file (relative to the beginning of the file) where each class begins. - // The classes are always sorted alphabetically by fully qualified name. - int classOffsetTable = buffer.position(); - - // Reserve enough room for the offset table here: we will backfill it with pointers - // as we're writing out the data structures below - for (int i = 0, n = classes.size(); i < n; i++) { - buffer.putInt(0); - } - - // 8. Member offset table (one integer per member, pointing to the byte offset in the - // file (relative to the beginning of the file) where each member entry begins. - // The members are always sorted alphabetically. - int methodOffsetTable = buffer.position(); - for (int i = 0, n = memberCount; i < n; i++) { - buffer.putInt(0); - } - - int nextEntry = buffer.position(); - int nextOffset = classOffsetTable; - - // 9. Class entry table. Each class entry consists of the fully qualified class name, - // in JVM format (using / instead of . in package names and $ for inner classes), - // followed by the byte 0 as a terminator, followed by the API version as a byte. - for (String clz : classes) { - buffer.position(nextOffset); - buffer.putInt(nextEntry); - nextOffset = buffer.position(); - buffer.position(nextEntry); - buffer.put(clz.getBytes(Charsets.UTF_8)); - buffer.put((byte) 0); - - ApiClass apiClass = classMap.get(clz); - assert apiClass != null : clz; - int since = apiClass.getSince(); - assert since == UnsignedBytes.toInt((byte) since) : since; // make sure it fits - buffer.put((byte) since); - - nextEntry = buffer.position(); - } - - // 10. Member entry table. Each member entry consists of the class number (as a short), - // followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte - // signature terminator, followed by the API level as a byte. - assert nextOffset == methodOffsetTable; - - for (int classNumber = 0, n = classes.size(); classNumber < n; classNumber++) { - String clz = classes.get(classNumber); - ApiClass apiClass = classMap.get(clz); - assert apiClass != null : clz; - List members = memberMap.get(apiClass); - Collections.sort(members); - - for (String member : members) { - buffer.position(nextOffset); - buffer.putInt(nextEntry); - nextOffset = buffer.position(); - buffer.position(nextEntry); - - Integer since; - if (member.indexOf('(') != -1) { - since = apiClass.getMethod(member, info); - } else { - since = apiClass.getField(member, info); - } - if (since == null) { - assert false : clz + ':' + member; - since = 1; - } - - assert classNumber == (short) classNumber; - buffer.putShort((short) classNumber); - byte[] signature = member.getBytes(Charsets.UTF_8); - for (int i = 0; i < signature.length; i++) { - // Make sure all signatures are really just simple ASCII - byte b = signature[i]; - assert b == (b & 0x7f) : member; - buffer.put(b); - // Skip types on methods - if (b == (byte) ')') { - break; + Set allMethods = apiClass.getAllMethods(info); + Set allFields = apiClass.getAllFields(info); + // Strip out all members that have been supported since version 1. + // This makes the database *much* leaner (down from about 4M to about + // 1.7M), and this just fills the table with entries that ultimately + // don't help the API checker since it just needs to know if something + // requires a version *higher* than the minimum. If in the future the + // database needs to answer queries about whether a method is public + // or not, then we'd need to put this data back in. + int clsSince = apiClass.getSince(); + List members = new ArrayList(allMethods.size() + allFields.size()); + for (String member : allMethods) { + if (apiClass.getMethod(member, info) != clsSince + || apiClass.getMemberDeprecatedIn(member, info) > 0) { + members.add(member); } } + for (String member : allFields) { + if (apiClass.getField(member, info) != clsSince + || apiClass.getMemberDeprecatedIn(member, info) > 0) { + members.add(member); + } + } + + estimatedSize += 2 + 4 * (apiClass.getInterfaces().size()); + if (apiClass.getSuperClasses().size() > 1) { + estimatedSize += 2 + 4 * (apiClass.getSuperClasses().size()); + } + + // Only include classes that have one or more members requiring version 2 or higher: + Collections.sort(members); + apiClass.members = members; + for (String member : members) { + estimatedSize += member.length(); + estimatedSize += 16; + } + } + + // Ensure the classes are sorted + Collections.sort(pkg.getClasses()); + } + + // Write header + ByteBuffer buffer = ByteBuffer.allocate(estimatedSize); + buffer.order(ByteOrder.BIG_ENDIAN); + buffer.put(FILE_HEADER.getBytes(Charsets.US_ASCII)); + buffer.put((byte) BINARY_FORMAT_VERSION); + + int indexCountOffset = buffer.position(); + int indexCount = 0; + + buffer.putInt(0); // placeholder + + // Write the number of packages in the package index + buffer.putInt(packages.size()); + + // Write package index + int newIndex = buffer.position(); + for (ApiPackage pkg : packages) { + pkg.indexOffset = newIndex; + newIndex += 4; + indexCount++; + } + + // Write class index + for (ApiPackage pkg : packages) { + for (ApiClass cls : pkg.getClasses()) { + cls.indexOffset = newIndex; + cls.index = indexCount; + newIndex += 4; + indexCount++; + } + } + + // Write member indices + for (ApiPackage pkg : packages) { + for (ApiClass cls : pkg.getClasses()) { + if (cls.members != null && !cls.members.isEmpty()) { + cls.memberOffsetBegin = newIndex; + cls.memberIndexStart = indexCount; + for (String ignored : cls.members) { + newIndex += 4; + indexCount++; + } + cls.memberOffsetEnd = newIndex; + cls.memberIndexLength = indexCount - cls.memberIndexStart; + } else { + cls.memberOffsetBegin = -1; + cls.memberOffsetEnd = -1; + cls.memberIndexStart = -1; + cls.memberIndexLength = 0; + } + } + } + + // Fill in the earlier index count + buffer.position(indexCountOffset); + buffer.putInt(indexCount); + buffer.position(newIndex); + + // Write member entries + for (ApiPackage pkg : packages) { + for (ApiClass apiClass : pkg.getClasses()) { + String clz = apiClass.getName(); + int index = apiClass.memberOffsetBegin; + for (String member : apiClass.members) { + // Update member offset to point to this entry + int start = buffer.position(); + buffer.position(index); + buffer.putInt(start); + index = buffer.position(); + buffer.position(start); + + int since; + if (member.indexOf('(') != -1) { + since = apiClass.getMethod(member, info); + } else { + since = apiClass.getField(member, info); + } + if (since == Integer.MAX_VALUE) { + assert false : clz + ':' + member; + since = 1; + } + + int deprecatedIn = apiClass.getMemberDeprecatedIn(member, info); + if (deprecatedIn != 0) { + assert deprecatedIn != -1 : deprecatedIn + " for " + member; + } + + byte[] signature = member.getBytes(Charsets.UTF_8); + for (byte b : signature) { + // Make sure all signatures are really just simple ASCII + assert b == (b & 0x7f) : member; + buffer.put(b); + // Skip types on methods + if (b == (byte) ')') { + break; + } + } + buffer.put((byte) 0); + int api = since; + assert api == UnsignedBytes.toInt((byte) api); + assert api >= 1 && api < 0xFF; // max that fits in a byte + + boolean isDeprecated = deprecatedIn > 0; + if (isDeprecated) { + api |= HAS_DEPRECATION_BYTE_FLAG; + } + + buffer.put((byte) api); + + if (isDeprecated) { + assert deprecatedIn == UnsignedBytes.toInt((byte) deprecatedIn); + buffer.put((byte) deprecatedIn); + } + } + assert index == apiClass.memberOffsetEnd : apiClass.memberOffsetEnd; + } + } + + // Write class entries. These are written together, rather than + // being spread out among the member entries, in order to have + // reference locality (search that a binary search through the classes + // are likely to look at entries near each other.) + for (ApiPackage pkg : packages) { + List classes = pkg.getClasses(); + for (ApiClass cls : classes) { + int index = buffer.position(); + buffer.position(cls.indexOffset); + buffer.putInt(index); + buffer.position(index); + String name = cls.getSimpleName(); + + byte[] nameBytes = name.getBytes(Charsets.UTF_8); + assert nameBytes.length < 254 : name; + buffer.put((byte)(nameBytes.length + 2)); // 2: terminating 0, and this byte itself + buffer.put(nameBytes); buffer.put((byte) 0); - int api = since; - assert api == UnsignedBytes.toInt((byte) api); - //assert api >= 1 && api < 0xFF; // max that fits in a byte - buffer.put((byte) api); - nextEntry = buffer.position(); + + // 3 bytes for beginning, 2 bytes for *length* + put3ByteInt(buffer, cls.memberIndexStart); + put2ByteInt(buffer, cls.memberIndexLength); + + ApiClass apiClass = classMap.get(cls.getName()); + assert apiClass != null : cls.getName(); + int since = apiClass.getSince(); + assert since == UnsignedBytes.toInt((byte) since) : since; // make sure it fits + int deprecatedIn = apiClass.getDeprecatedIn(); + boolean isDeprecated = deprecatedIn > 0; + // The first byte is deprecated in + if (isDeprecated) { + since |= HAS_DEPRECATION_BYTE_FLAG; + assert since == UnsignedBytes.toInt((byte) since) : since; // make sure it fits + } + buffer.put((byte) since); + if (isDeprecated) { + assert deprecatedIn == UnsignedBytes.toInt((byte) deprecatedIn) : deprecatedIn; + buffer.put((byte) deprecatedIn); + } + + List> interfaces = apiClass.getInterfaces(); + int count = 0; + if (interfaces != null && !interfaces.isEmpty()) { + for (Pair pair : interfaces) { + int api = pair.getSecond(); + if (api > apiClass.getSince()) { + count++; + } + } + } + List> supers = apiClass.getSuperClasses(); + if (supers != null && !supers.isEmpty()) { + for (Pair pair : supers) { + int api = pair.getSecond(); + if (api > apiClass.getSince()) { + count++; + } + } + } + buffer.put((byte)count); + if (count > 0) { + if (supers != null) { + for (Pair pair : supers) { + int api = pair.getSecond(); + if (api > apiClass.getSince()) { + ApiClass superClass = classMap.get(pair.getFirst()); + assert superClass != null : cls; + put3ByteInt(buffer, superClass.index); + buffer.put((byte) api); + } + } + } + if (interfaces != null) { + for (Pair pair : interfaces) { + int api = pair.getSecond(); + if (api > apiClass.getSince()) { + ApiClass interfaceClass = classMap.get(pair.getFirst()); + assert interfaceClass != null : cls; + put3ByteInt(buffer, interfaceClass.index); + buffer.put((byte) api); + } + } + } + } + } + } + + for (ApiPackage pkg : packages) { + int index = buffer.position(); + buffer.position(pkg.indexOffset); + buffer.putInt(index); + buffer.position(index); + + byte[] bytes = pkg.getName().getBytes(Charsets.UTF_8); + buffer.put(bytes); + buffer.put((byte)0); + + List classes = pkg.getClasses(); + if (classes.isEmpty()) { + put3ByteInt(buffer, 0); + put2ByteInt(buffer, 0); + } else { + // 3 bytes for beginning, 2 bytes for *length* + int firstClassIndex = classes.get(0).index; + int classCount = classes.get(classes.size() - 1).index - firstClassIndex + 1; + put3ByteInt(buffer, firstClassIndex); + put2ByteInt(buffer, classCount); } } @@ -582,13 +678,8 @@ public class ApiLookup { buffer.mark(); if (WRITE_STATS) { - System.out.println("Wrote " + classes.size() + " classes and " - + memberCount + " member entries"); System.out.print("Actual binary size: " + size + " bytes"); System.out.println(String.format(" (%.1fM)", size/(1024*1024.f))); - - System.out.println("Allocated size: " + (entryCount * BYTES_PER_ENTRY) + " bytes"); - System.out.println("Required bytes per entry: " + (size/ entryCount) + " bytes"); } // Now dump this out as a file @@ -597,9 +688,9 @@ public class ApiLookup { buffer.rewind(); buffer.get(b); if (file.exists()) { - file.delete(); + boolean deleted = file.delete(); + assert deleted : file; } - ByteSink sink = Files.asByteSink(file); sink.write(b); } @@ -622,9 +713,10 @@ public class ApiLookup { } } - private static int compare(byte[] data, int offset, byte terminator, String s, int max) { + private static int compare(byte[] data, int offset, byte terminator, String s, int sOffset, + int max) { int i = offset; - int j = 0; + int j = sOffset; for (; j < max; i++, j++) { byte b = data[i]; char c = s.charAt(j); @@ -640,22 +732,6 @@ public class ApiLookup { return data[i] - terminator; } - /** - * Quick determination whether a given class name is possibly interesting; this - * is a quick package prefix check to determine whether we need to consider - * the class at all. This let's us do less actual searching for the vast majority - * of APIs (in libraries, application code etc) that have nothing to do with the - * APIs in our packages. - * @param name the class name in VM format (e.g. using / instead of .) - * @return true if the owner is possibly relevant - */ - public static boolean isRelevantClass(String name) { - // TODO: Add quick switching here. This is tied to the database file so if - // we end up with unexpected prefixes there, this could break. For that reason, - // for now we consider everything relevant. - return true; - } - /** * Returns the API version required by the given class reference, * or -1 if this is not a known API class. Note that it may return -1 @@ -669,22 +745,12 @@ public class ApiLookup { * it's unknown or version 1. */ public int getClassVersion(@NonNull String className) { - if (!isRelevantClass(className)) { - return -1; - } - + //noinspection VariableNotUsedInsideIf if (mData != null) { - int classNumber = findClass(className); - if (classNumber != -1) { - int offset = mIndices[classNumber]; - while (mData[offset] != 0) { - offset++; - } - offset++; - return UnsignedBytes.toInt(mData[offset]); - } + return getClassVersion(findClass(className)); } else { - ApiClass clz = mInfo.getClass(className); + assert mInfo != null; + ApiClass clz = mInfo.getClass(className); if (clz != null) { int since = clz.getSince(); if (since == Integer.MAX_VALUE) { @@ -697,6 +763,113 @@ public class ApiLookup { return -1; } + /** + * Returns true if the given owner class is known in the API database. + * + * @param className the internal name of the class, e.g. its fully qualified name (as returned + * by Class.getName(), but with '.' replaced by '/' (and '$' for inner + * classes) + * @return true if this is a class found in the API database + */ + public boolean isKnownClass(@NonNull String className) { + return findClass(className) != -1; + } + + private int getClassVersion(int classNumber) { + if (classNumber != -1) { + int offset = seekClassData(classNumber, CLASS_HEADER_API); + int api = UnsignedBytes.toInt(mData[offset]) & API_MASK; + return api > 1 ? api : -1; + } + return -1; + } + + /** + * Returns the API version required to perform the given cast, or -1 if this is valid for all + * versions of the class (or, if these are not known classes or if the cast is not valid at + * all.)

Note also that this method should only be called for interfaces that are actually + * implemented by this class or extending the given super class (check elsewhere); it doesn't + * distinguish between interfaces implemented in the initial version of the class and interfaces + * not implemented at all. + * + * @param sourceClass the internal name of the class, e.g. its fully qualified name (as + * returned by Class.getName(), but with '.' replaced by '/'. + * @param destinationClass the class to cast the sourceClass to + * @return the minimum API version the method is supported for, or 1 or -1 if it's unknown. + */ + public int getValidCastVersion(@NonNull String sourceClass, + @NonNull String destinationClass) { + if (mData != null) { + int classNumber = findClass(sourceClass); + if (classNumber != -1) { + int interfaceNumber = findClass(destinationClass); + if (interfaceNumber != -1) { + int offset = seekClassData(classNumber, CLASS_HEADER_INTERFACES); + int interfaceCount = mData[offset++]; + for (int i = 0; i < interfaceCount; i++) { + int clsNumber = get3ByteInt(mData, offset); + offset += 3; + int api = mData[offset++]; + if (clsNumber == interfaceNumber) { + return api; + } + } + return getClassVersion(classNumber); + } + } + } else { + assert mInfo != null; + ApiClass clz = mInfo.getClass(sourceClass); + if (clz != null) { + List> interfaces = clz.getInterfaces(); + for (Pair pair : interfaces) { + String interfaceName = pair.getFirst(); + if (interfaceName.equals(destinationClass)) { + return pair.getSecond(); + } + } + } + } + + return -1; + } + /** + * Returns the API version the given class was deprecated in, or -1 if the class + * is not deprecated. + * + * @param className the internal name of the method's owner class, e.g. its + * fully qualified name (as returned by Class.getName(), but with + * '.' replaced by '/'. + * @return the API version the API was deprecated in, or -1 if + * it's unknown or version 0. + */ + public int getClassDeprecatedIn(@NonNull String className) { + if (mData != null) { + int classNumber = findClass(className); + if (classNumber != -1) { + int offset = seekClassData(classNumber, CLASS_HEADER_DEPRECATED); + if (offset == -1) { + // Not deprecated + return -1; + } + int deprecatedIn = UnsignedBytes.toInt(mData[offset]); + return deprecatedIn != 0 ? deprecatedIn : -1; + } + } else { + assert mInfo != null; + ApiClass clz = mInfo.getClass(className); + if (clz != null) { + int deprecatedIn = clz.getDeprecatedIn(); + if (deprecatedIn == Integer.MAX_VALUE) { + deprecatedIn = -1; + } + return deprecatedIn; + } + } + + return -1; + } + /** * Returns the API version required by the given method call. The method is * referred to by its {@code owner}, {@code name} and {@code desc} fields. @@ -708,25 +881,27 @@ public class ApiLookup { * fully qualified name (as returned by Class.getName(), but with * '.' replaced by '/'. * @param name the method's name - * @param desc the method's descriptor - see {@link org.jetbrains.org.objectweb.asm.Type} - * @return the minimum API version the method is supported for, or -1 if - * it's unknown or version 1. + * @param desc the method's descriptor - see {@link org.objectweb.asm.Type} + * @return the minimum API version the method is supported for, or 1 or -1 if + * it's unknown. */ public int getCallVersion( @NonNull String owner, @NonNull String name, @NonNull String desc) { - if (!isRelevantClass(owner)) { - return -1; - } - + //noinspection VariableNotUsedInsideIf if (mData != null) { int classNumber = findClass(owner); if (classNumber != -1) { - return findMember(classNumber, name, desc); + int api = findMember(classNumber, name, desc); + if (api == -1) { + return getClassVersion(classNumber); + } + return api; } } else { - ApiClass clz = mInfo.getClass(owner); + assert mInfo != null; + ApiClass clz = mInfo.getClass(owner); if (clz != null) { String signature = name + desc; int since = clz.getMethod(signature, mInfo); @@ -740,6 +915,45 @@ public class ApiLookup { return -1; } + /** + * Returns the API version the given call was deprecated in, or -1 if the method + * is not deprecated. + * + * @param owner the internal name of the method's owner class, e.g. its + * fully qualified name (as returned by Class.getName(), but with + * '.' replaced by '/'. + * @param name the method's name + * @param desc the method's descriptor - see {@link org.objectweb.asm.Type} + * @return the API version the API was deprecated in, or 1 or -1 if + * it's unknown. + */ + public int getCallDeprecatedIn( + @NonNull String owner, + @NonNull String name, + @NonNull String desc) { + //noinspection VariableNotUsedInsideIf + if (mData != null) { + int classNumber = findClass(owner); + if (classNumber != -1) { + int deprecatedIn = findMemberDeprecatedIn(classNumber, name, desc); + return deprecatedIn != 0 ? deprecatedIn : -1; + } + } else { + assert mInfo != null; + ApiClass clz = mInfo.getClass(owner); + if (clz != null) { + String signature = name + desc; + int deprecatedIn = clz.getMemberDeprecatedIn(signature, mInfo); + if (deprecatedIn == Integer.MAX_VALUE) { + deprecatedIn = -1; + } + return deprecatedIn; + } + } + + return -1; + } + /** * Returns the API version required to access the given field, or -1 if this * is not a known API method. Note that it may return -1 for classes @@ -750,22 +964,24 @@ public class ApiLookup { * fully qualified name (as returned by Class.getName(), but with * '.' replaced by '/'. * @param name the method's name - * @return the minimum API version the method is supported for, or -1 if - * it's unknown or version 1 + * @return the minimum API version the method is supported for, or 1 or -1 if + * it's unknown. */ public int getFieldVersion( @NonNull String owner, @NonNull String name) { - if (!isRelevantClass(owner)) { - return -1; - } - + //noinspection VariableNotUsedInsideIf if (mData != null) { int classNumber = findClass(owner); if (classNumber != -1) { - return findMember(classNumber, name, null); + int api = findMember(classNumber, name, null); + if (api == -1) { + return getClassVersion(classNumber); + } + return api; } } else { + assert mInfo != null; ApiClass clz = mInfo.getClass(owner); if (clz != null) { int since = clz.getField(name, mInfo); @@ -779,6 +995,42 @@ public class ApiLookup { return -1; } + /** + * Returns the API version the given field was deprecated in, or -1 if the field + * is not deprecated. + * + * @param owner the internal name of the method's owner class, e.g. its + * fully qualified name (as returned by Class.getName(), but with + * '.' replaced by '/'. + * @param name the method's name + * @return the API version the API was deprecated in, or 1 or -1 if + * it's unknown. + */ + public int getFieldDeprecatedIn( + @NonNull String owner, + @NonNull String name) { + //noinspection VariableNotUsedInsideIf + if (mData != null) { + int classNumber = findClass(owner); + if (classNumber != -1) { + int deprecatedIn = findMemberDeprecatedIn(classNumber, name, null); + return deprecatedIn != 0 ? deprecatedIn : -1; + } + } else { + assert mInfo != null; + ApiClass clz = mInfo.getClass(owner); + if (clz != null) { + int deprecatedIn = clz.getMemberDeprecatedIn(name, mInfo); + if (deprecatedIn == Integer.MAX_VALUE) { + deprecatedIn = -1; + } + return deprecatedIn; + } + } + + return -1; + } + /** * Returns true if the given owner (in VM format) is relevant to the database. * This allows quick filtering out of owners that won't return any data @@ -813,7 +1065,6 @@ public class ApiLookup { return false; } - /** * Returns true if the given owner (in VM format) is a valid Java package supported * in any version of Android. @@ -822,28 +1073,34 @@ public class ApiLookup { * @return true if the package is included in one or more versions of Android */ public boolean isValidJavaPackage(@NonNull String owner) { - int packageLength = owner.lastIndexOf('/'); - if (packageLength == -1) { - return false; - } + return findPackage(owner) != -1; + } + + /** Returns the package index of the given class, or -1 if it is unknown */ + private int findPackage(@NonNull String owner) { + assert owner.indexOf('.') == -1 : "Should use / instead of . in owner: " + owner; // The index array contains class indexes from 0 to classCount and // member indices from classCount to mIndices.length. int low = 0; - int high = mJavaPackages.length - 1; + int high = mPackageCount - 1; + // Compare the api info at the given index. + int classNameLength = owner.lastIndexOf('/'); while (low <= high) { int middle = (low + high) >>> 1; - int offset = middle; + int offset = mIndices[middle]; if (DEBUG_SEARCH) { - System.out.println("Comparing string " + owner + " with entry at " + offset - + ": " + mJavaPackages[offset]); + System.out.println("Comparing string " + owner.substring(0, classNameLength) + + " with entry at " + offset + ": " + dumpEntry(offset)); } - // Compare the api info at the given index. - int compare = comparePackage(mJavaPackages[offset], owner, packageLength); + int compare = compare(mData, offset, (byte) 0, owner, 0, classNameLength); if (compare == 0) { - return true; + if (DEBUG_SEARCH) { + System.out.println("Found " + dumpEntry(offset)); + } + return middle; } if (compare < 0) { @@ -852,53 +1109,98 @@ public class ApiLookup { high = middle - 1; } else { assert false; // compare == 0 already handled above - return false; - } - } - - return false; - } - - private static int comparePackage(String s1, String s2, int max) { - for (int i = 0; i < max; i++) { - if (i == s1.length()) { return -1; } - char c1 = s1.charAt(i); - char c2 = s2.charAt(i); - if (c1 != c2) { - return c1 - c2; - } } - if (s1.length() > max) { - return 1; - } + return -1; + } - return 0; + private static int get4ByteInt(@NonNull byte[] data, int offset) { + byte b1 = data[offset++]; + byte b2 = data[offset++]; + byte b3 = data[offset++]; + byte b4 = data[offset]; + // The byte data is always big endian. + return (b1 & 0xFF) << 24 | (b2 & 0xFF) << 16 | (b3 & 0xFF) << 8 | (b4 & 0xFF); + } + + private static void put3ByteInt(@NonNull ByteBuffer buffer, int value) { + // Big endian + byte b3 = (byte) (value & 0xFF); + value >>>= 8; + byte b2 = (byte) (value & 0xFF); + value >>>= 8; + byte b1 = (byte) (value & 0xFF); + buffer.put(b1); + buffer.put(b2); + buffer.put(b3); + } + + private static void put2ByteInt(@NonNull ByteBuffer buffer, int value) { + // Big endian + byte b2 = (byte) (value & 0xFF); + value >>>= 8; + byte b1 = (byte) (value & 0xFF); + buffer.put(b1); + buffer.put(b2); + } + + private static int get3ByteInt(@NonNull byte[] mData, int offset) { + byte b1 = mData[offset++]; + byte b2 = mData[offset++]; + byte b3 = mData[offset]; + // The byte data is always big endian. + return (b1 & 0xFF) << 16 | (b2 & 0xFF) << 8 | (b3 & 0xFF); + } + + private static int get2ByteInt(@NonNull byte[] data, int offset) { + byte b1 = data[offset++]; + byte b2 = data[offset]; + // The byte data is always big endian. + return (b1 & 0xFF) << 8 | (b2 & 0xFF); } /** Returns the class number of the given class, or -1 if it is unknown */ private int findClass(@NonNull String owner) { assert owner.indexOf('.') == -1 : "Should use / instead of . in owner: " + owner; - // The index array contains class indexes from 0 to classCount and - // member indices from classCount to mIndices.length. - int low = 0; - int high = mClassCount - 1; - // Compare the api info at the given index. + int packageNumber = findPackage(owner); + if (packageNumber == -1) { + return -1; + } + int curr = mIndices[packageNumber]; + while (mData[curr] != 0) { + curr++; + } + curr++; + + // 3 bytes for first offset + int low = get3ByteInt(mData, curr); + curr += 3; + + int length = get2ByteInt(mData, curr); + if (length == 0) { + return -1; + } + int high = low + length - 1; + int index = owner.lastIndexOf('/'); int classNameLength = owner.length(); while (low <= high) { int middle = (low + high) >>> 1; int offset = mIndices[middle]; + offset++; // skip the byte which points to the metadata after the name if (DEBUG_SEARCH) { - System.out.println("Comparing string " + owner + " with entry at " + offset - + ": " + dumpEntry(offset)); + System.out.println("Comparing string " + owner.substring(0, classNameLength) + + " with entry at " + offset + ": " + dumpEntry(offset)); } - int compare = compare(mData, offset, (byte) 0, owner, classNameLength); + int compare = compare(mData, offset, (byte) 0, owner, index + 1, classNameLength); if (compare == 0) { + if (DEBUG_SEARCH) { + System.out.println("Found " + dumpEntry(offset)); + } return middle; } @@ -916,10 +1218,49 @@ public class ApiLookup { } private int findMember(int classNumber, @NonNull String name, @Nullable String desc) { - // The index array contains class indexes from 0 to classCount and - // member indices from classCount to mIndices.length. - int low = mClassCount; - int high = mIndices.length - 1; + return findMember(classNumber, name, desc, false); + } + + private int findMemberDeprecatedIn(int classNumber, @NonNull String name, + @Nullable String desc) { + return findMember(classNumber, name, desc, true); + } + + private int seekClassData(int classNumber, int field) { + int offset = mIndices[classNumber]; + offset += mData[offset] & 0xFF; + if (field == CLASS_HEADER_MEMBER_OFFSETS) { + return offset; + } + offset += 5; // 3 bytes for start, 2 bytes for length + if (field == CLASS_HEADER_API) { + return offset; + } + boolean hasDeprecation = (mData[offset] & HAS_DEPRECATION_BYTE_FLAG) != 0; + offset++; + if (field == CLASS_HEADER_DEPRECATED) { + return hasDeprecation ? offset : -1; + } else if (hasDeprecation) { + offset++; + } + assert field == CLASS_HEADER_INTERFACES; + return offset; + } + + private int findMember(int classNumber, @NonNull String name, @Nullable String desc, + boolean deprecation) { + int curr = seekClassData(classNumber, CLASS_HEADER_MEMBER_OFFSETS); + + // 3 bytes for first offset + int low = get3ByteInt(mData, curr); + curr += 3; + + int length = get2ByteInt(mData, curr); + if (length == 0) { + return -1; + } + int high = low + length - 1; + while (low <= high) { int middle = (low + high) >>> 1; int offset = mIndices[middle]; @@ -929,38 +1270,56 @@ public class ApiLookup { " with entry at " + offset + ": " + dumpEntry(offset)); } - // Check class number: read short. The byte data is always big endian. - int entryClass = (mData[offset++] & 0xFF) << 8 | (mData[offset++] & 0xFF); - int compare = entryClass - classNumber; - if (compare == 0) { - if (desc != null) { - // Method - int nameLength = name.length(); - compare = compare(mData, offset, (byte) '(', name, nameLength); + int compare; + if (desc != null) { + // Method + int nameLength = name.length(); + compare = compare(mData, offset, (byte) '(', name, 0, nameLength); + if (compare == 0) { + offset += nameLength; + int argsEnd = desc.indexOf(')'); + // Only compare up to the ) -- after that we have a return value in the + // input description, which isn't there in the database + compare = compare(mData, offset, (byte) ')', desc, 0, argsEnd); if (compare == 0) { - offset += nameLength; - int argsEnd = desc.indexOf(')'); - // Only compare up to the ) -- after that we have a return value in the - // input description, which isn't there in the database - compare = compare(mData, offset, (byte) ')', desc, argsEnd); - if (compare == 0) { - offset += argsEnd + 1; + if (DEBUG_SEARCH) { + System.out.println("Found " + dumpEntry(offset)); + } - if (mData[offset++] == 0) { - // Yes, terminated argument list: get the API level - return UnsignedBytes.toInt(mData[offset]); + offset += argsEnd + 1; + + if (mData[offset++] == 0) { + // Yes, terminated argument list: get the API level + int api = UnsignedBytes.toInt(mData[offset]); + if (deprecation) { + if ((api & HAS_DEPRECATION_BYTE_FLAG) != 0) { + return UnsignedBytes.toInt(mData[offset + 1]); + } else { + return -1; + } + } else { + return api & API_MASK; } } } - } else { - // Field - int nameLength = name.length(); - compare = compare(mData, offset, (byte) 0, name, nameLength); - if (compare == 0) { - offset += nameLength; - if (mData[offset++] == 0) { - // Yes, terminated argument list: get the API level - return UnsignedBytes.toInt(mData[offset]); + } + } else { + // Field + int nameLength = name.length(); + compare = compare(mData, offset, (byte) 0, name, 0, nameLength); + if (compare == 0) { + offset += nameLength; + if (mData[offset++] == 0) { + // Yes, terminated argument list: get the API level + int api = UnsignedBytes.toInt(mData[offset]); + if (deprecation) { + if ((api & HAS_DEPRECATION_BYTE_FLAG) != 0) { + return UnsignedBytes.toInt(mData[offset + 1]); + } else { + return -1; + } + } else { + return api & API_MASK; } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiPackage.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiPackage.java new file mode 100644 index 00000000000..e233a214fba --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiPackage.java @@ -0,0 +1,69 @@ +/* + * 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.checks; + +import com.android.annotations.NonNull; +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Represents a package and its classes + */ +public class ApiPackage implements Comparable { + private final String mName; + private final List mClasses = Lists.newArrayListWithExpectedSize(100); + + // Persistence data: Used when writing out binary data in ApiLookup + int indexOffset; // offset of the package entry + + ApiPackage(@NonNull String name) { + mName = name; + } + + /** + * Returns the name of the class (fully qualified name) + * @return the name of the class + */ + @NonNull + public String getName() { + return mName; + } + + /** + * Returns the classes in this package + * @return the classes in this package + */ + @NonNull + public List getClasses() { + return mClasses; + } + + void addClass(@NonNull ApiClass clz) { + mClasses.add(clz); + } + + @Override + public int compareTo(@NonNull ApiPackage other) { + return mName.compareTo(other.mName); + } + + @Override + public String toString() { + return mName; + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiParser.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiParser.java index 26ba6413277..56a09bc8a76 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiParser.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiParser.java @@ -38,8 +38,10 @@ public class ApiParser extends DefaultHandler { private static final String ATTR_NAME = "name"; private static final String ATTR_SINCE = "since"; + private static final String ATTR_DEPRECATED = "deprecated"; - private final Map mClasses = new HashMap(); + private final Map mClasses = new HashMap(1000); + private final Map mPackages = new HashMap(); private ApiClass mCurrentClass; @@ -49,6 +51,7 @@ public class ApiParser extends DefaultHandler { Map getClasses() { return mClasses; } + Map getPackages() { return mPackages; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) @@ -59,14 +62,21 @@ public class ApiParser extends DefaultHandler { } try { + //noinspection StatementWithEmptyBody if (NODE_API.equals(localName)) { // do nothing. - } else if (NODE_CLASS.equals(localName)) { String name = attributes.getValue(ATTR_NAME); int since = Integer.parseInt(attributes.getValue(ATTR_SINCE)); - mCurrentClass = addClass(name, since); + String deprecatedAttr = attributes.getValue(ATTR_DEPRECATED); + int deprecatedIn; + if (deprecatedAttr != null) { + deprecatedIn = Integer.parseInt(deprecatedAttr); + } else { + deprecatedIn = 0; + } + mCurrentClass = addClass(name, since, deprecatedIn); } else if (NODE_EXTENDS.equals(localName)) { String name = attributes.getValue(ATTR_NAME); @@ -83,14 +93,15 @@ public class ApiParser extends DefaultHandler { } else if (NODE_METHOD.equals(localName)) { String name = attributes.getValue(ATTR_NAME); int since = getSince(attributes); - - mCurrentClass.addMethod(name, since); + int deprecatedIn = getDeprecatedIn(attributes); + mCurrentClass.addMethod(name, since, deprecatedIn); } else if (NODE_FIELD.equals(localName)) { String name = attributes.getValue(ATTR_NAME); int since = getSince(attributes); + int deprecatedIn = getDeprecatedIn(attributes); - mCurrentClass.addField(name, since); + mCurrentClass.addField(name, since, deprecatedIn); } @@ -99,11 +110,21 @@ public class ApiParser extends DefaultHandler { } } - private ApiClass addClass(String name, int apiLevel) { + private ApiClass addClass(String name, int apiLevel, int deprecatedIn) { + // There should not be any duplicates ApiClass theClass = mClasses.get(name); - if (theClass == null) { - theClass = new ApiClass(name, apiLevel); - mClasses.put(name, theClass); + assert theClass == null; + theClass = new ApiClass(name, apiLevel, deprecatedIn); + mClasses.put(name, theClass); + + String pkg = theClass.getPackage(); + if (pkg != null) { + ApiPackage apiPackage = mPackages.get(pkg); + if (apiPackage == null) { + apiPackage = new ApiPackage(pkg); + mPackages.put(pkg, apiPackage); + } + apiPackage.addClass(theClass); } return theClass; @@ -119,4 +140,15 @@ public class ApiParser extends DefaultHandler { return since; } + + private int getDeprecatedIn(Attributes attributes) { + int deprecatedIn = mCurrentClass.getDeprecatedIn(); + String deprecatedAttr = attributes.getValue(ATTR_DEPRECATED); + + if (deprecatedAttr != null) { + deprecatedIn = Integer.parseInt(deprecatedAttr); + } + + return deprecatedIn; + } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppCompatCallDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppCompatCallDetector.java index 062e85d256f..6e7b0a51cb7 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppCompatCallDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppCompatCallDetector.java @@ -21,28 +21,29 @@ import static com.android.tools.klint.detector.api.TextFormat.RAW; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.TextFormat; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; import java.util.Arrays; import java.util.List; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UFunction; -import org.jetbrains.uast.UastUtils; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - -public class AppCompatCallDetector extends Detector implements UastScanner { +public class AppCompatCallDetector extends Detector implements Detector.UastScanner { public static final Issue ISSUE = Issue.create( "AppCompatMethod", "Using Wrong AppCompat Method", @@ -52,7 +53,7 @@ public class AppCompatCallDetector extends Detector implements UastScanner { Category.CORRECTNESS, 6, Severity.WARNING, new Implementation( AppCompatCallDetector.class, - Scope.SOURCE_FILE_SCOPE)). + Scope.JAVA_FILE_SCOPE)). addMoreInfo("http://developer.android.com/tools/support-library/index.html"); private static final String GET_ACTION_BAR = "getActionBar"; @@ -69,66 +70,62 @@ public class AppCompatCallDetector extends Detector implements UastScanner { public AppCompatCallDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - @Override public void beforeCheckProject(@NonNull Context context) { Boolean dependsOnAppCompat = context.getProject().dependsOn(APPCOMPAT_LIB_ARTIFACT); mDependsOnAppCompat = dependsOnAppCompat != null && dependsOnAppCompat; } + @Nullable @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Arrays.asList( - GET_ACTION_BAR, - START_ACTION_MODE, - SET_PROGRESS_BAR_VIS, - SET_PROGRESS_BAR_IN_VIS, - SET_PROGRESS_BAR_INDETERMINATE, - REQUEST_WINDOW_FEATURE); + GET_ACTION_BAR, + START_ACTION_MODE, + SET_PROGRESS_BAR_VIS, + SET_PROGRESS_BAR_IN_VIS, + SET_PROGRESS_BAR_INDETERMINATE, + REQUEST_WINDOW_FEATURE); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - if (mDependsOnAppCompat && isAppBarActivityCall(context, node)) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + if (mDependsOnAppCompat && isAppBarActivityCall(context, node, method)) { + String name = method.getName(); String replace = null; - if (node.matchesFunctionName(GET_ACTION_BAR)) { + if (GET_ACTION_BAR.equals(name)) { replace = "getSupportActionBar"; - } else if (node.matchesFunctionName(START_ACTION_MODE)) { + } else if (START_ACTION_MODE.equals(name)) { replace = "startSupportActionMode"; - } else if (node.matchesFunctionName(SET_PROGRESS_BAR_VIS)) { + } else if (SET_PROGRESS_BAR_VIS.equals(name)) { replace = "setSupportProgressBarVisibility"; - } else if (node.matchesFunctionName(SET_PROGRESS_BAR_IN_VIS)) { + } else if (SET_PROGRESS_BAR_IN_VIS.equals(name)) { replace = "setSupportProgressBarIndeterminateVisibility"; - } else if (node.matchesFunctionName(SET_PROGRESS_BAR_INDETERMINATE)) { + } else if (SET_PROGRESS_BAR_INDETERMINATE.equals(name)) { replace = "setSupportProgressBarIndeterminate"; - } else if (node.matchesFunctionName(REQUEST_WINDOW_FEATURE)) { + } else if (REQUEST_WINDOW_FEATURE.equals(name)) { replace = "supportRequestWindowFeature"; } if (replace != null) { - String message = String.format(ERROR_MESSAGE_FORMAT, replace, node.getFunctionName()); - context.report(ISSUE, node, context.getLocation(node), message); + String message = String.format(ERROR_MESSAGE_FORMAT, replace, name); + context.report(ISSUE, node, context.getUastLocation(node), message); } } + } - private static boolean isAppBarActivityCall(@NonNull UastAndroidContext context, - @NonNull UCallExpression node) { - UFunction resolved = node.resolve(context); - if (resolved != null) { - UClass containingClass = UastUtils.getContainingClass(resolved); - if (containingClass != null && containingClass.isSubclassOf(CLASS_ACTIVITY)) { - // Make sure that the calling context is a subclass of ActionBarActivity; - // we don't want to flag these calls if they are in non-appcompat activities - // such as PreferenceActivity (see b.android.com/58512) - return UastUtils.getContainingClassOrEmpty(node) - .isSubclassOf("android.support.v7.app.ActionBarActivity"); - } + private static boolean isAppBarActivityCall(@NonNull JavaContext context, + @NonNull UCallExpression node, @NonNull PsiMethod method) { + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.isMemberInSubClassOf(method, CLASS_ACTIVITY, false)) { + // Make sure that the calling context is a subclass of ActionBarActivity; + // we don't want to flag these calls if they are in non-appcompat activities + // such as PreferenceActivity (see b.android.com/58512) + UClass cls = UastUtils.getParentOfType(node, UClass.class, true); + return cls != null && evaluator.extendsClass(cls, + "android.support.v7.app.ActionBarActivity", false); } return false; } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppIndexingApiDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppIndexingApiDetector.java new file mode 100644 index 00000000000..8e4f1ba6d3f --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppIndexingApiDetector.java @@ -0,0 +1,743 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_EXPORTED; +import static com.android.SdkConstants.ATTR_HOST; +import static com.android.SdkConstants.ATTR_PATH; +import static com.android.SdkConstants.ATTR_PATH_PREFIX; +import static com.android.SdkConstants.ATTR_SCHEME; +import static com.android.SdkConstants.CLASS_ACTIVITY; +import static com.android.xml.AndroidManifest.ATTRIBUTE_MIME_TYPE; +import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_PORT; +import static com.android.xml.AndroidManifest.NODE_ACTION; +import static com.android.xml.AndroidManifest.NODE_ACTIVITY; +import static com.android.xml.AndroidManifest.NODE_APPLICATION; +import static com.android.xml.AndroidManifest.NODE_CATEGORY; +import static com.android.xml.AndroidManifest.NODE_DATA; +import static com.android.xml.AndroidManifest.NODE_INTENT; +import static com.android.xml.AndroidManifest.NODE_MANIFEST; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.res2.AbstractResourceRepository; +import com.android.ide.common.res2.ResourceItem; +import com.android.ide.common.resources.ResourceUrl; +import com.android.resources.ResourceType; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.client.api.LintClient; +import com.android.tools.klint.client.api.XmlParser; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.XmlScanner; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LintUtils; +import com.android.tools.klint.detector.api.Project; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.XmlContext; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + + +/** + * Check if the usage of App Indexing is correct. + */ +public class AppIndexingApiDetector extends Detector implements XmlScanner, Detector.UastScanner { + + private static final Implementation URL_IMPLEMENTATION = new Implementation( + AppIndexingApiDetector.class, Scope.MANIFEST_SCOPE); + + @SuppressWarnings("unchecked") + private static final Implementation APP_INDEXING_API_IMPLEMENTATION = + new Implementation( + AppIndexingApiDetector.class, + EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST), + Scope.JAVA_FILE_SCOPE, Scope.MANIFEST_SCOPE); + + public static final Issue ISSUE_URL_ERROR = Issue.create( + "GoogleAppIndexingUrlError", //$NON-NLS-1$ + "URL not supported by app for Google App Indexing", + "Ensure the URL is supported by your app, to get installs and traffic to your" + + " app from Google Search.", + Category.USABILITY, 5, Severity.ERROR, URL_IMPLEMENTATION) + .addMoreInfo("https://g.co/AppIndexing/AndroidStudio"); + + public static final Issue ISSUE_APP_INDEXING = + Issue.create( + "GoogleAppIndexingWarning", //$NON-NLS-1$ + "Missing support for Google App Indexing", + "Adds URLs to get your app into the Google index, to get installs" + + " and traffic to your app from Google Search.", + Category.USABILITY, 5, Severity.WARNING, URL_IMPLEMENTATION) + .addMoreInfo("https://g.co/AppIndexing/AndroidStudio"); + + public static final Issue ISSUE_APP_INDEXING_API = + Issue.create( + "GoogleAppIndexingApiWarning", //$NON-NLS-1$ + "Missing support for Google App Indexing Api", + "Adds URLs to get your app into the Google index, to get installs" + + " and traffic to your app from Google Search.", + Category.USABILITY, 5, Severity.WARNING, APP_INDEXING_API_IMPLEMENTATION) + .addMoreInfo("https://g.co/AppIndexing/AndroidStudio") + .setEnabledByDefault(false); + + private static final String[] PATH_ATTR_LIST = new String[]{ATTR_PATH_PREFIX, ATTR_PATH}; + private static final String SCHEME_MISSING = "android:scheme is missing"; + private static final String HOST_MISSING = "android:host is missing"; + private static final String DATA_MISSING = "Missing data element"; + private static final String URL_MISSING = "Missing URL for the intent filter"; + private static final String NOT_BROWSABLE + = "Activity supporting ACTION_VIEW is not set as BROWSABLE"; + private static final String ILLEGAL_NUMBER = "android:port is not a legal number"; + + private static final String APP_INDEX_START = "start"; //$NON-NLS-1$ + private static final String APP_INDEX_END = "end"; //$NON-NLS-1$ + private static final String APP_INDEX_VIEW = "view"; //$NON-NLS-1$ + private static final String APP_INDEX_VIEW_END = "viewEnd"; //$NON-NLS-1$ + private static final String CLIENT_CONNECT = "connect"; //$NON-NLS-1$ + private static final String CLIENT_DISCONNECT = "disconnect"; //$NON-NLS-1$ + private static final String ADD_API = "addApi"; //$NON-NLS-1$ + + private static final String APP_INDEXING_API_CLASS + = "com.google.android.gms.appindexing.AppIndexApi"; + private static final String GOOGLE_API_CLIENT_CLASS + = "com.google.android.gms.common.api.GoogleApiClient"; + private static final String GOOGLE_API_CLIENT_BUILDER_CLASS + = "com.google.android.gms.common.api.GoogleApiClient.Builder"; + private static final String API_CLASS = "com.google.android.gms.appindexing.AppIndex"; + + public enum IssueType { + SCHEME_MISSING(AppIndexingApiDetector.SCHEME_MISSING), + HOST_MISSING(AppIndexingApiDetector.HOST_MISSING), + DATA_MISSING(AppIndexingApiDetector.DATA_MISSING), + URL_MISSING(AppIndexingApiDetector.URL_MISSING), + NOT_BROWSABLE(AppIndexingApiDetector.NOT_BROWSABLE), + ILLEGAL_NUMBER(AppIndexingApiDetector.ILLEGAL_NUMBER), + EMPTY_FIELD("cannot be empty"), + MISSING_SLASH("attribute should start with '/'"), + UNKNOWN("unknown error type"); + + private final String message; + + IssueType(String str) { + this.message = str; + } + + public static IssueType parse(String str) { + for (IssueType type : IssueType.values()) { + if (str.contains(type.message)) { + return type; + } + } + return UNKNOWN; + } + } + + // ---- Implements XmlScanner ---- + @Override + @Nullable + public Collection getApplicableElements() { + return Collections.singletonList(NODE_APPLICATION); + } + + @Override + public void visitElement(@NonNull XmlContext context, @NonNull Element application) { + List activities = extractChildrenByName(application, NODE_ACTIVITY); + boolean applicationHasActionView = false; + for (Element activity : activities) { + List intents = extractChildrenByName(activity, NODE_INTENT); + boolean activityHasActionView = false; + for (Element intent : intents) { + boolean actionView = hasActionView(intent); + if (actionView) { + activityHasActionView = true; + } + visitIntent(context, intent); + } + if (activityHasActionView) { + applicationHasActionView = true; + if (activity.hasAttributeNS(ANDROID_URI, ATTR_EXPORTED)) { + Attr exported = activity.getAttributeNodeNS(ANDROID_URI, ATTR_EXPORTED); + if (!exported.getValue().equals("true")) { + // Report error if the activity supporting action view is not exported. + context.report(ISSUE_URL_ERROR, activity, + context.getLocation(activity), + "Activity supporting ACTION_VIEW is not exported"); + } + } + } + } + if (!applicationHasActionView && !context.getProject().isLibrary()) { + // Report warning if there is no activity that supports action view. + context.report(ISSUE_APP_INDEXING, application, context.getLocation(application), + // This error message is more verbose than the other app indexing lint warnings, because it + // shows up on a blank project, and we want to make it obvious by just looking at the error + // message what this is + "App is not indexable by Google Search; consider adding at least one Activity with an ACTION-VIEW " + + "intent filter. See issue explanation for more details."); + } + } + + @Nullable + @Override + public List applicableSuperClasses() { + return Collections.singletonList(CLASS_ACTIVITY); + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + if (declaration.getName() == null) { + return; + } + + // In case linting the base class itself. + if (!context.getEvaluator().extendsClass(declaration, CLASS_ACTIVITY, true)) { + return; + } + + declaration.accept(new MethodVisitor(context, declaration)); + } + + static class MethodVisitor extends AbstractUastVisitor { + private final JavaContext mContext; + private final UClass mCls; + + private final List mStartMethods; + private final List mEndMethods; + private final List mConnectMethods; + private final List mDisconnectMethods; + private boolean mHasAddAppIndexApi; + + private MethodVisitor(JavaContext context, UClass cls) { + mCls = cls; + mContext = context; + mStartMethods = Lists.newArrayListWithExpectedSize(2); + mEndMethods = Lists.newArrayListWithExpectedSize(2); + mConnectMethods = Lists.newArrayListWithExpectedSize(2); + mDisconnectMethods = Lists.newArrayListWithExpectedSize(2); + } + + @Override + public boolean visitClass(UClass aClass) { + if (aClass.getPsi().equals(mCls.getPsi())) { + return super.visitClass(aClass); + } else { + // Don't go into inner classes + return true; + } + } + + @Override + public void afterVisitClass(UClass node) { + report(); + } + + @Override + public boolean visitCallExpression(UCallExpression node) { + if (UastExpressionUtils.isMethodCall(node)) { + visitMethodCallExpression(node); + } + return super.visitCallExpression(node); + } + + private void visitMethodCallExpression(UCallExpression node) { + String methodName = node.getMethodName(); + if (methodName == null) { + return; + } + + if (methodName.equals(APP_INDEX_START)) { + if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) { + mStartMethods.add(node); + } + } + else if (methodName.equals(APP_INDEX_END)) { + if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) { + mEndMethods.add(node); + } + } + else if (methodName.equals(APP_INDEX_VIEW)) { + if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) { + mStartMethods.add(node); + } + } + else if (methodName.equals(APP_INDEX_VIEW_END)) { + if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) { + mEndMethods.add(node); + } + } + else if (methodName.equals(CLIENT_CONNECT)) { + if (JavaEvaluator.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_CLASS)) { + mConnectMethods.add(node); + } + } + else if (methodName.equals(CLIENT_DISCONNECT)) { + if (JavaEvaluator.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_CLASS)) { + mDisconnectMethods.add(node); + } + } + else if (methodName.equals(ADD_API)) { + if (JavaEvaluator + .isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_BUILDER_CLASS)) { + List args = node.getValueArguments(); + if (!args.isEmpty()) { + PsiElement resolved = UastUtils.tryResolve(args.get(0)); + if (resolved instanceof PsiField && + JavaEvaluator.isMemberInClass((PsiField) resolved, API_CLASS)) { + mHasAddAppIndexApi = true; + } + } + } + } + } + + private void report() { + // finds the activity classes that need app activity annotation + Set activitiesToCheck = getActivitiesToCheck(mContext); + + // app indexing API used but no support in manifest + boolean hasIntent = activitiesToCheck.contains(mCls.getQualifiedName()); + if (!hasIntent) { + for (UCallExpression call : mStartMethods) { + mContext.report(ISSUE_APP_INDEXING_API, call, + mContext.getUastNameLocation(call), + "Missing support for Google App Indexing in the manifest"); + } + for (UCallExpression call : mEndMethods) { + mContext.report(ISSUE_APP_INDEXING_API, call, + mContext.getUastNameLocation(call), + "Missing support for Google App Indexing in the manifest"); + } + return; + } + + // `AppIndex.AppIndexApi.start / end / view / viewEnd` should exist + if (mStartMethods.isEmpty() && mEndMethods.isEmpty()) { + mContext.reportUast(ISSUE_APP_INDEXING_API, mCls, + mContext.getUastNameLocation(mCls), + "Missing support for Google App Indexing API"); + return; + } + + for (UCallExpression startNode : mStartMethods) { + List expressions = startNode.getValueArguments(); + if (expressions.isEmpty()) { + continue; + } + UExpression startClient = expressions.get(0); + + // GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)` + if (!mHasAddAppIndexApi) { + String message = String.format( + "GoogleApiClient `%1$s` has not added support for App Indexing API", + startClient.asSourceString()); + mContext.report(ISSUE_APP_INDEXING_API, startClient, + mContext.getUastLocation(startClient), message); + } + + // GoogleApiClient `connect` should exist + if (!hasOperand(startClient, mConnectMethods)) { + String message = String.format("GoogleApiClient `%1$s` is not connected", + startClient.asSourceString()); + mContext.report(ISSUE_APP_INDEXING_API, startClient, + mContext.getUastLocation(startClient), message); + } + + // `AppIndex.AppIndexApi.end` should pair with `AppIndex.AppIndexApi.start` + if (!hasFirstArgument(startClient, mEndMethods)) { + mContext.report(ISSUE_APP_INDEXING_API, startNode, + mContext.getUastNameLocation(startNode), + "Missing corresponding `AppIndex.AppIndexApi.end` method"); + } + } + + for (UCallExpression endNode : mEndMethods) { + List expressions = endNode.getValueArguments(); + if (expressions.isEmpty()) { + continue; + } + UExpression endClient = expressions.get(0); + + // GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)` + if (!mHasAddAppIndexApi) { + String message = String.format( + "GoogleApiClient `%1$s` has not added support for App Indexing API", + endClient.asSourceString()); + mContext.report(ISSUE_APP_INDEXING_API, endClient, + mContext.getUastLocation(endClient), message); + } + + // GoogleApiClient `disconnect` should exist + if (!hasOperand(endClient, mDisconnectMethods)) { + String message = String.format("GoogleApiClient `%1$s`" + + " is not disconnected", endClient.asSourceString()); + mContext.report(ISSUE_APP_INDEXING_API, endClient, + mContext.getUastLocation(endClient), message); + } + + // `AppIndex.AppIndexApi.start` should pair with `AppIndex.AppIndexApi.end` + if (!hasFirstArgument(endClient, mStartMethods)) { + mContext.report(ISSUE_APP_INDEXING_API, endNode, + mContext.getUastNameLocation(endNode), + "Missing corresponding `AppIndex.AppIndexApi.start` method"); + } + } + } + } + + /** + * Gets names of activities which needs app indexing. i.e. the activities have data tag in their + * intent filters. + * TODO: Cache the activities to speed up batch lint. + * + * @param context The context to check in. + */ + private static Set getActivitiesToCheck(Context context) { + Set activitiesToCheck = Sets.newHashSet(); + List manifestFiles = context.getProject().getManifestFiles(); + XmlParser xmlParser = context.getDriver().getClient().getXmlParser(); + if (xmlParser != null) { + // TODO: Avoid visit all manifest files before enable this check by default. + for (File manifest : manifestFiles) { + XmlContext xmlContext = + new XmlContext(context.getDriver(), context.getProject(), + null, manifest, null, xmlParser); + Document doc = xmlParser.parseXml(xmlContext); + if (doc != null) { + List children = LintUtils.getChildren(doc); + for (Element child : children) { + if (child.getNodeName().equals(NODE_MANIFEST)) { + List apps = extractChildrenByName(child, NODE_APPLICATION); + for (Element app : apps) { + List acts = extractChildrenByName(app, NODE_ACTIVITY); + for (Element act : acts) { + List intents = extractChildrenByName(act, NODE_INTENT); + for (Element intent : intents) { + List data = extractChildrenByName(intent, + NODE_DATA); + if (!data.isEmpty() && act.hasAttributeNS( + ANDROID_URI, ATTRIBUTE_NAME)) { + Attr attr = act.getAttributeNodeNS( + ANDROID_URI, ATTRIBUTE_NAME); + String activityName = attr.getValue(); + int dotIndex = activityName.indexOf('.'); + if (dotIndex <= 0) { + String pkg = context.getMainProject().getPackage(); + if (pkg != null) { + if (dotIndex == 0) { + activityName = pkg + activityName; + } + else { + activityName = pkg + '.' + activityName; + } + } + } + activitiesToCheck.add(activityName); + } + } + } + } + } + } + } + } + } + return activitiesToCheck; + } + + private static void visitIntent(@NonNull XmlContext context, @NonNull Element intent) { + boolean actionView = hasActionView(intent); + boolean browsable = isBrowsable(intent); + boolean isHttp = false; + boolean hasScheme = false; + boolean hasHost = false; + boolean hasPort = false; + boolean hasPath = false; + boolean hasMimeType = false; + Element firstData = null; + List children = extractChildrenByName(intent, NODE_DATA); + for (Element data : children) { + if (firstData == null) { + firstData = data; + } + if (isHttpSchema(data)) { + isHttp = true; + } + checkSingleData(context, data); + + for (String name : PATH_ATTR_LIST) { + if (data.hasAttributeNS(ANDROID_URI, name)) { + hasPath = true; + } + } + + if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) { + hasScheme = true; + } + + if (data.hasAttributeNS(ANDROID_URI, ATTR_HOST)) { + hasHost = true; + } + + if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) { + hasPort = true; + } + + if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_MIME_TYPE)) { + hasMimeType = true; + } + } + + // In data field, a URL is consisted by + // ://:[||] + // Each part of the URL should not have illegal character. + if ((hasPath || hasHost || hasPort) && !hasScheme) { + context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData), + SCHEME_MISSING); + } + + if ((hasPath || hasPort) && !hasHost) { + context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData), + HOST_MISSING); + } + + if (actionView && browsable) { + if (firstData == null) { + // If this activity is an ACTION_VIEW action with category BROWSABLE, but doesn't + // have data node, it may be a mistake and we will report error. + context.report(ISSUE_URL_ERROR, intent, context.getLocation(intent), + DATA_MISSING); + } else if (!hasScheme && !hasMimeType) { + // If this activity is an action view, is browsable, but has neither a + // URL nor mimeType, it may be a mistake and we will report error. + context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData), + URL_MISSING); + } + } + + // If this activity is an ACTION_VIEW action, has a http URL but doesn't have + // BROWSABLE, it may be a mistake and and we will report warning. + if (actionView && isHttp && !browsable) { + context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent), + NOT_BROWSABLE); + } + + if (actionView && !hasScheme) { + context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent), + "Missing URL"); + } + } + + /** + * Check if the intent filter supports action view. + * + * @param intent the intent filter + * @return true if it does + */ + private static boolean hasActionView(@NonNull Element intent) { + List children = extractChildrenByName(intent, NODE_ACTION); + for (Element action : children) { + if (action.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) { + Attr attr = action.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME); + if (attr.getValue().equals("android.intent.action.VIEW")) { + return true; + } + } + } + return false; + } + + /** + * Check if the intent filter is browsable. + * + * @param intent the intent filter + * @return true if it does + */ + private static boolean isBrowsable(@NonNull Element intent) { + List children = extractChildrenByName(intent, NODE_CATEGORY); + for (Element e : children) { + if (e.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) { + Attr attr = e.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME); + if (attr.getNodeValue().equals("android.intent.category.BROWSABLE")) { + return true; + } + } + } + return false; + } + + /** + * Check if the data node contains http schema + * + * @param data the data node + * @return true if it does + */ + private static boolean isHttpSchema(@NonNull Element data) { + if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) { + String value = data.getAttributeNodeNS(ANDROID_URI, ATTR_SCHEME).getValue(); + if (value.equalsIgnoreCase("http") || value.equalsIgnoreCase("https")) { + return true; + } + } + return false; + } + + private static void checkSingleData(@NonNull XmlContext context, @NonNull Element data) { + // path, pathPrefix and pathPattern should starts with /. + for (String name : PATH_ATTR_LIST) { + if (data.hasAttributeNS(ANDROID_URI, name)) { + Attr attr = data.getAttributeNodeNS(ANDROID_URI, name); + String path = replaceUrlWithValue(context, attr.getValue()); + if (!path.startsWith("/") && !path.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) { + context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr), + "android:" + name + " attribute should start with '/', but it is : " + + path); + } + } + } + + // port should be a legal number. + if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) { + Attr attr = data.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_PORT); + try { + String port = replaceUrlWithValue(context, attr.getValue()); + //noinspection ResultOfMethodCallIgnored + Integer.parseInt(port); + } catch (NumberFormatException e) { + context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr), + ILLEGAL_NUMBER); + } + } + + // Each field should be non empty. + NamedNodeMap attrs = data.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node item = attrs.item(i); + if (item.getNodeType() == Node.ATTRIBUTE_NODE) { + Attr attr = (Attr) attrs.item(i); + if (attr.getValue().isEmpty()) { + context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr), + attr.getName() + " cannot be empty"); + } + } + } + } + + private static String replaceUrlWithValue(@NonNull XmlContext context, + @NonNull String str) { + Project project = context.getProject(); + LintClient client = context.getClient(); + if (!client.supportsProjectResources()) { + return str; + } + ResourceUrl style = ResourceUrl.parse(str); + if (style == null || style.type != ResourceType.STRING || style.framework) { + return str; + } + AbstractResourceRepository resources = client.getProjectResources(project, true); + if (resources == null) { + return str; + } + List items = resources.getResourceItem(ResourceType.STRING, style.name); + if (items == null || items.isEmpty()) { + return str; + } + ResourceValue resourceValue = items.get(0).getResourceValue(false); + if (resourceValue == null) { + return str; + } + return resourceValue.getValue() == null ? str : resourceValue.getValue(); + } + + /** + * If a method with a certain argument exists in the list of methods. + * + * @param argument The first argument of the method. + * @param list The methods list. + * @return If such a method exists in the list. + */ + private static boolean hasFirstArgument(UExpression argument, List list) { + for (UCallExpression call : list) { + List expressions = call.getValueArguments(); + if (!expressions.isEmpty()) { + UExpression argument2 = expressions.get(0); + if (argument.asSourceString().equals(argument2.asSourceString())) { + return true; + } + } + } + return false; + } + + /** + * If a method with a certain operand exists in the list of methods. + * + * @param operand The operand of the method. + * @param list The methods list. + * @return If such a method exists in the list. + */ + private static boolean hasOperand(UExpression operand, List list) { + for (UCallExpression method : list) { + UElement operand2 = method.getReceiver(); + if (operand2 != null && operand.asSourceString().equals(operand2.asSourceString())) { + return true; + } + } + return false; + } + + private static List extractChildrenByName(@NonNull Element node, + @NonNull String name) { + List result = Lists.newArrayList(); + List children = LintUtils.getChildren(node); + for (Element child : children) { + if (child.getNodeName().equals(name)) { + result.add(child); + } + } + return result; + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BadHostnameVerifierDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BadHostnameVerifierDetector.java new file mode 100644 index 00000000000..cc6a6c5f338 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BadHostnameVerifierDetector.java @@ -0,0 +1,141 @@ +/* + * 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.checks; + +import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LintUtils; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReturnStatement; +import com.intellij.psi.PsiThrowStatement; + +import org.jetbrains.uast.*; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Collections; +import java.util.List; + +public class BadHostnameVerifierDetector extends Detector implements Detector.UastScanner { + + @SuppressWarnings("unchecked") + private static final Implementation IMPLEMENTATION = + new Implementation(BadHostnameVerifierDetector.class, + Scope.JAVA_FILE_SCOPE); + + public static final Issue ISSUE = Issue.create("BadHostnameVerifier", + "Insecure HostnameVerifier", + "This check looks for implementations of `HostnameVerifier` " + + "whose `verify` method always returns true (thus trusting any hostname) " + + "which could result in insecure network traffic caused by trusting arbitrary " + + "hostnames in TLS/SSL certificates presented by peers.", + Category.SECURITY, + 6, + Severity.WARNING, + IMPLEMENTATION); + + // ---- Implements UastScanner ---- + + @Nullable + @Override + public List applicableSuperClasses() { + return Collections.singletonList("javax.net.ssl.HostnameVerifier"); + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + JavaEvaluator evaluator = context.getEvaluator(); + for (PsiMethod method : declaration.findMethodsByName("verify", false)) { + if (evaluator.methodMatches(method, null, false, + TYPE_STRING, "javax.net.ssl.SSLSession")) { + ComplexVisitor visitor = new ComplexVisitor(context); + declaration.accept(visitor); + if (visitor.isComplex()) { + return; + } + + Location location = context.getNameLocation(method); + String message = String.format("`%1$s` always returns `true`, which " + + "could cause insecure network traffic due to trusting " + + "TLS/SSL server certificates for wrong hostnames", + method.getName()); + context.report(ISSUE, location, message); + break; + } + } + } + + private static class ComplexVisitor extends AbstractUastVisitor { + @SuppressWarnings("unused") + private final JavaContext mContext; + private boolean mComplex; + + public ComplexVisitor(JavaContext context) { + mContext = context; + } + + @Override + public boolean visitThrowExpression(UThrowExpression node) { + mComplex = true; + return super.visitThrowExpression(node); + } + + @Override + public boolean visitCallExpression(UCallExpression node) { + // TODO: Ignore certain known safe methods, e.g. Logging etc + mComplex = true; + return super.visitCallExpression(node); + } + + @Override + public boolean visitReturnExpression(UReturnExpression node) { + UExpression argument = node.getReturnExpression(); + if (argument != null) { + // TODO: Only do this if certain that there isn't some intermediate + // assignment, as exposed by the unit test + //Object value = ConstantEvaluator.evaluate(mContext, argument); + //if (Boolean.TRUE.equals(value)) { + if (UastLiteralUtils.isTrueLiteral(argument)) { + mComplex = false; + } else { + mComplex = true; // "return false" or some complicated logic + } + } + return super.visitReturnExpression(node); + } + + private boolean isComplex() { + return mComplex; + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BatteryDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BatteryDetector.java new file mode 100644 index 00000000000..ef8c5295560 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BatteryDetector.java @@ -0,0 +1,151 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.TAG_RECEIVER; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.ResourceXmlDetector; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.XmlContext; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; + +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.visitor.UastVisitor; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; + +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +/** + * Checks looking for issues that negatively affect battery life + */ +public class BatteryDetector extends ResourceXmlDetector implements + Detector.UastScanner { + + @SuppressWarnings("unchecked") + public static final Implementation IMPLEMENTATION = new Implementation( + BatteryDetector.class, + EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE), + Scope.MANIFEST_SCOPE, + Scope.JAVA_FILE_SCOPE); + + /** Issues that negatively affect battery life */ + public static final Issue ISSUE = Issue.create( + "BatteryLife", //$NON-NLS-1$ + "Battery Life Issues", + + "This issue flags code that either\n" + + "* negatively affects battery life, or\n" + + "* uses APIs that have recently changed behavior to prevent background tasks " + + "from consuming memory and battery excessively.\n" + + "\n" + + "Generally, you should be using `JobScheduler` or `GcmNetworkManager` instead.\n" + + "\n" + + "For more details on how to update your code, please see" + + "http://developer.android.com/preview/features/background-optimization.html", + + Category.CORRECTNESS, + 5, + Severity.WARNING, + IMPLEMENTATION) + .addMoreInfo("http://developer.android.com/preview/features/background-optimization.html"); + + /** Constructs a new {@link BatteryDetector} */ + public BatteryDetector() { + } + + @Override + public Collection getApplicableElements() { + return Collections.singletonList("action"); + } + + @Override + public void visitElement(@NonNull XmlContext context, @NonNull Element element) { + assert element.getTagName().equals("action"); + Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); + if (attr == null) { + return; + } + String name = attr.getValue(); + if ("android.net.conn.CONNECTIVITY_CHANGE".equals(name) + && element.getParentNode() != null + && element.getParentNode().getParentNode() != null + && TAG_RECEIVER.equals(element.getParentNode().getParentNode().getNodeName()) + && context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 24) { + String message = "Declaring a broadcastreceiver for " + + "`android.net.conn.CONNECTIVITY_CHANGE` is deprecated for apps targeting " + + "N and higher. In general, apps should not rely on this broadcast and " + + "instead use `JobScheduler` or `GCMNetworkManager`."; + context.report(ISSUE, element, context.getValueLocation(attr), message); + } + + if ("android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS".equals(name) + && context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 23) { + String message = getBatteryOptimizationsErrorMessage(); + context.report(ISSUE, element, context.getValueLocation(attr), message); + } + + if ("android.hardware.action.NEW_PICTURE".equals(name) + || "android.hardware.action.NEW_VIDEO".equals(name) + || "com.android.camera.NEW_PICTURE".equals(name)) { + String message = String.format("Use of %1$s is deprecated for all apps starting " + + "with the N release independent of the target SDK. Apps should not " + + "rely on these broadcasts and instead use `JobScheduler`", name); + context.report(ISSUE, element, context.getValueLocation(attr), message); + } + } + + @Nullable + @Override + public List getApplicableReferenceNames() { + return Collections.singletonList("ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"); + } + + @Override + public void visitReference(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UReferenceExpression reference, @NonNull PsiElement resolved) { + if (resolved instanceof PsiField && + context.getEvaluator().isMemberInSubClassOf((PsiField) resolved, + "android.provider.Settings", false) + && context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 23) { + String message = getBatteryOptimizationsErrorMessage(); + context.report(ISSUE, reference, context.getUastNameLocation(reference), message); + } + } + + @NonNull + private static String getBatteryOptimizationsErrorMessage() { + return "Use of `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` violates the " + + "Play Store Content Policy regarding acceptable use cases, as described in " + + "http://developer.android.com/training/monitoring-device-state/doze-standby.html"; + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BuiltinIssueRegistry.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BuiltinIssueRegistry.java index 92407a2d6c1..3990ffa94d7 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BuiltinIssueRegistry.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/BuiltinIssueRegistry.java @@ -30,28 +30,48 @@ import java.util.List; /** Registry which provides a list of checks to be performed on an Android project */ public class BuiltinIssueRegistry extends IssueRegistry { private static final List sIssues; - static final int INITIAL_CAPACITY = 220; + + static final int INITIAL_CAPACITY = 262; static { List issues = new ArrayList(INITIAL_CAPACITY); issues.add(AddJavascriptInterfaceDetector.ISSUE); issues.add(AlarmDetector.ISSUE); + issues.add(AllowAllHostnameVerifierDetector.ISSUE); issues.add(AlwaysShowActionDetector.ISSUE); - issues.add(AnnotationDetector.ISSUE); + issues.add(AndroidAutoDetector.INVALID_USES_TAG_ISSUE); + issues.add(AndroidAutoDetector.MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH); + issues.add(AndroidAutoDetector.MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE); + issues.add(AndroidAutoDetector.MISSING_ON_PLAY_FROM_SEARCH); + issues.add(AnnotationDetector.ANNOTATION_USAGE); + issues.add(AnnotationDetector.FLAG_STYLE); + issues.add(AnnotationDetector.INSIDE_METHOD); + issues.add(AnnotationDetector.SWITCH_TYPE_DEF); + issues.add(AnnotationDetector.UNIQUE); issues.add(ApiDetector.INLINED); issues.add(ApiDetector.OVERRIDE); issues.add(ApiDetector.UNSUPPORTED); + issues.add(ApiDetector.UNUSED); issues.add(AppCompatCallDetector.ISSUE); + issues.add(AppIndexingApiDetector.ISSUE_APP_INDEXING_API); + issues.add(AppIndexingApiDetector.ISSUE_URL_ERROR); + issues.add(AppIndexingApiDetector.ISSUE_APP_INDEXING); + issues.add(BadHostnameVerifierDetector.ISSUE); + issues.add(BatteryDetector.ISSUE); issues.add(CallSuperDetector.ISSUE); issues.add(CipherGetInstanceDetector.ISSUE); issues.add(CleanupDetector.COMMIT_FRAGMENT); issues.add(CleanupDetector.RECYCLE_RESOURCE); + issues.add(CleanupDetector.SHARED_PREF); issues.add(CommentDetector.EASTER_EGG); issues.add(CommentDetector.STOP_SHIP); issues.add(CustomViewDetector.ISSUE); issues.add(CutPasteDetector.ISSUE); issues.add(DateFormatDetector.DATE_FORMAT); + issues.add(SetTextDetector.SET_TEXT_I18N); + issues.add(UnsafeNativeCodeDetector.LOAD); + issues.add(UnsafeNativeCodeDetector.UNSAFE_NATIVE_CODE_LOCATION); issues.add(FragmentDetector.ISSUE); issues.add(GetSignaturesDetector.ISSUE); issues.add(HandlerDetector.ISSUE); @@ -69,15 +89,19 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(IconDetector.ICON_MIX_9PNG); issues.add(IconDetector.ICON_NODPI); issues.add(IconDetector.ICON_XML_AND_PNG); + issues.add(TrustAllX509TrustManagerDetector.ISSUE); issues.add(JavaPerformanceDetector.PAINT_ALLOC); issues.add(JavaPerformanceDetector.USE_SPARSE_ARRAY); issues.add(JavaPerformanceDetector.USE_VALUE_OF); issues.add(JavaScriptInterfaceDetector.ISSUE); issues.add(LayoutConsistencyDetector.INCONSISTENT_IDS); issues.add(LayoutInflationDetector.ISSUE); + issues.add(LeakDetector.ISSUE); + issues.add(LocaleDetector.STRING_LOCALE); issues.add(LogDetector.CONDITIONAL); issues.add(LogDetector.LONG_TAG); issues.add(LogDetector.WRONG_TAG); + issues.add(MathDetector.ISSUE); issues.add(MergeRootFrameLayoutDetector.ISSUE); issues.add(NonInternationalizedSmsDetector.ISSUE); issues.add(OverdrawDetector.ISSUE); @@ -85,22 +109,31 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(ParcelDetector.ISSUE); issues.add(PreferenceActivityDetector.ISSUE); issues.add(PrivateResourceDetector.ISSUE); + issues.add(ReadParcelableDetector.ISSUE); + issues.add(RecyclerViewDetector.DATA_BINDER); + issues.add(RecyclerViewDetector.FIXED_POSITION); + issues.add(RegistrationDetector.ISSUE); issues.add(RequiredAttributeDetector.ISSUE); issues.add(RtlDetector.COMPAT); issues.add(RtlDetector.ENABLED); issues.add(RtlDetector.SYMMETRY); issues.add(RtlDetector.USE_START); issues.add(SdCardDetector.ISSUE); + issues.add(SecureRandomDetector.ISSUE); issues.add(SecurityDetector.EXPORTED_PROVIDER); issues.add(SecurityDetector.EXPORTED_RECEIVER); issues.add(SecurityDetector.EXPORTED_SERVICE); + issues.add(SecurityDetector.SET_READABLE); + issues.add(SecurityDetector.SET_WRITABLE); issues.add(SecurityDetector.OPEN_PROVIDER); issues.add(SecurityDetector.WORLD_READABLE); issues.add(SecurityDetector.WORLD_WRITEABLE); issues.add(ServiceCastDetector.ISSUE); issues.add(SetJavaScriptEnabledDetector.ISSUE); - issues.add(SharedPrefsDetector.ISSUE); issues.add(SQLiteDetector.ISSUE); + issues.add(SslCertificateSocketFactoryDetector.CREATE_SOCKET); + issues.add(SslCertificateSocketFactoryDetector.GET_INSECURE); + issues.add(StringAuthLeakDetector.AUTH_LEAK); issues.add(StringFormatDetector.ARG_COUNT); issues.add(StringFormatDetector.ARG_TYPES); issues.add(StringFormatDetector.INVALID); @@ -114,10 +147,11 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(SupportAnnotationDetector.THREAD); issues.add(SupportAnnotationDetector.TYPE_DEF); issues.add(ToastDetector.ISSUE); - issues.add(UnusedResourceDetector.ISSUE); - issues.add(UnusedResourceDetector.ISSUE_IDS); + issues.add(UnsafeBroadcastReceiverDetector.ACTION_STRING); + issues.add(UnsafeBroadcastReceiverDetector.BROADCAST_SMS); issues.add(ViewConstructorDetector.ISSUE); issues.add(ViewHolderDetector.ISSUE); + issues.add(ViewTagDetector.ISSUE); issues.add(ViewTypeDetector.ISSUE); issues.add(WrongCallDetector.ISSUE); issues.add(WrongImportDetector.ISSUE); @@ -144,17 +178,17 @@ public class BuiltinIssueRegistry extends IssueRegistry { } else { int initialSize = 12; if (scope.contains(Scope.RESOURCE_FILE)) { - initialSize += 75; + initialSize += 80; } else if (scope.contains(Scope.ALL_RESOURCE_FILES)) { - initialSize += 10; + initialSize += 12; } - if (scope.contains(Scope.SOURCE_FILE)) { - initialSize += 55; + if (scope.contains(Scope.JAVA_FILE)) { + initialSize += 74; } else if (scope.contains(Scope.CLASS_FILE)) { initialSize += 15; } else if (scope.contains(Scope.MANIFEST)) { - initialSize += 30; + initialSize += 38; } else if (scope.contains(Scope.GRADLE_FILE)) { initialSize += 5; } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CallSuperDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CallSuperDetector.java index 4a84742e8dc..eb36dd9ee7a 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CallSuperDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CallSuperDetector.java @@ -18,40 +18,45 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.CLASS_VIEW; import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX; +import static com.android.tools.klint.checks.SupportAnnotationDetector.filterRelevantAnnotations; +import static com.android.tools.klint.detector.api.LintUtils.skipParentheses; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; -import java.io.File; -import java.util.List; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.USuperExpression; +import org.jetbrains.uast.expressions.UReferenceExpression; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; +import java.util.Collections; +import java.util.List; + /** * Makes sure that methods call super when overriding methods. */ -public class CallSuperDetector extends Detector implements UastScanner { +public class CallSuperDetector extends Detector implements Detector.UastScanner { private static final String CALL_SUPER_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "CallSuper"; //$NON-NLS-1$ private static final String ON_DETACHED_FROM_WINDOW = "onDetachedFromWindow"; //$NON-NLS-1$ private static final String ON_VISIBILITY_CHANGED = "onVisibilityChanged"; //$NON-NLS-1$ private static final Implementation IMPLEMENTATION = new Implementation( CallSuperDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Missing call to super */ public static final Issue ISSUE = Issue.create( @@ -70,41 +75,37 @@ public class CallSuperDetector extends Detector implements UastScanner { public CallSuperDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- + @Nullable @Override - public UastVisitor createUastVisitor(final UastAndroidContext context) { + public List> getApplicableUastTypes() { + return Collections.>singletonList(UMethod.class); + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull final JavaContext context) { return new AbstractUastVisitor() { @Override - public boolean visitFunction(@NotNull UFunction node) { - checkCallSuper(context, node); - return false; + public boolean visitMethod(UMethod method) { + checkCallSuper(context, method); + return super.visitMethod(method); } }; } - private static void checkCallSuper(@NonNull UastAndroidContext context, - @NonNull UFunction declaration) { + private static void checkCallSuper(@NonNull JavaContext context, + @NonNull UMethod method) { - UFunction superMethod = getRequiredSuperMethod(context, declaration); + PsiMethod superMethod = getRequiredSuperMethod(context, method); if (superMethod != null) { - if (!SuperCallVisitor.callsSuper(context, declaration, superMethod)) { - String methodName = declaration.getName(); + if (!SuperCallVisitor.callsSuper(method, superMethod)) { + String methodName = method.getName(); String message = "Overriding method should call `super." + methodName + "`"; - Location location = context.getLocation(declaration.getNameElement()); - context.report(ISSUE, declaration, location, message); + Location location = context.getUastNameLocation(method); + context.reportUast(ISSUE, method, location, message); } } } @@ -114,8 +115,14 @@ public class CallSuperDetector extends Detector implements UastScanner { * to be invoked, and if so, returns it (otherwise returns null) */ @Nullable - private static UFunction getRequiredSuperMethod(UastAndroidContext context, - @NonNull UFunction method) { + private static PsiMethod getRequiredSuperMethod(@NonNull JavaContext context, + @NonNull PsiMethod method) { + + JavaEvaluator evaluator = context.getEvaluator(); + PsiMethod directSuper = evaluator.getSuperMethod(method); + if (directSuper == null) { + return null; + } String name = method.getName(); if (ON_DETACHED_FROM_WINDOW.equals(name)) { @@ -124,43 +131,37 @@ public class CallSuperDetector extends Detector implements UastScanner { // is still dangerous if supporting older versions so flag // this for now (should make annotation carry metadata like // compileSdkVersion >= N). - if (!UastUtils.getContainingClassOrEmpty(method).isSubclassOf(CLASS_VIEW)) { + if (!evaluator.isMemberInSubClassOf(method, CLASS_VIEW, false)) { return null; } - List superFunctions = method.getSuperFunctions(context); - return superFunctions.isEmpty() ? null : superFunctions.get(0); + return directSuper; } else if (ON_VISIBILITY_CHANGED.equals(name)) { // From Android Wear API; doesn't yet have an annotation // but we want to enforce this right away until the AAR // is updated to supply it once @CallSuper is available in // the support library - if (!UastUtils.getContainingClassOrEmpty(method).isSubclassOf( - "android.support.wearable.watchface.WatchFaceService.Engine")) { + if (!evaluator.isMemberInSubClassOf(method, + "android.support.wearable.watchface.WatchFaceService.Engine", false)) { return null; } - List superFunctions = method.getSuperFunctions(context); - return superFunctions.isEmpty() ? null : superFunctions.get(0); + return directSuper; } // Look up annotations metadata - List superFunctions = method.getSuperFunctions(context); - UFunction directSuper = superFunctions.isEmpty() ? null : superFunctions.get(0); - UFunction superMethod = directSuper; + PsiMethod superMethod = directSuper; while (superMethod != null) { - for (UAnnotation annotation : superMethod.getAnnotations()) { - annotation = SupportAnnotationDetector.getRelevantAnnotation(annotation, context); - if (annotation != null) { - String fqName = annotation.getFqName(); - if (CALL_SUPER_ANNOTATION.equals(fqName)) { - return directSuper; - } else if (fqName != null && fqName.endsWith(".OverrideMustInvoke")) { - // Handle findbugs annotation on the fly too - return directSuper; - } + PsiAnnotation[] annotations = superMethod.getModifierList().getAnnotations(); + annotations = filterRelevantAnnotations(annotations); + for (PsiAnnotation annotation : annotations) { + String signature = annotation.getQualifiedName(); + if (CALL_SUPER_ANNOTATION.equals(signature)) { + return directSuper; + } else if (signature != null && signature.endsWith(".OverrideMustInvoke")) { + // Handle findbugs annotation on the fly too + return directSuper; } } - superFunctions = superMethod.getSuperFunctions(context); - superMethod = superFunctions.isEmpty() ? null : superFunctions.get(0); + superMethod = evaluator.getSuperMethod(superMethod); } return null; @@ -168,45 +169,31 @@ public class CallSuperDetector extends Detector implements UastScanner { /** Visits a method and determines whether the method calls its super method */ private static class SuperCallVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; - private final String mMethodContainingClassFqName; - private final String mMethodName; + private final PsiMethod mMethod; private boolean mCallsSuper; - public static boolean callsSuper( - @NonNull UastAndroidContext context, - @NonNull UFunction methodDeclaration, - @NonNull UFunction superMethod) { - SuperCallVisitor visitor = new SuperCallVisitor(context, superMethod); - methodDeclaration.accept(visitor); + public static boolean callsSuper(@NonNull UMethod method, + @NonNull PsiMethod superMethod) { + SuperCallVisitor visitor = new SuperCallVisitor(superMethod); + method.accept(visitor); return visitor.mCallsSuper; } - private SuperCallVisitor(@NonNull UastAndroidContext context, @NonNull UFunction function) { - mContext = context; - mMethodContainingClassFqName = UastUtils.getContainingClassOrEmpty(function).getFqName(); - mMethodName = function.getName(); + private SuperCallVisitor(@NonNull PsiMethod method) { + mMethod = method; } @Override - public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) { - UExpression receiver = node.getReceiver(); - UExpression selector = node.getSelector(); - - if (receiver instanceof USuperExpression && selector instanceof UCallExpression) { - UFunction resolvedFunction = ((UCallExpression) selector).resolve(mContext); - if (resolvedFunction != null && resolvedFunction.matchesNameWithContaining(mMethodContainingClassFqName, mMethodName)) { + public boolean visitSuperExpression(USuperExpression node) { + UElement parent = skipParentheses(node.getContainingElement()); + if (parent instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) parent).resolve(); + if (mMethod.equals(resolved)) { mCallsSuper = true; - return true; } } - - return false; - } - - @Override - public boolean visitElement(@NotNull UElement node) { - return mCallsSuper || super.visitElement(node); + + return super.visitSuperExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CipherGetInstanceDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CipherGetInstanceDetector.java index d6ada1679a3..cdb9f530f81 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CipherGetInstanceDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CipherGetInstanceDetector.java @@ -17,39 +17,44 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.google.common.collect.Sets; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + import java.util.Collections; import java.util.List; import java.util.Set; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Ensures that Cipher.getInstance is not called with AES as the parameter. */ -public class CipherGetInstanceDetector extends Detector implements UastScanner { +public class CipherGetInstanceDetector extends Detector implements Detector.UastScanner { public static final Issue ISSUE = Issue.create( "GetInstance", //$NON-NLS-1$ "Cipher.getInstance with ECB", - "`Cipher#getInstance` should not be called with ECB as the cipher mode or without " - + "setting the cipher mode because the default mode on android is ECB, which " - + "is insecure.", + "`Cipher#getInstance` should not be called with ECB as the cipher mode or without " + + "setting the cipher mode because the default mode on android is ECB, which " + + "is insecure.", Category.SECURITY, 9, Severity.WARNING, new Implementation( CipherGetInstanceDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); private static final String CIPHER = "javax.crypto.Cipher"; //$NON-NLS-1$ private static final String GET_INSTANCE = "getInstance"; //$NON-NLS-1$ @@ -57,62 +62,44 @@ public class CipherGetInstanceDetector extends Detector implements UastScanner { Sets.newHashSet("AES", "DES", "DESede"); //$NON-NLS-1$ private static final String ECB = "ECB"; //$NON-NLS-1$ - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- + @Nullable @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList(GET_INSTANCE); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - UClass containingClass = UastUtils.getContainingClass(node); - if (containingClass == null || !containingClass.isSubclassOf(CIPHER)) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + if (!context.getEvaluator().isMemberInSubClassOf(method, CIPHER, false)) { return; } - - List argumentList = node.getValueArguments(); - if (argumentList.size() == 1) { - UExpression expression = argumentList.get(0); - if (expression instanceof ULiteralExpression) { - ULiteralExpression argument = (ULiteralExpression)expression; - String parameter = argument.renderString(); - checkParameter(context, node, argument, parameter, false); - } else if (expression instanceof UResolvable) { - UDeclaration declaration = ((UResolvable)expression).resolve(context); - if (declaration instanceof UVariable) { - UVariable field = (UVariable) declaration; - UExpression initializer = field.getInitializer(); - if (initializer != null) { - Object value = initializer.evaluate(); - if (value instanceof String) { - checkParameter(context, node, expression, (String)value, true); - } - } - } + List arguments = node.getValueArguments(); + if (arguments.size() == 1) { + UExpression expression = arguments.get(0); + Object value = ConstantEvaluator.evaluate(context, expression); + if (value instanceof String) { + checkParameter(context, node, expression, (String)value, + !(expression instanceof ULiteralExpression)); } } } - private static void checkParameter(@NonNull UastAndroidContext context, - @NonNull UCallExpression call, @NonNull UExpression arg, @NonNull String value, - boolean includeValue) { + private static void checkParameter(@NonNull JavaContext context, + @NonNull UCallExpression call, @NonNull UElement node, @NonNull String value, + boolean includeValue) { if (ALGORITHM_ONLY.contains(value)) { String message = "`Cipher.getInstance` should not be called without setting the" + " encryption mode and padding"; - context.report(ISSUE, call, context.getLocation(arg), message); + context.report(ISSUE, call, context.getUastLocation(node), message); } else if (value.contains(ECB)) { String message = "ECB encryption mode should not be used"; if (includeValue) { message += " (was \"" + value + "\")"; } - context.report(ISSUE, call, context.getLocation(arg), message); + context.report(ISSUE, call, context.getUastLocation(node), message); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CleanupDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CleanupDetector.java index f471730fea3..e6689dd5649 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CleanupDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CleanupDetector.java @@ -16,37 +16,69 @@ package com.android.tools.klint.checks; +import static com.android.SdkConstants.CLASS_CONTENTPROVIDER; import static com.android.SdkConstants.CLASS_CONTEXT; +import static com.android.tools.klint.detector.api.LintUtils.skipParentheses; +import static com.intellij.psi.util.PsiTreeUtil.getParentOfType; +import static org.jetbrains.uast.UastUtils.getOutermostQualified; +import static org.jetbrains.uast.UastUtils.getParentOfType; +import static org.jetbrains.uast.UastUtils.getQualifiedChain; +import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; import com.google.common.collect.Lists; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; + +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UDoWhileExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UField; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.ULocalVariable; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UQualifiedReferenceExpression; +import org.jetbrains.uast.UReturnExpression; +import org.jetbrains.uast.UUnaryExpression; +import org.jetbrains.uast.UVariable; +import org.jetbrains.uast.UWhileExpression; +import org.jetbrains.uast.UastCallKind; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; import java.util.Arrays; import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; - /** * Checks for missing {@code recycle} calls on resources that encourage it, and * for missing {@code commit} calls on FragmentTransactions, etc. */ -public class CleanupDetector extends Detector implements UastScanner { +public class CleanupDetector extends Detector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION = new Implementation( CleanupDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Problems with missing recycle calls */ public static final Issue RECYCLE_RESOURCE = Issue.create( @@ -74,6 +106,21 @@ public class CleanupDetector extends Detector implements UastScanner { Severity.WARNING, IMPLEMENTATION); + /** The main issue discovered by this detector */ + public static final Issue SHARED_PREF = Issue.create( + "CommitPrefEdits", //$NON-NLS-1$ + "Missing `commit()` on `SharedPreference` editor", + + "After calling `edit()` on a `SharedPreference`, you must call `commit()` " + + "or `apply()` on the editor to save the results.", + + Category.CORRECTNESS, + 6, + Severity.WARNING, + new Implementation( + CleanupDetector.class, + Scope.JAVA_FILE_SCOPE)); + // Target method names private static final String RECYCLE = "recycle"; //$NON-NLS-1$ private static final String RELEASE = "release"; //$NON-NLS-1$ @@ -86,17 +133,17 @@ public class CleanupDetector extends Detector implements UastScanner { private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$ private static final String BEGIN_TRANSACTION = "beginTransaction"; //$NON-NLS-1$ private static final String COMMIT = "commit"; //$NON-NLS-1$ + private static final String APPLY = "apply"; //$NON-NLS-1$ private static final String COMMIT_ALLOWING_LOSS = "commitAllowingStateLoss"; //$NON-NLS-1$ private static final String QUERY = "query"; //$NON-NLS-1$ private static final String RAW_QUERY = "rawQuery"; //$NON-NLS-1$ private static final String QUERY_WITH_FACTORY = "queryWithFactory"; //$NON-NLS-1$ private static final String RAW_QUERY_WITH_FACTORY = "rawQueryWithFactory"; //$NON-NLS-1$ private static final String CLOSE = "close"; //$NON-NLS-1$ + private static final String EDIT = "edit"; //$NON-NLS-1$ private static final String MOTION_EVENT_CLS = "android.view.MotionEvent"; //$NON-NLS-1$ - private static final String RESOURCES_CLS = "android.content.res.Resources"; //$NON-NLS-1$ private static final String PARCEL_CLS = "android.os.Parcel"; //$NON-NLS-1$ - private static final String TYPED_ARRAY_CLS = "android.content.res.TypedArray"; //$NON-NLS-1$ private static final String VELOCITY_TRACKER_CLS = "android.view.VelocityTracker";//$NON-NLS-1$ private static final String DIALOG_FRAGMENT = "android.app.DialogFragment"; //$NON-NLS-1$ private static final String DIALOG_V4_FRAGMENT = @@ -116,35 +163,43 @@ public class CleanupDetector extends Detector implements UastScanner { = "android.content.ContentProviderClient"; public static final String CONTENT_RESOLVER_CLS = "android.content.ContentResolver"; - public static final String CONTENT_PROVIDER_CLS = "android.content.ContentProvider"; + @SuppressWarnings("SpellCheckingInspection") public static final String SQLITE_DATABASE_CLS = "android.database.sqlite.SQLiteDatabase"; public static final String CURSOR_CLS = "android.database.Cursor"; + public static final String ANDROID_CONTENT_SHARED_PREFERENCES = + "android.content.SharedPreferences"; //$NON-NLS-1$ + private static final String ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR = + "android.content.SharedPreferences.Editor"; //$NON-NLS-1$ + /** Constructs a new {@link CleanupDetector} */ public CleanupDetector() { } // ---- Implements UastScanner ---- - + @Nullable @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Arrays.asList( - // FragmentManager commit check - BEGIN_TRANSACTION, + // FragmentManager commit check + BEGIN_TRANSACTION, - // Recycle check - OBTAIN, OBTAIN_NO_HISTORY, - OBTAIN_STYLED_ATTRIBUTES, - OBTAIN_ATTRIBUTES, - OBTAIN_TYPED_ARRAY, + // Recycle check + OBTAIN, OBTAIN_NO_HISTORY, + OBTAIN_STYLED_ATTRIBUTES, + OBTAIN_ATTRIBUTES, + OBTAIN_TYPED_ARRAY, - // Release check - ACQUIRE_CPC, + // Release check + ACQUIRE_CPC, - // Cursor close check - QUERY, RAW_QUERY, QUERY_WITH_FACTORY, RAW_QUERY_WITH_FACTORY + // Cursor close check + QUERY, RAW_QUERY, QUERY_WITH_FACTORY, RAW_QUERY_WITH_FACTORY, + + // SharedPreferences check + EDIT ); } @@ -155,62 +210,70 @@ public class CleanupDetector extends Detector implements UastScanner { } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - if (node.matchesFunctionName(BEGIN_TRANSACTION)) { - checkTransactionCommits(context, node); + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + String name = method.getName(); + if (BEGIN_TRANSACTION.equals(name)) { + checkTransactionCommits(context, call, method); + } else if (EDIT.equals(name)) { + checkEditorApplied(context, call, method); } else { - checkResourceRecycled(context, node, node.getFunctionName()); + checkResourceRecycled(context, call, method); } } @Override - public void visitConstructor(UastAndroidContext context, UCallExpression functionCall, UFunction constructor) { - UClass containingClass = UastUtils.getContainingClass(constructor); + public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod constructor) { + PsiClass containingClass = constructor.getContainingClass(); if (containingClass != null) { - checkRecycled(context, functionCall, containingClass.getFqName(), RELEASE); + String type = containingClass.getQualifiedName(); + if (type != null) { + checkRecycled(context, node, type, RELEASE); + } } } - private static void checkResourceRecycled(@NonNull UastAndroidContext context, - @NonNull UCallExpression node, @NonNull String name) { + private static void checkResourceRecycled(@NonNull JavaContext context, + @NonNull UCallExpression node, @NonNull PsiMethod method) { + String name = method.getName(); // Recycle detector - UFunction method = node.resolve(context); - if (method == null) { - return; - } - UClass containingClass = UastUtils.getContainingClass(method); + PsiClass containingClass = method.getContainingClass(); if (containingClass == null) { return; } - + JavaEvaluator evaluator = context.getEvaluator(); if ((OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) && - containingClass.isSubclassOf(MOTION_EVENT_CLS)) { + evaluator.extendsClass(containingClass, MOTION_EVENT_CLS, false)) { checkRecycled(context, node, MOTION_EVENT_CLS, RECYCLE); - } else if (OBTAIN.equals(name) && containingClass.isSubclassOf(PARCEL_CLS)) { + } else if (OBTAIN.equals(name) && evaluator.extendsClass(containingClass, PARCEL_CLS, false)) { checkRecycled(context, node, PARCEL_CLS, RECYCLE); } else if (OBTAIN.equals(name) && - containingClass.isSubclassOf(VELOCITY_TRACKER_CLS)) { + evaluator.extendsClass(containingClass, VELOCITY_TRACKER_CLS, false)) { checkRecycled(context, node, VELOCITY_TRACKER_CLS, RECYCLE); } else if ((OBTAIN_STYLED_ATTRIBUTES.equals(name) || OBTAIN_ATTRIBUTES.equals(name) || OBTAIN_TYPED_ARRAY.equals(name)) && - (containingClass.isSubclassOf(CLASS_CONTEXT) || - containingClass.isSubclassOf(RESOURCES_CLS))) { - UType returnType = method.getReturnType(); - if (returnType != null && returnType.matchesFqName(TYPED_ARRAY_CLS)) { - checkRecycled(context, node, TYPED_ARRAY_CLS, RECYCLE); + (evaluator.extendsClass(containingClass, CLASS_CONTEXT, false) || + evaluator.extendsClass(containingClass, SdkConstants.CLASS_RESOURCES, false))) { + PsiType returnType = method.getReturnType(); + if (returnType instanceof PsiClassType) { + PsiClass cls = ((PsiClassType)returnType).resolve(); + if (cls != null && "android.content.res.TypedArray".equals(cls.getQualifiedName())) { + checkRecycled(context, node, "android.content.res.TypedArray", RECYCLE); + } } - } else if (ACQUIRE_CPC.equals(name) && containingClass.isSubclassOf( - CONTENT_RESOLVER_CLS)) { + } else if (ACQUIRE_CPC.equals(name) && evaluator.extendsClass(containingClass, + CONTENT_RESOLVER_CLS, false)) { checkRecycled(context, node, CONTENT_PROVIDER_CLIENT_CLS, RELEASE); } else if ((QUERY.equals(name) || RAW_QUERY.equals(name) || QUERY_WITH_FACTORY.equals(name) || RAW_QUERY_WITH_FACTORY.equals(name)) - && (containingClass.isSubclassOf(SQLITE_DATABASE_CLS) || - containingClass.isSubclassOf(CONTENT_RESOLVER_CLS) || - containingClass.isSubclassOf(CONTENT_PROVIDER_CLS) || - containingClass.isSubclassOf(CONTENT_PROVIDER_CLIENT_CLS))) { + && (evaluator.extendsClass(containingClass, SQLITE_DATABASE_CLS, false) || + evaluator.extendsClass(containingClass, CONTENT_RESOLVER_CLS, false) || + evaluator.extendsClass(containingClass, CLASS_CONTENTPROVIDER, false) || + evaluator.extendsClass(containingClass, CONTENT_PROVIDER_CLIENT_CLS, false))) { // Other potential cursors-returning methods that should be tracked: // android.app.DownloadManager#query // android.content.ContentProviderClient#query @@ -229,34 +292,33 @@ public class CleanupDetector extends Detector implements UastScanner { } } - private static void checkRecycled(@NonNull final UastAndroidContext context, @NonNull UElement node, + private static void checkRecycled(@NonNull final JavaContext context, @NonNull UCallExpression node, @NonNull final String recycleType, @NonNull final String recycleName) { - UVariable boundVariable = getVariable(context, node); + PsiVariable boundVariable = getVariableElement(node); if (boundVariable == null) { return; } - UFunction method = UastUtils.getContainingFunction(node); + UMethod method = getParentOfType(node, UMethod.class, true); if (method == null) { return; } - FinishVisitor visitor = new FinishVisitor(context, boundVariable) { @Override protected boolean isCleanupCall(@NonNull UCallExpression call) { - if (!call.matchesFunctionName(recycleName)) { + String methodName = call.getMethodName(); + if (!recycleName.equals(methodName)) { return false; } - UDeclaration resolved = call.resolve(mContext); - if (resolved != null) { - UClass containingClass = UastUtils.getContainingClassOrEmpty(resolved); - if (containingClass.isSubclassOf(recycleType)) { + PsiMethod method = call.resolve(); + if (method != null) { + PsiClass containingClass = method.getContainingClass(); + if (mContext.getEvaluator().extendsClass(containingClass, recycleType, false)) { // Yes, called the right recycle() method; now make sure // we're calling it on the right variable - UElement parent = call.getParent(); - if (parent instanceof UQualifiedExpression) { - UExpression operand = ((UQualifiedExpression) parent).getReceiver(); - resolved = UastUtils.resolveIfCan(operand, mContext); + UExpression operand = call.getReceiver(); + if (operand instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) operand).resolve(); //noinspection SuspiciousMethodCalls if (resolved != null && mVariables.contains(resolved)) { return true; @@ -283,46 +345,56 @@ public class CleanupDetector extends Detector implements UastScanner { "This `%1$s` should be freed up after use with `#%2$s()`", className, recycleName); } + UElement locationNode = node instanceof UCallExpression ? - ((UCallExpression) node).getFunctionNameElement() : node; - Location location = context.getLocation(locationNode); + ((UCallExpression) node).getMethodIdentifier() : node; + if (locationNode == null) { + locationNode = node; + } + Location location = context.getUastLocation(locationNode); context.report(RECYCLE_RESOURCE, node, location, message); } - private static boolean checkTransactionCommits(@NonNull UastAndroidContext context, - @NonNull UCallExpression node) { - if (isBeginTransaction(context, node)) { - UVariable boundVariable = getVariable(context, node); + private static void checkTransactionCommits(@NonNull JavaContext context, + @NonNull UCallExpression node, @NonNull PsiMethod calledMethod) { + if (isBeginTransaction(context, calledMethod)) { + PsiVariable boundVariable = getVariableElement(node, true); if (boundVariable == null && isCommittedInChainedCalls(context, node)) { - return true; + return; } if (boundVariable != null) { - UFunction method = UastUtils.getContainingFunction(node); + UMethod method = getParentOfType(node, UMethod.class, true); if (method == null) { - return true; + return; } FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) { @Override protected boolean isCleanupCall(@NonNull UCallExpression call) { if (isTransactionCommitMethodCall(mContext, call)) { - UExpression operand = UastUtils.getReceiver(call); - if (operand instanceof UResolvable) { - UDeclaration resolved = ((UResolvable) operand).resolve(mContext); + List chain = getQualifiedChain(getOutermostQualified(call)); + if (chain.isEmpty()) { + return false; + } + + UExpression operand = chain.get(0); + if (operand != null) { + PsiElement resolved = UastUtils.tryResolve(operand); //noinspection SuspiciousMethodCalls if (resolved != null && mVariables.contains(resolved)) { return true; - } else if (resolved instanceof UFunction - && operand instanceof UCallExpression - && isCommittedInChainedCalls(mContext, (UCallExpression) operand)) { + } else if (resolved instanceof PsiMethod + && operand instanceof UCallExpression + && isCommittedInChainedCalls(mContext, + (UCallExpression) operand)) { // Check that the target of the committed chains is the // right variable! while (operand instanceof UCallExpression) { - operand = UastUtils.getReceiver((UCallExpression)operand); + operand = ((UCallExpression) operand).getReceiver(); } - if (operand instanceof USimpleReferenceExpression) { - resolved = ((USimpleReferenceExpression)operand).resolve(mContext); + if (operand instanceof UReferenceExpression) { + resolved = ((UReferenceExpression) operand).resolve(); //noinspection SuspiciousMethodCalls if (resolved != null && mVariables.contains(resolved)) { return true; @@ -334,7 +406,7 @@ public class CleanupDetector extends Detector implements UastScanner { List arguments = call.getValueArguments(); if (arguments.size() == 2) { UExpression first = arguments.get(0); - UDeclaration resolved = UastUtils.resolveIfCan(first, mContext); + PsiElement resolved = UastUtils.tryResolve(first); //noinspection SuspiciousMethodCalls if (resolved != null && mVariables.contains(resolved)) { return true; @@ -347,31 +419,29 @@ public class CleanupDetector extends Detector implements UastScanner { method.accept(commitVisitor); if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) { - return true; + return; } } String message = "This transaction should be completed with a `commit()` call"; - context.report(COMMIT_FRAGMENT, node, context.getLocation(node.getFunctionReference()), - message); + context.report(COMMIT_FRAGMENT, node, context.getUastNameLocation(node), message); } - return false; } - private static boolean isCommittedInChainedCalls(@NonNull UastAndroidContext context, + private static boolean isCommittedInChainedCalls(@NonNull JavaContext context, @NonNull UCallExpression node) { // Look for chained calls since the FragmentManager methods all return "this" // to allow constructor chaining, e.g. // getFragmentManager().beginTransaction().addToBackStack("test") // .disallowAddToBackStack().hide(mFragment2).setBreadCrumbShortTitle("test") // .show(mFragment2).setCustomAnimations(0, 0).commit(); - List chain = UastUtils.getQualifiedChain(node); - if (chain.size() > 0) { + List chain = getQualifiedChain(getOutermostQualified(node)); + if (!chain.isEmpty()) { UExpression lastExpression = chain.get(chain.size() - 1); if (lastExpression instanceof UCallExpression) { UCallExpression methodInvocation = (UCallExpression) lastExpression; if (isTransactionCommitMethodCall(context, methodInvocation) - || isShowFragmentMethodCall(context, methodInvocation)) { + || isShowFragmentMethodCall(context, methodInvocation)) { return true; } } @@ -380,80 +450,272 @@ public class CleanupDetector extends Detector implements UastScanner { return false; } - private static boolean isTransactionCommitMethodCall(@NonNull UastAndroidContext context, + private static boolean isTransactionCommitMethodCall(@NonNull JavaContext context, @NonNull UCallExpression call) { - return (call.matchesFunctionName(COMMIT) || call.matchesFunctionName(COMMIT_ALLOWING_LOSS)) && - isMethodOnFragmentClass(context, call, + String methodName = call.getMethodName(); + return (COMMIT.equals(methodName) || COMMIT_ALLOWING_LOSS.equals(methodName)) && + isMethodOnFragmentClass(context, call, FRAGMENT_TRANSACTION_CLS, - FRAGMENT_TRANSACTION_V4_CLS); + FRAGMENT_TRANSACTION_V4_CLS, + true); } - private static boolean isShowFragmentMethodCall(@NonNull UastAndroidContext context, + private static boolean isShowFragmentMethodCall(@NonNull JavaContext context, @NonNull UCallExpression call) { - return call.matchesFunctionName(SHOW) + String methodName = call.getMethodName(); + return SHOW.equals(methodName) && isMethodOnFragmentClass(context, call, - DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT); + DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT, true); } private static boolean isMethodOnFragmentClass( - @NonNull UastAndroidContext context, + @NonNull JavaContext context, @NonNull UCallExpression call, @NonNull String fragmentClass, - @NonNull String v4FragmentClass) { - UFunction resolved = call.resolve(context); - if (resolved != null) { - UClass containingClass = UastUtils.getContainingClassOrEmpty(resolved); - return containingClass.isSubclassOf(fragmentClass) || - containingClass.isSubclassOf(v4FragmentClass); + @NonNull String v4FragmentClass, + boolean returnForUnresolved) { + PsiMethod method = call.resolve(); + if (method != null) { + PsiClass containingClass = method.getContainingClass(); + JavaEvaluator evaluator = context.getEvaluator(); + return evaluator.extendsClass(containingClass, fragmentClass, false) || + evaluator.extendsClass(containingClass, v4FragmentClass, false); + } else { + // If we *can't* resolve the method call, caller can decide + // whether to consider the method called or not + return returnForUnresolved; + } + } + + private static void checkEditorApplied(@NonNull JavaContext context, + @NonNull UCallExpression node, @NonNull PsiMethod calledMethod) { + if (isSharedEditorCreation(context, calledMethod)) { + PsiVariable boundVariable = getVariableElement(node, true); + if (isEditorCommittedInChainedCalls(context, node)) { + return; + } + + if (boundVariable != null) { + UMethod method = getParentOfType(node, UMethod.class, true); + if (method == null) { + return; + } + + FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) { + @Override + protected boolean isCleanupCall(@NonNull UCallExpression call) { + if (isEditorApplyMethodCall(mContext, call) + || isEditorCommitMethodCall(mContext, call)) { + UExpression operand = call.getReceiver(); + if (operand != null) { + PsiElement resolved = UastUtils.tryResolve(operand); + //noinspection SuspiciousMethodCalls + if (resolved != null && mVariables.contains(resolved)) { + return true; + } else if (resolved instanceof PsiMethod + && operand instanceof UCallExpression + && isCommittedInChainedCalls(mContext, + (UCallExpression) operand)) { + // Check that the target of the committed chains is the + // right variable! + while (operand instanceof UCallExpression) { + operand = ((UCallExpression)operand).getReceiver(); + } + if (operand instanceof UReferenceExpression) { + resolved = ((UReferenceExpression) operand).resolve(); + //noinspection SuspiciousMethodCalls + if (resolved != null && mVariables.contains(resolved)) { + return true; + } + } + } + } + } + return false; + } + }; + + method.accept(commitVisitor); + if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) { + return; + } + } else if (UastUtils.getParentOfType(node, UReturnExpression.class) != null) { + // Allocation is in a return statement + return; + } + + String message = "`SharedPreferences.edit()` without a corresponding `commit()` or " + + "`apply()` call"; + context.report(SHARED_PREF, node, context.getUastLocation(node), message); + } + } + + private static boolean isSharedEditorCreation(@NonNull JavaContext context, + @NonNull PsiMethod method) { + String methodName = method.getName(); + if (EDIT.equals(methodName)) { + PsiClass containingClass = method.getContainingClass(); + JavaEvaluator evaluator = context.getEvaluator(); + return evaluator.extendsClass(containingClass, ANDROID_CONTENT_SHARED_PREFERENCES, + false); } return false; } - @Nullable - public static UVariable getVariable(@NonNull UastAndroidContext context, - @NonNull UElement expression) { - if (!(expression instanceof UExpression)) { - return null; - } - - UQualifiedExpression topMostQualified = UastUtils.findOutermostQualifiedExpression(((UExpression) expression)); - if (topMostQualified == null) { - return null; - } - - UElement parent = topMostQualified.getParent(); - - if (parent instanceof UBinaryExpression) { - UBinaryExpression binaryExpression = (UBinaryExpression) parent; - if (binaryExpression.getOperator() == UastBinaryOperator.ASSIGN) { - UExpression lhs = binaryExpression.getLeftOperand(); - if (lhs instanceof UResolvable) { - UDeclaration resolved = ((UResolvable) lhs).resolve(context); - if (resolved instanceof UVariable) { - return (UVariable) resolved; - } + private static boolean isEditorCommittedInChainedCalls(@NonNull JavaContext context, + @NonNull UCallExpression node) { + List chain = getQualifiedChain(getOutermostQualified(node)); + if (!chain.isEmpty()) { + UExpression lastExpression = chain.get(chain.size() - 1); + if (lastExpression instanceof UCallExpression) { + UCallExpression methodInvocation = (UCallExpression) lastExpression; + if (isEditorCommitMethodCall(context, methodInvocation) + || isEditorApplyMethodCall(context, methodInvocation)) { + return true; } } - } else if (parent instanceof UVariable) { - return (UVariable) parent; + } + + return false; + } + + private static boolean isEditorCommitMethodCall(@NonNull JavaContext context, + @NonNull UCallExpression call) { + String methodName = call.getMethodName(); + if (COMMIT.equals(methodName)) { + PsiMethod method = call.resolve(); + if (method != null) { + PsiClass containingClass = method.getContainingClass(); + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.extendsClass(containingClass, + ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR, false)) { + suggestApplyIfApplicable(context, call); + return true; + } + } + } + + return false; + } + + private static boolean isEditorApplyMethodCall(@NonNull JavaContext context, + @NonNull UCallExpression call) { + String methodName = call.getMethodName(); + if (APPLY.equals(methodName)) { + PsiMethod method = call.resolve(); + if (method != null) { + PsiClass containingClass = method.getContainingClass(); + JavaEvaluator evaluator = context.getEvaluator(); + return evaluator.extendsClass(containingClass, + ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR, false); + } + } + + return false; + } + + private static void suggestApplyIfApplicable(@NonNull JavaContext context, + @NonNull UCallExpression node) { + if (context.getProject().getMinSdkVersion().getApiLevel() >= 9) { + // See if the return value is read: can only replace commit with + // apply if the return value is not considered + + UElement qualifiedNode = node; + UElement parent = skipParentheses(node.getContainingElement()); + while (parent instanceof UReferenceExpression) { + qualifiedNode = parent; + parent = skipParentheses(parent.getContainingElement()); + } + boolean returnValueIgnored = true; + + if (parent instanceof UCallExpression + || parent instanceof UVariable + || parent instanceof UBinaryExpression + || parent instanceof UUnaryExpression + || parent instanceof UReturnExpression) { + returnValueIgnored = false; + } else if (parent instanceof UIfExpression) { + UExpression condition = ((UIfExpression) parent).getCondition(); + returnValueIgnored = !condition.equals(qualifiedNode); + } else if (parent instanceof UWhileExpression) { + UExpression condition = ((UWhileExpression) parent).getCondition(); + returnValueIgnored = !condition.equals(qualifiedNode); + } else if (parent instanceof UDoWhileExpression) { + UExpression condition = ((UDoWhileExpression) parent).getCondition(); + returnValueIgnored = !condition.equals(qualifiedNode); + } + + if (returnValueIgnored) { + String message = "Consider using `apply()` instead; `commit` writes " + + "its data to persistent storage immediately, whereas " + + "`apply` will handle it in the background"; + context.report(SHARED_PREF, node, context.getUastLocation(node), message); + } + } + } + + /** Returns the variable the expression is assigned to, if any */ + @Nullable + public static PsiVariable getVariableElement(@NonNull UCallExpression rhs) { + return getVariableElement(rhs, false); + } + + @Nullable + public static PsiVariable getVariableElement(@NonNull UCallExpression rhs, + boolean allowChainedCalls) { + UElement parent = skipParentheses( + UastUtils.getQualifiedParentOrThis(rhs).getContainingElement()); + + // Handle some types of chained calls; e.g. you might have + // var = prefs.edit().put(key,value) + // and here we want to skip past the put call + if (allowChainedCalls) { + while (true) { + if ((parent instanceof UQualifiedReferenceExpression)) { + UElement parentParent = skipParentheses(parent.getContainingElement()); + if ((parentParent instanceof UQualifiedReferenceExpression)) { + parent = skipParentheses(parentParent.getContainingElement()); + } else if (parentParent instanceof UVariable + || parentParent instanceof UBinaryExpression) { + parent = parentParent; + break; + } else { + break; + } + } else { + break; + } + } + } + + if (UastExpressionUtils.isAssignment(parent)) { + UBinaryExpression assignment = (UBinaryExpression) parent; + assert assignment != null; + UExpression lhs = assignment.getLeftOperand(); + if (lhs instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) lhs).resolve(); + if (resolved instanceof PsiVariable && !(resolved instanceof PsiField)) { + // e.g. local variable, parameter - but not a field + return ((PsiVariable) resolved); + } + } + } else if (parent instanceof UVariable && !(parent instanceof UField)) { + return ((UVariable) parent).getPsi(); } return null; } - private static boolean isBeginTransaction(@NonNull UastAndroidContext context, - @NonNull UCallExpression node) { - assert node.matchesFunctionName(BEGIN_TRANSACTION) : node.renderString(); - if (node.matchesFunctionName(BEGIN_TRANSACTION)) { - UFunction method = node.resolve(context); - if (method != null) { - UClass containingClass = UastUtils.getContainingClassOrEmpty(method); - if (containingClass.isSubclassOf(FRAGMENT_MANAGER_CLS) - || containingClass.isSubclassOf(FRAGMENT_MANAGER_V4_CLS)) { - return true; - } + private static boolean isBeginTransaction(@NonNull JavaContext context, @NonNull PsiMethod method) { + String methodName = method.getName(); + if (BEGIN_TRANSACTION.equals(methodName)) { + PsiClass containingClass = method.getContainingClass(); + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.extendsClass(containingClass, FRAGMENT_MANAGER_CLS, false) + || evaluator.extendsClass(containingClass, FRAGMENT_MANAGER_V4_CLS, false)) { + return true; } } @@ -467,14 +729,17 @@ public class CleanupDetector extends Detector implements UastScanner { * case of a database cursor we're looking for a "close" call, etc. */ private abstract static class FinishVisitor extends AbstractUastVisitor { - protected final UastAndroidContext mContext; - protected final List mVariables; + protected final JavaContext mContext; + protected final List mVariables; + private final PsiVariable mOriginalVariableNode; + private boolean mContainsCleanup; private boolean mEscapes; - public FinishVisitor(UastAndroidContext context, @NonNull UVariable variable) { + public FinishVisitor(JavaContext context, @NonNull PsiVariable variableNode) { mContext = context; - mVariables = Lists.newArrayList(variable); + mOriginalVariableNode = variableNode; + mVariables = Lists.newArrayList(variableNode); } public boolean isCleanedUp() { @@ -486,37 +751,42 @@ public class CleanupDetector extends Detector implements UastScanner { } @Override - public boolean visitElement(@NotNull UElement node) { + public boolean visitElement(UElement node) { return mContainsCleanup || super.visitElement(node); } protected abstract boolean isCleanupCall(@NonNull UCallExpression call); @Override - public boolean visitCallExpression(@NotNull UCallExpression call) { - if (mContainsCleanup) { - return true; + public boolean visitCallExpression(UCallExpression node) { + if (node.getKind() == UastCallKind.METHOD_CALL) { + visitMethodCallExpression(node); } + return super.visitCallExpression(node); + } + private void visitMethodCallExpression(UCallExpression call) { + if (mContainsCleanup) { + return; + } + // Look for escapes if (!mEscapes) { for (UExpression expression : call.getValueArguments()) { - if (expression instanceof USimpleReferenceExpression) { - UDeclaration resolved = ((USimpleReferenceExpression) expression).resolve(mContext); + if (expression instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) expression).resolve(); //noinspection SuspiciousMethodCalls if (resolved != null && mVariables.contains(resolved)) { + boolean wasEscaped = mEscapes; mEscapes = true; // Special case: MotionEvent.obtain(MotionEvent): passing in an // event here does not recycle the event, and we also know it // doesn't escape - if (OBTAIN.equals(call.getFunctionName())) { - UFunction method = call.resolve(mContext); - if (method != null) { - UClass cls = UastUtils.getContainingClassOrEmpty(method); - if (cls.matchesFqName(MOTION_EVENT_CLS)) { - mEscapes = false; - } + if (OBTAIN.equals(call.getMethodName())) { + PsiMethod method = call.resolve(); + if (JavaEvaluator.isMemberInClass(method, MOTION_EVENT_CLS)) { + mEscapes = wasEscaped; } } } @@ -526,66 +796,78 @@ public class CleanupDetector extends Detector implements UastScanner { if (isCleanupCall(call)) { mContainsCleanup = true; - return true; - } else { - return false; } } @Override - public boolean visitVariable(@NotNull UVariable node) { - UExpression initializer = node.getInitializer(); - if (initializer instanceof USimpleReferenceExpression) { - UDeclaration resolved = ((USimpleReferenceExpression) initializer).resolve(mContext); + public boolean visitVariable(UVariable variable) { + if (variable instanceof ULocalVariable) { + UExpression initializer = variable.getUastInitializer(); + if (initializer instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) initializer).resolve(); + //noinspection SuspiciousMethodCalls + if (resolved != null && mVariables.contains(resolved)) { + mVariables.add(variable.getPsi()); + } + } + } + + return super.visitVariable(variable); + } + + @Override + public boolean visitBinaryExpression(UBinaryExpression expression) { + if (!UastExpressionUtils.isAssignment(expression)) { + return super.visitBinaryExpression(expression); + } + + // TEMPORARILY DISABLED; see testDatabaseCursorReassignment + // This can result in some false positives right now. Play it + // safe instead. + boolean clearLhs = false; + + UExpression rhs = expression.getRightOperand(); + if (rhs instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) rhs).resolve(); //noinspection SuspiciousMethodCalls if (resolved != null && mVariables.contains(resolved)) { - if (node.getKind() == UastVariableKind.LOCAL_VARIABLE) { - mVariables.add(node); - } else if (node.getKind() == UastVariableKind.MEMBER) { + clearLhs = false; + PsiElement lhs = UastUtils.tryResolve(expression.getLeftOperand()); + if (lhs instanceof PsiLocalVariable) { + mVariables.add(((PsiLocalVariable) lhs)); + } else if (lhs instanceof PsiField) { mEscapes = true; } } } - return false; - } - @Override - public boolean visitBinaryExpression(@NotNull UBinaryExpression node) { - if (node.getOperator() == UastBinaryOperator.ASSIGN) { - UExpression rhs = node.getRightOperand(); - if (rhs instanceof USimpleReferenceExpression) { - UDeclaration resolved = ((USimpleReferenceExpression) rhs).resolve(mContext); - UExpression leftOperand = node.getLeftOperand(); + //noinspection ConstantConditions + if (clearLhs) { + // If we reassign one of the variables, clear it out + PsiElement lhs = UastUtils.tryResolve(expression.getLeftOperand()); + //noinspection SuspiciousMethodCalls + if (lhs != null && !lhs.equals(mOriginalVariableNode) + && mVariables.contains(lhs)) { //noinspection SuspiciousMethodCalls - if (resolved != null && mVariables.contains(resolved) && leftOperand instanceof UResolvable) { - UDeclaration resolvedLhs = ((UResolvable) leftOperand).resolve(mContext); - if (resolvedLhs instanceof UVariable) { - UVariable variable = (UVariable) resolvedLhs; - if (variable.getKind() == UastVariableKind.LOCAL_VARIABLE) { - mVariables.add(variable); - } else if (variable.getKind() == UastVariableKind.MEMBER) { - mEscapes = true; - } - } - } + mVariables.remove(lhs); } } - - return false; + + return super.visitBinaryExpression(expression); } @Override - public boolean visitReturnExpression(@NotNull UReturnExpression node) { - UExpression value = node.getReturnExpression(); - if (value instanceof USimpleReferenceExpression) { - UDeclaration resolved = ((USimpleReferenceExpression) value).resolve(mContext); + public boolean visitReturnExpression(UReturnExpression node) { + UExpression returnValue = node.getReturnExpression(); + if (returnValue instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) returnValue).resolve(); //noinspection SuspiciousMethodCalls if (resolved != null && mVariables.contains(resolved)) { mEscapes = true; } } - return false; + return super.visitReturnExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CommentDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CommentDetector.java index d20ce344b35..85937739a71 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CommentDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CommentDetector.java @@ -17,28 +17,33 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiLiteralExpression; -import java.io.File; - -import org.jetbrains.uast.check.UastScanner; +import java.util.Collections; +import java.util.List; /** * Looks for issues in Java comments */ -public class CommentDetector extends Detector implements UastScanner { +public class CommentDetector extends Detector implements JavaPsiScanner { private static final String STOPSHIP_COMMENT = "STOPSHIP"; //$NON-NLS-1$ private static final Implementation IMPLEMENTATION = new Implementation( CommentDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Looks for hidden code */ public static final Issue EASTER_EGG = Issue.create( @@ -69,7 +74,7 @@ public class CommentDetector extends Detector implements UastScanner { private static final String ESCAPE_STRING = "\\u002a\\u002f"; //$NON-NLS-1$ - /** Lombok's AST only passes comment nodes for Javadoc so I need to do manual token scanning + /** The current AST only passes comment nodes for Javadoc so I need to do manual token scanning instead */ private static final boolean USE_AST = false; @@ -79,27 +84,17 @@ public class CommentDetector extends Detector implements UastScanner { } @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - - /*@Override - public List> getApplicableNodeTypes() { + public List> getApplicablePsiTypes() { if (USE_AST) { - return Collections.>singletonList(Comment.class); + return Collections.>singletonList( + PsiLiteralExpression.class); } else { return null; } } @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { + public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) { // Lombok does not generate comment nodes for block and line comments, only for // javadoc comments! if (USE_AST) { @@ -124,15 +119,15 @@ public class CommentDetector extends Detector implements UastScanner { if (end == -1) { end = n; } - checkComment(context, source, 0, start, end); + checkComment(context, null, source, 0, start, end); } else if (next == '*') { // Block comment int start = i + 2; - int end = source.indexOf("*//*", start); + int end = source.indexOf("*/", start); if (end == -1) { end = n; } - checkComment(context, source, 0, start, end); + checkComment(context, null, source, 0, start, end); } } } @@ -140,7 +135,7 @@ public class CommentDetector extends Detector implements UastScanner { } } - private static class CommentChecker extends ForwardingAstVisitor { + private static class CommentChecker extends JavaElementVisitor { private final JavaContext mContext; public CommentChecker(JavaContext context) { @@ -148,15 +143,16 @@ public class CommentDetector extends Detector implements UastScanner { } @Override - public boolean visitComment(Comment node) { - String contents = node.astContent(); - checkComment(mContext, contents, node.getPosition().getStart(), 0, contents.length()); - return super.visitComment(node); + public void visitComment(PsiComment comment) { + String contents = comment.getText(); + checkComment(mContext, comment, contents, comment.getTextRange().getStartOffset(), 0, + contents.length()); } } private static void checkComment( - @NonNull Context context, + @NonNull JavaContext context, + @Nullable PsiComment node, @NonNull String source, int offset, int start, @@ -171,7 +167,7 @@ public class CommentDetector extends Detector implements UastScanner { 0, ESCAPE_STRING.length())) { Location location = Location.create(context.file, source, offset + i - 1, offset + i - 1 + ESCAPE_STRING.length()); - context.report(EASTER_EGG, location, + context.report(EASTER_EGG, node, location, "Code might be hidden here; found unicode escape sequence " + "which is interpreted as comment end, compiled code follows"); } @@ -183,10 +179,10 @@ public class CommentDetector extends Detector implements UastScanner { // TODO: Only flag this issue in release mode?? Location location = Location.create(context.file, source, offset + i - 1, offset + i - 1 + STOPSHIP_COMMENT.length()); - context.report(STOP_SHIP, location, + context.report(STOP_SHIP, node, location, "`STOPSHIP` comment found; points to code which must be fixed prior " + "to release"); } } - }*/ + } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ControlFlowGraph.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ControlFlowGraph.java index 349d3b1456a..e7d83dff2f7 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ControlFlowGraph.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ControlFlowGraph.java @@ -21,24 +21,24 @@ import com.android.annotations.Nullable; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -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.FieldInsnNode; -import org.jetbrains.org.objectweb.asm.tree.FrameNode; -import org.jetbrains.org.objectweb.asm.tree.InsnList; -import org.jetbrains.org.objectweb.asm.tree.IntInsnNode; -import org.jetbrains.org.objectweb.asm.tree.JumpInsnNode; -import org.jetbrains.org.objectweb.asm.tree.LabelNode; -import org.jetbrains.org.objectweb.asm.tree.LdcInsnNode; -import org.jetbrains.org.objectweb.asm.tree.LineNumberNode; -import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode; -import org.jetbrains.org.objectweb.asm.tree.MethodNode; -import org.jetbrains.org.objectweb.asm.tree.TryCatchBlockNode; -import org.jetbrains.org.objectweb.asm.tree.TypeInsnNode; -import org.jetbrains.org.objectweb.asm.tree.analysis.Analyzer; -import org.jetbrains.org.objectweb.asm.tree.analysis.AnalyzerException; -import org.jetbrains.org.objectweb.asm.tree.analysis.BasicInterpreter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FrameNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TryCatchBlockNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicInterpreter; import java.lang.reflect.Field; import java.util.ArrayList; @@ -46,7 +46,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -//import org.jetbrains.org.objectweb.asm.util.Printer; +//import org.objectweb.asm.util.Printer; /** * A {@linkplain ControlFlowGraph} is a graph containing a node for each @@ -284,7 +284,10 @@ public class ControlFlowGraph { AbstractInsnNode curr = start; Node handlerNode = getNode(tcb.handler); while (curr != end && curr != null) { - if (curr.getType() == AbstractInsnNode.METHOD_INSN) { + // A method can throw can exception, or a throw instruction directly + if (curr.getType() == AbstractInsnNode.METHOD_INSN + || (curr.getType() == AbstractInsnNode.INSN + && curr.getOpcode() == Opcodes.ATHROW)) { // Method call; add exception edge to handler if (tcb.type == null) { // finally block: not an exception path @@ -388,6 +391,7 @@ public class ControlFlowGraph { * @return a dot description of this control flow graph, * useful for debugging */ + @SuppressWarnings("unused") public String toDot(@Nullable Set highlight) { StringBuilder sb = new StringBuilder(); sb.append("digraph G {\n"); diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CustomViewDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CustomViewDetector.java index ea907d12e11..de8c5ee9529 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CustomViewDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CustomViewDetector.java @@ -16,38 +16,44 @@ package com.android.tools.klint.checks; +import static com.android.SdkConstants.CLASS_CONTEXT; import static com.android.SdkConstants.CLASS_VIEW; import static com.android.SdkConstants.CLASS_VIEWGROUP; import static com.android.SdkConstants.DOT_LAYOUT_PARAMS; import static com.android.SdkConstants.R_STYLEABLE_PREFIX; +import static com.android.tools.klint.detector.api.LintUtils.skipParentheses; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiClass; +import com.intellij.psi.util.PsiTreeUtil; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; -import java.io.File; import java.util.Collections; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Makes sure that custom views use a declare styleable that matches * the name of the custom view */ -public class CustomViewDetector extends Detector implements UastScanner { +public class CustomViewDetector extends Detector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION = new Implementation( CustomViewDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Mismatched style and class names */ public static final Issue ISSUE = Issue.create( @@ -72,74 +78,62 @@ public class CustomViewDetector extends Detector implements UastScanner { public CustomViewDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList(OBTAIN_STYLED_ATTRIBUTES); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - if (node.getParent() instanceof UExpression) { - if (!context.getLintContext().isContextMethod(node)) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + if (skipParentheses(node.getContainingElement()) instanceof UExpression) { + if (!context.getEvaluator().isMemberInSubClassOf(method, CLASS_CONTEXT, false)) { return; } - List expressions = node.getValueArguments(); - int size = expressions.size(); + List arguments = node.getValueArguments(); + int size = arguments.size(); // Which parameter contains the styleable (attrs) ? int parameterIndex; if (size == 1) { // obtainStyledAttributes(int[] attrs) parameterIndex = 0; - } else if (size > 1) { + } else { // obtainStyledAttributes(int resid, int[] attrs) // obtainStyledAttributes(AttributeSet set, int[] attrs) // obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) parameterIndex = 1; - } else { + } + UExpression expression = arguments.get(parameterIndex); + if (!UastUtils.startsWithQualified(expression, R_STYLEABLE_PREFIX)) { return; } - UExpression expression = expressions.get(parameterIndex); - if (!(expression instanceof UQualifiedExpression)) { + List path = UastUtils.asQualifiedPath(expression); + if (path == null || path.size() < 3) { return; } - String s = expression.renderString(); - if (!s.startsWith(R_STYLEABLE_PREFIX)) { - return; - } - String styleableName = s.substring(R_STYLEABLE_PREFIX.length()); - - UClass resolvedClass = UastUtils.getContainingClass(node); - if (resolvedClass == null) { + String styleableName = path.get(2); + UClass cls = UastUtils.getParentOfType(node, UClass.class, false); + if (cls == null) { return; } - String className = resolvedClass.getName(); - if (resolvedClass.isSubclassOf(CLASS_VIEW)) { + String className = cls.getName(); + if (context.getEvaluator().extendsClass(cls, CLASS_VIEW, false)) { if (!styleableName.equals(className)) { String message = String.format( - "By convention, the custom view (`%1$s`) and the declare-styleable (`%2$s`) " - + "should have the same name (various editor features rely on " - + "this convention)", - className, styleableName); - context.report(ISSUE, node, context.getLocation(expression), message); + "By convention, the custom view (`%1$s`) and the declare-styleable (`%2$s`) " + + "should have the same name (various editor features rely on " + + "this convention)", + className, styleableName); + context.report(ISSUE, node, context.getUastLocation(expression), message); } - } else if (resolvedClass.isSubclassOf(CLASS_VIEWGROUP + DOT_LAYOUT_PARAMS)) { - UClass outer = UastUtils.getContainingClass(resolvedClass); + } else if (context.getEvaluator().extendsClass(cls, + CLASS_VIEWGROUP + DOT_LAYOUT_PARAMS, false)) { + PsiClass outer = PsiTreeUtil.getParentOfType(cls, PsiClass.class, true); if (outer == null) { return; } @@ -147,12 +141,12 @@ public class CustomViewDetector extends Detector implements UastScanner { String expectedName = layoutClassName + "_Layout"; if (!styleableName.equals(expectedName)) { String message = String.format( - "By convention, the declare-styleable (`%1$s`) for a layout parameter " - + "class (`%2$s`) is expected to be the surrounding " - + "class (`%3$s`) plus \"`_Layout`\", e.g. `%4$s`. " - + "(Various editor features rely on this convention.)", - styleableName, className, layoutClassName, expectedName); - context.report(ISSUE, node, context.getLocation(expression), message); + "By convention, the declare-styleable (`%1$s`) for a layout parameter " + + "class (`%2$s`) is expected to be the surrounding " + + "class (`%3$s`) plus \"`_Layout`\", e.g. `%4$s`. " + + "(Various editor features rely on this convention.)", + styleableName, className, layoutClassName, expectedName); + context.report(ISSUE, node, context.getUastLocation(expression), message); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CutPasteDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CutPasteDetector.java index c7ef9e5545e..740f8cb27fb 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CutPasteDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/CutPasteDetector.java @@ -17,34 +17,51 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.RESOURCE_CLZ_ID; +import static com.android.tools.klint.detector.api.LintUtils.skipParentheses; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; import com.google.common.collect.Maps; +import com.intellij.psi.PsiBreakStatement; +import com.intellij.psi.PsiContinueStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiIfStatement; +import com.intellij.psi.PsiJavaToken; +import com.intellij.psi.PsiLoopStatement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiReturnStatement; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiWhiteSpace; + +import org.jetbrains.uast.UArrayAccessExpression; +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.ULocalVariable; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UQualifiedReferenceExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.UastVisitor; -import java.io.File; import java.util.Collections; import java.util.List; import java.util.Map; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; - /** - * Detector looking for cut & paste issues + * Detector looking for cut & paste issues */ -public class CutPasteDetector extends Detector implements UastScanner { +public class CutPasteDetector extends Detector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "CutPasteId", //$NON-NLS-1$ @@ -62,9 +79,9 @@ public class CutPasteDetector extends Detector implements UastScanner { Severity.WARNING, new Implementation( CutPasteDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); - private UFunction mLastMethod; + private PsiMethod mLastMethod; private Map mIds; private Map mLhs; private Map mCallOperands; @@ -73,50 +90,49 @@ public class CutPasteDetector extends Detector implements UastScanner { public CutPasteDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - // ---- Implements UastScanner ---- - @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("findViewById"); //$NON-NLS-1$ } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - String lhs = getLhs(node); + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod calledMethod) { + String lhs = getLhs(call); if (lhs == null) { return; } - UFunction method = UastUtils.getContainingFunction(node); + UMethod method = UastUtils.getParentOfType(call, UMethod.class, false); if (method == null) { - return; + return; // prevent doing the same work for multiple findViewById calls in same method } else if (method != mLastMethod) { mIds = Maps.newHashMap(); mLhs = Maps.newHashMap(); mCallOperands = Maps.newHashMap(); mLastMethod = method; } + + String callOperand = call.getReceiver() != null + ? call.getReceiver().asSourceString() : ""; - UElement parent = node.getParent(); - String callOperand = ""; - if (parent instanceof UQualifiedExpression) { - callOperand = ((UQualifiedExpression)parent).getReceiver().renderString(); + List arguments = call.getValueArguments(); + if (arguments.isEmpty()) { + return; } - - UExpression first = node.getValueArguments().get(0); - if (first instanceof UQualifiedExpression) { - UQualifiedExpression select = (UQualifiedExpression) first; - String id = select.getSelector().renderString(); - UExpression operand = select.getReceiver(); - if (operand instanceof UQualifiedExpression) { - UQualifiedExpression type = (UQualifiedExpression) operand; - if (type.getSelector().renderString().equals(RESOURCE_CLZ_ID)) { + UExpression first = arguments.get(0); + if (first instanceof UReferenceExpression) { + UReferenceExpression psiReferenceExpression = (UReferenceExpression) first; + String id = psiReferenceExpression.getResolvedName(); + UElement operand = (first instanceof UQualifiedReferenceExpression) + ? ((UQualifiedReferenceExpression) first).getReceiver() + : null; + + if (operand instanceof UReferenceExpression) { + UReferenceExpression type = (UReferenceExpression) operand; + if (RESOURCE_CLZ_ID.equals(type.getResolvedName())) { if (mIds.containsKey(id)) { if (lhs.equals(mLhs.get(id))) { return; @@ -125,113 +141,161 @@ public class CutPasteDetector extends Detector implements UastScanner { return; } UCallExpression earlierCall = mIds.get(id); - if (!isReachableFrom(method, earlierCall, node)) { + if (!isReachableFrom(method, earlierCall, call)) { return; } - Location location = context.getLocation(node); - Location secondary = context.getLocation(earlierCall); - if (location != null && secondary != null) { - secondary.setMessage("First usage here"); - location.setSecondary(secondary); - context.report(ISSUE, node, location, String.format( - "The id `%1$s` has already been looked up in this method; possible " + - "cut & paste error?", first.toString())); - } + Location location = context.getUastLocation(call); + Location secondary = context.getUastLocation(earlierCall); + secondary.setMessage("First usage here"); + location.setSecondary(secondary); + context.report(ISSUE, call, location, String.format( + "The id `%1$s` has already been looked up in this method; possible " + + + "cut & paste error?", first.asSourceString())); } else { - mIds.put(id, node); + mIds.put(id, call); mLhs.put(id, lhs); mCallOperands.put(id, callOperand); } } + } } - } - + @Nullable private static String getLhs(@NonNull UCallExpression call) { - UElement parent = call.getParent(); - if (UastBinaryExpressionWithTypeUtils.isTypeCast(parent)) { - assert parent != null; - parent = parent.getParent(); + UElement parent = skipParentheses(call.getContainingElement()); + if (parent != null && UastExpressionUtils.isTypeCast(parent)) { + parent = parent.getContainingElement(); } - if (parent instanceof UVariable) { - UVariable vde = (UVariable) parent; - return vde.getName(); + if (parent instanceof ULocalVariable) { + return ((ULocalVariable) parent).getName(); } else if (parent instanceof UBinaryExpression) { UBinaryExpression be = (UBinaryExpression) parent; UExpression left = be.getLeftOperand(); - if (left instanceof USimpleReferenceExpression || left instanceof UQualifiedExpression) { - return be.getLeftOperand().toString(); + if (left instanceof UReferenceExpression) { + return left.asRenderString(); } else if (left instanceof UArrayAccessExpression) { UArrayAccessExpression aa = (UArrayAccessExpression) left; - return aa.getReceiver().toString(); + return aa.getReceiver().asSourceString(); + } + } else if (UastExpressionUtils.isAssignment(parent)) { + //noinspection ConstantConditions + UExpression left = ((UBinaryExpression) parent).getLeftOperand(); + if (left instanceof UReferenceExpression) { + return left.asSourceString(); + } else if (left instanceof UArrayAccessExpression) { + UArrayAccessExpression aa = (UArrayAccessExpression) left; + return aa.getReceiver().asSourceString(); } } return null; } - private static boolean isReachableFrom( - @NonNull UElement method, - @NonNull UCallExpression from, - @NonNull UCallExpression to) { - ReachableVisitor visitor = new ReachableVisitor(from, to); - method.accept(visitor); - return visitor.isReachable(); + static boolean isReachableFrom( + @NonNull PsiMethod method, + @NonNull PsiElement from, + @NonNull PsiElement to) { + PsiElement prev = from; + PsiElement curr = next(method, from, to, null); + //noinspection ConstantConditions + while (curr != null) { + if (containsElement(method, curr, to)) { + return true; + } + curr = next(method, curr, to, prev); + prev = curr; + } + + return false; } - private static class ReachableVisitor extends AbstractUastVisitor { - @NonNull private final UCallExpression mFrom; - @NonNull private final UCallExpression mTo; - private boolean mReachable; - private boolean mSeenEnd; + static boolean isReachableFrom( + @NonNull PsiMethod method, + @NonNull UElement from, + @NonNull UElement to) { + //TODO + return false; + } - public ReachableVisitor(@NonNull UCallExpression from, @NonNull UCallExpression to) { - mFrom = from; - mTo = to; + @Nullable + static PsiElement next( + @NonNull PsiMethod method, + @NonNull PsiElement curr, + @NonNull PsiElement target, + @Nullable PsiElement prev) { + + if (curr instanceof PsiMethod) { + return null; } - boolean isReachable() { - return mReachable; - } - - @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - if (node == mFrom) { - mReachable = true; - } else if (node == mTo) { - mSeenEnd = true; - + PsiElement parent = curr.getParent(); + if (curr instanceof PsiContinueStatement) { + PsiStatement continuedStatement = ((PsiContinueStatement) curr) + .findContinuedStatement(); + if (continuedStatement != null) { + if (containsElement(method, continuedStatement, target)) { + return target; + } + return next(method, continuedStatement, target, curr); + } else { + return next(method, parent, target, curr); + } + } else if (curr instanceof PsiBreakStatement) { + PsiStatement exitedStatement = ((PsiBreakStatement) curr).findExitedStatement(); + if (exitedStatement != null) { + return next(method, exitedStatement, target, curr); + } else { + return next(method, parent, target, curr); + } + } else if (curr instanceof PsiReturnStatement) { + return null; + } else if (curr instanceof PsiLoopStatement && prev != null && + containsElement(method, curr, prev)) { + // If we stepped *up* (from a last child nested in the loop) up to the loop + // itself, mark all children in the loop as reachable since we're iterating + if (containsElement(method, curr, target)) { + return target; } - return false; } - @Override - public boolean visitIfExpression(@NotNull UIfExpression node) { - UExpression condition = node.getCondition(); - UExpression body = node.getThenBranch(); - UElement elseBody = node.getElseBranch(); - condition.accept(this); + PsiElement sibling = curr.getNextSibling(); + while (sibling instanceof PsiWhiteSpace || sibling instanceof PsiJavaToken) { + // Skip whitespaces and tokens such as PsiJavaToken.SEMICOLON etc + sibling = sibling.getNextSibling(); + } + if (sibling == null) { + return next(method, parent, target, curr); + } - if (body != null) { - boolean wasReachable = mReachable; - body.accept(this); - mReachable = wasReachable; + if (parent instanceof PsiIfStatement && + curr == ((PsiIfStatement)parent).getThenBranch()) { + return next(method, parent, target, curr); + } else if (parent instanceof PsiLoopStatement) { + if (containsElement(method, parent, target)) { + return target; } - if (elseBody != null) { - boolean wasReachable = mReachable; - elseBody.accept(this); - mReachable = wasReachable; + } + + return sibling; + } + + private static boolean containsElement( + @NonNull PsiMethod method, + @NonNull PsiElement root, + @NonNull PsiElement element) { + //noinspection ConstantConditions + while (element != null && element != method) { + if (root.equals(element)) { + return true; } - return false; + element = element.getParent(); } - @Override - public boolean visitElement(@NotNull UElement node) { - return mSeenEnd; - } + return false; } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/DateFormatDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/DateFormatDetector.java index e602b67f50c..b5bc84eff0d 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/DateFormatDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/DateFormatDetector.java @@ -22,28 +22,30 @@ import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; import java.util.Collections; import java.util.List; -import org.jetbrains.uast.UFunction; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UType; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Checks for errors related to Date Formats */ -public class DateFormatDetector extends Detector implements UastScanner { +public class DateFormatDetector extends Detector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION = new Implementation( DateFormatDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Constructing SimpleDateFormat without an explicit locale */ public static final Issue DATE_FORMAT = Issue.create( @@ -75,12 +77,6 @@ public class DateFormatDetector extends Detector implements UastScanner { public DateFormatDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- @Nullable @@ -90,24 +86,28 @@ public class DateFormatDetector extends Detector implements UastScanner { } @Override - public void visitConstructor(UastAndroidContext context, UCallExpression functionCall, UFunction constructor) { + public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod constructor) { if (!specifiesLocale(constructor)) { - Location location = context.getLocation(functionCall); + Location location = context.getUastLocation(node); String message = - "To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, " + - "or `getTimeInstance()`, or use `new SimpleDateFormat(String template, " + - "Locale locale)` with for example `Locale.US` for ASCII dates."; - context.report(DATE_FORMAT, functionCall, location, message); + "To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, " + + "or `getTimeInstance()`, or use `new SimpleDateFormat(String template, " + + "Locale locale)` with for example `Locale.US` for ASCII dates."; + context.report(DATE_FORMAT, node, location, message); } } - private static boolean specifiesLocale(@NonNull UFunction method) { - for (int i = 0, n = method.getValueParameterCount(); i < n; i++) { - UType parameterType = method.getValueParameters().get(i).getType(); - if (parameterType.matchesFqName(LOCALE_CLS)) { - return true; + private static boolean specifiesLocale(@NonNull PsiMethod method) { + PsiParameterList parameterList = method.getParameterList(); + PsiParameter[] parameters = parameterList.getParameters(); + for (PsiParameter parameter : parameters) { + PsiType type = parameter.getType(); + if (type.getCanonicalText().equals(LOCALE_CLS)) { + return true; } } + return false; } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/FragmentDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/FragmentDetector.java index 860cc7b53c4..79d9756830d 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/FragmentDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/FragmentDetector.java @@ -20,22 +20,27 @@ import static com.android.SdkConstants.CLASS_FRAGMENT; import static com.android.SdkConstants.CLASS_V4_FRAGMENT; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiAnonymousClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UAnonymousClass; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; import java.util.Arrays; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Checks that Fragment subclasses can be instantiated via * {link {@link Class#newInstance()}}: the class is public, static, and has @@ -45,7 +50,7 @@ import org.jetbrains.uast.check.UastScanner; * http://stackoverflow.com/questions/8058809/fragment-activity-crashes-on-screen-rotate * (and countless duplicates) */ -public class FragmentDetector extends Detector implements UastScanner { +public class FragmentDetector extends Detector implements Detector.UastScanner { /** Are fragment subclasses instantiatable? */ public static final Issue ISSUE = Issue.create( "ValidFragment", //$NON-NLS-1$ @@ -64,7 +69,7 @@ public class FragmentDetector extends Detector implements UastScanner { Severity.FATAL, new Implementation( FragmentDetector.class, - Scope.SOURCE_FILE_SCOPE) + Scope.JAVA_FILE_SCOPE) ).addMoreInfo( "http://developer.android.com/reference/android/app/Fragment.html#Fragment()"); //$NON-NLS-1$ @@ -73,83 +78,74 @@ public class FragmentDetector extends Detector implements UastScanner { public FragmentDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- - + @Nullable @Override - public List getApplicableSuperClasses() { + public List applicableSuperClasses() { return Arrays.asList(CLASS_FRAGMENT, CLASS_V4_FRAGMENT); } - private boolean isAllParametersWithDefaultValue(UFunction function) { - List parameters = function.getValueParameters(); - for (UVariable parameter : parameters) { - if (parameter.getInitializer() == null) { - return false; - } - } - return true; - } - @Override - public void visitClass(UastAndroidContext context, UClass cls) { - if (cls.hasModifier(UastModifier.ABSTRACT) || cls.getKind() != UastClassKind.CLASS) { + public void checkClass(@NonNull JavaContext context, @NonNull UClass node) { + if (node instanceof UAnonymousClass) { + String message = "Fragments should be static such that they can be re-instantiated by " + + "the system, and anonymous classes are not static"; + context.reportUast(ISSUE, node, context.getUastNameLocation(node), message); return; } - if (!cls.getVisibility().isPublic()) { + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.isAbstract(node)) { + return; + } + + if (!evaluator.isPublic(node)) { String message = String.format("This fragment class should be public (%1$s)", - cls.getFqName()); - context.report(ISSUE, cls, context.getLocation(cls.getNameElement()), message); + node.getQualifiedName()); + context.reportUast(ISSUE, node, context.getUastNameLocation(node), message); return; } - if (UastUtils.getContainingClass(cls) != null && !cls.hasModifier(UastModifier.STATIC)) { + if (node.getContainingClass() != null && !evaluator.isStatic(node)) { String message = String.format( - "This fragment inner class should be static (%1$s)", cls.getName()); - context.report(ISSUE, cls, context.getLocation(cls.getNameElement()), message); + "This fragment inner class should be static (%1$s)", node.getQualifiedName()); + context.reportUast(ISSUE, node, context.getUastNameLocation(node), message); return; } boolean hasDefaultConstructor = false; boolean hasConstructor = false; - - for (UFunction constructor : cls.getConstructors()) { + for (PsiMethod constructor : node.getConstructors()) { hasConstructor = true; - if (constructor.getValueParameterCount() == 0 || isAllParametersWithDefaultValue(constructor)) { - if (constructor.getVisibility().isPublic()) { + if (constructor.getParameterList().getParametersCount() == 0) { + if (evaluator.isPublic(constructor)) { hasDefaultConstructor = true; } else { - Location location = context.getLocation(constructor.getNameElement()); + Location location = context.getNameLocation(constructor); context.report(ISSUE, constructor, location, - "The default constructor must be public"); + "The default constructor must be public"); // Also mark that we have a constructor so we don't complain again // below since we've already emitted a more specific error related // to the default constructor hasDefaultConstructor = true; } } else { - Location location = context.getLocation(constructor.getNameElement()); + Location location = context.getNameLocation(constructor); // TODO: Use separate issue for this which isn't an error String message = "Avoid non-default constructors in fragments: " - + "use a default constructor plus " - + "`Fragment#setArguments(Bundle)` instead"; + + "use a default constructor plus " + + "`Fragment#setArguments(Bundle)` instead"; context.report(ISSUE, constructor, location, message); } } if (!hasDefaultConstructor && hasConstructor) { String message = String.format( - "This fragment should provide a default constructor (a public " + - "constructor with no arguments) (`%1$s`)", - cls.getName()); - context.report(ISSUE, cls, context.getLocation(cls.getNameElement()), message); + "This fragment should provide a default constructor (a public " + + "constructor with no arguments) (`%1$s`)", + node.getQualifiedName()); + context.reportUast(ISSUE, node, context.getNameLocation(node), message); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/GetSignaturesDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/GetSignaturesDetector.java index 70cd7e974b1..a43817a4e2f 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/GetSignaturesDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/GetSignaturesDetector.java @@ -16,120 +16,84 @@ package com.android.tools.klint.checks; +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.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + import java.util.Collections; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - -public class GetSignaturesDetector extends Detector implements UastScanner { +public class GetSignaturesDetector extends Detector implements Detector.UastScanner { public static final Issue ISSUE = Issue.create( "PackageManagerGetSignatures", //$NON-NLS-1$ "Potential Multiple Certificate Exploit", "Improper validation of app signatures could lead to issues where a malicious app " + - "submits itself to the Play Store with both its real certificate and a fake " + - "certificate and gains access to functionality or information it shouldn't " + - "have due to another application only checking for the fake certificate and " + - "ignoring the rest. Please make sure to validate all signatures returned " + - "by this method.", + "submits itself to the Play Store with both its real certificate and a fake " + + "certificate and gains access to functionality or information it shouldn't " + + "have due to another application only checking for the fake certificate and " + + "ignoring the rest. Please make sure to validate all signatures returned " + + "by this method.", Category.SECURITY, 8, Severity.INFORMATIONAL, new Implementation( GetSignaturesDetector.class, - Scope.SOURCE_FILE_SCOPE)) + Scope.JAVA_FILE_SCOPE)) .addMoreInfo("https://bluebox.com/technical/android-fake-id-vulnerability/"); private static final String PACKAGE_MANAGER_CLASS = "android.content.pm.PackageManager"; //$NON-NLS-1$ private static final String GET_PACKAGE_INFO = "getPackageInfo"; //$NON-NLS-1$ private static final int GET_SIGNATURES_FLAG = 0x00000040; //$NON-NLS-1$ - // ---- Implements UastScanner ---- - + // ---- Implements JavaScanner ---- @Override - public List getApplicableFunctionNames() { + @Nullable + public List getApplicableMethodNames() { return Collections.singletonList(GET_PACKAGE_INFO); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - UFunction resolved = node.resolve(context); - - if (resolved == null || - !UastUtils.getContainingClassOrEmpty(resolved).isSubclassOf(PACKAGE_MANAGER_CLASS)) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + JavaEvaluator evaluator = context.getEvaluator(); + if (!evaluator.methodMatches(method, PACKAGE_MANAGER_CLASS, true, + JavaParser.TYPE_STRING, + JavaParser.TYPE_INT)) { return; } - List argumentList = node.getValueArguments(); - - // Ignore if the method doesn't fit our description. - if (argumentList.size() == 2) { - UType firstParameterType = argumentList.get(0).getExpressionType(); - if (firstParameterType != null - && firstParameterType.matchesFqName(JavaParser.TYPE_STRING)) { - maybeReportIssue(calculateValue(context, argumentList.get(1)), context, node); - } + List arguments = node.getValueArguments(); + UExpression second = arguments.get(1); + Object number = ConstantEvaluator.evaluate(context, second); + if (number instanceof Number) { + int flagValue = ((Number)number).intValue(); + maybeReportIssue(flagValue, context, node, second); } } - + private static void maybeReportIssue( - int flagValue, UastAndroidContext context, UCallExpression node) { + int flagValue, JavaContext context, UCallExpression node, + UExpression last) { if ((flagValue & GET_SIGNATURES_FLAG) != 0) { - context.report(ISSUE, node, context.getLocation(node.getValueArguments().get(1)), + context.report(ISSUE, node, context.getUastLocation(last), "Reading app signatures from getPackageInfo: The app signatures " + "could be exploited if not validated properly; " + "see issue explanation for details."); } } - - private static int calculateValue(UastAndroidContext context, UExpression expression) { - // This function assumes that the only inputs to the expression are static integer - // flags that combined via bitwise operands. - if (UastLiteralUtils.isIntegralLiteral(expression)) { - return (int) UastLiteralUtils.getLongValue((ULiteralExpression) expression); - } - - if (expression instanceof UResolvable) { - UDeclaration resolvedNode = ((UResolvable) expression).resolve(context); - if (resolvedNode instanceof UVariable) { - UExpression initializer = ((UVariable)resolvedNode).getInitializer(); - if (initializer != null) { - Object value = initializer.evaluate(); - if (value instanceof Integer) { - return (Integer)value; - } - } - } - } - - - if (expression instanceof UBinaryExpression) { - UBinaryExpression binaryExpression = (UBinaryExpression) expression; - UastBinaryOperator operator = binaryExpression.getOperator(); - int leftValue = calculateValue(context, binaryExpression.getLeftOperand()); - int rightValue = calculateValue(context, binaryExpression.getRightOperand()); - - if (operator == UastBinaryOperator.BITWISE_OR) { - return leftValue | rightValue; - } - if (operator == UastBinaryOperator.BITWISE_AND) { - return leftValue & rightValue; - } - if (operator == UastBinaryOperator.BITWISE_XOR) { - return leftValue ^ rightValue; - } - } - - return 0; - } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/HandlerDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/HandlerDetector.java index ed9a699bdb1..59a8517359b 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/HandlerDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/HandlerDetector.java @@ -16,28 +16,42 @@ package com.android.tools.klint.checks; +import static com.intellij.psi.util.PsiTreeUtil.getParentOfType; + import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UAnonymousClass; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UObjectLiteralExpression; +import org.jetbrains.uast.UastUtils; import java.util.Collections; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Checks that Handler implementations are top level classes or static. * See the corresponding check in the android.os.Handler source code. */ -public class HandlerDetector extends Detector implements UastScanner { +public class HandlerDetector extends Detector implements Detector.UastScanner { /** Potentially leaking handlers */ public static final Issue ISSUE = Issue.create( @@ -58,7 +72,7 @@ public class HandlerDetector extends Detector implements UastScanner { Severity.WARNING, new Implementation( HandlerDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); private static final String LOOPER_CLS = "android.os.Looper"; private static final String HANDLER_CLS = "android.os.Handler"; @@ -67,48 +81,65 @@ public class HandlerDetector extends Detector implements UastScanner { public HandlerDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- - + @Nullable @Override - public List getApplicableSuperClasses() { + public List applicableSuperClasses() { return Collections.singletonList(HANDLER_CLS); } @Override - public void visitClass(UastAndroidContext context, UClass node) { - if (UastUtils.isTopLevel(node)) { + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + // Only consider static inner classes + if (context.getEvaluator().isStatic(declaration)) { + return; + } + boolean isAnonymous = declaration instanceof UAnonymousClass; + if (declaration.getContainingClass() == null && !isAnonymous) { return; } - if (node.hasModifier(UastModifier.STATIC)) { + //// Only flag handlers using the default looper + //noinspection unchecked + UCallExpression invocation = UastUtils.getParentOfType( + declaration, UObjectLiteralExpression.class, true, UMethod.class); + if (invocation != null) { + if (isAnonymous && invocation.getValueArgumentCount() > 0) { + for (UExpression expression : invocation.getValueArguments()) { + PsiType type = expression.getExpressionType(); + if (type instanceof PsiClassType + && LOOPER_CLS.equals(type.getCanonicalText())) { + return; + } + } + } + } else if (hasLooperConstructorParameter(declaration)) { + // This is an inner class which takes a Looper parameter: + // possibly used correctly from elsewhere return; } - - // Only flag handlers using the default looper - if (hasLooperConstructorParameter(node)) { - return; + + Location location = context.getUastNameLocation(declaration); + String name; + if (isAnonymous) { + name = "anonymous " + ((UAnonymousClass)declaration).getBaseClassReference().getQualifiedName(); + } else { + name = declaration.getQualifiedName(); } - UElement locationNode = node.getNameElement(); - Location location = context.getLocation(locationNode); - context.report(ISSUE, node, location, String.format( - "This Handler class should be static or leaks might occur (%1$s)", - node.getName())); + //noinspection VariableNotUsedInsideIf + context.reportUast(ISSUE, declaration, location, String.format( + "This Handler class should be static or leaks might occur (%1$s)", + name)); } - private static boolean hasLooperConstructorParameter(@NonNull UClass cls) { - for (UFunction constructor : cls.getConstructors()) { - for (int i = 0, n = constructor.getValueParameterCount(); i < n; i++) { - UType type = constructor.getValueParameters().get(i).getType(); - if (type.matchesFqName(LOOPER_CLS)) { + private static boolean hasLooperConstructorParameter(@NonNull PsiClass cls) { + for (PsiMethod constructor : cls.getConstructors()) { + for (PsiParameter parameter : constructor.getParameterList().getParameters()) { + PsiType type = parameter.getType(); + if (LOOPER_CLS.equals(type.getCanonicalText())) { return true; } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/IconDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/IconDetector.java index 66f0d1d0a3a..3795f429356 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/IconDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/IconDetector.java @@ -34,9 +34,6 @@ import static com.android.SdkConstants.DRAWABLE_PREFIX; import static com.android.SdkConstants.DRAWABLE_XHDPI; import static com.android.SdkConstants.DRAWABLE_XXHDPI; import static com.android.SdkConstants.DRAWABLE_XXXHDPI; -import static com.android.SdkConstants.MENU_TYPE; -import static com.android.SdkConstants.R_CLASS; -import static com.android.SdkConstants.R_DRAWABLE_PREFIX; import static com.android.SdkConstants.TAG_ACTIVITY; import static com.android.SdkConstants.TAG_APPLICATION; import static com.android.SdkConstants.TAG_ITEM; @@ -49,30 +46,49 @@ import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.builder.model.ProductFlavor; import com.android.builder.model.ProductFlavorContainer; +import com.android.ide.common.resources.ResourceUrl; import com.android.resources.Density; import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Project; +import com.android.tools.klint.detector.api.ResourceEvaluator; import com.android.tools.klint.detector.api.ResourceXmlDetector; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiAnonymousClass; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.util.PsiTreeUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UAnonymousClass; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.USimpleNameReferenceExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Element; @@ -107,7 +123,7 @@ import javax.imageio.stream.ImageInputStream; * Checks for common icon problems, such as wrong icon sizes, placing icons in the * density independent drawable folder, etc. */ -public class IconDetector extends ResourceXmlDetector implements UastScanner { +public class IconDetector extends ResourceXmlDetector implements Detector.UastScanner { private static final boolean INCLUDE_LDPI; static { @@ -149,7 +165,7 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner { * the manifest, menu files etc to see how icons are used */ private static final EnumSet ICON_TYPE_SCOPE = EnumSet.of(Scope.ALL_RESOURCE_FILES, - Scope.SOURCE_FILE, Scope.MANIFEST); + Scope.JAVA_FILE, Scope.MANIFEST); private static final Implementation IMPLEMENTATION_JAVA = new Implementation( IconDetector.class, @@ -373,12 +389,6 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner { public IconDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; - } - @Override public void beforeCheckProject(@NonNull Context context) { mLauncherIcons = null; @@ -697,7 +707,9 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner { String message = String.format( "The `%1$s` icon has identical contents in the following configuration folders: %2$s", lastName, sb.toString()); + if (location != null) { context.report(DUPLICATES_CONFIGURATIONS, location, message); + } } else { StringBuilder sb = new StringBuilder(sameFiles.size() * 16); for (File file : sameFiles) { @@ -911,7 +923,9 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner { "The image `%1$s` varies significantly in its density-independent (dip) " + "size across the various density versions: %2$s", name, sb.toString()); - context.report(ICON_DIP_SIZE, location, message); + if (location != null) { + context.report(ICON_DIP_SIZE, location, message); + } } } } @@ -1958,108 +1972,97 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner { // ---- Implements UastScanner ---- - private static final String NOTIFICATION_CLASS = "Notification"; //$NON-NLS-1$ - private static final String NOTIFICATION_COMPAT_CLASS = "NotificationCompat"; //$NON-NLS-1$ - - private static final String NOTIFICATION_CLASS_FQNAME = - "android.app.Notification"; //$NON-NLS-1$ - - private static final String NOTIFICATION_COMPAT_CLASS_FQNAME = - "android.support.v4.app.NotificationCompat"; //$NON-NLS-1$ - - private static final String BUILDER_CLASS = "Builder"; //$NON-NLS-1$ - private static final String SET_SMALL_ICON = "setSmallIcon"; //$NON-NLS-1$ - private static final String ON_CREATE_OPTIONS_MENU = "onCreateOptionsMenu"; //$NON-NLS-1$ + private static final String NOTIFICATION_CLASS = "Notification"; + private static final String NOTIFICATION_BUILDER_CLASS = "Notification.Builder"; + private static final String NOTIFICATION_COMPAT_BUILDER_CLASS = "NotificationCompat.Builder"; + private static final String SET_SMALL_ICON = "setSmallIcon"; + private static final String ON_CREATE_OPTIONS_MENU = "onCreateOptionsMenu"; + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public List> getApplicableUastTypes() { + List> types = new ArrayList>(2); + types.add(UCallExpression.class); + types.add(UMethod.class); + return types; + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new NotificationFinder(context); } private final class NotificationFinder extends AbstractUastVisitor { - private UastAndroidContext mContext; + private final JavaContext mContext; - public NotificationFinder(UastAndroidContext mContext) { - this.mContext = mContext; + private NotificationFinder(JavaContext context) { + mContext = context; } @Override - public boolean visitFunction(@NotNull UFunction node) { - if (node.matchesName(ON_CREATE_OPTIONS_MENU)) { + public boolean visitMethod(UMethod method) { + if (ON_CREATE_OPTIONS_MENU.equals(method.getName())) { // Gather any R.menu references found in this method - node.accept(new MenuFinder()); + method.accept(new MenuFinder()); } - - return super.visitFunction(node); + return super.visitMethod(method); } @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - if (node.getKind() == UastCallKind.CONSTRUCTOR_CALL) { - visitConstructorInvocation(node); + public boolean visitCallExpression(UCallExpression node) { + if (UastExpressionUtils.isConstructorCall(node)) { + visitConstructorCall(node); } - return super.visitCallExpression(node); } - private void visitConstructorInvocation(UCallExpression node) { - USimpleReferenceExpression reference = node.getClassReference(); - if (reference == null) { + private void visitConstructorCall(UCallExpression node) { + UReferenceExpression classReference = node.getClassReference(); + if (classReference == null) { return; } - - List parts = UastUtils.asQualifiedIdentifiers(UastUtils.getQualifiedCallElement(node)); - - String typeName = reference.getIdentifier(); + PsiElement resolved = classReference.resolve(); + if (!(resolved instanceof PsiClass)) { + return; + } + String typeName = ((PsiClass) resolved).getName(); if (NOTIFICATION_CLASS.equals(typeName)) { List args = node.getValueArguments(); if (args.size() == 3) { - if (args.get(0) instanceof UQualifiedExpression - && handleSelect((UQualifiedExpression) args.get(0))) { + if (args.get(0) instanceof UReferenceExpression && handleSelect(args.get(0))) { return; } - UFunction method = UastUtils.getContainingFunction(node); - if (method != null) { - // Must track local types - String name = StringFormatDetector.getResourceForFirstArg(method, node); - if (name != null) { - if (mNotificationIcons == null) { - mNotificationIcons = Sets.newHashSet(); - } - mNotificationIcons.add(name); + ResourceUrl url = ResourceEvaluator.getResource(mContext, args.get(0)); + if (url != null + && (url.type == ResourceType.DRAWABLE + || url.type == ResourceType.COLOR + || url.type == ResourceType.MIPMAP)) { + if (mNotificationIcons == null) { + mNotificationIcons = Sets.newHashSet(); } + mNotificationIcons.add(url.name); } } - } else if (BUILDER_CLASS.equals(typeName)) { - boolean isBuilder = false; - if (parts != null && parts.size() == 1) { - isBuilder = true; - } else if (parts != null && parts.size() == 2) { - String clz = parts.get(0); - if (NOTIFICATION_CLASS.equals(clz) || NOTIFICATION_COMPAT_CLASS_FQNAME.equals(clz)) { - isBuilder = true; - } - } - - if (isBuilder) { - UFunction method = UastUtils.getContainingFunction(node); - if (method != null) { - SetIconFinder finder = new SetIconFinder(); - method.accept(finder); - } + } else if (NOTIFICATION_BUILDER_CLASS.equals(typeName) + || NOTIFICATION_COMPAT_BUILDER_CLASS.equals(typeName)) { + UMethod method = UastUtils.getParentOfType(node, UMethod.class, true); + if (method != null) { + SetIconFinder finder = new SetIconFinder(); + method.accept(finder); } } } } - private boolean handleSelect(UQualifiedExpression select) { - if (select.renderString().startsWith(R_DRAWABLE_PREFIX)) { - String name = select.getSelector().renderString(); + private boolean handleSelect(UElement select) { + ResourceUrl url = ResourceEvaluator.getResourceConstant(select); + if (url != null && url.type == ResourceType.DRAWABLE && !url.framework) { if (mNotificationIcons == null) { mNotificationIcons = Sets.newHashSet(); } - mNotificationIcons.add(name); + mNotificationIcons.add(url.name); return true; } @@ -2068,46 +2071,47 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner { } private final class SetIconFinder extends AbstractUastVisitor { + @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - if (SET_SMALL_ICON.equals(node.getFunctionName())) { - List args = node.getValueArguments(); - if (args.size() == 1 && args.get(0) instanceof UQualifiedExpression) { - handleSelect((UQualifiedExpression) args.get(0)); + public boolean visitCallExpression(UCallExpression expression) { + if (UastExpressionUtils.isMethodCall(expression)) { + if (SET_SMALL_ICON.equals(expression.getMethodName())) { + List arguments = expression.getValueArguments(); + if (arguments.size() == 1 && arguments.get(0) instanceof UReferenceExpression) { + handleSelect(arguments.get(0)); + } } } + return super.visitCallExpression(expression); + } - return super.visitCallExpression(node); + @Override + public boolean visitClass(UClass node) { + if (node instanceof UAnonymousClass) { + return true; + } + return super.visitClass(node); } } private final class MenuFinder extends AbstractUastVisitor { @Override - public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) { - // R.type.name - if (node.getReceiver() instanceof UQualifiedExpression) { - UQualifiedExpression select = (UQualifiedExpression) node.getReceiver(); - if (select.getReceiver() instanceof USimpleReferenceExpression) { - USimpleReferenceExpression reference = (USimpleReferenceExpression) select.getReceiver(); - if (R_CLASS.equals(reference.getIdentifier())) { - if (select.selectorMatches(MENU_TYPE)) { - String name = node.getSelector().renderString(); - // Reclassify icons in the given menu as action bar icons - if (mMenuToIcons != null) { - Collection icons = mMenuToIcons.get(name); - if (icons != null) { - if (mActionBarIcons == null) { - mActionBarIcons = Sets.newHashSet(); - } - mActionBarIcons.addAll(icons); - } - } + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + ResourceUrl url = ResourceEvaluator.getResourceConstant(node); + if (url != null && url.type == ResourceType.MENU && !url.framework) { + // Reclassify icons in the given menu as action bar icons + if (mMenuToIcons != null) { + Collection icons = mMenuToIcons.get(url.name); + if (icons != null) { + if (mActionBarIcons == null) { + mActionBarIcons = Sets.newHashSet(); } + mActionBarIcons.addAll(icons); } } } - return super.visitQualifiedExpression(node); + return super.visitSimpleNameReferenceExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java index b4eadc7f8f3..ac1b65d44f8 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java @@ -17,47 +17,68 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT; +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_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_CHARACTER_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE_WRAPPER; +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_WRAPPER; +import static com.android.tools.klint.detector.api.LintUtils.skipParentheses; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.JavaContext; -import com.android.tools.klint.detector.api.Project; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.TextFormat; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UParenthesizedExpression; +import org.jetbrains.uast.UPrefixExpression; +import org.jetbrains.uast.UQualifiedReferenceExpression; +import org.jetbrains.uast.USimpleNameReferenceExpression; +import org.jetbrains.uast.USuperExpression; +import org.jetbrains.uast.UThisExpression; +import org.jetbrains.uast.UThrowExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; -import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; -import org.jetbrains.uast.visitor.UastVisitor; - /** * Looks for performance issues in Java files, such as memory allocations during * drawing operations and using HashMap instead of SparseArray. */ -public class JavaPerformanceDetector extends Detector implements UastScanner { +public class JavaPerformanceDetector extends Detector implements Detector.UastScanner { - private static final Implementation - IMPLEMENTATION = new Implementation( + private static final Implementation IMPLEMENTATION = new Implementation( JavaPerformanceDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Allocating objects during a paint method */ public static final Issue PAINT_ALLOC = Issue.create( @@ -116,136 +137,113 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { Severity.WARNING, IMPLEMENTATION); - static final String ON_MEASURE = "onMeasure"; //$NON-NLS-1$ - static final String ON_DRAW = "onDraw"; //$NON-NLS-1$ - static final String ON_LAYOUT = "onLayout"; //$NON-NLS-1$ - private static final String INTEGER = "Integer"; //$NON-NLS-1$ - private static final String BOOLEAN = "Boolean"; //$NON-NLS-1$ - private static final String BYTE = "Byte"; //$NON-NLS-1$ - private static final String LONG = "Long"; //$NON-NLS-1$ - private static final String CHARACTER = "Character"; //$NON-NLS-1$ - private static final String DOUBLE = "Double"; //$NON-NLS-1$ - private static final String FLOAT = "Float"; //$NON-NLS-1$ - private static final String HASH_MAP = "HashMap"; //$NON-NLS-1$ - private static final String SPARSE_ARRAY = "SparseArray"; //$NON-NLS-1$ - private static final String CANVAS = "Canvas"; //$NON-NLS-1$ - private static final String LAYOUT = "layout"; //$NON-NLS-1$ + static final String ON_MEASURE = "onMeasure"; + static final String ON_DRAW = "onDraw"; + static final String ON_LAYOUT = "onLayout"; + private static final String LAYOUT = "layout"; + private static final String HASH_MAP = "java.util.HashMap"; + private static final String SPARSE_ARRAY = "android.util.SparseArray"; + public static final String CLASS_CANVAS = "android.graphics.Canvas"; /** Constructs a new {@link JavaPerformanceDetector} check */ public JavaPerformanceDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- + + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public List> getApplicableUastTypes() { + List> types = new ArrayList>(3); + types.add(UCallExpression.class); + types.add(UMethod.class); + return types; + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new PerformanceVisitor(context); } private static class PerformanceVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; private final boolean mCheckMaps; private final boolean mCheckAllocations; private final boolean mCheckValueOf; /** Whether allocations should be "flagged" in the current method */ private boolean mFlagAllocations; - public PerformanceVisitor(UastAndroidContext context) { + public PerformanceVisitor(JavaContext context) { mContext = context; - JavaContext lintContext = context.getLintContext(); - mCheckAllocations = lintContext.isEnabled(PAINT_ALLOC); - mCheckMaps = lintContext.isEnabled(USE_SPARSE_ARRAY); - mCheckValueOf = lintContext.isEnabled(USE_VALUE_OF); + mCheckAllocations = context.isEnabled(PAINT_ALLOC); + mCheckMaps = context.isEnabled(USE_SPARSE_ARRAY); + mCheckValueOf = context.isEnabled(USE_VALUE_OF); } @Override - public boolean visitFunction(@NotNull UFunction node) { + public boolean visitMethod(UMethod node) { mFlagAllocations = isBlockedAllocationMethod(node); - return super.visitFunction(node); + return super.visitMethod(node); } @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - UastCallKind kind = node.getKind(); - if (kind == UastCallKind.CONSTRUCTOR_CALL) { - visitConstructorInvocation(node); - } else if (kind == UastCallKind.FUNCTION_CALL) { - visitFunctionInvocation(node); + public boolean visitCallExpression(UCallExpression node) { + if (UastExpressionUtils.isConstructorCall(node)) { + visitConstructorCallExpression(node); + } else if (UastExpressionUtils.isMethodCall(node)) { + visitMethodCallExpression(node); } - return super.visitCallExpression(node); } - private void visitConstructorInvocation(UCallExpression node) { - USimpleReferenceExpression classReference = node.getClassReference(); - if (classReference == null) { - return; + private void visitConstructorCallExpression(UCallExpression node) { + String typeName = null; + UReferenceExpression classReference = node.getClassReference(); + if (mCheckMaps || mCheckValueOf) { + if (classReference != null) { + typeName = UastUtils.getQualifiedName(classReference); + } } - String typeName = null; if (mCheckMaps) { - UClass clazz = classReference.resolveClass(mContext); - if (clazz != null) { - typeName = clazz.getName(); - // TODO: Should we handle factory method constructions of HashMaps as well, - // e.g. via Guava? This is a bit trickier since we need to infer the type - // arguments from the calling context. - if (clazz.matchesName(HASH_MAP)) { - checkHashMap(node); - } else if (clazz.matchesName(SPARSE_ARRAY)) { - checkSparseArray(node); - } + // TODO: Should we handle factory method constructions of HashMaps as well, + // e.g. via Guava? This is a bit trickier since we need to infer the type + // arguments from the calling context. + if (HASH_MAP.equals(typeName)) { + checkHashMap(node); + } else if (SPARSE_ARRAY.equals(typeName)) { + checkSparseArray(node); } } if (mCheckValueOf) { - UType type = UastErrorType.INSTANCE; - if (typeName == null) { - UDeclaration resolvedDeclaration = classReference.resolve(mContext); - if (resolvedDeclaration instanceof UClass) { - type = ((UClass) resolvedDeclaration).getDefaultType(); - typeName = type.getName(); - } - } - if ((type.isInt() - || type.isBoolean() - || type.isFloat() - || type.isChar() - || type.isLong() - || type.isDouble() - || type.isShort() - || type.isByte()) + if (typeName != null + && (typeName.equals(TYPE_INTEGER_WRAPPER) + || typeName.equals(TYPE_BOOLEAN_WRAPPER) + || typeName.equals(TYPE_FLOAT_WRAPPER) + || typeName.equals(TYPE_CHARACTER_WRAPPER) + || typeName.equals(TYPE_LONG_WRAPPER) + || typeName.equals(TYPE_DOUBLE_WRAPPER) + || typeName.equals(TYPE_BYTE_WRAPPER)) + //&& node.astTypeReference().astParts().size() == 1 && node.getValueArgumentCount() == 1) { - String argument = node.getValueArguments().get(0).renderString(); - mContext.report(USE_VALUE_OF, node, mContext.getLocation(node), getUseValueOfErrorMessage( + String argument = node.getValueArguments().get(0).asSourceString(); + mContext.report(USE_VALUE_OF, node, mContext.getUastLocation(node), getUseValueOfErrorMessage( typeName, argument)); } } - if (mFlagAllocations && !(node.getParent() instanceof UThrowExpression) && mCheckAllocations) { + if (mFlagAllocations + && !(skipParentheses(node.getContainingElement()) instanceof UThrowExpression) + && mCheckAllocations) { // Make sure we're still inside the method declaration that marked // mInDraw as true, in case we've left it and we're in a static // block or something: - UElement method = node; - while (method != null) { - if (method instanceof UFunction) { - break; - } - method = method.getParent(); - } - if (method != null && isBlockedAllocationMethod(((UFunction) method)) + PsiMethod method = UastUtils.getParentOfType(node, UMethod.class); + if (method != null && isBlockedAllocationMethod(method) && !isLazilyInitialized(node)) { reportAllocation(node); } @@ -253,42 +251,46 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { } private void reportAllocation(UElement node) { - mContext.report(PAINT_ALLOC, node, mContext.getLocation(node), + mContext.report(PAINT_ALLOC, node, mContext.getUastLocation(node), "Avoid object allocations during draw/layout operations (preallocate and " + "reuse instead)"); } - private void visitFunctionInvocation(UCallExpression node) { - UExpression operand = UastUtils.getReceiver(node); + private void visitMethodCallExpression(UCallExpression node) { + if (!mFlagAllocations) { + return; + } + UExpression receiver = node.getReceiver(); + if (receiver == null) { + return; + } - if (mFlagAllocations && operand != null) { - // Look for forbidden methods - String methodName = node.getFunctionName(); - if ("createBitmap".equals(methodName) //$NON-NLS-1$ - || "createScaledBitmap".equals(methodName)) { //$NON-NLS-1$ - String operandText = operand.renderString(); - if (operandText.equals("Bitmap") //$NON-NLS-1$ - || operandText.equals("android.graphics.Bitmap")) { //$NON-NLS-1$ - if (!isLazilyInitialized(node)) { - reportAllocation(node); - } - } - } else if (methodName != null && methodName.startsWith("decode")) { //$NON-NLS-1$ - // decodeFile, decodeByteArray, ... - String operandText = operand.renderString(); - if (operandText.equals("BitmapFactory") //$NON-NLS-1$ - || operandText.equals("android.graphics.BitmapFactory")) { //$NON-NLS-1$ - if (!isLazilyInitialized(node)) { - reportAllocation(node); - } - } - } else if ("getClipBounds".equals(methodName)) { //$NON-NLS-1$ - if (node.getValueArgumentCount() == 0) { - mContext.report(PAINT_ALLOC, node, mContext.getLocation(node), - "Avoid object allocations during draw operations: Use " + - "`Canvas.getClipBounds(Rect)` instead of `Canvas.getClipBounds()` " + - "which allocates a temporary `Rect`"); - } + String functionName = node.getMethodName(); + if (functionName == null) { + return; + } + + // Look for forbidden methods + if (functionName.equals("createBitmap") //$NON-NLS-1$ + || functionName.equals("createScaledBitmap")) { //$NON-NLS-1$ + PsiMethod method = node.resolve(); + if (method != null && JavaEvaluator.isMemberInClass(method, + "android.graphics.Bitmap") && !isLazilyInitialized(node)) { + reportAllocation(node); + } + } else if (functionName.startsWith("decode")) { //$NON-NLS-1$ + // decodeFile, decodeByteArray, ... + PsiMethod method = node.resolve(); + if (method != null && JavaEvaluator.isMemberInClass(method, + "android.graphics.BitmapFactory") && !isLazilyInitialized(node)) { + reportAllocation(node); + } + } else if (functionName.equals("getClipBounds")) { //$NON-NLS-1$ + if (node.getValueArguments().isEmpty()) { + mContext.report(PAINT_ALLOC, node, mContext.getUastLocation(node), + "Avoid object allocations during draw operations: Use " + + "`Canvas.getClipBounds(Rect)` instead of `Canvas.getClipBounds()` " + + "which allocates a temporary `Rect`"); } } } @@ -314,9 +316,9 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { *

*/ private static boolean isLazilyInitialized(UElement node) { - UElement curr = node.getParent(); + UElement curr = node.getContainingElement(); while (curr != null) { - if (curr instanceof UFunction) { + if (curr instanceof UMethod) { return false; } else if (curr instanceof UIfExpression) { UIfExpression ifNode = (UIfExpression) curr; @@ -328,10 +330,9 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { // guarded (so lazily initialized and an allocation we won't complain // about.) List assignments = new ArrayList(); - UExpression thenBranch = ifNode.getThenBranch(); - if (thenBranch != null) { - AssignmentTracker visitor = new AssignmentTracker(assignments); - thenBranch.accept(visitor); + AssignmentTracker visitor = new AssignmentTracker(assignments); + if (ifNode.getThenExpression() != null) { + ifNode.getThenExpression().accept(visitor); } if (!assignments.isEmpty()) { List references = new ArrayList(); @@ -346,29 +347,40 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { return false; } - curr = curr.getParent(); + curr = curr.getContainingElement(); } return false; } /** Adds any variables referenced in the given expression into the given list */ - private static void addReferencedVariables(Collection variables, - UExpression expression) { + private static void addReferencedVariables( + @NonNull Collection variables, + @Nullable UExpression expression) { if (expression instanceof UBinaryExpression) { UBinaryExpression binary = (UBinaryExpression) expression; addReferencedVariables(variables, binary.getLeftOperand()); addReferencedVariables(variables, binary.getRightOperand()); - } else if (expression instanceof UUnaryExpression) { - UUnaryExpression unary = (UUnaryExpression) expression; + } else if (expression instanceof UPrefixExpression) { + UPrefixExpression unary = (UPrefixExpression) expression; addReferencedVariables(variables, unary.getOperand()); - } else if (expression instanceof USimpleReferenceExpression) { - USimpleReferenceExpression reference = (USimpleReferenceExpression) expression; + } else if (expression instanceof UParenthesizedExpression) { + UParenthesizedExpression exp = (UParenthesizedExpression) expression; + addReferencedVariables(variables, exp.getExpression()); + } else if (expression instanceof USimpleNameReferenceExpression) { + USimpleNameReferenceExpression reference = (USimpleNameReferenceExpression) expression; variables.add(reference.getIdentifier()); - } else if (expression instanceof UQualifiedExpression) { - UQualifiedExpression select = (UQualifiedExpression) expression; - if (select.getReceiver() instanceof UThisExpression && select.getSelector() instanceof USimpleReferenceExpression) { - variables.add(((USimpleReferenceExpression) select.getSelector()).getIdentifier()); + } else if (expression instanceof UQualifiedReferenceExpression) { + UQualifiedReferenceExpression ref = (UQualifiedReferenceExpression) expression; + UExpression receiver = ref.getReceiver(); + UExpression selector = ref.getSelector(); + if (receiver instanceof UThisExpression || receiver instanceof USuperExpression) { + String identifier = (selector instanceof USimpleNameReferenceExpression) + ? ((USimpleNameReferenceExpression) selector).getIdentifier() + : null; + if (identifier != null) { + variables.add(identifier); + } } } } @@ -377,27 +389,23 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { * Returns whether the given method declaration represents a method * where allocating objects is not allowed for performance reasons */ - private static boolean isBlockedAllocationMethod(UFunction node) { - return isOnDrawMethod(node) || isOnMeasureMethod(node) || isOnLayoutMethod(node) - || isLayoutMethod(node); + private boolean isBlockedAllocationMethod( + @NonNull PsiMethod node) { + JavaEvaluator evaluator = mContext.getEvaluator(); + return isOnDrawMethod(evaluator, node) + || isOnMeasureMethod(evaluator, node) + || isOnLayoutMethod(evaluator, node) + || isLayoutMethod(evaluator, node); } /** * Returns true if this method looks like it's overriding android.view.View's * {@code protected void onDraw(Canvas canvas)} */ - private static boolean isOnDrawMethod(UFunction node) { - if (ON_DRAW.equals(node.getName())) { - List parameters = node.getValueParameters(); - if (parameters.size() == 1) { - UVariable arg0 = parameters.get(0); - if (arg0.getType().matchesName(CANVAS)) { - return true; - } - } - } - - return false; + private static boolean isOnDrawMethod( + @NonNull JavaEvaluator evaluator, + @NonNull PsiMethod node) { + return ON_DRAW.equals(node.getName()) && evaluator.parametersMatch(node, CLASS_CANVAS); } /** @@ -406,148 +414,102 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { * {@code protected void onLayout(boolean changed, int left, int top, * int right, int bottom)} */ - private static boolean isOnLayoutMethod(UFunction node) { - if (ON_LAYOUT.equals(node.getName())) { - List parameters = node.getValueParameters(); - if (parameters.size() == 5) { - Iterator iterator = parameters.iterator(); - if (!iterator.hasNext()) { - return false; - } - - // Ensure that the argument list matches boolean, int, int, int, int - UType type = iterator.next().getType(); - if (!type.isBoolean() || !iterator.hasNext()) { - return false; - } - for (int i = 0; i < 4; i++) { - type = iterator.next().getType(); - if (!type.isInt()) { - return false; - } - if (!iterator.hasNext()) { - return i == 3; - } - } - } - } - - return false; + private static boolean isOnLayoutMethod( + @NonNull JavaEvaluator evaluator, + @NonNull PsiMethod node) { + return ON_LAYOUT.equals(node.getName()) && evaluator.parametersMatch(node, + TYPE_BOOLEAN, TYPE_INT, TYPE_INT, TYPE_INT, TYPE_INT); } /** * Returns true if this method looks like it's overriding android.view.View's * {@code protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)} */ - private static boolean isOnMeasureMethod(UFunction node) { - if (ON_MEASURE.equals(node.getName())) { - List parameters = node.getValueParameters(); - if (parameters.size() == 2) { - UVariable arg0 = parameters.get(0); - UVariable arg1 = parameters.get(parameters.size() - 1); - return arg0.getType().isInt() && arg1.getType().isInt(); - } - } - - return false; + private static boolean isOnMeasureMethod( + @NonNull JavaEvaluator evaluator, + @NonNull PsiMethod node) { + return ON_MEASURE.equals(node.getName()) && evaluator.parametersMatch(node, + TYPE_INT, TYPE_INT); } /** * Returns true if this method looks like it's overriding android.view.View's * {@code public void layout(int l, int t, int r, int b)} */ - private static boolean isLayoutMethod(UFunction node) { - if (LAYOUT.equals(node.getName())) { - List parameters = node.getValueParameters(); - if (parameters.size() == 4) { - Iterator iterator = parameters.iterator(); - for (int i = 0; i < 4; i++) { - if (!iterator.hasNext()) { - return false; - } - UVariable next = iterator.next(); - if (!next.getType().isInt()) { - return false; - } - } - return true; - } - } - - return false; + private static boolean isLayoutMethod( + @NonNull JavaEvaluator evaluator, + @NonNull PsiMethod node) { + return LAYOUT.equals(node.getName()) && evaluator.parametersMatch(node, + TYPE_INT, TYPE_INT, TYPE_INT, TYPE_INT); } - /** * Checks whether the given constructor call and type reference refers * to a HashMap constructor call that is eligible for replacement by a * SparseArray call instead */ - private void checkHashMap(UCallExpression node) { - // reference.hasTypeArguments returns false where it should not - List types = node.getTypeArguments(); + private void checkHashMap(@NonNull UCallExpression node) { + List types = node.getTypeArguments(); if (types.size() == 2) { - UType first = types.get(0); - - Project mainProject = mContext.getLintContext().getMainProject(); - int minSdk = mainProject.getMinSdk(); - - if (first.isInt() || first.isByte()) { - UType valueType = types.get(1); - String valueTypeText = valueType.getName(); - if (valueType.isInt()) { - mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node), + PsiType first = types.get(0); + String typeName = first.getCanonicalText(); + int minSdk = mContext.getMainProject().getMinSdk(); + if (TYPE_INTEGER_WRAPPER.equals(typeName) || TYPE_BYTE_WRAPPER.equals(typeName)) { + String valueType = types.get(1).getCanonicalText(); + if (valueType.equals(TYPE_INTEGER_WRAPPER)) { + mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node), "Use new `SparseIntArray(...)` instead for better performance"); - } else if (valueType.isLong() && minSdk >= 18) { - mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node), + } else if (valueType.equals(TYPE_LONG_WRAPPER) && minSdk >= 18) { + mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node), "Use `new SparseLongArray(...)` instead for better performance"); - } else if (valueType.isBoolean()) { - mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node), + } else if (valueType.equals(TYPE_BOOLEAN_WRAPPER)) { + mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node), "Use `new SparseBooleanArray(...)` instead for better performance"); } else { - mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node), + mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node), String.format( "Use `new SparseArray<%1$s>(...)` instead for better performance", - valueTypeText)); + valueType.substring(valueType.lastIndexOf('.') + 1))); } - } else if (first.isLong() && (minSdk >= 16 || - Boolean.TRUE == mainProject.dependsOn( + } else if (TYPE_LONG_WRAPPER.equals(typeName) && (minSdk >= 16 || + Boolean.TRUE == mContext.getMainProject().dependsOn( SUPPORT_LIB_ARTIFACT))) { boolean useBuiltin = minSdk >= 16; String message = useBuiltin ? "Use `new LongSparseArray(...)` instead for better performance" : "Use `new android.support.v4.util.LongSparseArray(...)` instead for better performance"; - mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node), + mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node), message); } } } - private void checkSparseArray(UCallExpression node) { - // reference.hasTypeArguments returns false where it should not - List types = node.getTypeArguments(); + private void checkSparseArray(@NonNull UCallExpression node) { + List types = node.getTypeArguments(); if (types.size() == 1) { - UType first = types.get(0); - if (first.isInt()) { - mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node), + String valueType = types.get(0).getCanonicalText(); + if (valueType.equals(TYPE_INTEGER_WRAPPER)) { + mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node), "Use `new SparseIntArray(...)` instead for better performance"); - } else if (first.isBoolean()) { - mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node), + } else if (valueType.equals(TYPE_BOOLEAN_WRAPPER)) { + mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node), "Use `new SparseBooleanArray(...)` instead for better performance"); } } } } - + private static String getUseValueOfErrorMessage(String typeName, String argument) { // Keep in sync with {@link #getReplacedType} below - return String.format("Use `%1$s.valueOf(%2$s)` instead", typeName, argument); + return String.format("Use `%1$s.valueOf(%2$s)` instead", + typeName.substring(typeName.lastIndexOf('.') + 1), argument); } /** * For an error message for an {@link #USE_VALUE_OF} issue reported by this detector, * returns the type being replaced. Intended to use for IDE quickfix implementations. */ + @SuppressWarnings("unused") // Used by the IDE @Nullable public static String getReplacedType(@NonNull String message, @NonNull TextFormat format) { message = format.toText(message); @@ -567,24 +529,28 @@ public class JavaPerformanceDetector extends Detector implements UastScanner { } @Override - public boolean visitBinaryExpression(@NotNull UBinaryExpression node) { - UastBinaryOperator operator = node.getOperator(); - if (operator == UastBinaryOperator.ASSIGN || operator == UastBinaryOperator.OR_ASSIGN) { + public boolean visitBinaryExpression(UBinaryExpression node) { + if (UastExpressionUtils.isAssignment(node)) { UExpression left = node.getLeftOperand(); - String variable = null; - if (left instanceof UQualifiedExpression && ((UQualifiedExpression) left).getReceiver() instanceof UThisExpression) { - UExpression selector = ((UQualifiedExpression) left).getSelector(); - if (selector instanceof USimpleReferenceExpression) { - variable = ((USimpleReferenceExpression) selector).getIdentifier(); + if (left instanceof UQualifiedReferenceExpression) { + UQualifiedReferenceExpression ref = (UQualifiedReferenceExpression) left; + if (ref.getReceiver() instanceof UThisExpression || + ref.getReceiver() instanceof USuperExpression) { + PsiElement resolved = ref.resolve(); + if (resolved instanceof PsiField) { + mVariables.add(((PsiField) resolved).getName()); + } + } else { + PsiElement resolved = ref.resolve(); + if (resolved instanceof PsiField) { + mVariables.add(((PsiField) resolved).getName()); + } } - } else if (left instanceof USimpleReferenceExpression) { - variable = ((USimpleReferenceExpression) left).getIdentifier(); - } - if (variable != null) { - mVariables.add(variable); + } else if (left instanceof USimpleNameReferenceExpression) { + mVariables.add(((USimpleNameReferenceExpression) left).getIdentifier()); } } - + return super.visitBinaryExpression(node); } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaScriptInterfaceDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaScriptInterfaceDetector.java index 3da9ab652c8..b4f93696868 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaScriptInterfaceDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaScriptInterfaceDetector.java @@ -17,31 +17,36 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; -import com.google.common.collect.Maps; +import com.android.tools.klint.detector.api.TypeEvaluator; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; import java.util.Collections; import java.util.List; -import java.util.Map; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; /** * Looks for addJavascriptInterface calls on interfaces have been properly annotated * with {@code @JavaScriptInterface} */ -public class JavaScriptInterfaceDetector extends Detector implements UastScanner { +public class JavaScriptInterfaceDetector extends Detector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "JavascriptInterface", //$NON-NLS-1$ @@ -55,7 +60,7 @@ public class JavaScriptInterfaceDetector extends Detector implements UastScanner Severity.ERROR, new Implementation( JavaScriptInterfaceDetector.class, - Scope.SOURCE_FILE_SCOPE)) + Scope.JAVA_FILE_SCOPE)) .addMoreInfo( "http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)"); //$NON-NLS-1$ @@ -67,227 +72,69 @@ public class JavaScriptInterfaceDetector extends Detector implements UastScanner public JavaScriptInterfaceDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; // because it relies on class loading referenced javascript interface - } - // ---- Implements UastScanner ---- - + @Nullable @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - if (context.getLintContext().getMainProject().getTargetSdk() < 17) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + if (context.getMainProject().getTargetSdk() < 17) { return; } - if (node.getValueArgumentCount() != 2) { + List arguments = call.getValueArguments(); + if (arguments.size() != 2) { return; } - if (!isCallOnWebView(context, node)) { + JavaEvaluator evaluator = context.getEvaluator(); + if (!JavaEvaluator.isMemberInClass(method, WEB_VIEW_CLS)) { return; } - UExpression first = node.getValueArguments().get(0); - UElement resolved = UastUtils.resolveIfCan(first, context); - if (resolved instanceof UVariable) { - // We're passing in a variable to the addJavaScriptInterface method; - // the variable may be of a more generic type than the actual - // value assigned to it. For example, we may have a scenario like this: - // Object object = new SpecificType(); - // addJavaScriptInterface(object, ...) - // Here the type of the variable is Object, but we know that it can - // contain objects of type SpecificType, so we should check that type instead. - UFunction method = UastUtils.getContainingFunction(node); - if (method != null) { - ConcreteTypeVisitor v = new ConcreteTypeVisitor(context, node); - method.accept(v); - resolved = v.getType(); - if (resolved == null) { - return; - } - } else { + UExpression first = arguments.get(0); + PsiType evaluated = TypeEvaluator.evaluate(context, first); + if (evaluated instanceof PsiClassType) { + PsiClassType classType = (PsiClassType) evaluated; + PsiClass cls = classType.resolve(); + if (cls == null) { return; } - } else if (resolved instanceof UFunction) { - UFunction method = (UFunction) resolved; - if (method.getKind() == UastFunctionKind.CONSTRUCTOR) { - resolved = UastUtils.getContainingClass(method); - } else { - UType returnType = method.getReturnType(); - if (returnType != null) { - UClass resolvedClass = returnType.resolve(context); - if (resolvedClass != null) { - resolved = resolvedClass; - } - } - } - } else { - UType type = first.getExpressionType(); - if (type != null) { - UClass resolvedClass = type.resolve(context); - if (resolvedClass != null) { - resolved = resolvedClass; - } - } - } - - if (resolved instanceof UClass) { - UClass cls = (UClass) resolved; - if (isJavaScriptAnnotated(context, cls)) { + if (isJavaScriptAnnotated(cls)) { return; } - Location location = context.getLocation(node.getFunctionNameElement()); + Location location = context.getUastNameLocation(call); String message = String.format( - "None of the methods in the added interface (%1$s) have been annotated " + - "with `@android.webkit.JavascriptInterface`; they will not " + - "be visible in API 17", cls.getName()); - context.report(ISSUE, node, location, message); + "None of the methods in the added interface (%1$s) have been annotated " + + "with `@android.webkit.JavascriptInterface`; they will not " + + "be visible in API 17", cls.getName()); + context.report(ISSUE, call, location, message); } } - private static boolean isCallOnWebView(UastAndroidContext context, UCallExpression call) { - UFunction resolved = call.resolve(context); - return UastUtils.getContainingClassOrEmpty(resolved).matchesFqName(WEB_VIEW_CLS); - - } - - private static boolean isJavaScriptAnnotated(UastAndroidContext context, UClass clz) { + private static boolean isJavaScriptAnnotated(PsiClass clz) { while (clz != null) { - for (UAnnotation annotation : clz.getAnnotations()) { - if (JAVASCRIPT_INTERFACE_CLS.equals(annotation.getFqName())) { + PsiModifierList modifierList = clz.getModifierList(); + if (modifierList != null + && modifierList.findAnnotation(JAVASCRIPT_INTERFACE_CLS) != null) { + return true; + } + + for (PsiMethod method : clz.getMethods()) { + if (method.getModifierList().findAnnotation(JAVASCRIPT_INTERFACE_CLS) != null) { return true; } } - for (UFunction method : clz.getFunctions()) { - for (UAnnotation annotation : method.getAnnotations()) { - if (JAVASCRIPT_INTERFACE_CLS.equals(annotation.getFqName())) { - return true; - } - } - } - - clz = clz.getSuperClass(context); + clz = clz.getSuperClass(); } return false; } - - private static class ConcreteTypeVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; - private final UCallExpression mTargetCall; - private boolean mFoundCall; - private Map mTypes = Maps.newIdentityHashMap(); - private Map mVariableTypes = Maps.newHashMap(); - - public ConcreteTypeVisitor(UastAndroidContext context, UCallExpression call) { - mContext = context; - mTargetCall = call; - } - - public UClass getType() { - UExpression first = mTargetCall.getValueArguments().get(0); - UClass resolvedClass = mTypes.get(first); - if (resolvedClass == null && first instanceof UResolvable) { - UElement resolved = ((UResolvable) first).resolve(mContext); - if (resolved instanceof UVariable) { - resolvedClass = mVariableTypes.get(resolved); - if (resolvedClass == null) { - return ((UVariable) resolved).getType().resolve(mContext); - } - } - } - return resolvedClass; - } - - @Override - public boolean visitElement(@NotNull UElement node) { - return mFoundCall || super.visitElement(node); - } - - @Override - public void afterVisitCallExpression(@NotNull UCallExpression node) { - if (node.getKind() == UastCallKind.FUNCTION_CALL && node == mTargetCall) { - mFoundCall = true; - } else if (node.getKind() == UastCallKind.CONSTRUCTOR_CALL) { - UFunction resolved = node.resolve(mContext); - if (resolved != null) { - mTypes.put(node, UastUtils.getContainingClass(resolved)); - } - } - } - - @Override - public void afterVisitSimpleReferenceExpression(@NotNull USimpleReferenceExpression node) { - if (mTypes.get(node) == null) { - UElement resolved = node.resolve(mContext); - if (resolved instanceof UVariable) { - UClass resolvedClass = mVariableTypes.get(resolved); - if (resolvedClass != null) { - mTypes.put(node, resolvedClass); - } - } - } - } - - @Override - public void afterVisitBinaryExpression(@NotNull UBinaryExpression node) { - if (node.getOperator() == UastBinaryOperator.ASSIGN) { - UExpression rhs = node.getRightOperand(); - UClass resolvedClass = mTypes.get(rhs); - if (resolvedClass != null) { - UExpression lhs = node.getLeftOperand(); - mTypes.put(lhs, resolvedClass); - if (lhs instanceof UResolvable) { - UDeclaration variable = ((UResolvable) lhs).resolve(mContext); - if (variable instanceof UVariable) { - mVariableTypes.put((UVariable) variable, resolvedClass); - } - } - } - } - } - - @Override - public void afterVisitIfExpression(@NotNull UIfExpression node) { - if (node.isTernary()) { - UClass resolvedClass = mTypes.get(node.getThenBranch()); - if (resolvedClass == null) { - resolvedClass = mTypes.get(node.getElseBranch()); - } - if (resolvedClass != null) { - mTypes.put(node, resolvedClass); - } - } - } - - @Override - public void afterVisitVariable(@NotNull UVariable node) { - UExpression initializer = node.getInitializer(); - if (initializer != null) { - UClass resolvedClass = mTypes.get(initializer); - if (resolvedClass != null) { - mTypes.put(node, resolvedClass); - mVariableTypes.put(node, resolvedClass); - } - } - } - - @Override - public void afterVisitBinaryExpressionWithType(@NotNull UBinaryExpressionWithType node) { - UClass resolvedClass = mTypes.get(node); - if (resolvedClass != null) { - mTypes.put(node, resolvedClass); - } - } - } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutConsistencyDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutConsistencyDetector.java index 0acd490502b..ddc1a69c26d 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutConsistencyDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutConsistencyDetector.java @@ -28,23 +28,26 @@ import com.android.resources.ResourceType; import com.android.tools.klint.client.api.LintDriver; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LayoutDetector; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; import com.android.utils.Pair; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiElement; import org.jetbrains.uast.UElement; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -65,7 +68,7 @@ import java.util.Set; /** * Checks for consistency in layouts across different resource folders */ -public class LayoutConsistencyDetector extends LayoutDetector implements UastScanner { +public class LayoutConsistencyDetector extends LayoutDetector implements Detector.UastScanner { /** Map from layout resource names to a list of files defining that resource, * and within each file the value is a map from string ids to the widget type @@ -112,7 +115,7 @@ public class LayoutConsistencyDetector extends LayoutDetector implements UastSca Severity.WARNING, new Implementation( LayoutConsistencyDetector.class, - Scope.SOURCE_AND_RESOURCE_FILES)); + Scope.JAVA_AND_RESOURCE_FILES)); /** Constructs a consistency check */ public LayoutConsistencyDetector() { @@ -123,12 +126,6 @@ public class LayoutConsistencyDetector extends LayoutDetector implements UastSca return folderType == ResourceFolderType.LAYOUT; } - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - @Override public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { Element root = document.getDocumentElement(); @@ -435,8 +432,10 @@ public class LayoutConsistencyDetector extends LayoutDetector implements UastSca } @Override - public void visitResourceReference(UastAndroidContext context, UElement element, String type, String name, boolean isFramework) { - if (!isFramework && type.equals(ResourceType.ID.getName())) { + public void visitResourceReference(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UElement node, @NonNull ResourceType type, @NonNull String name, + boolean isFramework) { + if (!isFramework && type == ResourceType.ID) { mRelevantIds.add(name); } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutInflationDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutInflationDetector.java index 94707237424..c66853f4770 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutInflationDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutInflationDetector.java @@ -18,18 +18,21 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; -import static org.jetbrains.uast.UastLiteralUtils.*; +import static com.android.tools.klint.checks.ViewHolderDetector.INFLATE; -import com.android.SdkConstants; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.res2.ResourceFile; import com.android.ide.common.res2.ResourceItem; import com.android.resources.ResourceType; +import com.android.tools.klint.client.api.AndroidReference; import com.android.tools.klint.client.api.LintClient; +import com.android.tools.klint.client.api.UastLintUtils; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.JavaContext; @@ -39,15 +42,16 @@ import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Project; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; import com.android.utils.Pair; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastLiteralUtils; +import org.jetbrains.uast.visitor.UastVisitor; import org.kxml2.io.KXmlParser; import org.w3c.dom.Attr; import org.w3c.dom.Document; @@ -61,20 +65,19 @@ import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Set; /** * Looks for layout inflation calls passing null as the view root */ -public class LayoutInflationDetector extends LayoutDetector implements UastScanner { +public class LayoutInflationDetector extends LayoutDetector implements Detector.UastScanner { @SuppressWarnings("unchecked") private static final Implementation IMPLEMENTATION = new Implementation( LayoutInflationDetector.class, - Scope.SOURCE_AND_RESOURCE_FILES, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_AND_RESOURCE_FILES, + Scope.JAVA_FILE_SCOPE); /** Passing in a null parent to a layout inflater */ public static final Issue ISSUE = Issue.create( @@ -98,12 +101,6 @@ public class LayoutInflationDetector extends LayoutDetector implements UastScann public LayoutInflationDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - @Override public void afterCheckProject(@NonNull Context context) { if (mPendingErrors != null) { @@ -146,52 +143,48 @@ public class LayoutInflationDetector extends LayoutDetector implements UastScann // ---- Implements UastScanner ---- + @Nullable @Override - public List getApplicableFunctionNames() { - return Collections.singletonList(ViewHolderDetector.INFLATE); + public List getApplicableMethodNames() { + return Collections.singletonList(INFLATE); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - assert ViewHolderDetector.INFLATE.equals(node.getFunctionName()); - if (node instanceof USimpleReferenceExpression) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + assert method.getName().equals(INFLATE); + if (call.getReceiver() == null) { return; } - List arguments = node.getValueArguments(); + List arguments = call.getValueArguments(); if (arguments.size() < 2) { return; } - Iterator iterator = arguments.iterator(); - UExpression first = iterator.next(); - UExpression second = iterator.next(); - if (!isNullLiteral(second) || !(first instanceof UQualifiedExpression)) { + + UExpression second = arguments.get(1); + if (!UastLiteralUtils.isNullLiteral(second)) { return; } - UQualifiedExpression select = (UQualifiedExpression) first; - UExpression receiver = select.getReceiver(); - UExpression selector = select.getSelector(); - if (receiver instanceof UQualifiedExpression && selector instanceof USimpleReferenceExpression) { - UQualifiedExpression rLayout = (UQualifiedExpression) receiver; - if (rLayout.selectorMatches(ResourceType.LAYOUT.getName()) && - rLayout.getReceiver().renderString().endsWith(SdkConstants.R_CLASS)) { - String layoutName = ((USimpleReferenceExpression)selector).getIdentifier(); - JavaContext lintContext = context.getLintContext(); + UExpression first = arguments.get(0); + AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(first); + if (androidReference == null) { + return; + } - if (lintContext.getScope().contains(Scope.RESOURCE_FILE)) { - // We're doing a full analysis run: we can gather this information - // incrementally - if (!lintContext.getDriver().isSuppressed(lintContext, ISSUE, node)) { - if (mPendingErrors == null) { - mPendingErrors = Lists.newArrayList(); - } - Location location = context.getLocation(second); - mPendingErrors.add(Pair.of(layoutName, location)); - } - } else if (hasLayoutParams(lintContext, layoutName)) { - context.report(ISSUE, node, context.getLocation(second), ERROR_MESSAGE); + String layoutName = androidReference.getName(); + if (context.getScope().contains(Scope.RESOURCE_FILE)) { + // We're doing a full analysis run: we can gather this information + // incrementally + if (!context.getDriver().isSuppressed(context, ISSUE, call)) { + if (mPendingErrors == null) { + mPendingErrors = Lists.newArrayList(); } + Location location = context.getUastLocation(second); + mPendingErrors.add(Pair.of(layoutName, location)); } + } else if (hasLayoutParams(context, layoutName)) { + context.report(ISSUE, call, context.getUastLocation(second), ERROR_MESSAGE); } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LeakDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LeakDetector.java new file mode 100644 index 00000000000..1e5f2e8b8ce --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LeakDetector.java @@ -0,0 +1,188 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.CLASS_CONTEXT; +import static com.android.SdkConstants.CLASS_FRAGMENT; +import static com.android.SdkConstants.CLASS_VIEW; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiType; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UField; +import org.jetbrains.uast.UVariable; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Collections; +import java.util.List; + +/** + * Looks for leaks via static fields + */ +public class LeakDetector extends Detector implements Detector.UastScanner { + /** Leaking data via static fields */ + public static final Issue ISSUE = Issue.create( + "StaticFieldLeak", //$NON-NLS-1$ + "Static Field Leaks", + + "A static field will leak contexts.", + + Category.PERFORMANCE, + 6, + Severity.WARNING, + new Implementation( + LeakDetector.class, + Scope.JAVA_FILE_SCOPE)); + + /** Constructs a new {@link LeakDetector} check */ + public LeakDetector() { + } + + // ---- Implements JavaScanner ---- + + @Override + public List> getApplicablePsiTypes() { + return Collections.>singletonList(PsiField.class); + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { + return new FieldChecker(context); + } + + private static class FieldChecker extends AbstractUastVisitor { + private final JavaContext mContext; + + private FieldChecker(JavaContext context) { + mContext = context; + } + + @Override + public boolean visitClass(@NotNull UClass node) { + return super.visitClass(node); + } + + @Override + public boolean visitVariable(UVariable node) { + if (node instanceof UField) { + checkField((UField) node); + } + return super.visitVariable(node); + } + + private void checkField(UField field) { + PsiModifierList modifierList = field.getModifierList(); + if (modifierList == null || !modifierList.hasModifierProperty(PsiModifier.STATIC)) { + return; + } + + PsiType type = field.getType(); + if (!(type instanceof PsiClassType)) { + return; + } + + String fqn = type.getCanonicalText(); + if (fqn.startsWith("java.")) { + return; + } + PsiClass cls = ((PsiClassType) type).resolve(); + if (cls == null) { + return; + } + if (fqn.startsWith("android.")) { + if (isLeakCandidate(cls, mContext.getEvaluator())) { + String message = "Do not place Android context classes in static fields; " + + "this is a memory leak (and also breaks Instant Run)"; + report(field, modifierList, message); + } + } else { + // User application object -- look to see if that one itself has + // static fields? + // We only check *one* level of indirection here + int count = 0; + for (PsiField referenced : cls.getAllFields()) { + // Only check a few; avoid getting bogged down on large classes + if (count++ == 20) { + break; + } + + PsiType innerType = referenced.getType(); + if (!(innerType instanceof PsiClassType)) { + continue; + } + + fqn = innerType.getCanonicalText(); + if (fqn.startsWith("java.")) { + continue; + } + PsiClass innerCls = ((PsiClassType) innerType).resolve(); + if (innerCls == null) { + continue; + } + if (fqn.startsWith("android.")) { + if (isLeakCandidate(innerCls, mContext.getEvaluator())) { + String message = + "Do not place Android context classes in static fields " + + "(static reference to `" + + cls.getName() + "` which has field " + + "`" + referenced.getName() + "` pointing to `" + + innerCls.getName() + "`); " + + "this is a memory leak (and also breaks Instant Run)"; + report(field, modifierList, message); + break; + } + } + } + } + } + + private void report(@NonNull PsiField field, @NonNull PsiModifierList modifierList, + @NonNull String message) { + Location location = mContext.getLocation( + modifierList.getTextRange().getLength() > 0 ? modifierList : field); + mContext.report(ISSUE, field, location, message); + } + } + + private static boolean isLeakCandidate( + @NonNull PsiClass cls, + @NonNull JavaEvaluator evaluator) { + return evaluator.extendsClass(cls, CLASS_CONTEXT, false) + || evaluator.extendsClass(cls, CLASS_VIEW, false) + || evaluator.extendsClass(cls, CLASS_FRAGMENT, false); + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LocaleDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LocaleDetector.java new file mode 100644 index 00000000000..98dd50bd33a --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LocaleDetector.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import static com.android.SdkConstants.FORMAT_METHOD; +import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; + +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.LintClient; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Checks for errors related to locale handling + */ +public class LocaleDetector extends Detector implements Detector.UastScanner { + private static final Implementation IMPLEMENTATION = new Implementation( + LocaleDetector.class, + Scope.JAVA_FILE_SCOPE); + + /** Calling risky convenience methods */ + public static final Issue STRING_LOCALE = Issue.create( + "DefaultLocale", //$NON-NLS-1$ + "Implied default locale in case conversion", + + "Calling `String#toLowerCase()` or `#toUpperCase()` *without specifying an " + + "explicit locale* is a common source of bugs. The reason for that is that those " + + "methods will use the current locale on the user's device, and even though the " + + "code appears to work correctly when you are developing the app, it will fail " + + "in some locales. For example, in the Turkish locale, the uppercase replacement " + + "for `i` is *not* `I`.\n" + + "\n" + + "If you want the methods to just perform ASCII replacement, for example to convert " + + "an enum name, call `String#toUpperCase(Locale.US)` instead. If you really want to " + + "use the current locale, call `String#toUpperCase(Locale.getDefault())` instead.", + + Category.CORRECTNESS, + 6, + Severity.WARNING, + IMPLEMENTATION) + .addMoreInfo( + "http://developer.android.com/reference/java/util/Locale.html#default_locale"); //$NON-NLS-1$ + + /** Constructs a new {@link LocaleDetector} */ + public LocaleDetector() { + } + + // ---- Implements JavaScanner ---- + + @Override + public List getApplicableMethodNames() { + if (LintClient.isStudio()) { + // In the IDE, don't flag toUpperCase/toLowerCase; these + // are already flagged by built-in IDE inspections, so we don't + // want duplicate warnings. + return Collections.singletonList(FORMAT_METHOD); + } else { + return Arrays.asList( + // Only when not running in the IDE + "toLowerCase", //$NON-NLS-1$ + "toUpperCase", //$NON-NLS-1$ + FORMAT_METHOD + ); + } + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + if (JavaEvaluator.isMemberInClass(method, TYPE_STRING)) { + String name = method.getName(); + if (name.equals(FORMAT_METHOD)) { + checkFormat(context, method, call); + } else if (method.getParameterList().getParametersCount() == 0) { + Location location = context.getUastNameLocation(call); + String message = String.format( + "Implicitly using the default locale is a common source of bugs: " + + "Use `%1$s(Locale)` instead", name); + context.report(STRING_LOCALE, call, location, message); + } + } + } + + /** Returns true if the given node is a parameter to a Logging call */ + private static boolean isLoggingParameter(@NonNull UCallExpression node) { + UCallExpression parentCall = + UastUtils.getParentOfType(node, UCallExpression.class, true); + if (parentCall != null) { + String name = parentCall.getMethodName(); + if (name != null && name.length() == 1) { // "d", "i", "e" etc in Log + PsiMethod method = parentCall.resolve(); + return JavaEvaluator.isMemberInClass(method, LogDetector.LOG_CLS); + } + } + + return false; + } + + private static void checkFormat( + @NonNull JavaContext context, + @NonNull PsiMethod method, + @NonNull UCallExpression call) { + // Only check the non-locale version of String.format + if (method.getParameterList().getParametersCount() == 0 + || !context.getEvaluator().parameterHasType(method, 0, TYPE_STRING)) { + return; + } + List expressions = call.getValueArguments(); + if (expressions.isEmpty()) { + return; + } + + // Find the formatting string + UExpression first = expressions.get(0); + Object value = ConstantEvaluator.evaluate(context, first); + if (!(value instanceof String)) { + return; + } + + String format = (String) value; + if (StringFormatDetector.isLocaleSpecific(format)) { + if (isLoggingParameter(call)) { + return; + } + Location location = context.getUastLocation(call); + String message = + "Implicitly using the default locale is a common source of bugs: " + + "Use `String.format(Locale, ...)` instead"; + context.report(STRING_LOCALE, call, location, message); + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java index a5e221cba61..e9b44085d5a 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java @@ -20,7 +20,10 @@ import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; 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.UastLintUtils; import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; @@ -28,21 +31,40 @@ import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiVariable; + +import org.jetbrains.uast.UBinaryExpressionWithType; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UClassInitializer; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UField; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UQualifiedReferenceExpression; +import org.jetbrains.uast.USimpleNameReferenceExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; import java.util.Arrays; -import java.util.Iterator; +import java.util.Collections; +import java.util.HashMap; import java.util.List; - -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import java.util.Locale; +import java.util.Map; /** * Detector for finding inefficiencies and errors in logging calls. */ -public class LogDetector extends Detector implements UastScanner { +public class LogDetector extends Detector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION = new Implementation( - LogDetector.class, Scope.SOURCE_FILE_SCOPE); + LogDetector.class, Scope.JAVA_FILE_SCOPE); /** Log call missing surrounding if */ @@ -93,69 +115,74 @@ public class LogDetector extends Detector implements UastScanner { @SuppressWarnings("SpellCheckingInspection") private static final String IS_LOGGABLE = "isLoggable"; //$NON-NLS-1$ - private static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$ + public static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$ private static final String PRINTLN = "println"; //$NON-NLS-1$ - // ---- Implements UastScanner ---- + private static final Map TAG_PAIRS; + static { + Map pairs = new HashMap(); + pairs.put("d", "DEBUG"); + pairs.put("e", "ERROR"); + pairs.put("i", "INFO"); + pairs.put("v", "VERBOSE"); + pairs.put("w", "WARN"); + TAG_PAIRS = Collections.unmodifiableMap(pairs); + } + + // ---- Implements Detector.UastScanner ---- @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Arrays.asList( - "d", //$NON-NLS-1$ - "e", //$NON-NLS-1$ - "i", //$NON-NLS-1$ - "v", //$NON-NLS-1$ - "w", //$NON-NLS-1$ - PRINTLN, - IS_LOGGABLE); + "d", //$NON-NLS-1$ + "e", //$NON-NLS-1$ + "i", //$NON-NLS-1$ + "v", //$NON-NLS-1$ + "w", //$NON-NLS-1$ + PRINTLN, + IS_LOGGABLE); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - UFunction method = node.resolve(context); - if (method == null) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + JavaEvaluator evaluator = context.getEvaluator(); + if (!JavaEvaluator.isMemberInClass(method, LOG_CLS)) { return; } - if (!UastUtils.getContainingClassOrEmpty(method).matchesFqName(LOG_CLS)) { - return; - } - - String name = node.getFunctionName(); + String name = method.getName(); boolean withinConditional = IS_LOGGABLE.equals(name) || - checkWithinConditional(context, node.getParent(), node); + checkWithinConditional(context, node.getContainingElement(), node); // See if it's surrounded by an if statement (and it's one of the non-error, spammy // log methods (info, verbose, etc)) if (("i".equals(name) || "d".equals(name) || "v".equals(name) || PRINTLN.equals(name)) - && !withinConditional - && performsWork(node) - && context.getLintContext().isEnabled(CONDITIONAL)) { + && !withinConditional + && performsWork(node) + && context.isEnabled(CONDITIONAL)) { String message = String.format("The log call Log.%1$s(...) should be " + - "conditional: surround with `if (Log.isLoggable(...))` or " + - "`if (BuildConfig.DEBUG) { ... }`", - node.getFunctionName()); - context.report(CONDITIONAL, node, context.getLocation(node), message); + "conditional: surround with `if (Log.isLoggable(...))` or " + + "`if (BuildConfig.DEBUG) { ... }`", + node.getMethodIdentifier()); + context.report(CONDITIONAL, node, context.getUastLocation(node), message); } // Check tag length - if (context.getLintContext().isEnabled(LONG_TAG)) { + if (context.isEnabled(LONG_TAG)) { int tagArgumentIndex = PRINTLN.equals(name) ? 1 : 0; - if (method.getValueParameterCount() > tagArgumentIndex - && method.getValueParameters().get(tagArgumentIndex).getType().matchesFqName(TYPE_STRING) - && node.getValueArgumentCount() == method.getValueParameterCount()) { - Iterator iterator = node.getValueArguments().iterator(); - if (tagArgumentIndex == 1) { - iterator.next(); - } - UExpression argument = iterator.next(); - String tag = argument.evaluateString(); + PsiParameterList parameterList = method.getParameterList(); + List argumentList = node.getValueArguments(); + if (evaluator.parameterHasType(method, tagArgumentIndex, TYPE_STRING) + && parameterList.getParametersCount() == argumentList.size()) { + UExpression argument = argumentList.get(tagArgumentIndex); + String tag = ConstantEvaluator.evaluateString(context, argument, true); if (tag != null && tag.length() > 23) { String message = String.format( - "The logging tag can be at most 23 characters, was %1$d (%2$s)", - tag.length(), tag); - context.report(LONG_TAG, node, context.getLocation(node), message); + "The logging tag can be at most 23 characters, was %1$d (%2$s)", + tag.length(), tag); + context.report(LONG_TAG, node, context.getUastLocation(node), message); } } } @@ -163,33 +190,41 @@ public class LogDetector extends Detector implements UastScanner { } /** Returns true if the given logging call performs "work" to compute the message */ - private static boolean performsWork( - @NonNull UCallExpression node) { - int messageArgumentIndex = PRINTLN.equals(node.getFunctionName()) ? 2 : 1; - if (node.getValueArgumentCount() >= messageArgumentIndex) { - Iterator iterator = node.getValueArguments().iterator(); - UExpression argument = null; - for (int i = 0; i <= messageArgumentIndex; i++) { - argument = iterator.next(); - } + private static boolean performsWork(@NonNull UCallExpression node) { + String referenceName = node.getMethodName(); + if (referenceName == null) { + return false; + } + int messageArgumentIndex = PRINTLN.equals(referenceName) ? 2 : 1; + List arguments = node.getValueArguments(); + if (arguments.size() > messageArgumentIndex) { + UExpression argument = arguments.get(messageArgumentIndex); if (argument == null) { return false; } - if (UastLiteralUtils.isStringLiteral(argument) || argument instanceof USimpleReferenceExpression) { + if (argument instanceof ULiteralExpression) { return false; } - if (argument instanceof UBinaryExpression) { - String string = argument.evaluateString(); + if (argument instanceof UBinaryExpressionWithType) { + String string = UastUtils.evaluateString(argument); //noinspection VariableNotUsedInsideIf if (string != null) { // does it resolve to a constant? return false; } - } else if (argument instanceof UQualifiedExpression) { - String string = argument.evaluateString(); + } else if (argument instanceof USimpleNameReferenceExpression) { + // Just a simple local variable/field reference + return false; + } else if (argument instanceof UQualifiedReferenceExpression) { + String string = UastUtils.evaluateString(argument); //noinspection VariableNotUsedInsideIf if (string != null) { return false; } + PsiElement resolved = ((UQualifiedReferenceExpression) argument).resolve(); + if (resolved instanceof PsiVariable) { + // Just a reference to a property/field, parameter or variable + return false; + } } // Method invocations etc @@ -198,96 +233,78 @@ public class LogDetector extends Detector implements UastScanner { return false; } - + private static boolean checkWithinConditional( - @NonNull UastAndroidContext context, + @NonNull JavaContext context, @Nullable UElement curr, @NonNull UCallExpression logCall) { while (curr != null) { if (curr instanceof UIfExpression) { UIfExpression ifNode = (UIfExpression) curr; - UExpression condition = ifNode.getCondition(); - if (condition instanceof UCallExpression) { - UCallExpression call = (UCallExpression) condition; - if (IS_LOGGABLE.equals(call.getFunctionName())) { - checkTagConsistent(context, logCall, call); - } - } else if (condition instanceof UQualifiedExpression) { - UCallExpression call = UastUtils.getCallElementFromQualified((UQualifiedExpression) condition); - if (call != null && IS_LOGGABLE.equals(call.getFunctionName())) { + List chain = UastUtils.getQualifiedChain(ifNode.getCondition()); + if (!chain.isEmpty() && chain.get(chain.size() - 1) instanceof UCallExpression) { + UCallExpression call = (UCallExpression) chain.get(chain.size() - 1); + if (IS_LOGGABLE.equals(call.getMethodName())) { checkTagConsistent(context, logCall, call); } } return true; } else if (curr instanceof UCallExpression + || curr instanceof UMethod + || curr instanceof UClassInitializer + || curr instanceof UField || curr instanceof UClass) { // static block break; } - curr = curr.getParent(); + curr = curr.getContainingElement(); } return false; } /** Checks that the tag passed to Log.s and Log.isLoggable match */ - private static void checkTagConsistent(UastAndroidContext context, UCallExpression logCall, - UCallExpression call) { - Iterator isLogIterator = call.getValueArguments().iterator(); - Iterator logIterator = logCall.getValueArguments().iterator(); - if (!isLogIterator.hasNext() || !logIterator.hasNext()) { + private static void checkTagConsistent(JavaContext context, UCallExpression logCall, + UCallExpression isLoggableCall) { + List isLoggableArguments = isLoggableCall.getValueArguments(); + List logArguments = logCall.getValueArguments(); + if (isLoggableArguments.isEmpty() || logArguments.isEmpty()) { return; } - UExpression isLoggableTag = isLogIterator.next(); - UExpression logTag = logIterator.next(); + UExpression isLoggableTag = isLoggableArguments.get(0); + UExpression logTag = logArguments.get(0); - String logCallName = logCall.getFunctionName(); + String logCallName = logCall.getMethodName(); if (logCallName == null) { return; } - boolean isPrintln = PRINTLN.equals(logCallName); - if (isPrintln) { - if (!logIterator.hasNext()) { - return; - } - logTag = logIterator.next(); + if (isPrintln && logArguments.size() > 1) { + logTag = logArguments.get(1); } - JavaContext lintContext = context.getLintContext(); if (logTag != null) { - String isLoggableTagString = null; - String logTagString = null; - boolean isOk = true; - - Object isLoggableTagValue = isLoggableTag.evaluate(); - Object logTagValue = logTag.evaluate(); - - if (isLoggableTagValue instanceof String && logTagValue instanceof String) { - isLoggableTagString = (String) isLoggableTagValue; - logTagString = (String) logTagValue; - isOk = isLoggableTagString.equals(logTagString); - } else if (isLoggableTag instanceof UResolvable && logTag instanceof UResolvable) { - UDeclaration isLoggableTagResolved = ((UResolvable) isLoggableTag).resolve(context); - UDeclaration logTagResolved = ((UResolvable) logTag).resolve(context); - if (isLoggableTagResolved != null && logTagResolved != null) { - isOk = isLoggableTagResolved.equals(logTagResolved); - } - } - - if (!isOk) { - Location location = context.getLocation(logTag); - Location alternate = context.getLocation(isLoggableTag); - if (location != null && alternate != null) { + if (!UastLintUtils.areIdentifiersEqual(isLoggableTag, logTag)) { + PsiNamedElement resolved1 = UastUtils.tryResolveNamed(isLoggableTag); + PsiNamedElement resolved2 = UastUtils.tryResolveNamed(logTag); + if ((resolved1 == null || resolved2 == null || !resolved1.equals(resolved2)) + && context.isEnabled(WRONG_TAG)) { + Location location = context.getUastLocation(logTag); + Location alternate = context.getUastLocation(isLoggableTag); alternate.setMessage("Conflicting tag"); location.setSecondary(alternate); - + String isLoggableDescription = resolved1 != null + ? resolved1.getName() + : isLoggableTag.asRenderString(); + String logCallDescription = resolved2 != null + ? resolved2.getName() + : logTag.asRenderString(); String message = String.format( "Mismatched tags: the `%1$s()` and `isLoggable()` calls typically " + - "should pass the same tag: `%2$s` versus `%3$s`", + "should pass the same tag: `%2$s` versus `%3$s`", logCallName, - isLoggableTagValue, - logTagValue); - context.report(WRONG_TAG, call, location, message); + isLoggableDescription, + logCallDescription); + context.report(WRONG_TAG, isLoggableCall, location, message); } } } @@ -295,50 +312,40 @@ public class LogDetector extends Detector implements UastScanner { // Check log level versus the actual log call type (e.g. flag // if (Log.isLoggable(TAG, Log.DEBUG) Log.info(TAG, "something") - if (logCallName.length() != 1 || !isLogIterator.hasNext()) { // e.g. println + if (logCallName.length() != 1 || isLoggableArguments.size() < 2) { // e.g. println return; } - UExpression isLoggableLevel = isLogIterator.next(); + UExpression isLoggableLevel = isLoggableArguments.get(1); if (isLoggableLevel == null) { return; } - String levelString = isLoggableLevel.toString(); - if (isLoggableLevel instanceof UQualifiedExpression) { - String identifier = ((UQualifiedExpression)isLoggableLevel).getSelectorAsIdentifier(); - if (identifier != null) { - levelString = identifier; - } - } - if (levelString.isEmpty()) { + + PsiNamedElement resolved = UastUtils.tryResolveNamed(isLoggableLevel); + if (resolved == null) { return; } - char levelChar = Character.toLowerCase(levelString.charAt(0)); - if (logCallName.charAt(0) == levelChar || !lintContext.isEnabled(WRONG_TAG)) { - return; - } - switch (levelChar) { - case 'd': - case 'e': - case 'i': - case 'v': - case 'w': - break; - default: - // Some other char; e.g. user passed in a literal value or some - // local constant or variable alias + + if (resolved instanceof PsiVariable) { + PsiClass containingClass = UastUtils.getContainingClass(resolved); + if (containingClass == null + || !"android.util.Log".equals(containingClass.getQualifiedName()) + || resolved.getName() == null + || resolved.getName().equals(TAG_PAIRS.get(logCallName))) { return; - } - String expectedCall = String.valueOf(levelChar); - String message = String.format( - "Mismatched logging levels: when checking `isLoggable` level `%1$s`, the " + - "corresponding log call should be `Log.%2$s`, not `Log.%3$s`", - levelString, expectedCall, logCallName); - Location location = context.getLocation(logCall.getFunctionNameElement()); - Location alternate = context.getLocation(isLoggableLevel); - if (location != null && alternate != null) { + } + + String expectedCall = resolved.getName().substring(0, 1) + .toLowerCase(Locale.getDefault()); + + String message = String.format( + "Mismatched logging levels: when checking `isLoggable` level `%1$s`, the " + + "corresponding log call should be `Log.%2$s`, not `Log.%3$s`", + resolved.getName(), expectedCall, logCallName); + Location location = context.getUastLocation(logCall.getMethodIdentifier()); + Location alternate = context.getUastLocation(isLoggableLevel); alternate.setMessage("Conflicting tag"); location.setSecondary(alternate); - context.report(WRONG_TAG, call, location, message); + context.report(WRONG_TAG, isLoggableCall, location, message); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/MathDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/MathDetector.java new file mode 100644 index 00000000000..bc6a2ca0eab --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/MathDetector.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Arrays; +import java.util.List; + +/** + * Looks for usages of {@link Math} methods which can be replaced with + * {@code android.util.FloatMath} methods to avoid casting. + */ +public class MathDetector extends Detector implements Detector.UastScanner { + /** The main issue discovered by this detector */ + public static final Issue ISSUE = Issue.create( + "FloatMath", //$NON-NLS-1$ + "Using `FloatMath` instead of `Math`", + + "In older versions of Android, using `android.util.FloatMath` was recommended " + + "for performance reasons when operating on floats. However, on modern hardware " + + "doubles are just as fast as float (though they take more memory), and in " + + "recent versions of Android, `FloatMath` is actually slower than using `java.lang.Math` " + + "due to the way the JIT optimizes `java.lang.Math`. Therefore, you should use " + + "`Math` instead of `FloatMath` if you are only targeting Froyo and above.", + + Category.PERFORMANCE, + 3, + Severity.WARNING, + new Implementation( + MathDetector.class, + Scope.JAVA_FILE_SCOPE)) + .addMoreInfo( + "http://developer.android.com/guide/practices/design/performance.html#avoidfloat"); //$NON-NLS-1$ + + /** Constructs a new {@link MathDetector} check */ + public MathDetector() { + } + + // ---- Implements JavaScanner ---- + + @Nullable + @Override + public List getApplicableMethodNames() { + return Arrays.asList( + "sin", //$NON-NLS-1$ + "cos", //$NON-NLS-1$ + "ceil", //$NON-NLS-1$ + "sqrt", //$NON-NLS-1$ + "floor" //$NON-NLS-1$ + ); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + if (context.getEvaluator().isMemberInClass(method, "android.util.FloatMath") + && context.getProject().getMinSdk() >= 8) { + String message = String.format( + "Use `java.lang.Math#%1$s` instead of `android.util.FloatMath#%1$s()` " + + "since it is faster as of API 8", method.getName()); + + Location location = context.getUastLocation(call.getMethodIdentifier()); + context.report(ISSUE, call, location, message); + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/MergeRootFrameLayoutDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/MergeRootFrameLayoutDetector.java index bc87d42c6e0..26f6e1cab45 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/MergeRootFrameLayoutDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/MergeRootFrameLayoutDetector.java @@ -21,37 +21,45 @@ import static com.android.SdkConstants.ATTR_BACKGROUND; import static com.android.SdkConstants.ATTR_FOREGROUND; import static com.android.SdkConstants.ATTR_LAYOUT; import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; -import static com.android.SdkConstants.DOT_JAVA; import static com.android.SdkConstants.FRAME_LAYOUT; import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.R_LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.VIEW_INCLUDE; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceType; +import com.android.tools.klint.client.api.AndroidReference; +import com.android.tools.klint.client.api.UastLintUtils; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LayoutDetector; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Location.Handle; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; import com.android.utils.Pair; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; -import org.jetbrains.uast.UExpression; import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UQualifiedExpression; -import org.jetbrains.uast.UastUtils; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UQualifiedReferenceExpression; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Element; import org.w3c.dom.Node; -import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -64,7 +72,7 @@ import java.util.Set; /** * Checks whether a root FrameLayout can be replaced with a {@code } tag. */ -public class MergeRootFrameLayoutDetector extends LayoutDetector implements UastScanner { +public class MergeRootFrameLayoutDetector extends LayoutDetector implements Detector.UastScanner { /** * Set of layouts that we want to enable the warning for. We only warn for * {@code }'s that are the root of a layout included from @@ -95,7 +103,7 @@ public class MergeRootFrameLayoutDetector extends LayoutDetector implements Uast Severity.WARNING, new Implementation( MergeRootFrameLayoutDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.SOURCE_FILE))) + EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE))) .addMoreInfo( "http://android-developers.blogspot.com/2009/03/android-layout-tricks-3-optimize-by.html"); //$NON-NLS-1$ @@ -103,17 +111,6 @@ public class MergeRootFrameLayoutDetector extends LayoutDetector implements Uast public MergeRootFrameLayoutDetector() { } - @Override - @NonNull - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA); - } - @Override public void afterCheckProject(@NonNull Context context) { if (mPending != null && mWhitelistedLayouts != null) { @@ -188,27 +185,26 @@ public class MergeRootFrameLayoutDetector extends LayoutDetector implements Uast mWhitelistedLayouts.add(layout); } - // Implements UastScanner - + // Implements JavaScanner @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("setContentView"); //$NON-NLS-1$ } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - if (node.getValueArgumentCount() != 1) { - return; - } + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + List expressions = call.getValueArguments(); + + if (expressions.size() == 1) { + AndroidReference androidReference = + UastLintUtils.toAndroidReferenceViaResolve(expressions.get(0)); - List argumentList = node.getValueArguments(); - UExpression argument = argumentList.get(0); - if (argument instanceof UQualifiedExpression) { - String expression = argument.renderString(); - if (expression.startsWith(R_LAYOUT_RESOURCE_PREFIX)) { - whiteListLayout(expression.substring(R_LAYOUT_RESOURCE_PREFIX.length())); + if (androidReference != null && androidReference.getType() == ResourceType.LAYOUT) { + whiteListLayout(androidReference.getName()); } } + } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/NonInternationalizedSmsDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/NonInternationalizedSmsDetector.java index 69d3b8c6cbb..4781d1efedc 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/NonInternationalizedSmsDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/NonInternationalizedSmsDetector.java @@ -17,27 +17,26 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import java.io.File; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + import java.util.ArrayList; import java.util.List; -import org.jetbrains.uast.UExpression; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.ULiteralExpression; -import org.jetbrains.uast.USimpleReferenceExpression; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** Detector looking for text messages sent to an unlocalized phone number. */ -public class NonInternationalizedSmsDetector extends Detector implements UastScanner { +public class NonInternationalizedSmsDetector extends Detector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "UnlocalizedSms", //$NON-NLS-1$ @@ -52,50 +51,50 @@ public class NonInternationalizedSmsDetector extends Detector implements UastSca Severity.WARNING, new Implementation( NonInternationalizedSmsDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); /** Constructs a new {@link NonInternationalizedSmsDetector} check */ public NonInternationalizedSmsDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - - // ---- Implements UastScanner ---- + // ---- Implements JavaScanner ---- @Override - public List getApplicableFunctionNames() { - List methodNames = new ArrayList(2); - methodNames.add("sendTextMessage"); //$NON-NLS-1$ - methodNames.add("sendMultipartTextMessage"); //$NON-NLS-1$ - return methodNames; + public List getApplicableMethodNames() { + List methodNames = new ArrayList(2); + methodNames.add("sendTextMessage"); //$NON-NLS-1$ + methodNames.add("sendMultipartTextMessage"); //$NON-NLS-1$ + return methodNames; } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - String functionName = node.getFunctionName(); - - assert "sendTextMessage".equals(functionName) || //$NON-NLS-1$ - "sendMultipartTextMessage".equals(functionName); //$NON-NLS-1$ - - List args = node.getValueArguments(); - if (args.size() == 5) { - UExpression destinationAddress = args.get(0); - if (destinationAddress instanceof ULiteralExpression - && ((ULiteralExpression)destinationAddress).isString()) { - String number = (String) ((ULiteralExpression) destinationAddress).getValue(); - - if (number != null && !number.startsWith("+")) { //$NON-NLS-1$ - context.report(ISSUE, node, context.getLocation(destinationAddress), - "To make sure the SMS can be sent by all users, please start the SMS number " + - "with a + and a country code or restrict the code invocation to people in the country " + - "you are targeting."); - } - } + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + if (call.getReceiver() == null) { + // "sendTextMessage"/"sendMultipartTextMessage" in the code with no operand + return; } + + List args = call.getValueArguments(); + if (args.size() != 5) { + return; + } + UExpression destinationAddress = args.get(0); + if (!(destinationAddress instanceof ULiteralExpression)) { + return; + } + Object literal = ((ULiteralExpression) destinationAddress).getValue(); + if (!(literal instanceof String)) { + return; + } + String number = (String) literal; + if (number.startsWith("+")) { //$NON-NLS-1$ + return; + } + context.report(ISSUE, call, context.getUastLocation(destinationAddress), + "To make sure the SMS can be sent by all users, please start the SMS number " + + "with a + and a country code or restrict the code invocation to people in the " + + "country you are targeting."); } } \ No newline at end of file diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverdrawDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverdrawDetector.java index 1c7b1168e68..7f5cc938830 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverdrawDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverdrawDetector.java @@ -16,32 +16,65 @@ package com.android.tools.klint.checks; -import static com.android.SdkConstants.*; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_BACKGROUND; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_PARENT; +import static com.android.SdkConstants.ATTR_THEME; +import static com.android.SdkConstants.ATTR_TILE_MODE; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.DRAWABLE_PREFIX; +import static com.android.SdkConstants.NULL_RESOURCE; +import static com.android.SdkConstants.R_CLASS; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.TAG_ACTIVITY; +import static com.android.SdkConstants.TAG_APPLICATION; +import static com.android.SdkConstants.TAG_BITMAP; +import static com.android.SdkConstants.TAG_STYLE; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.TRANSPARENT_COLOR; +import static com.android.SdkConstants.VALUE_DISABLED; import static com.android.tools.klint.detector.api.LintUtils.endsWith; import static com.android.utils.SdkUtils.getResourceFieldName; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.tools.klint.client.api.AndroidReference; +import com.android.tools.klint.client.api.UastLintUtils; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LayoutDetector; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Project; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; import com.android.utils.Pair; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiAnonymousClass; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UAnonymousClass; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UQualifiedReferenceExpression; +import org.jetbrains.uast.USimpleNameReferenceExpression; import org.jetbrains.uast.visitor.AbstractUastVisitor; -import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -54,17 +87,14 @@ import java.util.Collection; 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; /** * Check which looks for overdraw problems where view areas are painted and then * painted over, meaning that the bottom paint operation is a waste of time. */ -public class OverdrawDetector extends LayoutDetector implements UastScanner { - private static final String R_STYLE_PREFIX = "R.style."; //$NON-NLS-1$ +public class OverdrawDetector extends LayoutDetector implements Detector.UastScanner { private static final String SET_THEME = "setTheme"; //$NON-NLS-1$ /** The main issue discovered by this detector */ @@ -97,7 +127,7 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner { Severity.WARNING, new Implementation( OverdrawDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.SOURCE_FILE, Scope.ALL_RESOURCE_FILES))); + EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES))); /** Mapping from FQN activity names to theme names registered in the manifest */ private Map mActivityToTheme; @@ -111,10 +141,6 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner { /** List of theme names registered in the project which have blank backgrounds */ private List mBlankThemes; - /** Set of activities registered in the manifest. We will limit the Java analysis to - * these. */ - private Set mActivities; - /** List of drawable resources that are not flagged for overdraw (XML drawables * except for {@code } drawables without tiling) */ private List mValidDrawables; @@ -140,17 +166,6 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner { || folderType == ResourceFolderType.DRAWABLE; } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA); - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - /** Is the given theme a "blank" theme (one not painting its background) */ private boolean isBlankTheme(String name) { if (name.startsWith("@android:style/Theme_")) { //$NON-NLS-1$ @@ -384,11 +399,6 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner { } } - if (mActivities == null) { - mActivities = new HashSet(); - } - mActivities.add(name); - String theme = element.getAttributeNS(ANDROID_URI, ATTR_THEME); if (theme != null && !theme.isEmpty()) { if (mActivityToTheme == null) { @@ -470,72 +480,75 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner { // ---- Implements UastScanner ---- + + @Nullable @Override - public UastVisitor createUastVisitor(@NonNull UastAndroidContext context) { - if (!context.getLintContext().getProject().getReportIssues()) { - return null; + public List applicableSuperClasses() { + return Collections.singletonList("android.app.Activity"); + } + + @Nullable + @Override + public List> getApplicableUastTypes() { + return Collections.>singletonList(UClass.class); + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + if (!context.getProject().getReportIssues()) { + return; + } + String name = declaration.getQualifiedName(); + if (name != null) { + declaration.accept(new OverdrawVisitor(name, declaration)); } - return new OverdrawVisitor(); } private class OverdrawVisitor extends AbstractUastVisitor { - private static final String ACTIVITY = "Activity"; //$NON-NLS-1$ - private String mClassFqn; + private final String mName; + private final PsiClass mCls; - @Override - public boolean visitClass(@NotNull UClass node) { - String name = node.getName(); - if (mActivities != null && mActivities.contains(mClassFqn) || name.endsWith(ACTIVITY) - || node.isSubclassOf(CLASS_ACTIVITY)) { - mClassFqn = node.getFqName(); - return false; - } - - return true; // Done: No need to look inside this class + public OverdrawVisitor(String name, PsiClass cls) { + mName = name; + mCls = cls; } - // Store R.layout references in activity classes in a map mapping back layouts - // to activities @Override - public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) { - if (node.getSelector().renderString().equals("layout") //$NON-NLS-1$ - && node.getReceiver() instanceof USimpleReferenceExpression - && ((USimpleReferenceExpression) node.getReceiver()).getIdentifier() - .equals("R") //$NON-NLS-1$ - && node.getParent() instanceof UQualifiedExpression) { - String layout = ((UQualifiedExpression) node.getParent()).getSelector().renderString(); - registerLayoutActivity(layout, mClassFqn); + public boolean visitClass(UClass node) { + // Don't go into inner classes + if (mCls.equals(node.getPsi())) { + return true; } - - return false; + + return super.visitClass(node); } + @Override + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(node); + if (androidReference != null && androidReference.getType() == ResourceType.LAYOUT) { + registerLayoutActivity(androidReference.getName(), mName); + } - // Look for setTheme(R.style.whatever) and register as a theme registration - // for the current activity - + return super.visitSimpleNameReferenceExpression(node); + } @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - if (SET_THEME.equals(node.getFunctionName())) { + public boolean visitCallExpression(UCallExpression node) { + if (SET_THEME.equals(node.getMethodName()) && node.getValueArgumentCount() == 1) { // Look at argument - List args = node.getValueArguments(); - if (args.size() == 1) { - UExpression arg = args.get(0); - if (arg instanceof UQualifiedExpression) { - String resource = arg.renderString(); - if (resource.startsWith(R_STYLE_PREFIX)) { - if (mActivityToTheme == null) { - mActivityToTheme = new HashMap(); - } - String name = ((UQualifiedExpression) arg).getSelector().renderString(); - mActivityToTheme.put(mClassFqn, STYLE_RESOURCE_PREFIX + name); - } + UExpression arg = node.getValueArguments().get(0); + AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(arg); + if (androidReference != null && androidReference.getType() == ResourceType.STYLE) { + String style = androidReference.getName(); + if (mActivityToTheme == null) { + mActivityToTheme = new HashMap(); } + mActivityToTheme.put(mName, STYLE_RESOURCE_PREFIX + style); } } - return false; + return super.visitCallExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java index a2347bf66cf..a1c90740f3a 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java @@ -16,26 +16,32 @@ package com.android.tools.klint.checks; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.util.PsiTreeUtil; + +import org.jetbrains.uast.UClass; import java.util.Collections; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Checks that subclasses of certain APIs are overriding all methods that were abstract * in one or more earlier API levels that are still targeted by the minSdkVersion * of this project. */ -public class OverrideConcreteDetector extends Detector implements UastScanner { +public class OverrideConcreteDetector extends Detector implements Detector.UastScanner { /** Are previously-abstract methods all overridden? */ public static final Issue ISSUE = Issue.create( "OverrideAbstract", //$NON-NLS-1$ @@ -55,7 +61,7 @@ public class OverrideConcreteDetector extends Detector implements UastScanner { Severity.FATAL, new Implementation( OverrideConcreteDetector.class, - Scope.SOURCE_FILE_SCOPE) + Scope.JAVA_FILE_SCOPE) ); // This check is currently hardcoded for the specific case of the @@ -76,24 +82,22 @@ public class OverrideConcreteDetector extends Detector implements UastScanner { public OverrideConcreteDetector() { } - // ---- Implements UastScanner ---- - + // ---- Implements JavaScanner ---- + @Nullable @Override - public List getApplicableSuperClasses() { + public List applicableSuperClasses() { return Collections.singletonList(NOTIFICATION_LISTENER_SERVICE_FQN); } @Override - public void visitClass(UastAndroidContext context, UClass node) { - if (node == null) { - return; - } - if (node.hasModifier(UastModifier.ABSTRACT)) { + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.isAbstract(declaration)) { return; } - int minSdk = Math.max(context.getLintContext().getProject().getMinSdk(), getTargetApi(node)); + int minSdk = Math.max(context.getProject().getMinSdk(), getTargetApi(declaration)); if (minSdk >= CONCRETE_IN) { return; } @@ -101,28 +105,26 @@ public class OverrideConcreteDetector extends Detector implements UastScanner { String[] methodNames = {ON_NOTIFICATION_POSTED, ON_NOTIFICATION_REMOVED}; for (String methodName : methodNames) { boolean found = false; - List allFunctions = UastUtils.getAllFunctions(node, context); - for (UFunction method : allFunctions) { - if (!method.matchesName(methodName)) { - continue; - } - + for (PsiMethod method : declaration.findMethodsByName(methodName, true)) { // Make sure it's not the base method, but that it's been defined // in a subclass, concretely - UClass containingClass = UastUtils.getContainingClassOrEmpty(method); - if (containingClass.matchesFqName(NOTIFICATION_LISTENER_SERVICE_FQN)) { + PsiClass containingClass = method.getContainingClass(); + if (containingClass == null) { + continue; + } + if (NOTIFICATION_LISTENER_SERVICE_FQN.equals(containingClass.getQualifiedName())) { continue; } // Make sure subclass isn't just defining another abstract definition // of the method - if (method.hasModifier(UastModifier.ABSTRACT)) { + if (evaluator.isAbstract(method)) { continue; } // Make sure it has the exact right signature - if (method.getValueParameterCount() != 1) { + if (method.getParameterList().getParametersCount() != 1) { continue; // Wrong signature } - if (!method.getValueParameters().get(0).getType().matchesFqName(STATUS_BAR_NOTIFICATION_FQN)) { + if (!evaluator.parameterHasType(method, 0, STATUS_BAR_NOTIFICATION_FQN)) { continue; } @@ -132,25 +134,24 @@ public class OverrideConcreteDetector extends Detector implements UastScanner { if (!found) { String message = String.format( - "Must override `%1$s.%2$s(%3$s)`: Method was abstract until %4$d, and your `minSdkVersion` is %5$d", - NOTIFICATION_LISTENER_SERVICE_FQN, methodName, - STATUS_BAR_NOTIFICATION_FQN, CONCRETE_IN, minSdk); - context.report(ISSUE, node, context.getLocation(node.getNameElement()), - message); + "Must override `%1$s.%2$s(%3$s)`: Method was abstract until %4$d, and your `minSdkVersion` is %5$d", + NOTIFICATION_LISTENER_SERVICE_FQN, methodName, + STATUS_BAR_NOTIFICATION_FQN, CONCRETE_IN, minSdk); + context.reportUast(ISSUE, declaration, context.getUastNameLocation(declaration), message); break; } } } - private static int getTargetApi(UClass node) { + private static int getTargetApi(@NonNull PsiClass node) { while (node != null) { - int targetApi = ApiDetector.Companion.getTargetApi(node.getAnnotations()); + int targetApi = ApiDetector.getTargetApi(node.getModifierList()); if (targetApi != -1) { return targetApi; } - node = UastUtils.getContainingClass(node); + node = PsiTreeUtil.getParentOfType(node, PsiClass.class, true); } return -1; diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ParcelDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ParcelDetector.java index 3e4826bd951..d81e06415d4 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ParcelDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ParcelDetector.java @@ -15,27 +15,33 @@ */ package com.android.tools.klint.checks; +import static com.android.SdkConstants.CLASS_PARCELABLE; + import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiModifier; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; -import org.jetbrains.uast.visitor.UastVisitor; +import org.jetbrains.uast.UAnonymousClass; +import org.jetbrains.uast.UClass; + +import java.util.Collections; +import java.util.List; /** * Looks for Parcelable classes that are missing a CREATOR field */ -public class ParcelDetector extends Detector implements UastScanner { +public class ParcelDetector extends Detector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( @@ -45,81 +51,55 @@ public class ParcelDetector extends Detector implements UastScanner { "According to the `Parcelable` interface documentation, " + "\"Classes implementing the Parcelable interface must also have a " + "static field called `CREATOR`, which is an object implementing the " + - "`Parcelable.Creator` interface.", + "`Parcelable.Creator` interface.\"", - Category.USABILITY, + Category.CORRECTNESS, 3, Severity.ERROR, new Implementation( ParcelDetector.class, - Scope.SOURCE_FILE_SCOPE)) + Scope.JAVA_FILE_SCOPE)) .addMoreInfo("http://developer.android.com/reference/android/os/Parcelable.html"); /** Constructs a new {@link ParcelDetector} check */ public ParcelDetector() { } - @NonNull + // ---- Implements JavaScanner ---- + + @Nullable @Override - public Speed getSpeed() { - return Speed.FAST; + public List applicableSuperClasses() { + return Collections.singletonList(CLASS_PARCELABLE); } - // ---- Implements UastScanner ---- - @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { - return new ParcelVisitor(context); - } - - private static class ParcelVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; - - public ParcelVisitor(UastAndroidContext context) { - mContext = context; + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + if (declaration instanceof UAnonymousClass) { + // Anonymous classes aren't parcelable + return; } - @Override - public boolean visitClass(@NotNull UClass node) { - // Only applies to concrete classes - if (node.getKind() != UastClassKind.CLASS || node.hasModifier(UastModifier.ABSTRACT)) { - return true; - } - - for (UType reference : node.getSuperTypes()) { - String name = reference.getName(); - if (name.equals("Parcelable")) { - UVariable field = UastUtils.findStaticMemberOfType(node, "CREATOR", UVariable.class); - boolean hasField = field != null && field.hasModifier(UastModifier.JVM_FIELD) && field.hasModifier(UastModifier.STATIC); - boolean hasNamedCompanionObject = false; - if (!hasField) { - for (UClass companion : node.getCompanions()) { - if (companion.getName().equals("CREATOR")) { - hasNamedCompanionObject = true; - break; - } - } - - } - if (!hasField && !hasNamedCompanionObject) { - // Make doubly sure that we're really implementing - // android.os.Parcelable - UClass parcelable = reference.resolve(mContext); - if (parcelable != null) { - if (!parcelable.isSubclassOf("android.os.Parcelable")) { - return true; - } - } - Location location = mContext.getLocation(node.getNameElement()); - mContext.report(ISSUE, node, location, - "This class implements `Parcelable` but does not " - + "provide a `CREATOR` field"); - } - } - } - - return true; + // Only applies to concrete classes + if (declaration.isInterface()) { + return; + } + if (declaration.hasModifierProperty(PsiModifier.ABSTRACT)) { + return; } + // Parceling spans is handled in TextUtils#CHAR_SEQUENCE_CREATOR + if (context.getEvaluator().implementsInterface(declaration, + "android.text.ParcelableSpan", false)) { + return; + } + + PsiField field = declaration.findFieldByName("CREATOR", false); + if (field == null) { + Location location = context.getUastNameLocation(declaration); + context.reportUast(ISSUE, declaration, location, + "This class implements `Parcelable` but does not " + + "provide a `CREATOR` field"); + } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionFinder.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionFinder.java new file mode 100644 index 00000000000..7a8cfbc49f4 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionFinder.java @@ -0,0 +1,254 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.CLASS_INTENT; +import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION; +import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_READ; +import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_WRITE; +import static org.jetbrains.uast.UastBinaryExpressionWithTypeKind.TYPE_CAST; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.UastLintUtils; +import com.android.tools.klint.detector.api.JavaContext; +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.PsiExpressionList; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiLiteral; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiParenthesizedExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiTypeCastExpression; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.util.PsiTreeUtil; + +import org.jetbrains.uast.UBinaryExpressionWithType; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UParenthesizedExpression; +import org.jetbrains.uast.UastCallKind; +import org.jetbrains.uast.UastLiteralUtils; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; + +import java.util.List; + +/** + * Utility for locating permissions required by an intent or content resolver + */ +public class PermissionFinder { + /** + * Operation that has a permission requirement -- such as a method call, + * a content resolver read or write operation, an intent, etc. + */ + public enum Operation { + CALL, ACTION, READ, WRITE; + + /** Prefix to use when describing a name with a permission requirement */ + public String prefix() { + switch (this) { + case ACTION: + return "by intent"; + case READ: + return "to read"; + case WRITE: + return "to write"; + case CALL: + default: + return "by"; + } + } + } + + /** A permission requirement given a name and operation */ + public static class Result { + @NonNull public final PermissionRequirement requirement; + @NonNull public final String name; + @NonNull public final Operation operation; + + public Result( + @NonNull Operation operation, + @NonNull PermissionRequirement requirement, + @NonNull String name) { + this.operation = operation; + this.requirement = requirement; + this.name = name; + } + } + + /** + * Searches for a permission requirement for the given parameter in the given call + * + * @param operation the operation to look up + * @param context the context to use for lookup + * @param parameter the parameter which contains the value which implies the permission + * @return the result with the permission requirement, or null if nothing is found + */ + @Nullable + public static Result findRequiredPermissions( + @NonNull Operation operation, + @NonNull JavaContext context, + @NonNull UElement parameter) { + + // To find the permission required by an intent, we proceed in 3 steps: + // (1) Locate the parameter in the start call that corresponds to + // the Intent + // + // (2) Find the place where the intent is initialized, and figure + // out the action name being passed to it. + // + // (3) Find the place where the action is defined, and look for permission + // annotations on that action declaration! + + return new PermissionFinder(context, operation).search(parameter); + } + + private PermissionFinder(@NonNull JavaContext context, @NonNull Operation operation) { + mContext = context; + mOperation = operation; + } + + @NonNull private final JavaContext mContext; + @NonNull private final Operation mOperation; + + @Nullable + public Result search(@NonNull UElement node) { + if (UastLiteralUtils.isNullLiteral(node)) { + return null; + } else if (node instanceof UIfExpression) { + UIfExpression expression = (UIfExpression) node; + if (expression.getThenExpression() != null) { + Result result = search(expression.getThenExpression()); + if (result != null) { + return result; + } + } + if (expression.getElseExpression() != null) { + Result result = search(expression.getElseExpression()); + if (result != null) { + return result; + } + } + } else if (UastExpressionUtils.isTypeCast(node)) { + UBinaryExpressionWithType cast = (UBinaryExpressionWithType) node; + UExpression operand = cast.getOperand(); + return search(operand); + } else if (node instanceof UParenthesizedExpression) { + UParenthesizedExpression parens = (UParenthesizedExpression) node; + UExpression expression = parens.getExpression(); + if (expression != null) { + return search(expression); + } + } else if (UastExpressionUtils.isConstructorCall(node) && mOperation == Operation.ACTION) { + // Identifies "new Intent(argument)" calls and, if found, continues + // resolving the argument instead looking for the action definition + UCallExpression call = (UCallExpression) node; + UReferenceExpression classReference = call.getClassReference(); + String type = classReference != null ? UastUtils.getQualifiedName(classReference) : null; + if (CLASS_INTENT.equals(type)) { + List expressions = call.getValueArguments(); + if (!expressions.isEmpty()) { + UExpression action = expressions.get(0); + if (action != null) { + return search(action); + } + } + } + return null; + } else if (node instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) node).resolve(); + if (resolved instanceof PsiField) { + PsiField field = (PsiField) resolved; + if (mOperation == Operation.ACTION) { + PsiModifierList modifierList = field.getModifierList(); + PsiAnnotation annotation = modifierList != null + ? modifierList.findAnnotation(PERMISSION_ANNOTATION) : null; + if (annotation != null) { + return getPermissionRequirement(field, annotation); + } + } else if (mOperation == Operation.READ || mOperation == Operation.WRITE) { + String fqn = mOperation == Operation.READ + ? PERMISSION_ANNOTATION_READ : PERMISSION_ANNOTATION_WRITE; + PsiModifierList modifierList = field.getModifierList(); + PsiAnnotation annotation = modifierList != null + ? modifierList.findAnnotation(fqn) : null; + if (annotation != null) { + PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes(); + PsiNameValuePair o = attributes.length == 1 ? attributes[0] : null; + if (o != null && o.getValue() instanceof PsiAnnotation) { + annotation = (PsiAnnotation) o.getValue(); + if (PERMISSION_ANNOTATION.equals(annotation.getQualifiedName())) { + return getPermissionRequirement(field, annotation); + } + } else { + // The complex annotations used for read/write cannot be + // expressed in the external annotations format, so they're inlined. + // (See Extractor.AnnotationData#write). + // + // Instead we've inlined the fields of the annotation on the + // outer one: + return getPermissionRequirement(field, annotation); + } + } + } else { + assert false : mOperation; + } + } + + if (resolved instanceof PsiVariable) { + PsiVariable variable = (PsiVariable) resolved; + UExpression lastAssignment = + UastLintUtils.findLastAssignment(variable, node, mContext); + + if (lastAssignment != null) { + return search(lastAssignment); + } + } + } + + return null; + } + + @NonNull + private Result getPermissionRequirement( + @NonNull PsiField field, + @NonNull PsiAnnotation annotation) { + PermissionRequirement requirement = PermissionRequirement.create(mContext, annotation); + PsiClass containingClass = field.getContainingClass(); + String name = containingClass != null + ? containingClass.getName() + "." + field.getName() + : field.getName(); + assert name != null; + return new Result(mOperation, requirement, name); + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionHolder.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionHolder.java index b145aa5086b..32ff92d6ab1 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionHolder.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionHolder.java @@ -18,6 +18,8 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.sdklib.AndroidVersion; import java.util.Collections; import java.util.Set; @@ -31,34 +33,67 @@ public interface PermissionHolder { /** Returns true if the permission holder has been granted the given permission */ boolean hasPermission(@NonNull String permission); - /** Returns true if the given permission is known to be revocable for targetSdkVersion >= M */ + /** Returns true if the given permission is known to be revocable for targetSdkVersion ≥ M */ boolean isRevocable(@NonNull String permission); + @NonNull + AndroidVersion getMinSdkVersion(); + + @NonNull + AndroidVersion getTargetSdkVersion(); + /** * A convenience implementation of {@link PermissionHolder} backed by a set */ class SetPermissionLookup implements PermissionHolder { - private Set myGrantedPermissions; - private Set myRevocablePermissions; + private final Set mGrantedPermissions; + private final Set mRevocablePermissions; + private final AndroidVersion mMinSdkVersion; + private final AndroidVersion mTargetSdkVersion; - public SetPermissionLookup(@NonNull Set grantedPermissions, - @NonNull Set revocablePermissions) { - myGrantedPermissions = grantedPermissions; - myRevocablePermissions = revocablePermissions; + public SetPermissionLookup( + @NonNull Set grantedPermissions, + @NonNull Set revocablePermissions, + @NonNull AndroidVersion minSdkVersion, + @NonNull AndroidVersion targetSdkVersion) { + mGrantedPermissions = grantedPermissions; + mRevocablePermissions = revocablePermissions; + mMinSdkVersion = minSdkVersion; + mTargetSdkVersion = targetSdkVersion; } + @VisibleForTesting + public SetPermissionLookup(@NonNull Set grantedPermissions, + @NonNull Set revocablePermissions) { + this(grantedPermissions, revocablePermissions, AndroidVersion.DEFAULT, + AndroidVersion.DEFAULT); + } + + @VisibleForTesting public SetPermissionLookup(@NonNull Set grantedPermissions) { this(grantedPermissions, Collections.emptySet()); } @Override public boolean hasPermission(@NonNull String permission) { - return myGrantedPermissions.contains(permission); + return mGrantedPermissions.contains(permission); } @Override public boolean isRevocable(@NonNull String permission) { - return myRevocablePermissions.contains(permission); + return mRevocablePermissions.contains(permission); + } + + @NonNull + @Override + public AndroidVersion getMinSdkVersion() { + return mMinSdkVersion; + } + + @NonNull + @Override + public AndroidVersion getTargetSdkVersion() { + return mTargetSdkVersion; } /** @@ -69,7 +104,9 @@ public interface PermissionHolder { @NonNull public static PermissionHolder join(@NonNull PermissionHolder lookup, @NonNull PermissionRequirement requirement) { - SetPermissionLookup empty = new SetPermissionLookup(Collections.emptySet()); + SetPermissionLookup empty = new SetPermissionLookup(Collections.emptySet(), + Collections.emptySet(), lookup.getMinSdkVersion(), + lookup.getTargetSdkVersion()); return join(lookup, requirement.getMissingPermissions(empty)); } @@ -92,6 +129,18 @@ public interface PermissionHolder { public boolean isRevocable(@NonNull String permission) { return lookup.isRevocable(permission); } + + @NonNull + @Override + public AndroidVersion getMinSdkVersion() { + return lookup.getMinSdkVersion(); + } + + @NonNull + @Override + public AndroidVersion getTargetSdkVersion() { + return lookup.getTargetSdkVersion(); + } }; } return lookup; diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionRequirement.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionRequirement.java index 0b37e07e3ec..b7317d6bf05 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionRequirement.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionRequirement.java @@ -24,23 +24,21 @@ import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_COND import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; -import com.android.tools.klint.detector.api.Context; +import com.android.sdklib.AndroidVersion; +import com.android.tools.klint.detector.api.ConstantEvaluator; +import com.android.tools.klint.detector.api.JavaContext; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.intellij.psi.JavaTokenType; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiArrayInitializerMemberValue; +import com.intellij.psi.tree.IElementType; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - -import com.intellij.psi.JavaPsiFacade; -import com.intellij.psi.PsiElementFactory; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.java.JavaUastLanguagePlugin; -import org.jetbrains.uast.visitor.AbstractUastVisitor; /** * A permission requirement is a boolean expression of permission names that a @@ -50,7 +48,9 @@ public abstract class PermissionRequirement { public static final String ATTR_PROTECTION_LEVEL = "protectionLevel"; //$NON-NLS-1$ public static final String VALUE_DANGEROUS = "dangerous"; //$NON-NLS-1$ - private final UAnnotation annotation; + protected final PsiAnnotation annotation; + private int firstApi; + private int lastApi; @SuppressWarnings("ConstantConditions") public static final PermissionRequirement NONE = new PermissionRequirement(null) { @@ -59,6 +59,11 @@ public abstract class PermissionRequirement { return true; } + @Override + public boolean appliesTo(@NonNull PermissionHolder available) { + return false; + } + @Override public boolean isConditional() { return false; @@ -86,7 +91,7 @@ public abstract class PermissionRequirement { @Nullable @Override - public UastBinaryOperator getOperator() { + public IElementType getOperator() { return null; } @@ -97,56 +102,227 @@ public abstract class PermissionRequirement { } }; - private PermissionRequirement(@NonNull UAnnotation annotation) { + private PermissionRequirement(@NonNull PsiAnnotation annotation) { this.annotation = annotation; } @NonNull public static PermissionRequirement create( - @Nullable UastAndroidContext context, - @NonNull UAnnotation annotation) { - String value = (String)annotation.getValue(ATTR_VALUE); - if (value != null && !value.isEmpty()) { - for (int i = 0, n = value.length(); i < n; i++) { - char c = value.charAt(i); - // See if it's a complex expression and if so build it up - if (c == '&' || c == '|' || c == '^') { - return Complex.parse(annotation, context, value); - } - } + @NonNull JavaContext context, + @NonNull PsiAnnotation annotation) { + String value = getAnnotationStringValue(annotation, ATTR_VALUE); + if (value != null && !value.isEmpty()) { return new Single(annotation, value); } - Object v = annotation.getValue(ATTR_ANY_OF); - if (v != null) { - if (v instanceof String[]) { - String[] anyOf = (String[])v; - if (anyOf.length > 0) { - return new Many(annotation, UastBinaryOperator.LOGICAL_OR, anyOf); - } - } else if (v instanceof String) { - String[] anyOf = new String[] { (String)v }; - return new Many(annotation, UastBinaryOperator.LOGICAL_OR, anyOf); + String[] anyOf = getAnnotationStringValues(annotation, ATTR_ANY_OF); + if (anyOf != null) { + if (anyOf.length > 1) { + return new Many(annotation, JavaTokenType.OROR, anyOf); + } else if (anyOf.length == 1) { + return new Single(annotation, anyOf[0]); } } - v = annotation.getValue(ATTR_ALL_OF); - if (v != null) { - if (v instanceof String[]) { - String[] allOf = (String[])v; - if (allOf.length > 0) { - return new Many(annotation, UastBinaryOperator.LOGICAL_AND, allOf); - } - } else if (v instanceof String) { - String[] allOf = new String[] { (String)v }; - return new Many(annotation, UastBinaryOperator.LOGICAL_AND, allOf); + String[] allOf = getAnnotationStringValues(annotation, ATTR_ALL_OF); + if (allOf != null) { + if (allOf.length > 1) { + return new Many(annotation, JavaTokenType.ANDAND, allOf); + } else if (allOf.length == 1) { + return new Single(annotation, allOf[0]); } } return NONE; } + @Nullable + public static Boolean getAnnotationBooleanValue(@Nullable PsiAnnotation annotation, + @NonNull String name) { + if (annotation != null) { + PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name); + if (attributeValue == null && ATTR_VALUE.equals(name)) { + attributeValue = annotation.findDeclaredAttributeValue(null); + } + // Use constant evaluator since we want to resolve field references as well + if (attributeValue != null) { + Object o = ConstantEvaluator.evaluate(null, attributeValue); + if (o instanceof Boolean) { + return (Boolean) o; + } + } + } + + return null; + } + + @Nullable + public static Long getAnnotationLongValue(@Nullable PsiAnnotation annotation, + @NonNull String name) { + if (annotation != null) { + PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name); + if (attributeValue == null && ATTR_VALUE.equals(name)) { + attributeValue = annotation.findDeclaredAttributeValue(null); + } + // Use constant evaluator since we want to resolve field references as well + if (attributeValue != null) { + Object o = ConstantEvaluator.evaluate(null, attributeValue); + if (o instanceof Number) { + return ((Number)o).longValue(); + } + } + } + + return null; + } + + @Nullable + public static Double getAnnotationDoubleValue(@Nullable PsiAnnotation annotation, + @NonNull String name) { + if (annotation != null) { + PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name); + if (attributeValue == null && ATTR_VALUE.equals(name)) { + attributeValue = annotation.findDeclaredAttributeValue(null); + } + // Use constant evaluator since we want to resolve field references as well + if (attributeValue != null) { + Object o = ConstantEvaluator.evaluate(null, attributeValue); + if (o instanceof Number) { + return ((Number)o).doubleValue(); + } + } + } + + return null; + } + + @Nullable + public static String getAnnotationStringValue(@Nullable PsiAnnotation annotation, + @NonNull String name) { + if (annotation != null) { + PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name); + if (attributeValue == null && ATTR_VALUE.equals(name)) { + attributeValue = annotation.findDeclaredAttributeValue(null); + } + // Use constant evaluator since we want to resolve field references as well + if (attributeValue != null) { + Object o = ConstantEvaluator.evaluate(null, attributeValue); + if (o instanceof String) { + return (String) o; + } + } + } + + return null; + } + + @Nullable + public static String[] getAnnotationStringValues(@Nullable PsiAnnotation annotation, + @NonNull String name) { + if (annotation != null) { + PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name); + if (attributeValue == null && ATTR_VALUE.equals(name)) { + attributeValue = annotation.findDeclaredAttributeValue(null); + } + if (attributeValue instanceof PsiArrayInitializerMemberValue) { + PsiAnnotationMemberValue[] initializers = + ((PsiArrayInitializerMemberValue) attributeValue).getInitializers(); + List result = Lists.newArrayListWithCapacity(initializers.length); + ConstantEvaluator constantEvaluator = new ConstantEvaluator(null); + for (PsiAnnotationMemberValue element : initializers) { + Object o = constantEvaluator.evaluate(element); + if (o instanceof String) { + result.add((String)o); + } + } + if (result.isEmpty()) { + return null; + } else { + return result.toArray(new String[0]); + } + } else { + // Use constant evaluator since we want to resolve field references as well + if (attributeValue != null) { + Object o = ConstantEvaluator.evaluate(null, attributeValue); + if (o instanceof String) { + return new String[]{(String) o}; + } else if (o instanceof String[]) { + return (String[])o; + } else if (o instanceof Object[]) { + Object[] array = (Object[]) o; + List strings = Lists.newArrayListWithCapacity(array.length); + for (Object element : array) { + if (element instanceof String) { + strings.add((String) element); + } + } + return strings.toArray(new String[0]); + } + } + } + } + + return null; + } + + /** + * Returns false if this permission does not apply given the specified minimum and + * target sdk versions + * + * @param minSdkVersion the minimum SDK version + * @param targetSdkVersion the target SDK version + * @return true if this permission requirement applies for the given versions + */ + /** + * Returns false if this permission does not apply given the specified minimum and target + * sdk versions + * + * @param available the permission holder which also knows the min and target versions + * @return true if this permission requirement applies for the given versions + */ + protected boolean appliesTo(@NonNull PermissionHolder available) { + if (firstApi == 0) { // initialized? + firstApi = -1; // initialized, not specified + + // Not initialized + String range = getAnnotationStringValue(annotation, "apis"); + if (range != null) { + // Currently only support the syntax "a..b" where a and b are inclusive end points + // and where "a" and "b" are optional + int index = range.indexOf(".."); + if (index != -1) { + try { + if (index > 0) { + firstApi = Integer.parseInt(range.substring(0, index)); + } else { + firstApi = 1; + } + if (index + 2 < range.length()) { + lastApi = Integer.parseInt(range.substring(index + 2)); + } else { + lastApi = Integer.MAX_VALUE; + } + } catch (NumberFormatException ignore) { + } + } + } + } + + if (firstApi != -1) { + AndroidVersion minSdkVersion = available.getMinSdkVersion(); + if (minSdkVersion.getFeatureLevel() > lastApi) { + return false; + } + + AndroidVersion targetSdkVersion = available.getTargetSdkVersion(); + if (targetSdkVersion.getFeatureLevel() < firstApi) { + return false; + } + } + return true; + } + /** * Returns whether this requirement is conditional, meaning that there are * some circumstances in which the requirement is not necessary. For @@ -165,9 +341,9 @@ public abstract class PermissionRequirement { * @return true if this requirement is conditional */ public boolean isConditional() { - Object o = annotation.getValue(ATTR_CONDITIONAL); - if (o instanceof Boolean) { - return (Boolean)o; + Boolean o = getAnnotationBooleanValue(annotation, ATTR_CONDITIONAL); + if (o != null) { + return o; } return false; } @@ -228,7 +404,7 @@ public abstract class PermissionRequirement { * for leaf nodes */ @Nullable - public abstract UastBinaryOperator getOperator(); + public abstract IElementType getOperator(); /** * Returns nested requirements, combined via {@link #getOperator()} @@ -240,7 +416,7 @@ public abstract class PermissionRequirement { private static class Single extends PermissionRequirement { public final String name; - public Single(@NonNull UAnnotation annotation, @NonNull String name) { + public Single(@NonNull PsiAnnotation annotation, @NonNull String name) { super(annotation); this.name = name; } @@ -252,7 +428,7 @@ public abstract class PermissionRequirement { @Nullable @Override - public UastBinaryOperator getOperator() { + public IElementType getOperator() { return null; } @@ -274,7 +450,7 @@ public abstract class PermissionRequirement { @Override public boolean isSatisfied(@NonNull PermissionHolder available) { - return available.hasPermission(name); + return available.hasPermission(name) || !appliesTo(available); } @Override @@ -299,14 +475,14 @@ public abstract class PermissionRequirement { } } - protected static void appendOperator(StringBuilder sb, UastBinaryOperator operator) { + protected static void appendOperator(StringBuilder sb, IElementType operator) { sb.append(' '); - if (operator == UastBinaryOperator.LOGICAL_AND) { + if (operator == JavaTokenType.ANDAND) { sb.append("and"); - } else if (operator == UastBinaryOperator.LOGICAL_OR) { + } else if (operator == JavaTokenType.OROR) { sb.append("or"); } else { - assert operator == UastBinaryOperator.BITWISE_XOR : operator; + assert operator == JavaTokenType.XOR : operator; sb.append("xor"); } sb.append(' '); @@ -316,16 +492,16 @@ public abstract class PermissionRequirement { * Require a series of permissions, all with the same operator. */ private static class Many extends PermissionRequirement { - public final UastBinaryOperator operator; + public final IElementType operator; public final List permissions; public Many( - @NonNull UAnnotation annotation, - UastBinaryOperator operator, + @NonNull PsiAnnotation annotation, + IElementType operator, String[] names) { super(annotation); - assert operator == UastBinaryOperator.LOGICAL_OR - || operator == UastBinaryOperator.LOGICAL_AND : operator; + assert operator == JavaTokenType.OROR + || operator == JavaTokenType.ANDAND : operator; assert names.length >= 2; this.operator = operator; this.permissions = Lists.newArrayListWithExpectedSize(names.length); @@ -355,17 +531,17 @@ public abstract class PermissionRequirement { @Override public boolean isSatisfied(@NonNull PermissionHolder available) { - if (operator == UastBinaryOperator.LOGICAL_AND) { + if (operator == JavaTokenType.ANDAND) { for (PermissionRequirement requirement : permissions) { - if (!requirement.isSatisfied(available)) { + if (!requirement.isSatisfied(available) && requirement.appliesTo(available)) { return false; } } return true; } else { - assert operator == UastBinaryOperator.LOGICAL_OR : operator; + assert operator == JavaTokenType.OROR : operator; for (PermissionRequirement requirement : permissions) { - if (requirement.isSatisfied(available)) { + if (requirement.isSatisfied(available) || !requirement.appliesTo(available)) { return true; } } @@ -411,7 +587,7 @@ public abstract class PermissionRequirement { @Override public boolean isRevocable(@NonNull PermissionHolder revocable) { // TODO: Pass in the available set of permissions here, and if - // the operator is BinaryOperator.LOGICAL_OR, only return revocable=true + // the operator is JavaTokenType.OROR, only return revocable=true // if an unsatisfied permission is also revocable. In other words, // if multiple permissions are allowed, and some of them are satisfied and // not revocable the overall permission requirement is not revocable. @@ -425,7 +601,7 @@ public abstract class PermissionRequirement { @Nullable @Override - public UastBinaryOperator getOperator() { + public IElementType getOperator() { return operator; } @@ -436,211 +612,9 @@ public abstract class PermissionRequirement { } } - /** - * Require multiple permissions. This is a group of permissions with some - * associated boolean logic, such as "B or (C and (D or E))". - */ - private static class Complex extends PermissionRequirement { - public final UastBinaryOperator operator; - public final PermissionRequirement left; - public final PermissionRequirement right; - - public Complex( - @NonNull UAnnotation annotation, - UastBinaryOperator operator, - PermissionRequirement left, - PermissionRequirement right) { - super(annotation); - this.operator = operator; - this.left = left; - this.right = right; - } - - @Override - public boolean isSingle() { - return false; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - boolean needsParentheses = left instanceof Complex && - ((Complex) left).operator != UastBinaryOperator.LOGICAL_AND; - if (needsParentheses) { - sb.append('('); - } - sb.append(left.toString()); - if (needsParentheses) { - sb.append(')'); - } - - appendOperator(sb, operator); - - needsParentheses = right instanceof Complex && - ((Complex) right).operator != UastBinaryOperator.LOGICAL_AND; - if (needsParentheses) { - sb.append('('); - } - sb.append(right.toString()); - if (needsParentheses) { - sb.append(')'); - } - - return sb.toString(); - } - - @Override - public boolean isSatisfied(@NonNull PermissionHolder available) { - boolean satisfiedLeft = left.isSatisfied(available); - boolean satisfiedRight = right.isSatisfied(available); - if (operator == UastBinaryOperator.LOGICAL_AND) { - return satisfiedLeft && satisfiedRight; - } else if (operator == UastBinaryOperator.LOGICAL_OR) { - return satisfiedLeft || satisfiedRight; - } else { - assert operator == UastBinaryOperator.BITWISE_XOR : operator; - return satisfiedLeft ^ satisfiedRight; - } - } - - @Override - public String describeMissingPermissions(@NonNull PermissionHolder available) { - boolean satisfiedLeft = left.isSatisfied(available); - boolean satisfiedRight = right.isSatisfied(available); - if (operator == UastBinaryOperator.LOGICAL_AND || operator == UastBinaryOperator.LOGICAL_OR) { - if (satisfiedLeft) { - if (satisfiedRight) { - return ""; - } - return right.describeMissingPermissions(available); - } else if (satisfiedRight) { - return left.describeMissingPermissions(available); - } else { - StringBuilder sb = new StringBuilder(); - sb.append(left.describeMissingPermissions(available)); - appendOperator(sb, operator); - sb.append(right.describeMissingPermissions(available)); - return sb.toString(); - } - } else { - assert operator == UastBinaryOperator.BITWISE_XOR : operator; - return toString(); - } - } - - @Override - protected void addMissingPermissions(@NonNull PermissionHolder available, - @NonNull Set missing) { - boolean satisfiedLeft = left.isSatisfied(available); - boolean satisfiedRight = right.isSatisfied(available); - if (operator == UastBinaryOperator.LOGICAL_AND || operator == UastBinaryOperator.LOGICAL_OR) { - if (satisfiedLeft) { - if (satisfiedRight) { - return; - } - right.addMissingPermissions(available, missing); - } else if (satisfiedRight) { - left.addMissingPermissions(available, missing); - } else { - left.addMissingPermissions(available, missing); - right.addMissingPermissions(available, missing); - } - } else { - assert operator == UastBinaryOperator.BITWISE_XOR : operator; - left.addMissingPermissions(available, missing); - right.addMissingPermissions(available, missing); - } - } - - @Override - protected void addRevocablePermissions(@NonNull Set result, - @NonNull PermissionHolder revocable) { - left.addRevocablePermissions(result, revocable); - right.addRevocablePermissions(result, revocable); - } - - @Override - public boolean isRevocable(@NonNull PermissionHolder revocable) { - // TODO: If operator == BinaryOperator.LOGICAL_OR only return - // revocable the there isn't a non-revocable term which is also satisfied. - return left.isRevocable(revocable) || right.isRevocable(revocable); - } - - @NonNull - public static PermissionRequirement parse(@NonNull UAnnotation annotation, - @Nullable UastAndroidContext uastContext, @NonNull final String value) { - // Parse an expression of the form (A op1 B op2 C) op3 (D op4 E) etc. - // We'll just use the Java parser to handle this to ensure that operator - // precedence etc is correct. - if (uastContext == null) { - return NONE; - } - Context context = uastContext.getLintContext(); - - PsiElementFactory factory = JavaPsiFacade.getInstance( - context.getClient().getProject()).getElementFactory(); - - UElement node = JavaUastLanguagePlugin.INSTANCE.getConverter(). - convertWithParent(factory.createClassFromText( - "class Test { void test() {\n" - + "boolean result = " + value - + ";\n}\n}" - , null).getContainingFile()); - - if (node != null) { - final AtomicReference reference = new AtomicReference(); - node.accept(new AbstractUastVisitor() { - @Override - public boolean visitVariable(@NotNull UVariable node) { - reference.set(node.getInitializer()); - return true; - } - }); - UExpression expression = reference.get(); - if (expression != null) { - return parse(annotation, expression); - } - } - - return NONE; - } - - private static PermissionRequirement parse( - @NonNull UAnnotation annotation, - @NonNull UExpression expression) { - if (expression instanceof UQualifiedExpression) { - return new Single(annotation, expression.renderString()); - } else if (expression instanceof UBinaryExpression) { - UBinaryExpression binaryExpression = (UBinaryExpression) expression; - UastBinaryOperator operator = binaryExpression.getOperator(); - if (operator == UastBinaryOperator.LOGICAL_AND - || operator == UastBinaryOperator.LOGICAL_OR - || operator == UastBinaryOperator.BITWISE_XOR) { - PermissionRequirement left = parse(annotation, binaryExpression.getLeftOperand()); - PermissionRequirement right = parse(annotation, binaryExpression.getRightOperand()); - return new Complex(annotation, operator, left, right); - } - } - return NONE; - } - - @Nullable - @Override - public UastBinaryOperator getOperator() { - return operator; - } - - @NonNull - @Override - public Iterable getChildren() { - return Arrays.asList(left, right); - } - } - /** * Returns true if the given permission name is a revocable permission for - * targetSdkVersion >= 23 + * targetSdkVersion ≥ 23 * * @param name permission name * @return true if this is a revocable permission @@ -661,6 +635,7 @@ public abstract class PermissionRequirement { "android.permission.READ_CALL_LOG", "android.permission.READ_CELL_BROADCASTS", "android.permission.READ_CONTACTS", + "android.permission.READ_EXTERNAL_STORAGE", "android.permission.READ_PHONE_STATE", "android.permission.READ_PROFILE", "android.permission.READ_SMS", @@ -675,6 +650,8 @@ public abstract class PermissionRequirement { "android.permission.WRITE_CALENDAR", "android.permission.WRITE_CALL_LOG", "android.permission.WRITE_CONTACTS", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.WRITE_SETTINGS", "android.permission.WRITE_PROFILE", "android.permission.WRITE_SOCIAL_STREAM", "com.android.voicemail.permission.ADD_VOICEMAIL", diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PluralsDatabase.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PluralsDatabase.java new file mode 100644 index 00000000000..8bea3e70aad --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PluralsDatabase.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tools.klint.checks; + +import static com.android.tools.klint.checks.PluralsDatabase.Quantity.few; +import static com.android.tools.klint.checks.PluralsDatabase.Quantity.many; +import static com.android.tools.klint.checks.PluralsDatabase.Quantity.one; +import static com.android.tools.klint.checks.PluralsDatabase.Quantity.two; +import static com.android.tools.klint.checks.PluralsDatabase.Quantity.zero; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.detector.api.LintUtils; +import com.google.common.collect.Maps; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +/** + * Database used by the {@link PluralsDetector} to get information + * about plural forms for a given language + */ +public class PluralsDatabase { + private static final EnumSet NONE = EnumSet.noneOf(Quantity.class); + + private static final PluralsDatabase sInstance = new PluralsDatabase(); + private final Map> mPlurals = Maps.newHashMap(); + + /** Bit set if this language uses quantity zero */ + @SuppressWarnings("PointlessBitwiseExpression") + static final int FLAG_ZERO = 1 << 0; + /** Bit set if this language uses quantity one */ + static final int FLAG_ONE = 1 << 1; + /** Bit set if this language uses quantity two */ + static final int FLAG_TWO = 1 << 2; + /** Bit set if this language uses quantity few */ + static final int FLAG_FEW = 1 << 3; + /** Bit set if this language uses quantity many */ + static final int FLAG_MANY = 1 << 4; + /** Bit set if this language has multiple values that match quantity zero */ + static final int FLAG_MULTIPLE_ZERO = 1 << 5; + /** Bit set if this language has multiple values that match quantity one */ + static final int FLAG_MULTIPLE_ONE = 1 << 6; + /** Bit set if this language has multiple values that match quantity two */ + static final int FLAG_MULTIPLE_TWO = 1 << 7; + + @NonNull + public static PluralsDatabase get() { + return sInstance; + } + + private static int getFlags(@NonNull String language) { + int index = getLanguageIndex(language); + if (index != -1) { + return FLAGS[index]; + } + return 0; + } + + private static int getLanguageIndex(@NonNull String language) { + int index = Arrays.binarySearch(LANGUAGE_CODES, language); + if (index >= 0) { + assert LANGUAGE_CODES[index].equals(language); + return index; + } else { + return -1; + } + } + + @Nullable + public EnumSet getRelevant(@NonNull String language) { + EnumSet set = mPlurals.get(language); + if (set == null) { + int index = getLanguageIndex(language); + if (index == -1) { + mPlurals.put(language, NONE); + return null; + } + + // Process each item and look for relevance + int flag = FLAGS[index]; + + set = EnumSet.noneOf(Quantity.class); + if ((flag & FLAG_ZERO) != 0) { + set.add(zero); + } + if ((flag & FLAG_ONE) != 0) { + set.add(one); + } + if ((flag & FLAG_TWO) != 0) { + set.add(two); + } + if ((flag & FLAG_FEW) != 0) { + set.add(few); + } + if ((flag & FLAG_MANY) != 0) { + set.add(many); + } + + mPlurals.put(language, set); + } + return set == NONE ? null : set; + } + + @SuppressWarnings("MethodMayBeStatic") + public boolean hasMultipleValuesForQuantity( + @NonNull String language, + @NonNull Quantity quantity) { + if (quantity == one) { + return (getFlags(language) & FLAG_MULTIPLE_ONE) != 0; + } else if (quantity == two) { + return (getFlags(language) & FLAG_MULTIPLE_TWO) != 0; + } else { + return quantity == zero && (getFlags(language) & FLAG_MULTIPLE_ZERO) != 0; + } + } + + @SuppressWarnings("MethodMayBeStatic") + @Nullable + public String findIntegerExamples(@NonNull String language, @NonNull Quantity quantity) { + if (quantity == one) { + return getExampleForQuantityOne(language); + } else if (quantity == two) { + return getExampleForQuantityTwo(language); + } else if (quantity == zero) { + return getExampleForQuantityZero(language); + } else { + return null; + } + } + + public enum Quantity { + // deliberately lower case to match attribute names + few, many, one, two, zero, other; + + @Nullable + public static Quantity get(@NonNull String name) { + for (Quantity quantity : values()) { + if (name.equals(quantity.name())) { + return quantity; + } + } + + return null; + } + + public static String formatSet(@NonNull EnumSet set) { + List list = new ArrayList(set.size()); + for (Quantity quantity : set) { + list.add('`' + quantity.name() + '`'); + } + return LintUtils.formatList(list, Integer.MAX_VALUE); + } + } + + // GENERATED DATA. + // This data is generated by the #testDatabaseAccurate method in PluralsDatabaseTest + // which will generate the following if it can find an ICU plurals database file + // in the unit test data folder. + + /** Set of language codes relevant to plurals data */ + private static final String[] LANGUAGE_CODES = new String[] { + "af", "ak", "am", "ar", "as", "az", "be", "bg", "bh", "bm", + "bn", "bo", "br", "bs", "ca", "ce", "cs", "cy", "da", "de", + "dv", "dz", "ee", "el", "en", "eo", "es", "et", "eu", "fa", + "ff", "fi", "fo", "fr", "fy", "ga", "gd", "gl", "gu", "gv", + "ha", "he", "hi", "hr", "hu", "hy", "id", "ig", "ii", "in", + "is", "it", "iu", "iw", "ja", "ji", "jv", "ka", "kk", "kl", + "km", "kn", "ko", "ks", "ku", "kw", "ky", "lb", "lg", "ln", + "lo", "lt", "lv", "mg", "mk", "ml", "mn", "mr", "ms", "mt", + "my", "nb", "nd", "ne", "nl", "nn", "no", "nr", "ny", "om", + "or", "os", "pa", "pl", "ps", "pt", "rm", "ro", "ru", "se", + "sg", "si", "sk", "sl", "sn", "so", "sq", "sr", "ss", "st", + "sv", "sw", "ta", "te", "th", "ti", "tk", "tl", "tn", "to", + "tr", "ts", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "wa", + "wo", "xh", "yi", "yo", "zh", "zu" + }; + + /** + * Relevant flags for each language (corresponding to each language listed + * in the same position in {@link #LANGUAGE_CODES}) + */ + private static final int[] FLAGS = new int[] { + 0x0002, 0x0042, 0x0042, 0x001f, 0x0042, 0x0002, 0x005a, 0x0002, + 0x0042, 0x0000, 0x0042, 0x0000, 0x00de, 0x004a, 0x0002, 0x0002, + 0x000a, 0x001f, 0x0002, 0x0002, 0x0002, 0x0000, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0042, 0x0042, 0x0002, + 0x0002, 0x0042, 0x0002, 0x001e, 0x00ce, 0x0002, 0x0042, 0x00ce, + 0x0002, 0x0016, 0x0042, 0x004a, 0x0002, 0x0042, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0042, 0x0002, 0x0006, 0x0016, 0x0000, 0x0002, + 0x0000, 0x0002, 0x0002, 0x0002, 0x0000, 0x0042, 0x0000, 0x0002, + 0x0002, 0x0006, 0x0002, 0x0002, 0x0002, 0x0042, 0x0000, 0x004a, + 0x0063, 0x0042, 0x0042, 0x0002, 0x0002, 0x0042, 0x0000, 0x001a, + 0x0000, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0042, 0x001a, 0x0002, 0x0042, + 0x0002, 0x000a, 0x005a, 0x0006, 0x0000, 0x0042, 0x000a, 0x00ce, + 0x0002, 0x0002, 0x0002, 0x004a, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0000, 0x0042, 0x0002, 0x0042, 0x0002, 0x0000, + 0x0002, 0x0002, 0x0002, 0x005a, 0x0002, 0x0002, 0x0002, 0x0000, + 0x0002, 0x0042, 0x0000, 0x0002, 0x0002, 0x0000, 0x0000, 0x0042 + }; + + @Nullable + private static String getExampleForQuantityZero(@NonNull String language) { + int index = getLanguageIndex(language); + switch (index) { + // set14 + case 72: // lv + return "0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, \u2026"; + case -1: + default: + return null; + } + } + + @Nullable + private static String getExampleForQuantityOne(@NonNull String language) { + int index = getLanguageIndex(language); + switch (index) { + // set1 + case 2: // am + case 4: // as + case 10: // bn + case 29: // fa + case 38: // gu + case 42: // hi + case 61: // kn + case 77: // mr + case 135: // zu + return "0, 1"; + // set11 + case 50: // is + return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026"; + // set12 + case 74: // mk + return "1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, \u2026"; + // set13 + case 117: // tl + return "0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, \u2026"; + // set14 + case 72: // lv + return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026"; + // set2 + case 30: // ff + case 33: // fr + case 45: // hy + return "0, 1"; + // set20 + case 13: // bs + case 43: // hr + case 107: // sr + return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026"; + // set21 + case 36: // gd + return "1, 11"; + // set22 + case 103: // sl + return "1, 101, 201, 301, 401, 501, 601, 701, 1001, \u2026"; + // set27 + case 6: // be + return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026"; + // set28 + case 71: // lt + return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026"; + // set30 + case 98: // ru + case 123: // uk + return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026"; + // set31 + case 12: // br + return "1, 21, 31, 41, 51, 61, 81, 101, 1001, \u2026"; + // set33 + case 39: // gv + return "1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, \u2026"; + // set4 + case 101: // si + return "0, 1"; + // set5 + case 1: // ak + case 8: // bh + case 69: // ln + case 73: // mg + case 92: // pa + case 115: // ti + case 129: // wa + return "0, 1"; + // set7 + case 95: // pt + return "0, 1"; + case -1: + default: + return null; + } + } + + @Nullable + private static String getExampleForQuantityTwo(@NonNull String language) { + int index = getLanguageIndex(language); + switch (index) { + // set21 + case 36: // gd + return "2, 12"; + // set22 + case 103: // sl + return "2, 102, 202, 302, 402, 502, 602, 702, 1002, \u2026"; + // set31 + case 12: // br + return "2, 22, 32, 42, 52, 62, 82, 102, 1002, \u2026"; + // set33 + case 39: // gv + return "2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, \u2026"; + case -1: + default: + return null; + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PreferenceActivityDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PreferenceActivityDetector.java index 1d3bb238bc7..130c5d1c4c0 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PreferenceActivityDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PreferenceActivityDetector.java @@ -23,21 +23,23 @@ import static com.android.SdkConstants.TAG_ACTIVITY; import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; +import com.android.tools.klint.detector.api.Detector.XmlScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UFunction; -import org.jetbrains.uast.UastUtils; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; import org.w3c.dom.Element; import java.util.Collection; @@ -51,7 +53,7 @@ import java.util.Map; * Ensures that PreferenceActivity and its subclasses are never exported. */ public class PreferenceActivityDetector extends Detector - implements Detector.XmlScanner, UastScanner { + implements XmlScanner, Detector.UastScanner { public static final Issue ISSUE = Issue.create( "ExportedPreferenceActivity", //$NON-NLS-1$ "PreferenceActivity should not be exported", @@ -62,7 +64,7 @@ public class PreferenceActivityDetector extends Detector Severity.WARNING, new Implementation( PreferenceActivityDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.SOURCE_FILE))) + EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE))) .addMoreInfo("http://securityintelligence.com/" + "new-vulnerability-android-framework-fragment-injection"); private static final String PREFERENCE_ACTIVITY = "android.preference.PreferenceActivity"; //$NON-NLS-1$ @@ -71,13 +73,8 @@ public class PreferenceActivityDetector extends Detector private final Map mExportedActivities = new HashMap(); - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements XmlScanner ---- + @Override public Collection getApplicableElements() { return Collections.singletonList(TAG_ACTIVITY); @@ -91,7 +88,7 @@ public class PreferenceActivityDetector extends Detector if (fqcn.equals(PREFERENCE_ACTIVITY) && !context.getDriver().isSuppressed(context, ISSUE, element)) { String message = "`PreferenceActivity` should not be exported"; - context.report(ISSUE, context.getLocation(element), message); + context.report(ISSUE, element, context.getLocation(element), message); } mExportedActivities.put(fqcn, context.createLocationHandle(element)); } @@ -122,41 +119,42 @@ public class PreferenceActivityDetector extends Detector // ---- Implements UastScanner ---- - + @Nullable @Override - public List getApplicableSuperClasses() { + public List applicableSuperClasses() { return Collections.singletonList(PREFERENCE_ACTIVITY); } @Override - public void visitClass(UastAndroidContext context, UClass node) { - if (!context.getLintContext().getProject().getReportIssues()) { + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + if (!context.getProject().getReportIssues()) { return; } - String className = node.getName(); - if (node.isSubclassOf(PREFERENCE_ACTIVITY) - && mExportedActivities.containsKey(className)) { - + JavaEvaluator evaluator = context.getEvaluator(); + String className = declaration.getQualifiedName(); + if (evaluator.extendsClass(declaration, PREFERENCE_ACTIVITY, false) + && mExportedActivities.containsKey(className)) { // Ignore the issue if we target an API greater than 19 and the class in // question specifically overrides isValidFragment() and thus knowingly white-lists // valid fragments. - if (context.getLintContext().getMainProject().getTargetSdk() >= 19 - && overridesIsValidFragment(node)) { + if (context.getMainProject().getTargetSdk() >= 19 + && overridesIsValidFragment(evaluator, declaration)) { return; } String message = String.format( - "`PreferenceActivity` subclass `%1$s` should not be exported", - className); - context.report(ISSUE, node, mExportedActivities.get(className).resolve(), message); + "`PreferenceActivity` subclass `%1$s` should not be exported", + className); + Location location = mExportedActivities.get(className).resolve(); + context.reportUast(ISSUE, declaration, location, message); } } - private static boolean overridesIsValidFragment(UClass resolvedClass) { - List functions = UastUtils.findFunctions(resolvedClass, IS_VALID_FRAGMENT); - for (UFunction func : functions) { - if (func.getValueParameterCount() == 1 - && func.getValueParameters().get(0).getType().matchesFqName(TYPE_STRING)) { + private static boolean overridesIsValidFragment( + @NonNull JavaEvaluator evaluator, + @NonNull PsiClass resolvedClass) { + for (PsiMethod method : resolvedClass.findMethodsByName(IS_VALID_FRAGMENT, false)) { + if (evaluator.parametersMatch(method, TYPE_STRING)) { return true; } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PrivateResourceDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PrivateResourceDetector.java index c696bdaaf21..23b693c50dd 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PrivateResourceDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/PrivateResourceDetector.java @@ -46,20 +46,20 @@ import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Project; import com.android.tools.klint.detector.api.ResourceXmlDetector; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; import org.jetbrains.uast.UElement; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -73,15 +73,16 @@ import java.util.List; /** * Check which looks for access of private resources. */ -public class PrivateResourceDetector extends ResourceXmlDetector implements UastScanner { +public class PrivateResourceDetector extends ResourceXmlDetector implements + Detector.UastScanner { /** Attribute for overriding a resource */ private static final String ATTR_OVERRIDE = "override"; @SuppressWarnings("unchecked") private static final Implementation IMPLEMENTATION = new Implementation( PrivateResourceDetector.class, - Scope.SOURCE_AND_RESOURCE_FILES, - Scope.SOURCE_FILE_SCOPE, + Scope.JAVA_AND_RESOURCE_FILES, + Scope.JAVA_FILE_SCOPE, Scope.RESOURCE_FILE_SCOPE); /** The main issue discovered by this detector */ @@ -103,12 +104,6 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast public PrivateResourceDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- @Override @@ -117,21 +112,15 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast } @Override - public void visitResourceReference( - UastAndroidContext uastContext, - UElement element, - String type, - String name, - boolean isFramework - ) { - Context context = uastContext.getLintContext(); - Project project = context.getProject(); - if (project.isGradleProject() && !isFramework) { + public void visitResourceReference(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UElement node, @NonNull ResourceType resourceType, @NonNull String name, + boolean isFramework) { + if (context.getProject().isGradleProject() && !isFramework) { + Project project = context.getProject(); if (project.getGradleProjectModel() != null && project.getCurrentVariant() != null) { - ResourceType resourceType = ResourceType.getEnum(type); - if (resourceType != null && isPrivate(context, resourceType, name)) { + if (isPrivate(context, resourceType, name)) { String message = createUsageErrorMessage(context, resourceType, name); - uastContext.report(ISSUE, element, uastContext.getLocation(element), message); + context.report(ISSUE, node, context.getUastLocation(node), message); } } } @@ -211,6 +200,15 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast } private static boolean isPrivate(Context context, ResourceType type, String name) { + if (type == ResourceType.ID) { + // No need to complain about "overriding" id's. There's no harm + // in doing so. (This avoids warning about cases like for example + // appcompat's (private) @id/title resource, which would otherwise + // flag any attempt to create a resource named title in the user's + // project. + return false; + } + if (context.getProject().isGradleProject()) { ResourceVisibilityLookup lookup = context.getProject().getResourceVisibility(); return lookup.isPrivate(type, name); @@ -286,9 +284,9 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast context.report(ISSUE, location, message); } } - + private static String createOverrideErrorMessage(@NonNull Context context, - @NonNull ResourceType type, @NonNull String name) { + @NonNull ResourceType type, @NonNull String name) { String libraryName = getLibraryName(context, type, name); return String.format("Overriding `@%1$s/%2$s` which is marked as private in %3$s. If " + "deliberate, use tools:override=\"true\", otherwise pick a " diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ReadParcelableDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ReadParcelableDetector.java new file mode 100644 index 00000000000..8cd8cc6eac1 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ReadParcelableDetector.java @@ -0,0 +1,119 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.CLASS_PARCEL; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiClass; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastLiteralUtils; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Arrays; +import java.util.List; + +/** + * Looks for Parcelable classes that are missing a CREATOR field + */ +public class ReadParcelableDetector extends Detector implements Detector.UastScanner { + + /** The main issue discovered by this detector */ + public static final Issue ISSUE = Issue.create( + "ParcelClassLoader", //$NON-NLS-1$ + "Default Parcel Class Loader", + + "The documentation for `Parcel#readParcelable(ClassLoader)` (and its variations) " + + "says that you can pass in `null` to pick up the default class loader. However, " + + "that ClassLoader is a system class loader and is not able to find classes in " + + "your own application.\n" + + "\n" + + "If you are writing your own classes into the `Parcel` (not just SDK classes like " + + "`String` and so on), then you should supply a `ClassLoader` for your application " + + "instead; a simple way to obtain one is to just call `getClass().getClassLoader()` " + + "from your own class.", + + Category.CORRECTNESS, + 3, + Severity.WARNING, + new Implementation( + ReadParcelableDetector.class, + Scope.JAVA_FILE_SCOPE)) + .addMoreInfo("http://developer.android.com/reference/android/os/Parcel.html"); + + /** Constructs a new {@link ReadParcelableDetector} check */ + public ReadParcelableDetector() { + } + + // ---- Implements UastScanner ---- + + @Override + public List getApplicableMethodNames() { + return Arrays.asList( + "readParcelable", + "readParcelableArray", + "readBundle", + "readArray", + "readSparseArray", + "readValue", + "readPersistableBundle" + ); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + PsiClass containingClass = method.getContainingClass(); + if (containingClass == null) { + return; + } + if (!(CLASS_PARCEL.equals(containingClass.getQualifiedName()))) { + return; + } + + List expressions = node.getValueArguments(); + int argumentCount = expressions.size(); + if (argumentCount == 0) { + String message = String.format("Using the default class loader " + + "will not work if you are restoring your own classes. Consider " + + "using for example `%1$s(getClass().getClassLoader())` instead.", + node.getMethodName()); + Location location = context.getUastLocation(node); + context.report(ISSUE, node, location, message); + } else if (argumentCount == 1) { + UExpression parameter = expressions.get(0); + if (UastLiteralUtils.isNullLiteral(parameter)) { + String message = "Passing null here (to use the default class loader) " + + "will not work if you are restoring your own classes. Consider " + + "using for example `getClass().getClassLoader()` instead."; + Location location = context.getUastLocation(node); + context.report(ISSUE, node, location, message); + } + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RecyclerViewDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RecyclerViewDetector.java new file mode 100644 index 00000000000..a2e24ec31a9 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RecyclerViewDetector.java @@ -0,0 +1,362 @@ +/* + * 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.checks; + + +import static com.android.tools.klint.checks.CutPasteDetector.isReachableFrom; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiVariable; + +import org.jetbrains.uast.UAnonymousClass; +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.USimpleNameReferenceExpression; +import org.jetbrains.uast.UVariable; +import org.jetbrains.uast.UastBinaryOperator; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Checks related to RecyclerView usage. + */ +public class RecyclerViewDetector extends Detector implements Detector.UastScanner { + + public static final Implementation IMPLEMENTATION = new Implementation( + RecyclerViewDetector.class, + Scope.JAVA_FILE_SCOPE); + + public static final Issue FIXED_POSITION = Issue.create( + "RecyclerView", //$NON-NLS-1$ + "RecyclerView Problems", + "`RecyclerView` will *not* call `onBindViewHolder` again when the position of " + + "the item changes in the data set unless the item itself is " + + "invalidated or the new position cannot be determined.\n" + + "\n" + + "For this reason, you should *only* use the position parameter " + + "while acquiring the related data item inside this method, and " + + "should *not* keep a copy of it.\n" + + "\n" + + "If you need the position of an item later on (e.g. in a click " + + "listener), use `getAdapterPosition()` which will have the updated " + + "adapter position.", + Category.CORRECTNESS, + 8, + Severity.ERROR, + IMPLEMENTATION); + + public static final Issue DATA_BINDER = Issue.create( + "PendingBindings", //$NON-NLS-1$ + "Missing Pending Bindings", + "When using a `ViewDataBinding` in a `onBindViewHolder` method, you *must* " + + "call `executePendingBindings()` before the method exits; otherwise " + + "the data binding runtime will update the UI in the next animation frame " + + "causing a delayed update and potential jumps if the item resizes.", + Category.CORRECTNESS, + 8, + Severity.ERROR, + IMPLEMENTATION); + + private static final String VIEW_ADAPTER = "android.support.v7.widget.RecyclerView.Adapter"; //$NON-NLS-1$ + private static final String ON_BIND_VIEW_HOLDER = "onBindViewHolder"; //$NON-NLS-1$ + + // ---- Implements UastScanner ---- + + @Nullable + @Override + public List applicableSuperClasses() { + return Collections.singletonList(VIEW_ADAPTER); + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + JavaEvaluator evaluator = context.getEvaluator(); + for (PsiMethod method : declaration.findMethodsByName(ON_BIND_VIEW_HOLDER, false)) { + int size = evaluator.getParameterCount(method); + if (size == 2 || size == 3) { + checkMethod(context, method, declaration); + } + } + + super.checkClass(context, declaration); + } + + private static void checkMethod(@NonNull JavaContext context, + @NonNull PsiMethod declaration, @NonNull PsiClass cls) { + PsiParameter[] parameters = declaration.getParameterList().getParameters(); + PsiParameter viewHolder = parameters[0]; + PsiParameter parameter = parameters[1]; + + ParameterEscapesVisitor visitor = new ParameterEscapesVisitor(context, cls, parameter); + context.getUastContext().getMethodBody(declaration).accept(visitor); + if (visitor.variableEscapes()) { + reportError(context, viewHolder, parameter); + } + + // Look for pending data binder calls that aren't executed before the method finishes + List dataBinderReferences = visitor.getDataBinders(); + checkDataBinders(context, declaration, dataBinderReferences); + } + + private static void reportError(@NonNull JavaContext context, PsiParameter viewHolder, + PsiParameter parameter) { + String variablePrefix = viewHolder.getName(); + if (variablePrefix == null) { + variablePrefix = "ViewHolder"; + } + String message = String.format("Do not treat position as fixed; only use immediately " + + "and call `%1$s.getAdapterPosition()` to look it up later", + variablePrefix); + context.report(FIXED_POSITION, parameter, context.getLocation(parameter), + message); + } + + private static void checkDataBinders(@NonNull JavaContext context, + @NonNull PsiMethod declaration, List references) { + if (references != null && !references.isEmpty()) { + List targets = Lists.newArrayList(); + List sources = Lists.newArrayList(); + for (UCallExpression ref : references) { + if (isExecutePendingBindingsCall(ref)) { + targets.add(ref); + } else { + sources.add(ref); + } + } + + // Only operate on the last call in each block: ignore siblings with the same parent + // That way if you have + // dataBinder.foo(); + // dataBinder.bar(); + // dataBinder.baz(); + // we only flag the *last* of these calls as needing an executePendingBindings + // afterwards. We do this with a parent map such that we correctly pair + // elements when they have nested references within (such as if blocks.) + Map parentToChildren = Maps.newHashMap(); + for (UCallExpression reference : sources) { + // Note: We're using a map, not a multimap, and iterating forwards: + // this means that the *last* element will overwrite previous entries, + // and we end up with the last reference for each parent which is what we + // want + UExpression statement = UastUtils.getParentOfType(reference, UExpression.class, true); + if (statement != null) { + parentToChildren.put(statement.getContainingElement(), reference); + } + } + + for (UCallExpression source : parentToChildren.values()) { + UExpression sourceBinderReference = source.getReceiver(); + PsiField sourceDataBinder = getDataBinderReference(sourceBinderReference); + assert sourceDataBinder != null; + + boolean reachesTarget = false; + for (UCallExpression target : targets) { + if (sourceDataBinder.equals(getDataBinderReference(target.getReceiver())) + // TODO: Provide full control flow graph, or at least provide an + // isReachable method which can take multiple targets + && isReachableFrom(declaration, source, target)) { + reachesTarget = true; + break; + } + } + if (!reachesTarget) { + String message = String.format( + "You must call `%1$s.executePendingBindings()` " + + "before the `onBind` method exits, otherwise, the DataBinding " + + "library will update the UI in the next animation frame " + + "causing a delayed update & potential jumps if the item " + + "resizes.", + sourceBinderReference.asSourceString()); + context.report(DATA_BINDER, source, context.getUastLocation(source), message); + } + } + } + } + + private static boolean isExecutePendingBindingsCall(UCallExpression call) { + return "executePendingBindings".equals(call.getMethodName()); + } + + @Nullable + private static PsiField getDataBinderReference(@Nullable UExpression element) { + if (element instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) element).resolve(); + if (resolved instanceof PsiField) { + PsiField field = (PsiField) resolved; + if ("dataBinder".equals(field.getName())) { + return field; + } + } + } + + return null; + } + + /** + * Determines whether a given variable "escapes" either to a field or to a nested + * runnable. (We deliberately ignore variables that escape via method calls.) + */ + private static class ParameterEscapesVisitor extends AbstractUastVisitor { + protected final JavaContext mContext; + private final List mVariables; + private final PsiClass mBindClass; + private boolean mEscapes; + private boolean mFoundInnerClass; + + private ParameterEscapesVisitor(JavaContext context, + @NonNull PsiClass bindClass, + @NonNull PsiParameter variable) { + mContext = context; + mVariables = Lists.newArrayList(variable); + mBindClass = bindClass; + } + + private boolean variableEscapes() { + return mEscapes; + } + + @Override + public boolean visitVariable(UVariable variable) { + UExpression initializer = variable.getUastInitializer(); + if (initializer instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) initializer).resolve(); + //noinspection SuspiciousMethodCalls + if (resolved != null && mVariables.contains(resolved)) { + if (resolved instanceof PsiLocalVariable) { + mVariables.add(variable); + } else if (resolved instanceof PsiField) { + mEscapes = true; + } + } + } + + return super.visitVariable(variable); + } + + @Override + public boolean visitBinaryExpression(UBinaryExpression node) { + if (node.getOperator() instanceof UastBinaryOperator.AssignOperator) { + UExpression rhs = node.getRightOperand(); + boolean clearLhs = true; + if (rhs instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) rhs).resolve(); + //noinspection SuspiciousMethodCalls + if (resolved != null && mVariables.contains(resolved)) { + clearLhs = false; + PsiElement resolvedLhs = UastUtils.tryResolve(node.getLeftOperand()); + if (resolvedLhs instanceof PsiLocalVariable) { + PsiLocalVariable variable = (PsiLocalVariable) resolvedLhs; + mVariables.add(variable); + } else if (resolvedLhs instanceof PsiField) { + mEscapes = true; + } + } + } + if (clearLhs) { + // If we reassign one of the variables, clear it out + PsiElement resolved = UastUtils.tryResolve(node.getLeftOperand()); + //noinspection SuspiciousMethodCalls + if (resolved != null && mVariables.contains(resolved)) { + //noinspection SuspiciousMethodCalls + mVariables.remove(resolved); + } + } + } + return super.visitBinaryExpression(node); + } + + @Override + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + if (mFoundInnerClass) { + // Check to see if this reference is inside the same class as the original + // onBind (e.g. is this a reference from an inner class, or a reference + // to a variable assigned from there) + PsiElement resolved = node.resolve(); + //noinspection SuspiciousMethodCalls + if (resolved != null && mVariables.contains(resolved)) { + PsiClass outer = UastUtils.getParentOfType(node, UClass.class, true); + if (!mBindClass.equals(outer)) { + mEscapes = true; + } + } + } + + return super.visitSimpleNameReferenceExpression(node); + } + + @Override + public boolean visitClass(UClass node) { + if (node instanceof UAnonymousClass || !node.isStatic()) { + mFoundInnerClass = true; + } + + return super.visitClass(node); + } + + // Also look for data binder references + + private List mDataBinders = null; + + @Nullable + private List getDataBinders() { + return mDataBinders; + } + + @Override + public boolean visitCallExpression(UCallExpression expression) { + if (UastExpressionUtils.isMethodCall(expression)) { + UExpression methodExpression = expression.getReceiver(); + PsiField dataBinder = getDataBinderReference(methodExpression); + //noinspection VariableNotUsedInsideIf + if (dataBinder != null) { + if (mDataBinders == null) { + mDataBinders = Lists.newArrayList(); + } + mDataBinders.add(expression); + } + } + + return super.visitCallExpression(expression); + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RegistrationDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RegistrationDetector.java new file mode 100644 index 00000000000..b6b3ddebbb1 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RegistrationDetector.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.CLASS_ACTIVITY; +import static com.android.SdkConstants.CLASS_APPLICATION; +import static com.android.SdkConstants.CLASS_BROADCASTRECEIVER; +import static com.android.SdkConstants.CLASS_CONTENTPROVIDER; +import static com.android.SdkConstants.CLASS_SERVICE; +import static com.android.SdkConstants.TAG_ACTIVITY; +import static com.android.SdkConstants.TAG_APPLICATION; +import static com.android.SdkConstants.TAG_PROVIDER; +import static com.android.SdkConstants.TAG_RECEIVER; +import static com.android.SdkConstants.TAG_SERVICE; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.builder.model.AndroidProject; +import com.android.builder.model.ProductFlavorContainer; +import com.android.builder.model.SourceProviderContainer; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LayoutDetector; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.XmlContext; +import com.android.utils.SdkUtils; +import com.google.common.collect.Maps; +import com.intellij.psi.PsiClass; + +import org.jetbrains.uast.UClass; +import org.w3c.dom.Element; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +/** + * Checks for missing manifest registrations for activities, services etc + * and also makes sure that they are registered with the correct tag + */ +public class RegistrationDetector extends LayoutDetector implements Detector.UastScanner { + /** Unregistered activities and services */ + public static final Issue ISSUE = Issue.create( + "Registered", //$NON-NLS-1$ + "Class is not registered in the manifest", + + "Activities, services and content providers should be registered in the " + + "`AndroidManifest.xml` file using ``, `` and `` tags.\n" + + "\n" + + "If your activity is simply a parent class intended to be subclassed by other " + + "\"real\" activities, make it an abstract class.", + + Category.CORRECTNESS, + 6, + Severity.WARNING, + new Implementation( + RegistrationDetector.class, + EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE))) + .addMoreInfo( + "http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$ + + protected Map mManifestRegistrations; + + /** Constructs a new {@link RegistrationDetector} */ + public RegistrationDetector() { + } + + // ---- Implements XmlScanner ---- + + @Override + public Collection getApplicableElements() { + return Arrays.asList(sTags); + } + + @Override + public void visitElement(@NonNull XmlContext context, @NonNull Element element) { + if (!element.hasAttributeNS(ANDROID_URI, ATTR_NAME)) { + // For example, application appears in manifest and doesn't always have a name + return; + } + String fqcn = getFqcn(context, element); + String tag = element.getTagName(); + String frameworkClass = tagToClass(tag); + if (frameworkClass != null) { + String signature = fqcn; + if (mManifestRegistrations == null) { + mManifestRegistrations = Maps.newHashMap(); + } + mManifestRegistrations.put(signature, frameworkClass); + if (signature.indexOf('$') != -1) { + signature = signature.replace('$', '.'); + mManifestRegistrations.put(signature, frameworkClass); + } + } + } + + /** + * Returns the fully qualified class name for a manifest entry element that + * specifies a name attribute + * + * @param context the query context providing the project + * @param element the element + * @return the fully qualified class name + */ + @NonNull + private static String getFqcn(@NonNull XmlContext context, @NonNull Element element) { + String className = element.getAttributeNS(ANDROID_URI, ATTR_NAME); + if (className.startsWith(".")) { //$NON-NLS-1$ + return context.getProject().getPackage() + className; + } else if (className.indexOf('.') == -1) { + // According to the manifest element documentation, this is not + // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html ) + // but it appears in manifest files and appears to be supported by the runtime + // so handle this in code as well: + return context.getProject().getPackage() + '.' + className; + } // else: the class name is already a fully qualified class name + + return className; + } + + // ---- Implements UastScanner ---- + + @Nullable + @Override + public List applicableSuperClasses() { + return Arrays.asList( + // Common super class for Activity, ContentProvider, Service, Application + // (as well as some other classes not registered in the manifest, such as + // Fragment and VoiceInteractionSession) + "android.content.ComponentCallbacks2", + CLASS_BROADCASTRECEIVER); + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass cls) { + if (cls.getName() == null) { + // anonymous class; can't be registered + return; + } + + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.isAbstract(cls) || evaluator.isPrivate(cls)) { + // Abstract classes do not need to be registered, and + // private classes are clearly not intended to be registered + return; + } + + String rightTag = getTag(evaluator, cls); + if (rightTag == null) { + // some non-registered Context, such as a BackupAgent + return; + } + String className = cls.getQualifiedName(); + if (className == null) { + return; + } + if (mManifestRegistrations != null) { + String framework = mManifestRegistrations.get(className); + if (framework == null) { + reportMissing(context, cls, className, rightTag); + } else if (!evaluator.extendsClass(cls, framework, false)) { + reportWrongTag(context, cls, rightTag, className, framework); + } + } else { + reportMissing(context, cls, className, rightTag); + } + } + + private static void reportWrongTag( + @NonNull JavaContext context, + @NonNull PsiClass node, + @NonNull String rightTag, + @NonNull String className, + @NonNull String framework) { + String wrongTag = classToTag(framework); + if (wrongTag == null) { + return; + } + Location location = context.getNameLocation(node); + String message = String.format("`%1$s` is %2$s but is registered " + + "in the manifest as %3$s", className, describeTag(rightTag), + describeTag(wrongTag)); + context.report(ISSUE, node, location, message); + } + + private static String describeTag(@NonNull String tag) { + String article = tag.startsWith("a") ? "an" : "a"; // an for activity and application + return String.format("%1$s `<%2$s>`", article, tag); + } + + private static void reportMissing( + @NonNull JavaContext context, + @NonNull PsiClass node, + @NonNull String className, + @NonNull String tag) { + if (tag.equals(TAG_RECEIVER)) { + // Receivers can be registered in code; don't flag these. + return; + } + + // Don't flag activities registered in test source sets + if (context.getProject().isGradleProject()) { + AndroidProject model = context.getProject().getGradleProjectModel(); + if (model != null) { + String javaSource = context.file.getPath(); + // Test source set? + + for (SourceProviderContainer extra : model.getDefaultConfig().getExtraSourceProviders()) { + String artifactName = extra.getArtifactName(); + if (AndroidProject.ARTIFACT_ANDROID_TEST.equals(artifactName)) { + for (File file : extra.getSourceProvider().getJavaDirectories()) { + if (SdkUtils.startsWithIgnoreCase(javaSource, file.getPath())) { + return; + } + } + } + } + + for (ProductFlavorContainer container : model.getProductFlavors()) { + for (SourceProviderContainer extra : container.getExtraSourceProviders()) { + String artifactName = extra.getArtifactName(); + if (AndroidProject.ARTIFACT_ANDROID_TEST.equals(artifactName)) { + for (File file : extra.getSourceProvider().getJavaDirectories()) { + if (SdkUtils.startsWithIgnoreCase(javaSource, file.getPath())) { + return; + } + } + } + } + } + } + } + + Location location = context.getNameLocation(node); + String message = String.format("The `<%1$s> %2$s` is not registered in the manifest", + tag, className); + context.report(ISSUE, node, location, message); + } + + private static String getTag(@NonNull JavaEvaluator evaluator, @NonNull PsiClass cls) { + String tag = null; + for (String s : sClasses) { + if (evaluator.extendsClass(cls, s, false)) { + tag = classToTag(s); + break; + } + } + return tag; + } + + /** The manifest tags we care about */ + private static final String[] sTags = new String[] { + TAG_ACTIVITY, + TAG_SERVICE, + TAG_RECEIVER, + TAG_PROVIDER, + TAG_APPLICATION + // Keep synchronized with {@link #sClasses} + }; + + /** The corresponding framework classes that the tags in {@link #sTags} should extend */ + private static final String[] sClasses = new String[] { + CLASS_ACTIVITY, + CLASS_SERVICE, + CLASS_BROADCASTRECEIVER, + CLASS_CONTENTPROVIDER, + CLASS_APPLICATION + // Keep synchronized with {@link #sTags} + }; + + /** Looks up the corresponding framework class a given manifest tag's class should extend */ + private static String tagToClass(String tag) { + for (int i = 0, n = sTags.length; i < n; i++) { + if (sTags[i].equals(tag)) { + return sClasses[i]; + } + } + + return null; + } + + /** Looks up the tag a given framework class should be registered with */ + protected static String classToTag(String className) { + for (int i = 0, n = sClasses.length; i < n; i++) { + if (sClasses[i].equals(className)) { + return sTags[i]; + } + } + + return null; + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RequiredAttributeDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RequiredAttributeDetector.java index df31c354861..3a4d4c15bff 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RequiredAttributeDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RequiredAttributeDetector.java @@ -25,8 +25,8 @@ import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_PARENT; import static com.android.SdkConstants.ATTR_STYLE; +import static com.android.SdkConstants.AUTO_URI; import static com.android.SdkConstants.FD_RES_LAYOUT; -import static com.android.SdkConstants.FN_RESOURCE_BASE; import static com.android.SdkConstants.FQCN_GRID_LAYOUT_V7; import static com.android.SdkConstants.GRID_LAYOUT; import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; @@ -34,33 +34,49 @@ import static com.android.SdkConstants.REQUEST_FOCUS; import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; import static com.android.SdkConstants.TABLE_LAYOUT; import static com.android.SdkConstants.TABLE_ROW; +import static com.android.SdkConstants.TAG_DATA; +import static com.android.SdkConstants.TAG_IMPORT; import static com.android.SdkConstants.TAG_ITEM; +import static com.android.SdkConstants.TAG_LAYOUT; import static com.android.SdkConstants.TAG_STYLE; +import static com.android.SdkConstants.TAG_VARIABLE; import static com.android.SdkConstants.VIEW_INCLUDE; import static com.android.SdkConstants.VIEW_MERGE; import static com.android.resources.ResourceFolderType.LAYOUT; import static com.android.resources.ResourceFolderType.VALUES; import static com.android.tools.klint.detector.api.LintUtils.getLayoutName; +import static com.android.tools.klint.detector.api.LintUtils.isNullLiteral; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; +import com.android.ide.common.resources.ResourceUrl; import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LayoutDetector; +import com.android.tools.klint.detector.api.ResourceEvaluator; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastLiteralUtils; +import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -68,7 +84,6 @@ import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -76,7 +91,7 @@ import java.util.Set; /** * Ensures that layout width and height attributes are specified */ -public class RequiredAttributeDetector extends LayoutDetector implements UastScanner { +public class RequiredAttributeDetector extends LayoutDetector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "RequiredSize", //$NON-NLS-1$ @@ -93,7 +108,12 @@ public class RequiredAttributeDetector extends LayoutDetector implements UastSca Severity.ERROR, new Implementation( RequiredAttributeDetector.class, - EnumSet.of(Scope.SOURCE_FILE, Scope.ALL_RESOURCE_FILES))); + EnumSet.of(Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES))); + + public static final String PERCENT_RELATIVE_LAYOUT + = "android.support.percent.PercentRelativeLayout"; + public static final String ATTR_LAYOUT_WIDTH_PERCENT = "layout_widthPercent"; + public static final String ATTR_LAYOUT_HEIGHT_PERCENT = "layout_heightPercent"; /** Map from each style name to parent style */ @Nullable private Map mStyleParents; @@ -130,12 +150,6 @@ public class RequiredAttributeDetector extends LayoutDetector implements UastSca public RequiredAttributeDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - @Override public boolean appliesTo(@NonNull ResourceFolderType folderType) { return folderType == LAYOUT || folderType == VALUES; @@ -416,6 +430,14 @@ public class RequiredAttributeDetector extends LayoutDetector implements UastSca return; } + // Data binding: these tags shouldn't specify width/height + if (tag.equals(TAG_LAYOUT) + || tag.equals(TAG_VARIABLE) + || tag.equals(TAG_DATA) + || tag.equals(TAG_IMPORT)) { + return; + } + String parentTag = element.getParentNode() != null ? element.getParentNode().getNodeName() : ""; if (TABLE_LAYOUT.equals(parentTag) @@ -425,6 +447,16 @@ public class RequiredAttributeDetector extends LayoutDetector implements UastSca return; } + // PercentRelativeLayout or PercentFrameLayout? + boolean isPercent = parentTag.startsWith("android.support.percent.Percent"); + if (isPercent) { + hasWidth |= element.hasAttributeNS(AUTO_URI, ATTR_LAYOUT_WIDTH_PERCENT); + hasHeight |= element.hasAttributeNS(AUTO_URI, ATTR_LAYOUT_HEIGHT_PERCENT); + if (hasWidth && hasHeight) { + return; + } + } + if (!context.getProject().getReportIssues()) { // If this is a library project not being analyzed, ignore it return; @@ -495,6 +527,15 @@ public class RequiredAttributeDetector extends LayoutDetector implements UastSca attribute); } } + if (isPercent) { + String escapedLayoutWidth = '`' + ATTR_LAYOUT_WIDTH + '`'; + String escapedLayoutHeight = '`' + ATTR_LAYOUT_HEIGHT + '`'; + String escapedLayoutWidthPercent = '`' + ATTR_LAYOUT_WIDTH_PERCENT + '`'; + String escapedLayoutHeightPercent = '`' + ATTR_LAYOUT_HEIGHT_PERCENT + '`'; + message = message.replace(escapedLayoutWidth, escapedLayoutWidth + " or " + + escapedLayoutWidthPercent).replace(escapedLayoutHeight, + escapedLayoutHeight + " or " + escapedLayoutHeightPercent); + } context.report(ISSUE, element, context.getLocation(element), message); } @@ -529,92 +570,51 @@ public class RequiredAttributeDetector extends LayoutDetector implements UastSca } } - // ---- Implements JavaScanner ---- - + // ---- Implements UastScanner ---- @Override - public List getApplicableFunctionNames() { + @Nullable + public List getApplicableMethodNames() { return Collections.singletonList("inflate"); //$NON-NLS-1$ } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { // Handle // View#inflate(Context context, int resource, ViewGroup root) // LayoutInflater#inflate(int resource, ViewGroup root) // LayoutInflater#inflate(int resource, ViewGroup root, boolean attachToRoot) - List args = node.getValueArguments(); + List args = call.getValueArguments(); String layout = null; int index = 0; - for (Iterator iterator = args.iterator(); iterator.hasNext(); index++) { - UExpression expression = iterator.next(); - if (expression instanceof UQualifiedExpression) { - UQualifiedExpression outer = (UQualifiedExpression) expression; - UExpression operand = outer.getReceiver(); - if (operand instanceof UQualifiedExpression) { - UQualifiedExpression inner = (UQualifiedExpression) operand; - if (inner.getReceiver() instanceof USimpleReferenceExpression) { - USimpleReferenceExpression reference = (USimpleReferenceExpression) inner.getReceiver(); - if (FN_RESOURCE_BASE.equals(reference.getIdentifier()) - // TODO: constant - && inner.selectorMatches("layout")) { - layout = LAYOUT_RESOURCE_PREFIX + outer.getSelector().renderString(); - break; - } - } else if (inner.getReceiver() instanceof UQualifiedExpression) { - UQualifiedExpression reference = (UQualifiedExpression) inner.getReceiver(); - if (reference.selectorMatches(FN_RESOURCE_BASE) - // TODO: constant - && inner.selectorMatches("layout")) { - layout = LAYOUT_RESOURCE_PREFIX + outer.getSelector().renderString(); - break; - } - } - } + ResourceEvaluator evaluator = new ResourceEvaluator(context); + for (UExpression expression : args) { + ResourceUrl url = evaluator.getResource(expression); + if (url != null && url.type == ResourceType.LAYOUT) { + layout = url.toString(); + break; } + index++; } if (layout == null) { - UFunction containingFunction = UastUtils.getContainingFunction(node); - if (containingFunction != null) { - // Must track local types - index = 0; - String name = StringFormatDetector.getResourceArg(containingFunction, node, index); - if (name == null) { - index = 1; - name = StringFormatDetector.getResourceArg(containingFunction, node, index); - } - if (name != null) { - layout = LAYOUT_RESOURCE_PREFIX + name; - } - } - if (layout == null) { - // Flow analysis didn't succeed - return; - } + // Flow analysis didn't succeed + return; } // In all the applicable signatures, the view root argument is immediately after // the layout resource id. int viewRootPos = index + 1; if (viewRootPos < args.size()) { - int i = 0; - Iterator iterator = args.iterator(); - while (iterator.hasNext() && i < viewRootPos) { - iterator.next(); - i++; - } - if (iterator.hasNext()) { - UExpression viewRoot = iterator.next(); - if (viewRoot instanceof ULiteralExpression && - ((ULiteralExpression)viewRoot).isNull()) { - // Yep, this one inflates the given view with a null parent: - // Tag it as such. For now just use the include data structure since - // it has the same net effect - recordIncludeWidth(layout, true); - recordIncludeHeight(layout, true); - } + UExpression viewRoot = args.get(viewRootPos); + if (UastLiteralUtils.isNullLiteral(viewRoot)) { + // Yep, this one inflates the given view with a null parent: + // Tag it as such. For now just use the include data structure since + // it has the same net effect + recordIncludeWidth(layout, true); + recordIncludeHeight(layout, true); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RtlDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RtlDetector.java index 421f20ec1f5..946dba57bf6 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RtlDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/RtlDetector.java @@ -58,24 +58,25 @@ import static com.android.SdkConstants.TAG_APPLICATION; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LayoutDetector; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Project; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.EmptyUastVisitor; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.USimpleNameReferenceExpression; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; @@ -92,14 +93,14 @@ import java.util.Locale; /** * Check which looks for RTL issues (right-to-left support) in layouts */ -public class RtlDetector extends LayoutDetector implements UastScanner { +public class RtlDetector extends LayoutDetector implements Detector.UastScanner { @SuppressWarnings("unchecked") private static final Implementation IMPLEMENTATION = new Implementation( RtlDetector.class, - EnumSet.of(Scope.RESOURCE_FILE, Scope.SOURCE_FILE, Scope.MANIFEST), + EnumSet.of(Scope.RESOURCE_FILE, Scope.JAVA_FILE, Scope.MANIFEST), Scope.RESOURCE_FILE_SCOPE, - Scope.SOURCE_FILE_SCOPE, + Scope.JAVA_FILE_SCOPE, Scope.MANIFEST_SCOPE ); @@ -186,11 +187,9 @@ public class RtlDetector extends LayoutDetector implements UastScanner { private static final String RIGHT_FIELD = "RIGHT"; //$NON-NLS-1$ private static final String LEFT_FIELD = "LEFT"; //$NON-NLS-1$ - private static final String GRAVITY_CLASS = "Gravity"; //$NON-NLS-1$ private static final String FQCN_GRAVITY = "android.view.Gravity"; //$NON-NLS-1$ - private static final String FQCN_GRAVITY_PREFIX = "android.view.Gravity."; //$NON-NLS-1$ - private static final String ATTR_SUPPORTS_RTL = "supportsRtl"; //$NON-NLS-1$ private static final String ATTR_TEXT_ALIGNMENT = "textAlignment"; //$NON-NLS-1$ + static final String ATTR_SUPPORTS_RTL = "supportsRtl"; //$NON-NLS-1$ /** API version in which RTL support was added */ private static final int RTL_API = 17; @@ -207,12 +206,6 @@ public class RtlDetector extends LayoutDetector implements UastScanner { public RtlDetector() { } - @Override - @NonNull - public Speed getSpeed() { - return Speed.NORMAL; - } - private boolean rtlApplies(@NonNull Context context) { Project project = context.getMainProject(); if (project.getTargetSdk() < RTL_API) { @@ -558,76 +551,59 @@ public class RtlDetector extends LayoutDetector implements UastScanner { // ---- Implements UastScanner ---- + + @Nullable @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; + public List> getApplicableUastTypes() { + return Collections.>singletonList(USimpleNameReferenceExpression.class); } + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { - if (rtlApplies(context.getLintContext())) { + public UastVisitor createUastVisitor(@NonNull JavaContext context) { + if (rtlApplies(context)) { return new IdentifierChecker(context); } - return EmptyUastVisitor.INSTANCE; + return null; } private static class IdentifierChecker extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; - public IdentifierChecker(UastAndroidContext context) { + public IdentifierChecker(JavaContext context) { mContext = context; } @Override - public boolean visitSimpleReferenceExpression(@NotNull USimpleReferenceExpression node) { + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { String identifier = node.getIdentifier(); boolean isLeft = LEFT_FIELD.equals(identifier); boolean isRight = RIGHT_FIELD.equals(identifier); if (!isLeft && !isRight) { - return false; + return super.visitSimpleNameReferenceExpression(node); } - UElement parent = node.getParent(); - if (parent instanceof UImportStatement || parent instanceof UVariable) { - return false; - } - - UElement resolved = node.resolve(mContext); - if (resolved != null) { - if (!(resolved instanceof UVariable)) { - return false; - } else { - UClass containingClass = UastUtils.getContainingClass(resolved); - if (containingClass == null || !containingClass.matchesFqName(FQCN_GRAVITY)) { - return false; - } - } + PsiElement resolved = node.resolve(); + if (!(resolved instanceof PsiField)) { + return super.visitSimpleNameReferenceExpression(node); } else { - // Can't resolve types (for example while editing code with errors): - // rely on heuristics like import statements and class qualifiers - if (parent instanceof UQualifiedExpression && - !(UastUtils.matchesQualified(((UQualifiedExpression) parent).getReceiver(), GRAVITY_CLASS))) { - return false; - } - if (parent instanceof USimpleReferenceExpression) { - // No operand: make sure it's statically imported - if (!LintUtils.isImported(mContext.getLintContext().getCompilationUnit(), - FQCN_GRAVITY_PREFIX + identifier)) { - return false; - } + PsiField field = (PsiField) resolved; + if (!JavaEvaluator.isMemberInClass(field, FQCN_GRAVITY)) { + return super.visitSimpleNameReferenceExpression(node); } } String message = String.format( - "Use \"`Gravity.%1$s`\" instead of \"`Gravity.%2$s`\" to ensure correct " - + "behavior in right-to-left locales", - (isLeft ? GRAVITY_VALUE_START : GRAVITY_VALUE_END).toUpperCase(Locale.US), - (isLeft ? GRAVITY_VALUE_LEFT : GRAVITY_VALUE_RIGHT).toUpperCase(Locale.US)); - Location location = mContext.getLocation(node); + "Use \"`Gravity.%1$s`\" instead of \"`Gravity.%2$s`\" to ensure correct " + + "behavior in right-to-left locales", + (isLeft ? GRAVITY_VALUE_START : GRAVITY_VALUE_END).toUpperCase(Locale.US), + (isLeft ? GRAVITY_VALUE_LEFT : GRAVITY_VALUE_RIGHT).toUpperCase(Locale.US)); + + Location location = mContext.getUastLocation(node); mContext.report(USE_START, node, location, message); - return true; + return super.visitSimpleNameReferenceExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SQLiteDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SQLiteDetector.java index f30a9b0b415..c75dbc0a29e 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SQLiteDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SQLiteDetector.java @@ -18,26 +18,33 @@ package com.android.tools.klint.checks; import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; import java.util.Collections; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Detector which looks for problems related to SQLite usage */ -public class SQLiteDetector extends Detector implements UastScanner { +public class SQLiteDetector extends Detector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION = new Implementation( - SQLiteDetector.class, Scope.SOURCE_FILE_SCOPE); + SQLiteDetector.class, Scope.JAVA_FILE_SCOPE); /** Using STRING instead of TEXT for columns */ public static final Issue ISSUE = Issue.create( @@ -71,36 +78,38 @@ public class SQLiteDetector extends Detector implements UastScanner { // ---- Implements Detector.JavaScanner ---- @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("execSQL"); //$NON-NLS-1$ } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - UFunction resolvedFunction = node.resolve(context); - UClass containingClass = UastUtils.getContainingClass(resolvedFunction); - if (resolvedFunction == null || containingClass == null) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod uMethod) { + PsiMethod method = uMethod.getPsi(); + JavaEvaluator evaluator = context.getEvaluator(); + + if (!JavaEvaluator.isMemberInClass(method, "android.database.sqlite.SQLiteDatabase")) { return; } - if (!containingClass.matchesFqName("android.database.sqlite.SQLiteDatabase")) { + int parameterCount = evaluator.getParameterCount(method); + if (parameterCount == 0) { + return; + } + if (!evaluator.parameterHasType(method, 0, TYPE_STRING)) { return; } - // Try to resolve the String and look for STRING keys - if (resolvedFunction.getValueParameterCount() > 0 - && resolvedFunction.getValueParameters().get(0).getType().matchesFqName(TYPE_STRING) - && node.getValueArgumentCount() == resolvedFunction.getValueParameterCount()) { - UExpression argument = node.getValueArguments().get(0); - String sql = argument.evaluateString(); - if (sql != null && (sql.startsWith("CREATE TABLE") || sql.startsWith("ALTER TABLE")) + UExpression argument = call.getValueArguments().get(0); + String sql = ConstantEvaluator.evaluateString(context, argument, true); + if (sql != null && (sql.startsWith("CREATE TABLE") || sql.startsWith("ALTER TABLE")) && sql.matches(".*\\bSTRING\\b.*")) { - String message = "Using column type STRING; did you mean to use TEXT? " - + "(STRING is a numeric type and its value can be adjusted; for example," - + "strings that look like integers can drop leading zeroes. See issue " - + "explanation for details.)"; - context.report(ISSUE, node, context.getLocation(node), message); - } + String message = "Using column type STRING; did you mean to use TEXT? " + + "(STRING is a numeric type and its value can be adjusted; for example, " + + "strings that look like integers can drop leading zeroes. See issue " + + "explanation for details.)"; + context.report(ISSUE, call, context.getUastLocation(call), message); } + } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SdCardDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SdCardDetector.java index 44262431113..1eaf45a38ed 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SdCardDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SdCardDetector.java @@ -17,29 +17,31 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; - -import java.io.File; import org.jetbrains.annotations.NotNull; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; import org.jetbrains.uast.ULiteralExpression; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UastLiteralUtils; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; +import java.util.Collections; +import java.util.List; + /** * Looks for hardcoded references to /sdcard/. */ -public class SdCardDetector extends Detector implements UastScanner { +public class SdCardDetector extends Detector implements Detector.UastScanner { /** Hardcoded /sdcard/ references */ public static final Issue ISSUE = Issue.create( "SdCardPath", //$NON-NLS-1$ @@ -57,7 +59,7 @@ public class SdCardDetector extends Detector implements UastScanner { Severity.WARNING, new Implementation( SdCardDetector.class, - Scope.SOURCE_FILE_SCOPE)) + Scope.JAVA_FILE_SCOPE)) .addMoreInfo( "http://developer.android.com/guide/topics/data/data-storage.html#filesExternal"); //$NON-NLS-1$ @@ -65,62 +67,61 @@ public class SdCardDetector extends Detector implements UastScanner { public SdCardDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } // ---- Implements UastScanner ---- + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public List> getApplicableUastTypes() { + return Collections.>singletonList(ULiteralExpression.class); + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new StringChecker(context); } private static class StringChecker extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; - public StringChecker(UastAndroidContext context) { + private StringChecker(JavaContext context) { mContext = context; } @Override - public boolean visitLiteralExpression(@NotNull ULiteralExpression node) { - if (!node.isString()) return false; + public boolean visitClass(@NotNull UClass node) { + return super.visitClass(node); + } - String s = (String) node.getValue(); - if (s == null || s.isEmpty()) { - return false; - } - char c = s.charAt(0); - if (c != '/' && c != 'f') { - return false; - } + @Override + public boolean visitLiteralExpression(ULiteralExpression node) { + String s = UastLiteralUtils.getValueIfStringLiteral(node); + if (s != null && !s.isEmpty()) { + char c = s.charAt(0); + if (c != '/' && c != 'f') { + return false; + } - if (s.startsWith("/sdcard") //$NON-NLS-1$ - || s.startsWith("/mnt/sdcard/") //$NON-NLS-1$ - || s.startsWith("/system/media/sdcard") //$NON-NLS-1$ - || s.startsWith("file://sdcard/") //$NON-NLS-1$ - || s.startsWith("file:///sdcard/")) { //$NON-NLS-1$ - String message = "Do not hardcode \"/sdcard/\"; " + - "use `Environment.getExternalStorageDirectory().getPath()` instead"; - Location location = mContext.getLocation(node); - mContext.report(ISSUE, node, location, message); - } else if (s.startsWith("/data/data/") //$NON-NLS-1$ - || s.startsWith("/data/user/")) { //$NON-NLS-1$ - String message = "Do not hardcode \"`/data/`\"; " + - "use `Context.getFilesDir().getPath()` instead"; - Location location = mContext.getLocation(node); - mContext.report(ISSUE, node, location, message); + if (s.startsWith("/sdcard") //$NON-NLS-1$ + || s.startsWith("/mnt/sdcard/") //$NON-NLS-1$ + || s.startsWith("/system/media/sdcard") //$NON-NLS-1$ + || s.startsWith("file://sdcard/") //$NON-NLS-1$ + || s.startsWith("file:///sdcard/")) { //$NON-NLS-1$ + String message = "Do not hardcode \"/sdcard/\"; " + + "use `Environment.getExternalStorageDirectory().getPath()` instead"; + Location location = mContext.getUastLocation(node); + mContext.report(ISSUE, node, location, message); + } else if (s.startsWith("/data/data/") //$NON-NLS-1$ + || s.startsWith("/data/user/")) { //$NON-NLS-1$ + String message = "Do not hardcode \"`/data/`\"; " + + "use `Context.getFilesDir().getPath()` instead"; + Location location = mContext.getUastLocation(node); + mContext.report(ISSUE, node, location, message); + } } - - return false; + + return super.visitLiteralExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecureRandomDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecureRandomDetector.java new file mode 100644 index 00000000000..63410a1232e --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecureRandomDetector.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.TypeEvaluator; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Collections; +import java.util.List; + +/** + * Checks for hardcoded seeds with random numbers. + */ +public class SecureRandomDetector extends Detector implements Detector.UastScanner { + /** Unregistered activities and services */ + public static final Issue ISSUE = Issue.create( + "SecureRandom", //$NON-NLS-1$ + "Using a fixed seed with `SecureRandom`", + + "Specifying a fixed seed will cause the instance to return a predictable sequence " + + "of numbers. This may be useful for testing but it is not appropriate for secure use.", + + Category.SECURITY, + 9, + Severity.WARNING, + new Implementation( + SecureRandomDetector.class, + Scope.JAVA_FILE_SCOPE)) + .addMoreInfo("http://developer.android.com/reference/java/security/SecureRandom.html"); + + private static final String SET_SEED = "setSeed"; //$NON-NLS-1$ + public static final String JAVA_SECURITY_SECURE_RANDOM = "java.security.SecureRandom"; + public static final String JAVA_UTIL_RANDOM = "java.util.Random"; + + /** Constructs a new {@link SecureRandomDetector} */ + public SecureRandomDetector() { + } + + // ---- Implements UastScanner ---- + + @Nullable + @Override + public List getApplicableMethodNames() { + return Collections.singletonList(SET_SEED); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + List arguments = call.getValueArguments(); + if (arguments.isEmpty()) { + return; + } + UExpression seedArgument = arguments.get(0); + JavaEvaluator evaluator = context.getEvaluator(); + if (JavaEvaluator.isMemberInClass(method, JAVA_SECURITY_SECURE_RANDOM) + || evaluator.isMemberInSubClassOf(method, JAVA_UTIL_RANDOM, false) + && isSecureRandomReceiver(context, call)) { + // Called with a fixed seed? + Object seed = ConstantEvaluator.evaluate(context, seedArgument); + //noinspection VariableNotUsedInsideIf + if (seed != null) { + context.report(ISSUE, call, context.getUastLocation(call), + "Do not call `setSeed()` on a `SecureRandom` with a fixed seed: " + + "it is not secure. Use `getSeed()`."); + } else { + // Called with a simple System.currentTimeMillis() seed or something like that? + PsiElement resolvedArgument = UastUtils.tryResolve(seedArgument); + if (resolvedArgument instanceof PsiMethod) { + PsiMethod seedMethod = (PsiMethod) resolvedArgument; + String methodName = seedMethod.getName(); + if (methodName.equals("currentTimeMillis") + || methodName.equals("nanoTime")) { + context.report(ISSUE, call, context.getUastLocation(call), + "It is dangerous to seed `SecureRandom` with the current " + + "time because that value is more predictable to " + + "an attacker than the default seed."); + } + } + } + } + } + + /** + * Returns true if the given invocation is assigned a SecureRandom type + */ + private static boolean isSecureRandomReceiver( + @NonNull JavaContext context, + @NonNull UCallExpression call) { + UElement operand = call.getReceiver(); + return operand != null && isSecureRandomType(context, operand); + } + + /** + * Returns true if the node evaluates to an instance of type SecureRandom + */ + private static boolean isSecureRandomType( + @NonNull JavaContext context, + @NonNull UElement node) { + PsiType type = TypeEvaluator.evaluate(context, node); + return type != null && JAVA_SECURITY_SECURE_RANDOM.equals(type.getCanonicalText()); + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecurityDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecurityDetector.java index 352802d660f..00a66479c51 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecurityDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecurityDetector.java @@ -16,7 +16,6 @@ package com.android.tools.klint.checks; -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_EXPORTED; import static com.android.SdkConstants.ATTR_NAME; @@ -37,37 +36,39 @@ import static com.android.SdkConstants.TAG_SERVICE; import static com.android.xml.AndroidManifest.NODE_ACTION; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.XmlScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.XmlContext; -import org.jetbrains.annotations.NotNull; import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.USimpleReferenceExpression; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.USimpleNameReferenceExpression; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; -import java.io.File; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; /** * Checks that exported services request a permission. */ -public class SecurityDetector extends Detector implements Detector.XmlScanner, UastScanner { +public class SecurityDetector extends Detector implements XmlScanner, Detector.UastScanner { private static final Implementation IMPLEMENTATION_MANIFEST = new Implementation( SecurityDetector.class, @@ -75,7 +76,7 @@ public class SecurityDetector extends Detector implements Detector.XmlScanner, U private static final Implementation IMPLEMENTATION_JAVA = new Implementation( SecurityDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Exported services */ public static final Issue EXPORTED_SERVICE = Issue.create( @@ -129,10 +130,36 @@ public class SecurityDetector extends Detector implements Detector.XmlScanner, U Severity.WARNING, IMPLEMENTATION_MANIFEST); + /** Using java.io.File.setReadable(true, false) to set file world-readable */ + public static final Issue SET_READABLE = Issue.create( + "SetWorldReadable", + "`File.setReadable()` used to make file world-readable", + "Setting files world-readable is very dangerous, and likely to cause security " + + "holes in applications. It is strongly discouraged; instead, applications should " + + "use more formal mechanisms for interactions such as `ContentProvider`, " + + "`BroadcastReceiver`, and `Service`.", + Category.SECURITY, + 6, + Severity.WARNING, + IMPLEMENTATION_JAVA); + + /** Using java.io.File.setWritable(true, false) to set file world-writable */ + public static final Issue SET_WRITABLE = Issue.create( + "SetWorldWritable", + "`File.setWritable()` used to make file world-writable", + "Setting files world-writable is very dangerous, and likely to cause security " + + "holes in applications. It is strongly discouraged; instead, applications should " + + "use more formal mechanisms for interactions such as `ContentProvider`, " + + "`BroadcastReceiver`, and `Service`.", + Category.SECURITY, + 6, + Severity.WARNING, + IMPLEMENTATION_JAVA); + /** Using the world-writable flag */ public static final Issue WORLD_WRITEABLE = Issue.create( "WorldWriteableFiles", //$NON-NLS-1$ - "`openFileOutput()` call passing `MODE_WORLD_WRITEABLE`", + "`openFileOutput()` or similar call passing `MODE_WORLD_WRITEABLE`", "There are cases where it is appropriate for an application to write " + "world writeable files, but these should be reviewed carefully to " + "ensure that they contain no private data, and that if the file is " + @@ -147,7 +174,7 @@ public class SecurityDetector extends Detector implements Detector.XmlScanner, U /** Using the world-readable flag */ public static final Issue WORLD_READABLE = Issue.create( "WorldReadableFiles", //$NON-NLS-1$ - "`openFileOutput()` call passing `MODE_WORLD_READABLE`", + "`openFileOutput()` or similar call passing `MODE_WORLD_READABLE`", "There are cases where it is appropriate for an application to write " + "world readable files, but these should be reviewed carefully to " + "ensure that they contain no private data that is leaked to other " + @@ -157,21 +184,12 @@ public class SecurityDetector extends Detector implements Detector.XmlScanner, U Severity.WARNING, IMPLEMENTATION_JAVA); + private static final String FILE_CLASS = "java.io.File"; //$NON-NLS-1$ + /** Constructs a new {@link SecurityDetector} check */ public SecurityDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return file.getName().equals(ANDROID_MANIFEST_XML); - } - // ---- Implements Detector.XmlScanner ---- @Override @@ -352,47 +370,89 @@ public class SecurityDetector extends Detector implements Detector.XmlScanner, U } } - // ---- Implements UastScanner ---- + // ---- Implements Detector.JavaScanner ---- @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public List getApplicableMethodNames() { + // These are the API calls that can accept a MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE + // argument. + List values = new ArrayList(3); + values.add("openFileOutput"); //$NON-NLS-1$ + values.add("getSharedPreferences"); //$NON-NLS-1$ + values.add("getDir"); //$NON-NLS-1$ + // These API calls can be used to set files world-readable or world-writable + values.add("setReadable"); //$NON-NLS-1$ + values.add("setWritable"); //$NON-NLS-1$ + return values; + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + List args = node.getValueArguments(); + String methodName = node.getMethodName(); + if (context.getEvaluator().isMemberInSubClassOf(method, FILE_CLASS, false)) { + // Report calls to java.io.File.setReadable(true, false) or + // java.io.File.setWritable(true, false) + if ("setReadable".equals(methodName)) { + if (args.size() == 2 && + Boolean.TRUE.equals(ConstantEvaluator.evaluate(context, args.get(0))) && + Boolean.FALSE.equals(ConstantEvaluator.evaluate(context, args.get(1)))) { + context.report(SET_READABLE, node, context.getUastLocation(node), + "Setting file permissions to world-readable can be " + + "risky, review carefully"); + } + return; + } else if ("setWritable".equals(methodName)) { + if (args.size() == 2 && + Boolean.TRUE.equals(ConstantEvaluator.evaluate(context, args.get(0))) && + Boolean.FALSE.equals(ConstantEvaluator.evaluate(context, args.get(1)))) { + context.report(SET_WRITABLE, node, context.getUastLocation(node), + "Setting file permissions to world-writable can be " + + "risky, review carefully"); + } + return; + } + } + + assert visitor != null; + for (UExpression arg : args) { + arg.accept(visitor); + } + + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new IdentifierVisitor(context); } private static class IdentifierVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; - private final AbstractUastVisitor identifierHandler = new AbstractUastVisitor() { - @Override - public boolean visitSimpleReferenceExpression(@NotNull USimpleReferenceExpression node) { - if ("MODE_WORLD_WRITEABLE".equals(node.getIdentifier())) { //$NON-NLS-1$ - Location location = mContext.getLocation(node); - mContext.report(WORLD_WRITEABLE, node, location, - "Using `MODE_WORLD_WRITEABLE` when creating files can be " + - "risky, review carefully"); - } else if ("MODE_WORLD_READABLE".equals(node.getIdentifier())) { //$NON-NLS-1$ - Location location = mContext.getLocation(node); - mContext.report(WORLD_READABLE, node, location, - "Using `MODE_WORLD_READABLE` when creating files can be " + - "risky, review carefully"); - } - - return false; - } - }; - - public IdentifierVisitor(UastAndroidContext context) { + private IdentifierVisitor(JavaContext context) { + super(); mContext = context; } - + @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - String name = node.getFunctionName(); - if ("openFileOutput".equals(name) || "getSharedPreferences".equals(name)) { - return false; + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + String name = node.getIdentifier(); + + if ("MODE_WORLD_WRITEABLE".equals(name)) { //$NON-NLS-1$ + Location location = mContext.getUastLocation(node); + mContext.report(WORLD_WRITEABLE, node, location, + "Using `MODE_WORLD_WRITEABLE` when creating files can be " + + "risky, review carefully"); + } else if ("MODE_WORLD_READABLE".equals(name)) { //$NON-NLS-1$ + Location location = mContext.getUastLocation(node); + mContext.report(WORLD_READABLE, node, location, + "Using `MODE_WORLD_READABLE` when creating files can be " + + "risky, review carefully"); } - return true; + return super.visitSimpleNameReferenceExpression(node); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ServiceCastDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ServiceCastDetector.java index 719c28c5471..3313cddfb09 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ServiceCastDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ServiceCastDetector.java @@ -19,27 +19,35 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; import com.google.common.collect.Maps; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; + +import org.jetbrains.uast.UBinaryExpressionWithType; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.UastVisitor; -import java.io.File; import java.util.Collections; import java.util.List; import java.util.Map; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Detector looking for casts on th result of context.getSystemService which are suspect */ -public class ServiceCastDetector extends Detector implements UastScanner { +public class ServiceCastDetector extends Detector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "ServiceCast", //$NON-NLS-1$ @@ -54,56 +62,54 @@ public class ServiceCastDetector extends Detector implements UastScanner { Severity.FATAL, new Implementation( ServiceCastDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); /** Constructs a new {@link ServiceCastDetector} check */ public ServiceCastDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - // ---- Implements UastScanner ---- - + // ---- Implements JavaScanner ---- @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("getSystemService"); //$NON-NLS-1$ } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - UExpression receiver = UastUtils.getQualifiedCallElement(node); - UElement parent = receiver.getParent(); - if (!(parent instanceof UBinaryExpressionWithType) - || ((UBinaryExpressionWithType)parent).getOperationKind() != UastBinaryExpressionWithTypeKind.TYPE_CAST) { - return; - } + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + UElement parent = LintUtils.skipParentheses( + UastUtils.getQualifiedParentOrThis(call).getContainingElement()); + if (UastExpressionUtils.isTypeCast(parent)) { + UBinaryExpressionWithType cast = (UBinaryExpressionWithType) parent; - UBinaryExpressionWithType cast = (UBinaryExpressionWithType) parent; - List args = node.getValueArguments(); - if (args.size() == 1) { - String name = stripPackage(args.get(0).renderString()); - String expectedClass = getExpectedType(name); - if (expectedClass != null) { - String castType = cast.getType().getName(); - if (castType.indexOf('.') == -1) { - expectedClass = stripPackage(expectedClass); + List args = call.getValueArguments(); + if (args.size() == 1 && args.get(0) instanceof UReferenceExpression) { + PsiElement resolvedServiceConst = ((UReferenceExpression) args.get(0)).resolve(); + if (!(resolvedServiceConst instanceof PsiField)) { + return; } - if (!castType.equals(expectedClass)) { - // It's okay to mix and match - // android.content.ClipboardManager and android.text.ClipboardManager - if (isClipboard(castType) && isClipboard(expectedClass)) { - return; + String name = ((PsiField) resolvedServiceConst).getName(); + String expectedClass = getExpectedType(name); + if (expectedClass != null && cast != null) { + String castType = cast.getType().getCanonicalText(); + if (castType.indexOf('.') == -1) { + expectedClass = stripPackage(expectedClass); } + if (!castType.equals(expectedClass)) { + // It's okay to mix and match + // android.content.ClipboardManager and android.text.ClipboardManager + if (isClipboard(castType) && isClipboard(expectedClass)) { + return; + } - String message = String.format( - "Suspicious cast to `%1$s` for a `%2$s`: expected `%3$s`", - stripPackage(castType), name, stripPackage(expectedClass)); - context.report(ISSUE, node, context.getLocation(cast), message); + String message = String.format( + "Suspicious cast to `%1$s` for a `%2$s`: expected `%3$s`", + stripPackage(castType), name, stripPackage(expectedClass)); + context.report(ISSUE, call, context.getUastLocation(cast), message); + } } + } } } @@ -123,14 +129,14 @@ public class ServiceCastDetector extends Detector implements UastScanner { } @Nullable - private static String getExpectedType(@NonNull String value) { - return getServiceMap().get(value); + private static String getExpectedType(@Nullable String value) { + return value != null ? getServiceMap().get(value) : null; } @NonNull private static Map getServiceMap() { if (sServiceMap == null) { - final int EXPECTED_SIZE = 49; + final int EXPECTED_SIZE = 55; sServiceMap = Maps.newHashMapWithExpectedSize(EXPECTED_SIZE); sServiceMap.put("ACCESSIBILITY_SERVICE", "android.view.accessibility.AccessibilityManager"); @@ -144,13 +150,15 @@ public class ServiceCastDetector extends Detector implements UastScanner { sServiceMap.put("BLUETOOTH_SERVICE", "android.bluetooth.BluetoothManager"); sServiceMap.put("CAMERA_SERVICE", "android.hardware.camera2.CameraManager"); sServiceMap.put("CAPTIONING_SERVICE", "android.view.accessibility.CaptioningManager"); - sServiceMap.put("CLIPBOARD_SERVICE", "android.text.ClipboardManager"); + sServiceMap.put("CARRIER_CONFIG_SERVICE", "android.telephony.CarrierConfigManager"); + sServiceMap.put("CLIPBOARD_SERVICE", "android.text.ClipboardManager"); // also allow @Deprecated android.content.ClipboardManager sServiceMap.put("CONNECTIVITY_SERVICE", "android.net.ConnectivityManager"); sServiceMap.put("CONSUMER_IR_SERVICE", "android.hardware.ConsumerIrManager"); sServiceMap.put("DEVICE_POLICY_SERVICE", "android.app.admin.DevicePolicyManager"); sServiceMap.put("DISPLAY_SERVICE", "android.hardware.display.DisplayManager"); sServiceMap.put("DOWNLOAD_SERVICE", "android.app.DownloadManager"); sServiceMap.put("DROPBOX_SERVICE", "android.os.DropBoxManager"); + sServiceMap.put("FINGERPRINT_SERVICE", "android.hardware.fingerprint.FingerprintManager"); sServiceMap.put("INPUT_METHOD_SERVICE", "android.view.inputmethod.InputMethodManager"); sServiceMap.put("INPUT_SERVICE", "android.hardware.input.InputManager"); sServiceMap.put("JOB_SCHEDULER_SERVICE", "android.app.job.JobScheduler"); @@ -161,6 +169,8 @@ public class ServiceCastDetector extends Detector implements UastScanner { sServiceMap.put("MEDIA_PROJECTION_SERVICE", "android.media.projection.MediaProjectionManager"); sServiceMap.put("MEDIA_ROUTER_SERVICE", "android.media.MediaRouter"); sServiceMap.put("MEDIA_SESSION_SERVICE", "android.media.session.MediaSessionManager"); + sServiceMap.put("MIDI_SERVICE", "android.media.midi.MidiManager"); + sServiceMap.put("NETWORK_STATS_SERVICE", "android.app.usage.NetworkStatsManager"); sServiceMap.put("NFC_SERVICE", "android.nfc.NfcManager"); sServiceMap.put("NOTIFICATION_SERVICE", "android.app.NotificationManager"); sServiceMap.put("NSD_SERVICE", "android.net.nsd.NsdManager"); @@ -172,13 +182,15 @@ public class ServiceCastDetector extends Detector implements UastScanner { sServiceMap.put("STORAGE_SERVICE", "android.os.storage.StorageManager"); sServiceMap.put("TELECOM_SERVICE", "android.telecom.TelecomManager"); sServiceMap.put("TELEPHONY_SERVICE", "android.telephony.TelephonyManager"); + sServiceMap.put("TELEPHONY_SUBSCRIPTION_SERVICE", "android.telephony.SubscriptionManager"); sServiceMap.put("TEXT_SERVICES_MANAGER_SERVICE", "android.view.textservice.TextServicesManager"); sServiceMap.put("TV_INPUT_SERVICE", "android.media.tv.TvInputManager"); sServiceMap.put("UI_MODE_SERVICE", "android.app.UiModeManager"); + sServiceMap.put("USAGE_STATS_SERVICE", "android.app.usage.UsageStatsManager"); sServiceMap.put("USB_SERVICE", "android.hardware.usb.UsbManager"); sServiceMap.put("USER_SERVICE", "android.os.UserManager"); sServiceMap.put("VIBRATOR_SERVICE", "android.os.Vibrator"); - sServiceMap.put("WALLPAPER_SERVICE", "com.android.server.WallpaperService"); + sServiceMap.put("WALLPAPER_SERVICE", "android.service.wallpaper.WallpaperService"); sServiceMap.put("WIFI_P2P_SERVICE", "android.net.wifi.p2p.WifiP2pManager"); sServiceMap.put("WIFI_SERVICE", "android.net.wifi.WifiManager"); sServiceMap.put("WINDOW_SERVICE", "android.view.WindowManager"); diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetJavaScriptEnabledDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetJavaScriptEnabledDetector.java index 0b1b5e96bc5..a73612b6c97 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetJavaScriptEnabledDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetJavaScriptEnabledDetector.java @@ -16,24 +16,29 @@ package com.android.tools.klint.checks; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + import java.util.Collections; import java.util.List; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Looks for invocations of android.webkit.WebSettings.setJavaScriptEnabled. */ -public class SetJavaScriptEnabledDetector extends Detector implements UastScanner { +public class SetJavaScriptEnabledDetector extends Detector implements Detector.UastScanner { /** Invocations of setJavaScriptEnabled */ public static final Issue ISSUE = Issue.create("SetJavaScriptEnabled", //$NON-NLS-1$ "Using `setJavaScriptEnabled`", @@ -46,7 +51,7 @@ public class SetJavaScriptEnabledDetector extends Detector implements UastScanne Severity.WARNING, new Implementation( SetJavaScriptEnabledDetector.class, - Scope.SOURCE_FILE_SCOPE)) + Scope.JAVA_FILE_SCOPE)) .addMoreInfo( "http://developer.android.com/guide/practices/security.html"); //$NON-NLS-1$ @@ -57,21 +62,21 @@ public class SetJavaScriptEnabledDetector extends Detector implements UastScanne // ---- Implements UastScanner ---- @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - if (node.getValueArgumentCount() != 1) { - return; - } - - Object value = node.getValueArguments().get(0).evaluate(); - if (value instanceof Boolean && (Boolean) value) { - context.report(ISSUE, node, context.getLocation(node), - "Using `setJavaScriptEnabled` can introduce XSS vulnerabilities " + - "into you application, review carefully."); + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + List arguments = call.getValueArguments(); + if (arguments.size() == 1) { + Object constant = ConstantEvaluator.evaluate(context, arguments.get(0)); + if (constant != null && !Boolean.FALSE.equals(constant)) { + context.report(ISSUE, call, context.getUastLocation(call), + "Using `setJavaScriptEnabled` can introduce XSS vulnerabilities " + + "into you application, review carefully."); + } } } @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("setJavaScriptEnabled"); } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetTextDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetTextDetector.java new file mode 100644 index 00000000000..cd141df5d60 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetTextDetector.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UQualifiedReferenceExpression; +import org.jetbrains.uast.UastBinaryOperator; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Collections; +import java.util.List; + +/** + * Checks for errors related to TextView#setText and internationalization + */ +public class SetTextDetector extends Detector implements Detector.UastScanner { + + private static final Implementation IMPLEMENTATION = new Implementation( + SetTextDetector.class, + Scope.JAVA_FILE_SCOPE); + + /** Constructing SimpleDateFormat without an explicit locale */ + public static final Issue SET_TEXT_I18N = Issue.create( + "SetTextI18n", //$NON-NLS-1$ + "TextView Internationalization", + + "When calling `TextView#setText`\n" + + "* Never call `Number#toString()` to format numbers; it will not handle fraction " + + "separators and locale-specific digits properly. Consider using `String#format` " + + "with proper format specifications (`%d` or `%f`) instead.\n" + + "* Do not pass a string literal (e.g. \"Hello\") to display text. Hardcoded " + + "text can not be properly translated to other languages. Consider using Android " + + "resource strings instead.\n" + + "* Do not build messages by concatenating text chunks. Such messages can not be " + + "properly translated.", + + Category.I18N, + 6, + Severity.WARNING, + IMPLEMENTATION) + .addMoreInfo("http://developer.android.com/guide/topics/resources/localization.html"); + + + private static final String METHOD_NAME = "setText"; + private static final String TO_STRING_NAME = "toString"; + private static final String CHAR_SEQUENCE_CLS = "java.lang.CharSequence"; + private static final String NUMBER_CLS = "java.lang.Number"; + private static final String TEXT_VIEW_CLS = "android.widget.TextView"; + + // Pattern to match string literal that require translation. These are those having word + // characters in it. + private static final String WORD_PATTERN = ".*\\w{2,}.*"; + + /** Constructs a new {@link SetTextDetector} */ + public SetTextDetector() { + } + + // ---- Implements JavaScanner ---- + + @Nullable + @Override + public List getApplicableMethodNames() { + return Collections.singletonList(METHOD_NAME); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + JavaEvaluator evaluator = context.getEvaluator(); + if (!evaluator.isMemberInSubClassOf(method, TEXT_VIEW_CLS, false)) { + return; + } + if (method.getParameterList().getParametersCount() > 0 && + evaluator.parameterHasType(method, 0, CHAR_SEQUENCE_CLS)) { + checkNode(context, call.getValueArguments().get(0)); + } + } + + private static void checkNode(@NonNull JavaContext context, @Nullable UElement node) { + if (node instanceof ULiteralExpression) { + Object value = ((ULiteralExpression) node).getValue(); + if (value instanceof String && value.toString().matches(WORD_PATTERN)) { + context.report(SET_TEXT_I18N, node, context.getUastLocation(node), + "String literal in `setText` can not be translated. Use Android " + + "resources instead."); + } + } else if (node instanceof UCallExpression) { + PsiMethod calledMethod = ((UCallExpression) node).resolve(); + if (calledMethod != null && TO_STRING_NAME.equals(calledMethod.getName())) { + PsiClass containingClass = UastUtils.getContainingClass(calledMethod); + if (containingClass == null) { + return; + } + + PsiClass superClass = containingClass.getSuperClass(); + if (superClass != null && NUMBER_CLS.equals(superClass.getQualifiedName())) { + context.report(SET_TEXT_I18N, node, context.getUastLocation(node), + "Number formatting does not take into account locale settings. " + + "Consider using `String.format` instead."); + } + } + } else if (node instanceof UQualifiedReferenceExpression) { + UQualifiedReferenceExpression expression = (UQualifiedReferenceExpression) node; + checkNode(context, expression.getReceiver()); + checkNode(context, expression.getSelector()); + } else if (node instanceof UBinaryExpression) { + UBinaryExpression expression = (UBinaryExpression) node; + if (expression.getOperator() == UastBinaryOperator.PLUS) { + context.report(SET_TEXT_I18N, node, context.getUastLocation(node), + "Do not concatenate text displayed with `setText`. " + + "Use resource string with placeholders."); + } + checkNode(context, expression.getLeftOperand()); + checkNode(context, expression.getRightOperand()); + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SharedPrefsDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SharedPrefsDetector.java deleted file mode 100644 index 4020af57aa2..00000000000 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SharedPrefsDetector.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tools.klint.checks; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; -import com.android.tools.klint.detector.api.Detector; -import com.android.tools.klint.detector.api.Implementation; -import com.android.tools.klint.detector.api.Issue; -import com.android.tools.klint.detector.api.LintUtils; -import com.android.tools.klint.detector.api.Scope; -import com.android.tools.klint.detector.api.Severity; - -import java.io.File; -import java.util.Collections; -import java.util.List; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.java.JavaUAssertExpression; -import org.jetbrains.uast.visitor.AbstractUastVisitor; - -/** - * Detector looking for SharedPreferences.edit() calls without a corresponding - * commit() or apply() call - */ -public class SharedPrefsDetector extends Detector implements UastScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "CommitPrefEdits", //$NON-NLS-1$ - "Missing `commit()` on `SharedPreference` editor", - - "After calling `edit()` on a `SharedPreference`, you must call `commit()` " + - "or `apply()` on the editor to save the results.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - new Implementation( - SharedPrefsDetector.class, - Scope.SOURCE_FILE_SCOPE)); - - public static final String ANDROID_CONTENT_SHARED_PREFERENCES = - "android.content.SharedPreferences"; //$NON-NLS-1$ - private static final String ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR = - "android.content.SharedPreferences.Editor"; //$NON-NLS-1$ - - /** Constructs a new {@link SharedPrefsDetector} check */ - public SharedPrefsDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - - // ---- Implements UastScanner ---- - - @Override - public List getApplicableFunctionNames() { - return Collections.singletonList("edit"); //$NON-NLS-1$ - } - - @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - assert "edit".equals(node.getFunctionName()); - - boolean verifiedType = false; - UFunction resolve = node.resolve(context); - if (resolve != null) { - UType returnType = resolve.getReturnType(); - if (returnType == null || - !returnType.matchesFqName(ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR)) { - return; - } - verifiedType = true; - } - - UElement parent = node.getParent(); - if (!(parent instanceof UQualifiedExpression)) { - return; - } - UElement operand = ((UQualifiedExpression)parent).getReceiver(); - - // Looking for the specific pattern where you assign the edit() result - // to a local variable; this means we won't recognize some other usages - // of the API (e.g. assigning it to a previously declared variable) but - // is needed until we have type attribution in the AST itself. - - UVariable definition = getLhs(parent); - boolean allowCommitBeforeTarget; - if (definition == null) { - if (operand instanceof USimpleReferenceExpression) { - UDeclaration resolvedDeclaration = (((USimpleReferenceExpression)operand).resolve(context)); - if (resolvedDeclaration instanceof UVariable) { - String type = ((UVariable)resolvedDeclaration).getType().getName(); - if (!type.equals("SharedPreferences")) { //$NON-NLS-1$ - return; - } - } - allowCommitBeforeTarget = true; - } else { - return; - } - } else { - if (!verifiedType) { - UType type = definition.getType(); - UClass clazz = type.resolveOrEmpty(context); - String possiblefqName = clazz.getFqName(); - if (possiblefqName == null) { - possiblefqName = clazz.getName(); - } - if (possiblefqName.endsWith("SharedPreferences.Editor")) { //$NON-NLS-1$ - if (!type.matchesFqName("Editor") || //$NON-NLS-1$ - !LintUtils.isImported(context.getLintContext().getCompilationUnit(), - ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR)) { - return; - } - } - } - allowCommitBeforeTarget = false; - } - - UFunction method = UastUtils.getContainingFunction(parent); - if (method == null) { - return; - } - - CommitFinder finder = new CommitFinder(context, node, allowCommitBeforeTarget); - method.accept(finder); - if (!finder.isCommitCalled()) { - context.report(ISSUE, method, context.getLocation(node), - "`SharedPreferences.edit()` without a corresponding `commit()` or `apply()` call"); - } - } - - @Nullable - private static UVariable getLhs(@NonNull UElement node) { - while (node != null) { - if (node instanceof UFunction) { - return null; - } - if (node instanceof UVariable) { - return ((UVariable)node); - } - - node = node.getParent(); - } - - return null; - } - - private static class CommitFinder extends AbstractUastVisitor { - /** The target edit call */ - private final UCallExpression mTarget; - /** whether it allows the commit call to be seen before the target node */ - private final boolean mAllowCommitBeforeTarget; - - private final UastAndroidContext mContext; - - /** Whether we've found one of the commit/cancel methods */ - private boolean mFound; - /** Whether we've seen the target edit node yet */ - private boolean mSeenTarget; - - private CommitFinder(UastAndroidContext context, UCallExpression target, - boolean allowCommitBeforeTarget) { - mContext = context; - mTarget = target; - mAllowCommitBeforeTarget = allowCommitBeforeTarget; - } - - @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - UElement qualifiedElement = UastUtils.getQualifiedCallElement(node); - - if (node == mTarget) { - mSeenTarget = true; - } else if (mAllowCommitBeforeTarget || mSeenTarget || - (qualifiedElement instanceof UQualifiedExpression && - ((UQualifiedExpression)qualifiedElement).getReceiver() == mTarget)) { - String name = node.getFunctionName(); - boolean isCommit = "commit".equals(name); - if (isCommit || "apply".equals(name)) { //$NON-NLS-1$ //$NON-NLS-2$ - // TODO: Do more flow analysis to see whether we're really calling commit/apply - // on the right type of object? - mFound = true; - - UFunction method = node.resolve(mContext); - if (method != null) { - UClass clz = UastUtils.getContainingClassOrEmpty(method); - if (clz.isSubclassOf("android.content.SharedPreferences.Editor") - && mContext.getLintContext().getProject().getMinSdkVersion().getApiLevel() >= 9) { - // See if the return value is read: can only replace commit with - // apply if the return value is not considered - boolean returnValueIgnored = false; - if (qualifiedElement instanceof UFunction || - qualifiedElement instanceof UClass || - qualifiedElement instanceof UBlockExpression) { - returnValueIgnored = true; - } else if (qualifiedElement instanceof UIfExpression) { - returnValueIgnored = ((UIfExpression) qualifiedElement).getCondition() != node; - } else if (qualifiedElement instanceof UReturnExpression) { - returnValueIgnored = false; - } else if (qualifiedElement instanceof UVariable) { - returnValueIgnored = false; - } else if (qualifiedElement instanceof UForExpression) { - returnValueIgnored = ((UForExpression) qualifiedElement).getCondition() != node; - } else if (qualifiedElement instanceof UWhileExpression) { - returnValueIgnored = ((UWhileExpression) qualifiedElement).getCondition() != node; - } else if (qualifiedElement instanceof UDoWhileExpression) { - returnValueIgnored = ((UDoWhileExpression) qualifiedElement).getCondition() != node; - } else if (qualifiedElement instanceof USwitchClauseExpression) { - List caseValues = ((USwitchClauseExpression) qualifiedElement).getCaseValues(); - for (UExpression caseValue : caseValues) { - if (caseValue == node) { - returnValueIgnored = false; - break; - } - } - } else if (qualifiedElement instanceof JavaUAssertExpression) { - returnValueIgnored = !((JavaUAssertExpression) qualifiedElement).getCondition().equals(node); - } else { - returnValueIgnored = true; - } - if (returnValueIgnored && isCommit) { - String message = "Consider using `apply()` instead; `commit` writes " - + "its data to persistent storage immediately, whereas " - + "`apply` will handle it in the background"; - mContext.report(ISSUE, node, mContext.getLocation(node), message); - } - } - } - } - } - - return false; - } - - @Override - public boolean visitReturnExpression(@NotNull UReturnExpression node) { - if (mTarget.equals(node.getExpressionType())) { - mFound = true; - } - - return false; - } - - boolean isCommitCalled() { - return mFound; - } - } -} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SslCertificateSocketFactoryDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SslCertificateSocketFactoryDetector.java new file mode 100644 index 00000000000..37dcba201e4 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SslCertificateSocketFactoryDetector.java @@ -0,0 +1,119 @@ +/* + * 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.checks; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Arrays; +import java.util.List; + +public class SslCertificateSocketFactoryDetector extends Detector implements Detector.UastScanner { + + private static final Implementation IMPLEMENTATION_JAVA = new Implementation( + SslCertificateSocketFactoryDetector.class, + Scope.JAVA_FILE_SCOPE); + + public static final Issue CREATE_SOCKET = Issue.create( + "SSLCertificateSocketFactoryCreateSocket", //$NON-NLS-1$ + "Insecure call to `SSLCertificateSocketFactory.createSocket()`", + "When `SSLCertificateSocketFactory.createSocket()` is called with an `InetAddress` " + + "as the first parameter, TLS/SSL hostname verification is not performed, which " + + "could result in insecure network traffic caused by trusting arbitrary " + + "hostnames in TLS/SSL certificates presented by peers. In this case, developers " + + "must ensure that the `InetAddress` is explicitly verified against the certificate " + + "through other means, such as by calling " + + "`SSLCertificateSocketFactory.getDefaultHostnameVerifier() to get a " + + "`HostnameVerifier` and calling `HostnameVerifier.verify()`.", + Category.SECURITY, + 6, + Severity.WARNING, + IMPLEMENTATION_JAVA); + + public static final Issue GET_INSECURE = Issue.create( + "SSLCertificateSocketFactoryGetInsecure", //$NON-NLS-1$ + "Call to `SSLCertificateSocketFactory.getInsecure()`", + "The `SSLCertificateSocketFactory.getInsecure()` method returns " + + "an SSLSocketFactory with all TLS/SSL security checks disabled, which " + + "could result in insecure network traffic caused by trusting arbitrary " + + "TLS/SSL certificates presented by peers. This method should be " + + "avoided unless needed for a special circumstance such as " + + "debugging. Instead, `SSLCertificateSocketFactory.getDefault()` " + + "should be used.", + Category.SECURITY, + 6, + Severity.WARNING, + IMPLEMENTATION_JAVA); + + private static final String INET_ADDRESS_CLASS = + "java.net.InetAddress"; + + private static final String SSL_CERTIFICATE_SOCKET_FACTORY_CLASS = + "android.net.SSLCertificateSocketFactory"; + + // ---- Implements JavaScanner ---- + + @Override + public List getApplicableMethodNames() { + // Detect calls to potentially insecure SSLCertificateSocketFactory methods + return Arrays.asList("createSocket", "getInsecure"); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + if (context.getEvaluator().isMemberInSubClassOf(method, + SSL_CERTIFICATE_SOCKET_FACTORY_CLASS, false)) { + String methodName = method.getName(); + if ("createSocket".equals(methodName)) { + List args = call.getValueArguments(); + if (!args.isEmpty()) { + PsiType type = args.get(0).getExpressionType(); + if (type != null + && (INET_ADDRESS_CLASS.equals(type.getCanonicalText()) + || context.getEvaluator().extendsClass(((PsiClassType)type).resolve(), + INET_ADDRESS_CLASS, false))) { + context.report(CREATE_SOCKET, call, context.getUastLocation(call), + "Use of `SSLCertificateSocketFactory.createSocket()` " + + "with an InetAddress parameter can cause insecure " + + "network traffic due to trusting arbitrary hostnames in " + + "TLS/SSL certificates presented by peers"); + } + } + } else if ("getInsecure".equals(methodName)) { + context.report(GET_INSECURE, call, context.getUastLocation(call), + "Use of `SSLCertificateSocketFactory.getInsecure()` can cause " + + "insecure network traffic due to trusting arbitrary TLS/SSL " + + "certificates presented by peers"); + } + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringAuthLeakDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringAuthLeakDetector.java new file mode 100644 index 00000000000..9a6e6bacb0c --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringAuthLeakDetector.java @@ -0,0 +1,77 @@ +package com.android.tools.klint.checks; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.Scope; + +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Detector that looks for leaked credentials in strings. + */ +public class StringAuthLeakDetector extends Detector implements Detector.UastScanner { + + /** Looks for hidden code */ + public static final Issue AUTH_LEAK = Issue.create( + "AuthLeak", "Code might contain an auth leak", + "Strings in java apps can be discovered by decompiling apps, this lint check looks " + + "for code which looks like it may contain an url with a username and password", + Category.SECURITY, 6, Severity.WARNING, + new Implementation(StringAuthLeakDetector.class, Scope.JAVA_FILE_SCOPE)); + + @Nullable + @Override + public List> getApplicableUastTypes() { + return Collections.>singletonList(ULiteralExpression.class); + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { + return new AuthLeakChecker(context); + } + + private static class AuthLeakChecker extends AbstractUastVisitor { + private final static String LEGAL_CHARS = "([\\w_.!~*\'()%;&=+$,-]+)"; // From RFC 2396 + private final static Pattern AUTH_REGEXP = + Pattern.compile("([\\w+.-]+)://" + LEGAL_CHARS + ':' + LEGAL_CHARS + '@' + + LEGAL_CHARS); + + private final JavaContext mContext; + + private AuthLeakChecker(JavaContext context) { + mContext = context; + } + + @Override + public boolean visitLiteralExpression(ULiteralExpression node) { + if (node.getValue() instanceof String) { + Matcher matcher = AUTH_REGEXP.matcher((String)node.getValue()); + if (matcher.find()) { + String password = matcher.group(3); + if (password == null || (password.startsWith("%") && password.endsWith("s"))) { + return super.visitLiteralExpression(node); + } + Location location = mContext.getUastLocation(node); + mContext.report(AUTH_LEAK, node, location, "Possible credential leak"); + } + } + return super.visitLiteralExpression(node); + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringFormatDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringFormatDetector.java index fc08a5f269e..c89c9311c10 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringFormatDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringFormatDetector.java @@ -17,22 +17,23 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.ATTR_NAME; +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.DOT_JAVA; import static com.android.SdkConstants.FORMAT_METHOD; import static com.android.SdkConstants.GET_STRING_METHOD; -import static com.android.SdkConstants.R_CLASS; -import static com.android.SdkConstants.R_PREFIX; import static com.android.SdkConstants.TAG_STRING; -import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN; -import static com.android.tools.klint.client.api.JavaParser.TYPE_BYTE; -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_NULL; +import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_BYTE_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_CHARACTER_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_INTEGER_WRAPPER; +import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG_WRAPPER; import static com.android.tools.klint.client.api.JavaParser.TYPE_OBJECT; -import static com.android.tools.klint.client.api.JavaParser.TYPE_SHORT; +import static com.android.tools.klint.client.api.JavaParser.TYPE_SHORT_WRAPPER; import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING; import com.android.annotations.NonNull; @@ -41,11 +42,14 @@ import com.android.annotations.VisibleForTesting; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.res2.ResourceItem; +import com.android.ide.common.resources.ResourceUrl; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.client.api.LintClient; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.JavaContext; @@ -53,20 +57,30 @@ import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Location.Handle; import com.android.tools.klint.detector.api.Position; +import com.android.tools.klint.detector.api.ResourceEvaluator; import com.android.tools.klint.detector.api.ResourceXmlDetector; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; import com.android.tools.klint.detector.api.XmlContext; import com.android.utils.Pair; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -79,7 +93,6 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -90,10 +103,9 @@ import java.util.regex.Pattern; * Check which looks for problems with formatting strings such as inconsistencies between * translations or between string declaration and string usage in Java. *

- * TODO: Verify booleans! * TODO: Handle Resources.getQuantityString as well */ -public class StringFormatDetector extends ResourceXmlDetector implements UastScanner { +public class StringFormatDetector extends ResourceXmlDetector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION_XML = new Implementation( StringFormatDetector.class, Scope.ALL_RESOURCES_SCOPE); @@ -101,8 +113,8 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca @SuppressWarnings("unchecked") private static final Implementation IMPLEMENTATION_XML_AND_JAVA = new Implementation( StringFormatDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.SOURCE_FILE), - Scope.SOURCE_FILE_SCOPE); + EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE), + Scope.JAVA_FILE_SCOPE); /** Whether formatting strings are invalid */ @@ -202,8 +214,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca private Map>> mFormatStrings; /** - * Map of strings that contain percents that aren't formatting strings; these - * should not be passed to String.format. + * Map of strings that do not contain any formatting. */ private final Map mNotFormatStrings = new HashMap(); @@ -243,7 +254,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca if (childNodes.getLength() == 1) { Node child = childNodes.item(0); if (child.getNodeType() == Node.TEXT_NODE) { - checkTextNode(context, element, strip(child.getNodeValue())); + checkTextNode(context, element, stripQuotes(child.getNodeValue())); } } else { // Concatenate children and build up a plain string. @@ -261,7 +272,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca private static void addText(StringBuilder sb, Node node) { if (node.getNodeType() == Node.TEXT_NODE) { - sb.append(strip(node.getNodeValue().trim())); + sb.append(stripQuotes(node.getNodeValue().trim())); } else { NodeList childNodes = node.getChildNodes(); for (int i = 0, n = childNodes.getLength(); i < n; i++) { @@ -270,21 +281,40 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca } } - private static String strip(String s) { - if (s.length() < 2) { - return s; - } - char first = s.charAt(0); - char last = s.charAt(s.length() - 1); - if (first == last && (first == '\'' || first == '"')) { - return s.substring(1, s.length() - 1); + /** + * Removes all the unescaped quotes. See + * Escaping apostrophes and quotes + */ + @VisibleForTesting + static String stripQuotes(String s) { + StringBuilder sb = new StringBuilder(); + boolean isEscaped = false; + boolean isQuotedBlock = false; + for (int i = 0, len = s.length(); i < len; i++) { + char current = s.charAt(i); + if (isEscaped) { + sb.append(current); + isEscaped = false; + } else { + isEscaped = current == '\\'; // Next char will be escaped so we will just copy it + if (current == '"') { + isQuotedBlock = !isQuotedBlock; + } else if (current == '\'') { + if (isQuotedBlock) { + // We only add single quotes when they are within a quoted block + sb.append(current); + } + } else { + sb.append(current); + } + } } - return s; + return sb.toString(); } private void checkTextNode(XmlContext context, Element element, String text) { - String name = null; + String name = element.getAttribute(ATTR_NAME); boolean found = false; boolean foundPlural = false; @@ -296,10 +326,6 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca j++; } if (c == '%') { - if (name == null) { - name = element.getAttribute(ATTR_NAME); - } - // Also make sure this String isn't an unformatted String String formatted = element.getAttribute("formatted"); //$NON-NLS-1$ if (!formatted.isEmpty() && !Boolean.parseBoolean(formatted)) { @@ -358,28 +384,44 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca } } - if (found && name != null) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } + if (!context.getProject().getReportIssues()) { + // If this is a library project not being analyzed, ignore it + return; + } - // Record it for analysis when seen in Java code - if (mFormatStrings == null) { - mFormatStrings = new HashMap>>(); - } - - List> list = mFormatStrings.get(name); - if (list == null) { - list = new ArrayList>(); - mFormatStrings.put(name, list); - } + if (name != null) { Handle handle = context.createLocationHandle(element); handle.setClientData(element); - list.add(Pair.of(handle, text)); + if (found) { + // Record it for analysis when seen in Java code + if (mFormatStrings == null) { + mFormatStrings = new HashMap>>(); + } + + List> list = mFormatStrings.get(name); + if (list == null) { + list = new ArrayList>(); + mFormatStrings.put(name, list); + } + list.add(Pair.of(handle, text)); + } else { + if (!isReference(text)) { + mNotFormatStrings.put(name, handle); + } + } } } + private static boolean isReference(@NonNull String text) { + for (int i = 0, n = text.length(); i < n; i++) { + char c = text.charAt(i); + if (!Character.isWhitespace(c)) { + return c == '@' || c == '?'; + } + } + return false; + } + /** * Checks whether the text begins with a non-unit word, pointing to a string * that should probably be a plural instead. This @@ -466,6 +508,11 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca // Check argument counts if (checkCount) { + Handle notFormatted = mNotFormatStrings.get(name); + if (notFormatted != null) { + list = ImmutableList.>builder() + .add(Pair.of(notFormatted, name)).addAll(list).build(); + } checkArity(context, name, list); } @@ -788,7 +835,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca // Argument Index "(\\d+\\$)?" + //$NON-NLS-1$ // Flags - "([-+#, 0(\\<]*)?" + //$NON-NLS-1$ + "([-+#, 0(<]*)?" + //$NON-NLS-1$ // Width "(\\d+)?" + //$NON-NLS-1$ // Precision @@ -862,7 +909,6 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca * {@code seenArguments} parameter is not null, put the indices of any * observed arguments into it. */ - @VisibleForTesting static int getFormatArgumentCount(@NonNull String s, @Nullable Set seenArguments) { Matcher matcher = FORMAT.matcher(s); int index = 0; @@ -978,130 +1024,207 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca return false; } - // ---- Implements UastScanner ---- - - @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Arrays.asList(FORMAT_METHOD, GET_STRING_METHOD); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - if (mFormatStrings == null && !context.getLintContext().getClient().supportsProjectResources()) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod method) { + if (mFormatStrings == null && !context.getClient().supportsProjectResources()) { return; } - UExpression receiver = UastUtils.getReceiver(node); + JavaEvaluator evaluator = context.getEvaluator(); + String methodName = method.getName(); + if (methodName.equals(FORMAT_METHOD)) { + if (JavaEvaluator.isMemberInClass(method, TYPE_STRING)) { + // Check formatting parameters for + // java.lang.String#format(String format, Object... formatArgs) + // java.lang.String#format(Locale locale, String format, Object... formatArgs) + checkStringFormatCall(context, method, node, + method.getParameterList().getParametersCount() == 3); - String methodName = node.getFunctionName(); - if (FORMAT_METHOD.equals(methodName)) { - // String.format(getResources().getString(R.string.foo), arg1, arg2, ...) - // Check that the arguments in R.string.foo match arg1, arg2, ... - if (receiver instanceof USimpleReferenceExpression) { - USimpleReferenceExpression ref = (USimpleReferenceExpression) receiver; - if ("String".equals(ref.getIdentifier())) { //$NON-NLS-1$ - // Found a String.format call - // Look inside to see if we can find an R string - // Find surrounding method - checkFormatCall(context, node); - } + // TODO: Consider also enforcing + // java.util.Formatter#format(String string, Object... formatArgs) } } else { - // getResources().getString(R.string.foo, arg1, arg2, ...) - // Check that the arguments in R.string.foo match arg1, arg2, ... - if (node.getValueArgumentCount() > 1 && receiver != null ) { - checkFormatCall(context, node); - } - } - } + // Look up any of these string formatting methods: + // android.content.res.Resources#getString(@StringRes int resId, Object... formatArgs) + // android.content.Context#getString(@StringRes int resId, Object... formatArgs) + // android.app.Fragment#getString(@StringRes int resId, Object... formatArgs) + // android.support.v4.app.Fragment#getString(@StringRes int resId, Object... formatArgs) - private void checkFormatCall(UastAndroidContext context, UCallExpression node) { - UFunction current = UastUtils.getContainingFunction(node); - if (current != null) { - checkStringFormatCall(context, current, node); + // Many of these also define a plain getString method: + // android.content.res.Resources#getString(@StringRes int resId) + // However, while it's possible that these contain formatting strings) it's + // also possible that they're looking up strings that are not intended to be used + // for formatting so while we may want to warn about this it's not necessarily + // an error. + if (method.getParameterList().getParametersCount() < 2) { + return; + } + + if (evaluator.isMemberInSubClassOf(method, CLASS_RESOURCES, false) || + evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT, false) || + evaluator.isMemberInSubClassOf(method, CLASS_FRAGMENT, false) || + evaluator.isMemberInSubClassOf(method, CLASS_V4_FRAGMENT, false)) { + checkStringFormatCall(context, method, node, false); + } + + // TODO: Consider also looking up + // android.content.res.Resources#getQuantityString(@PluralsRes int id, int quantity, + // Object... formatArgs) + // though this will require being smarter about cross referencing formatting + // strings since we'll need to go via the quantity string definitions } } /** - * Check the given String.format call (with the given arguments) to see if - * the string format is being used correctly - * + * Checks a String.format call that is using a string that doesn't contain format placeholders. * @param context the context to report errors to - * @param method the method containing the {@link String#format} call * @param call the AST node for the {@link String#format} + * @param name the string name + * @param handle the string location + */ + private static void checkNotFormattedHandle( + JavaContext context, + UCallExpression call, + String name, + Handle handle) { + Object clientData = handle.getClientData(); + if (clientData instanceof Node) { + if (context.getDriver().isSuppressed(null, INVALID, (Node) clientData)) { + return; + } + } + Location location = context.getUastLocation(call); + Location secondary = handle.resolve(); + secondary.setMessage("This definition does not require arguments"); + location.setSecondary(secondary); + String message = String.format( + "Format string '`%1$s`' is not a valid format string so it should not be " + + "passed to `String.format`", + name); + context.report(INVALID, call, location, message); + } + + /** + * Check the given String.format call (with the given arguments) to see if the string format is + * being used correctly + * @param context the context to report errors to + * @param calledMethod the method being called + * @param call the AST node for the {@link String#format} + * @param specifiesLocale whether the first parameter is a locale string, shifting the */ private void checkStringFormatCall( - UastAndroidContext context, - UFunction method, - UCallExpression call) { + JavaContext context, + PsiMethod calledMethod, + UCallExpression call, + boolean specifiesLocale) { + int argIndex = specifiesLocale ? 1 : 0; List args = call.getValueArguments(); - if (args.isEmpty()) { + + if (args.size() <= argIndex) { return; } - JavaContext lintContext = context.getLintContext(); - UastStringTracker tracker = new UastStringTracker(lintContext, method, call, 0); - method.accept(tracker); - String name = tracker.getFormatStringName(); - if (name == null) { + UExpression argument = args.get(argIndex); + ResourceUrl resource = ResourceEvaluator.getResource(context, argument); + if (resource == null || resource.framework || resource.type != ResourceType.STRING) { return; } + String name = resource.name; if (mIgnoreStrings != null && mIgnoreStrings.contains(name)) { return; } - if (mNotFormatStrings.containsKey(name)) { - Handle handle = mNotFormatStrings.get(name); - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (lintContext.getDriver().isSuppressed(null, INVALID, (Node) clientData)) { - return; + boolean passingVarArgsArray = false; + int callCount = args.size() - 1 - argIndex; + + if (callCount == 1) { + // If instead of a varargs call like + // getString(R.string.foo, arg1, arg2, arg3) + // the code is calling the varargs method with a packed Object array, as in + // getString(R.string.foo, new Object[] { arg1, arg2, arg3 }) + // we'll need to handle that such that we don't think this is a single + // argument + + UExpression lastArg = args.get(args.size() - 1); + PsiParameterList parameterList = calledMethod.getParameterList(); + int parameterCount = parameterList.getParametersCount(); + if (parameterCount > 0 && parameterList.getParameters()[parameterCount - 1].isVarArgs()) { + boolean knownArity = false; + + boolean argWasReference = false; + if (lastArg instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) lastArg).resolve(); + + + if (resolved instanceof PsiVariable) { + UExpression initializer = context.getUastContext().getInitializerBody ( + (PsiVariable) resolved); + if (UastExpressionUtils.isConstructorCall(initializer)) { + argWasReference = true; + // Now handled by check below + lastArg = initializer; + } + } + } + + if (UastExpressionUtils.isNewArray(lastArg)) { + UCallExpression callExpression = (UCallExpression) lastArg; + + if (UastExpressionUtils.isNewArrayWithInitializer(lastArg)) { + callCount = callExpression.getValueArgumentCount(); + knownArity = true; + } else if (UastExpressionUtils.isNewArrayWithDimensions(lastArg)) { + List arrayDimensions = callExpression.getValueArguments(); + if (arrayDimensions.size() == 1) { + UExpression first = arrayDimensions.get(0); + if (first instanceof ULiteralExpression) { + Object o = ((ULiteralExpression) first).getValue(); + if (o instanceof Integer) { + callCount = (Integer)o; + knownArity = true; + } + } + } + } + + if (!knownArity) { + if (!argWasReference) { + return; + } + } else { + passingVarArgsArray = true; + } } } - Location location = handle.resolve(); - String message = String.format( - "Format string '`%1$s`' is not a valid format string so it should not be " + - "passed to `String.format`", - name); - context.report(INVALID, call, location, message); - return; } - Iterator argIterator = args.iterator(); - UExpression first = argIterator.next(); - UExpression second = argIterator.hasNext() ? argIterator.next() : null; - - boolean specifiesLocale; - UType parameterType = first.getExpressionType(); - if (parameterType != null) { - specifiesLocale = isLocaleReference(parameterType.getName()); - } else if (!FORMAT_METHOD.equals(call.getFunctionName())) { - specifiesLocale = false; - } else { - // No type information with this AST; use string patterns instead to make - // an educated guess - String firstName = first.renderString(); - specifiesLocale = firstName.startsWith("Locale.") //$NON-NLS-1$ - || firstName.contains("locale") //$NON-NLS-1$ - || firstName.equals("null") //$NON-NLS-1$ - || (second != null && second.renderString().contains("getString") //$NON-NLS-1$ - && !firstName.contains("getString") //$NON-NLS-1$ - && !firstName.contains(R_PREFIX) - && !(UastLiteralUtils.isStringLiteral(first))); + if (callCount > 0 && mNotFormatStrings.containsKey(name)) { + checkNotFormattedHandle(context, call, name, mNotFormatStrings.get(name)); + return; } List> list = mFormatStrings != null ? mFormatStrings.get(name) : null; if (list == null) { - LintClient client = lintContext.getClient(); + LintClient client = context.getClient(); if (client.supportsProjectResources() && - !lintContext.getScope().contains(Scope.RESOURCE_FILE)) { + !context.getScope().contains(Scope.RESOURCE_FILE)) { AbstractResourceRepository resources = client - .getProjectResources(lintContext.getMainProject(), true); - List items = resources - .getResourceItem(ResourceType.STRING, name); + .getProjectResources(context.getMainProject(), true); + List items; + if (resources != null) { + items = resources.getResourceItem(ResourceType.STRING, name); + } else { + // Must be a non-Android module + items = null; + } if (items != null) { for (final ResourceItem item : items) { ResourceValue v = item.getResourceValue(false); @@ -1136,6 +1259,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca // If the user marked the string with } + Handle handle = client.createResourceItemHandle(item); if (isFormattingString) { if (list == null) { list = Lists.newArrayList(); @@ -1144,8 +1268,9 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca } mFormatStrings.put(name, list); } - Handle handle = client.createResourceItemHandle(item); list.add(Pair.of(handle, value)); + } else if (callCount > 0) { + checkNotFormattedHandle(context, call, name, handle); } } } @@ -1165,32 +1290,30 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca } int count = getFormatArgumentCount(s, null); Handle handle = pair.getFirst(); - if (count != args.size() - 1 - (specifiesLocale ? 1 : 0)) { - if (isSharedPreferenceGetString(context, call)) { - continue; - } - - Location location = context.getLocation(call); + if (count != callCount) { + Location location = context.getUastLocation(call); Location secondary = handle.resolve(); - - if (location != null && secondary != null) { - secondary.setMessage(String.format("This definition requires %1$d arguments", - count)); - location.setSecondary(secondary); - String message = String.format( - "Wrong argument count, format string `%1$s` requires `%2$d` but format " + - "call supplies `%3$d`", - name, count, args.size() - 1 - (specifiesLocale ? 1 : 0)); - context.report(ARG_TYPES, method, location, message); - if (reported == null) { - reported = Sets.newHashSet(); - } - reported.add(s); + secondary.setMessage(String.format("This definition requires %1$d arguments", + count)); + location.setSecondary(secondary); + String message = String.format( + "Wrong argument count, format string `%1$s` requires `%2$d` but format " + + "call supplies `%3$d`", + name, count, callCount); + context.report(ARG_TYPES, call, location, message); + if (reported == null) { + reported = Sets.newHashSet(); } + reported.add(s); } else { + if (passingVarArgsArray) { + // Can't currently check these: make sure we don't incorrectly + // flag parameters on the Object[] instead of the wrapped parameters + return; + } for (int i = 1; i <= count; i++) { - int argumentIndex = i + (specifiesLocale ? 1 : 0); - Class type = tracker.getArgumentType(argumentIndex); + int argumentIndex = i + argIndex; + PsiType type = args.get(argumentIndex).getExpressionType(); if (type != null) { boolean valid = true; String formatType = getFormatArgumentType(s, i); @@ -1211,7 +1334,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca // unusual and probably not intended. case 'b': case 'B': - valid = type == Boolean.TYPE; + valid = isBooleanType(type); break; // Numeric: integer and floats in various formats @@ -1226,17 +1349,12 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca case 'G': case 'a': case 'A': - valid = type == Integer.TYPE - || type == Float.TYPE - || type == Double.TYPE - || type == Long.TYPE - || type == Byte.TYPE - || type == Short.TYPE; + valid = isNumericType(type, true); break; case 'c': case 'C': // Unicode character - valid = type == Character.TYPE; + valid = isCharacterType(type); break; case 'h': case 'H': // Hex print of hash code of objects @@ -1246,35 +1364,66 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca // numbers since you may have meant more // specific formatting. Use special issue // explanation for this? - valid = type != Boolean.TYPE && - !Number.class.isAssignableFrom(type); + valid = !isBooleanType(type) && + !isNumericType(type, false); break; } if (!valid) { - if (isSharedPreferenceGetString(context, call)) { - continue; - } - - UExpression argument = tracker.getArgument(argumentIndex); - Location location = context.getLocation(argument); + Location location = context.getUastLocation(args.get(argumentIndex)); Location secondary = handle.resolve(); - if (location != null && secondary != null) { - secondary.setMessage("Conflicting argument declaration here"); - location.setSecondary(secondary); - - String message = String.format( - "Wrong argument type for formatting argument '#%1$d' " + - "in `%2$s`: conversion is '`%3$s`', received `%4$s` " + - "(argument #%5$d in method call)", - i, name, formatType, type.getSimpleName(), - argumentIndex + 1); - context.report(ARG_TYPES, method, location, message); - if (reported == null) { - reported = Sets.newHashSet(); + secondary.setMessage("Conflicting argument declaration here"); + location.setSecondary(secondary); + String suggestion = null; + if (isBooleanType(type)) { + suggestion = "`b`"; + } else if (isCharacterType(type)) { + suggestion = "'c'"; + } else if (PsiType.INT.equals(type) + || PsiType.LONG.equals(type) + || PsiType.BYTE.equals(type) + || PsiType.SHORT.equals(type)) { + suggestion = "`d`, 'o' or `x`"; + } else if (PsiType.FLOAT.equals(type) + || PsiType.DOUBLE.equals(type)) { + suggestion = "`e`, 'f', 'g' or `a`"; + } else if (type instanceof PsiClassType) { + String fqn = type.getCanonicalText(); + if (TYPE_INTEGER_WRAPPER.equals(fqn) + || TYPE_LONG_WRAPPER.equals(fqn) + || TYPE_BYTE_WRAPPER.equals(fqn) + || TYPE_SHORT_WRAPPER.equals(fqn)) { + suggestion = "`d`, 'o' or `x`"; + } else if (TYPE_FLOAT_WRAPPER.equals(fqn) + || TYPE_DOUBLE_WRAPPER.equals(fqn)) { + suggestion = "`d`, 'o' or `x`"; + } else if (TYPE_OBJECT.equals(fqn)) { + suggestion = "'s' or 'h'"; } - reported.add(s); } + + if (suggestion != null) { + suggestion = " (Did you mean formatting character " + + suggestion + "?)"; + } else { + suggestion = ""; + } + + String canonicalText = type.getCanonicalText(); + canonicalText = canonicalText.substring( + canonicalText.lastIndexOf('.') + 1); + + String message = String.format( + "Wrong argument type for formatting argument '#%1$d' " + + "in `%2$s`: conversion is '`%3$s`', received `%4$s` " + + "(argument #%5$d in method call)%6$s", + i, name, formatType, canonicalText, + argumentIndex + 1, suggestion); + context.report(ARG_TYPES, call, location, message); + if (reported == null) { + reported = Sets.newHashSet(); + } + reported.add(s); } } } @@ -1283,358 +1432,61 @@ public class StringFormatDetector extends ResourceXmlDetector implements UastSca } } - private static boolean isSharedPreferenceGetString(@NonNull UastAndroidContext context, - @NonNull UCallExpression call) { - if (!GET_STRING_METHOD.equals(call.getFunctionName())) { - return false; + private static boolean isCharacterType(PsiType type) { + //return PsiType.CHAR.isAssignableFrom(type); + if (type == PsiType.CHAR) { + return true; + } + if (type instanceof PsiClassType) { + String fqn = type.getCanonicalText(); + return TYPE_CHARACTER_WRAPPER.equals(fqn); } - UFunction resolvedMethod = call.resolve(context); - if (resolvedMethod != null) { - UClass containingClass = UastUtils.getContainingClassOrEmpty(resolvedMethod); - return containingClass.isSubclassOf(SharedPrefsDetector.ANDROID_CONTENT_SHARED_PREFERENCES); - } - - return false; // not certain + return false; } - private static boolean isLocaleReference(@Nullable UType reference) { - return reference != null && isLocaleReference(reference.getName()); + private static boolean isBooleanType(PsiType type) { + //return PsiType.BOOLEAN.isAssignableFrom(type); + if (type == PsiType.BOOLEAN) { + return true; + } + if (type instanceof PsiClassType) { + String fqn = type.getCanonicalText(); + return TYPE_BOOLEAN_WRAPPER.equals(fqn); + } + + return false; } - private static boolean isLocaleReference(@Nullable String typeName) { - return typeName != null && (typeName.equals("Locale") //$NON-NLS-1$ - || typeName.equals("java.util.Locale")); //$NON-NLS-1$ - } - - /** Returns the resource name corresponding to the first argument in the given call */ - @Nullable - public static String getResourceForFirstArg( - @NonNull UElement method, - @NonNull UElement call) { - assert call instanceof UCallExpression; - UastStringTracker tracker = new UastStringTracker(null, method, call, 0); - method.accept(tracker); - - return tracker.getFormatStringName(); - } - - /** Returns the resource name corresponding to the given argument in the given call */ - @Nullable - public static String getResourceArg( - @NonNull UElement method, - @NonNull UElement call, - int argIndex) { - assert call instanceof UCallExpression; - UastStringTracker tracker = new UastStringTracker(null, method, call, argIndex); - method.accept(tracker); - - return tracker.getFormatStringName(); - } - - /** - * Given a variable reference, finds the original R.string value corresponding to it. - * For example: - *

-     * {@code
-     *  String target = "World";
-     *  String hello = getResources().getString(R.string.hello);
-     *  String output = String.format(hello, target);
-     * }
-     * 
- * - * Given the {@code String.format} call, we want to find out what R.string resource - * corresponds to the first argument, in this case {@code R.string.hello}. - * To do this, we look for R.string references, and track those through assignments - * until we reach the target node. - *

- * In addition, it also does some primitive type tracking such that it (in some cases) - * can answer questions about the types of variables. This allows it to check whether - * certain argument types are valid. Note however that it does not do full-blown - * type analysis by checking method call signatures and so on. - */ - private static class UastStringTracker extends AbstractUastVisitor { - /** Method we're searching within */ - private final UElement mTop; - /** The argument index in the method we're targeting */ - private final int mArgIndex; - /** Map from variable name to corresponding string resource name */ - private final Map mMap = new HashMap(); - /** Map from variable name to corresponding type */ - private final Map> mTypes = new HashMap>(); - /** The AST node for the String.format we're interested in */ - private final UElement mTargetNode; - private boolean mDone; - @Nullable - private JavaContext mContext; - - /** - * Result: the name of the string resource being passed to the - * String.format, if any - */ - private String mName; - - public UastStringTracker(@Nullable JavaContext context, UElement top, UElement targetNode, int argIndex) { - mContext = context; - mTop = top; - mArgIndex = argIndex; - mTargetNode = targetNode; + //PsiType:java.lang.Boolean + private static boolean isNumericType(@NonNull PsiType type, boolean allowBigNumbers) { + if (PsiType.INT.equals(type) + || PsiType.FLOAT.equals(type) + || PsiType.DOUBLE.equals(type) + || PsiType.LONG.equals(type) + || PsiType.BYTE.equals(type) + || PsiType.SHORT.equals(type)) { + return true; } - public String getFormatStringName() { - return mName; - } - - /** Returns the argument type of the given formatting argument of the - * target node. Note: This is in the formatting string, which is one higher - * than the String.format parameter number, since the first argument is the - * formatting string itself. - * - * @param argument the argument number - * @return the class (such as {@link Integer#TYPE} etc) or null if not known - */ - public Class getArgumentType(int argument) { - UExpression arg = getArgument(argument); - if (arg != null) { - // Look up type based on the source code literals - Class type = getType(arg); - if (type != null) { - return type; - } - - // If the AST supports type resolution, use that for other types - // of expressions - if (mContext != null) { - return getTypeClass(arg.getExpressionType()); - } + if (type instanceof PsiClassType) { + String fqn = type.getCanonicalText(); + if (TYPE_INTEGER_WRAPPER.equals(fqn) + || TYPE_FLOAT_WRAPPER.equals(fqn) + || TYPE_DOUBLE_WRAPPER.equals(fqn) + || TYPE_LONG_WRAPPER.equals(fqn) + || TYPE_BYTE_WRAPPER.equals(fqn) + || TYPE_SHORT_WRAPPER.equals(fqn)) { + return true; } - - return null; - } - - private static Class getTypeClass(@Nullable UType type) { - if (type != null) { - return getTypeClass(type.getName()); - } - return null; - } - - private static Class getTypeClass(@Nullable String fqcn) { - if (fqcn == null) { - return null; - } else if (fqcn.equals(TYPE_STRING) || fqcn.equals("String")) { //$NON-NLS-1$ - return String.class; - } else if (fqcn.equals(TYPE_INT)) { - return Integer.TYPE; - } else if (fqcn.equals(TYPE_BOOLEAN)) { - return Boolean.TYPE; - } else if (fqcn.equals(TYPE_NULL)) { - return Object.class; - } else if (fqcn.equals(TYPE_LONG)) { - return Long.TYPE; - } else if (fqcn.equals(TYPE_FLOAT)) { - return Float.TYPE; - } else if (fqcn.equals(TYPE_DOUBLE)) { - return Double.TYPE; - } else if (fqcn.equals(TYPE_CHAR)) { - return Character.TYPE; - } else if (fqcn.equals("BigDecimal") //$NON-NLS-1$ - || fqcn.equals("java.math.BigDecimal")) { //$NON-NLS-1$ - return Float.TYPE; - } else if (fqcn.equals("BigInteger") //$NON-NLS-1$ - || fqcn.equals("java.math.BigInteger")) { //$NON-NLS-1$ - return Integer.TYPE; - } else if (fqcn.equals(TYPE_OBJECT)) { - return null; - } else if (fqcn.startsWith("java.lang.")) { - if (fqcn.equals("java.lang.Integer") - || fqcn.equals("java.lang.Short") - || fqcn.equals("java.lang.Byte") - || fqcn.equals("java.lang.Long")) { - return Integer.TYPE; - } else if (fqcn.equals("java.lang.Float") - || fqcn.equals("java.lang.Double")) { - return Float.TYPE; - } else { - return null; - } - } else if (fqcn.equals(TYPE_BYTE)) { - return Byte.TYPE; - } else if (fqcn.equals(TYPE_SHORT)) { - return Short.TYPE; - } else { - return null; - } - } - - public UExpression getArgument(int argument) { - if (!(mTargetNode instanceof UCallExpression)) { - return null; - } - UCallExpression call = (UCallExpression) mTargetNode; - List args = call.getValueArguments(); - if (argument < 0 || argument >= args.size()) { - return null; - } - - return args.get(argument); - } - - @Override - public boolean visitElement(@NotNull UElement node) { - return mDone || super.visitElement(node); - } - - @Override - public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) { - if (node.selectorMatches(R_CLASS) && //$NON-NLS-1$ - node.getParent() instanceof UQualifiedExpression && - node.getParent().getParent() instanceof UQualifiedExpression) { - - // See if we're on the right hand side of an assignment - UElement current = node.getParent().getParent(); - String reference = ((UQualifiedExpression) current).getSelector().renderString(); - - while (current != null && current != mTop && !(current instanceof UVariable)) { - if (current == mTargetNode) { - mName = reference; - mDone = true; - return false; - } - current = current.getParent(); - } - if (current instanceof UVariable) { - UVariable entry = (UVariable) current; - String variable = entry.getName(); - mMap.put(variable, reference); - } - } - - return false; - } - - @Nullable - private UExpression getTargetArgument() { - Iterator iterator; - if (mTargetNode instanceof UCallExpression) { - iterator = ((UCallExpression) mTargetNode).getValueArguments().iterator(); - } else { - return null; - } - int i = 0; - while (i < mArgIndex && iterator.hasNext()) { - iterator.next(); - i++; - } - if (iterator.hasNext()) { - UExpression next = iterator.next(); - if (next != null && mContext != null && iterator.hasNext()) { - UType type = next.getExpressionType(); - if (isLocaleReference(type)) { - next = iterator.next(); - } else if (type == null - && next.renderString().startsWith("Locale.")) { //$NON-NLS-1$ - next = iterator.next(); - } - } - return next; - } - - return null; - } - - @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - if (node == mTargetNode) { - UExpression arg = getTargetArgument(); - if (arg instanceof USimpleReferenceExpression) { - USimpleReferenceExpression reference = (USimpleReferenceExpression) arg; - String variable = reference.getIdentifier(); - mName = mMap.get(variable); - mDone = true; + if (allowBigNumbers) { + if ("java.math.BigInteger".equals(fqn) || + "java.math.BigDecimal".equals(fqn)) { return true; } } - - // Is this a getString() call? On a resource object? If so, - // promote the resource argument up to the left hand side - return false; } - @Override - public boolean visitVariable(@NotNull UVariable node) { - String name = node.getName(); - UExpression rhs = node.getInitializer(); - Class type = getType(rhs); - if (type != null) { - mTypes.put(name, type); - } else { - // Make sure we're not visiting the String.format node itself. If you have - // msg = String.format("%1$s", msg) - // then we'd be wiping out the type of "msg" before visiting the - // String.format call! - if (rhs != mTargetNode) { - mTypes.remove(name); - } - } - - return false; - } - - private Class getType(UExpression expression) { - if (expression == null) { - return null; - } - - if (expression instanceof USimpleReferenceExpression) { - USimpleReferenceExpression reference = (USimpleReferenceExpression)expression; - String variable = reference.getIdentifier(); - Class type = mTypes.get(variable); - if (type != null) { - return type; - } - } else if (expression instanceof UCallExpression) { - UCallExpression method = (UCallExpression)expression; - String methodName = method.getFunctionName(); - if (GET_STRING_METHOD.equals(methodName)) { - return String.class; - } - } else if (expression instanceof ULiteralExpression) { - Object value = ((ULiteralExpression)expression).getValue(); - if (value == null) { - return Object.class; - } else if (value instanceof String) { - return String.class; - } else if (value instanceof Integer) { - return Integer.TYPE; - } else if (value instanceof Long) { - return Long.TYPE; - } else if (value instanceof Byte) { - return Byte.TYPE; - } else if (value instanceof Short) { - return Short.TYPE; - } else if (value instanceof Float) { - return Float.TYPE; - } else if (value instanceof Double) { - return Double.TYPE; - } else if (value instanceof Character) { - return Character.TYPE; - } else if (value instanceof Boolean) { - return Boolean.TYPE; - } - } - - UType type = expression.getExpressionType(); - if (type != null) { - Class typeClass = getTypeClass(type); - if (typeClass != null) { - return typeClass; - } else { - return Object.class; - } - } - - return null; - } + return false; } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SupportAnnotationDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SupportAnnotationDetector.java index 31dddd726cb..2eb517ab5b3 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SupportAnnotationDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/SupportAnnotationDetector.java @@ -19,39 +19,100 @@ package com.android.tools.klint.checks; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_VALUE; +import static com.android.SdkConstants.CLASS_INTENT; +import static com.android.SdkConstants.CLASS_VIEW; import static com.android.SdkConstants.INT_DEF_ANNOTATION; -import static com.android.SdkConstants.R_CLASS; import static com.android.SdkConstants.STRING_DEF_ANNOTATION; import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX; import static com.android.SdkConstants.TAG_PERMISSION; import static com.android.SdkConstants.TAG_USES_PERMISSION; +import static com.android.SdkConstants.TAG_USES_PERMISSION_SDK_23; +import static com.android.SdkConstants.TAG_USES_PERMISSION_SDK_M; import static com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE; import static com.android.resources.ResourceType.COLOR; +import static com.android.resources.ResourceType.DIMEN; import static com.android.resources.ResourceType.DRAWABLE; import static com.android.resources.ResourceType.MIPMAP; +import static com.android.tools.klint.checks.PermissionFinder.Operation.ACTION; +import static com.android.tools.klint.checks.PermissionFinder.Operation.READ; +import static com.android.tools.klint.checks.PermissionFinder.Operation.WRITE; +import static com.android.tools.klint.checks.PermissionRequirement.ATTR_PROTECTION_LEVEL; +import static com.android.tools.klint.checks.PermissionRequirement.VALUE_DANGEROUS; +import static com.android.tools.klint.checks.PermissionRequirement.getAnnotationBooleanValue; +import static com.android.tools.klint.checks.PermissionRequirement.getAnnotationDoubleValue; +import static com.android.tools.klint.checks.PermissionRequirement.getAnnotationLongValue; +import static com.android.tools.klint.checks.PermissionRequirement.getAnnotationStringValue; +import static com.android.tools.klint.detector.api.LintUtils.skipParentheses; +import static com.android.tools.klint.detector.api.ResourceEvaluator.COLOR_INT_ANNOTATION; +import static com.android.tools.klint.detector.api.ResourceEvaluator.PX_ANNOTATION; +import static com.android.tools.klint.detector.api.ResourceEvaluator.RES_SUFFIX; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.resources.ResourceType; +import com.android.sdklib.AndroidVersion; +import com.android.tools.klint.checks.PermissionFinder.Operation; +import com.android.tools.klint.checks.PermissionFinder.Result; +import com.android.tools.klint.checks.PermissionHolder.SetPermissionLookup; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.client.api.LintClient; +import com.android.tools.klint.client.api.UastLintUtils; import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ConstantEvaluator; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Project; +import com.android.tools.klint.detector.api.ResourceEvaluator; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.TextFormat; import com.android.utils.XmlUtils; -import com.google.common.collect.Lists; +import com.google.common.base.Joiner; import com.google.common.collect.Sets; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiArrayInitializerMemberValue; +import com.intellij.psi.PsiArrayType; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiLiteral; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiModifierList; +import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; -import kotlin.Pair; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.java.JavaUFunction; +import org.jetbrains.uast.UAnonymousClass; +import org.jetbrains.uast.UBinaryExpression; +import org.jetbrains.uast.UBinaryExpressionWithType; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UCatchClause; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UEnumConstant; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UField; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UParenthesizedExpression; +import org.jetbrains.uast.UPrefixExpression; +import org.jetbrains.uast.UTryExpression; +import org.jetbrains.uast.UVariable; +import org.jetbrains.uast.UastBinaryOperator; +import org.jetbrains.uast.UastOperator; +import org.jetbrains.uast.UastPrefixOperator; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.expressions.UReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Document; @@ -60,8 +121,10 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -75,10 +138,11 @@ import java.util.Set; * specifying toInclusive without setting to, combining @ColorInt with any @ResourceTypeRes, * using @CheckResult on a void method, etc. */ -public class SupportAnnotationDetector extends Detector implements UastScanner { +@SuppressWarnings("WeakerAccess") +public class SupportAnnotationDetector extends Detector implements Detector.UastScanner { public static final Implementation IMPLEMENTATION - = new Implementation(SupportAnnotationDetector.class, Scope.SOURCE_FILE_SCOPE); + = new Implementation(SupportAnnotationDetector.class, Scope.JAVA_FILE_SCOPE); /** Method result should be used */ public static final Issue RANGE = Issue.create( @@ -200,7 +264,6 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { "http://developer.android.com/guide/components/processes-and-threads.html#Threads"); public static final String CHECK_RESULT_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "CheckResult"; //$NON-NLS-1$ - public static final String COLOR_INT_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "ColorInt"; //$NON-NLS-1$ public static final String INT_RANGE_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "IntRange"; //$NON-NLS-1$ public static final String FLOAT_RANGE_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "FloatRange"; //$NON-NLS-1$ public static final String SIZE_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "Size"; //$NON-NLS-1$ @@ -209,8 +272,10 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { public static final String MAIN_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "MainThread"; //$NON-NLS-1$ public static final String WORKER_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "WorkerThread"; //$NON-NLS-1$ public static final String BINDER_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "BinderThread"; //$NON-NLS-1$ + public static final String ANY_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "AnyThread"; //$NON-NLS-1$ + public static final String PERMISSION_ANNOTATION_READ = PERMISSION_ANNOTATION + ".Read"; //$NON-NLS-1$ + public static final String PERMISSION_ANNOTATION_WRITE = PERMISSION_ANNOTATION + ".Write"; //$NON-NLS-1$ - public static final String RES_SUFFIX = "Res"; public static final String THREAD_SUFFIX = "Thread"; public static final String ATTR_SUGGEST = "suggest"; public static final String ATTR_TO = "to"; @@ -224,14 +289,6 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { public static final String ATTR_ANY_OF = "anyOf"; public static final String ATTR_CONDITIONAL = "conditional"; - /** - * 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; - /** * Constructs a new {@link SupportAnnotationDetector} check */ @@ -239,152 +296,243 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } private void checkMethodAnnotation( - @NonNull UastAndroidContext context, - @NonNull UFunction method, - @NonNull UCallExpression node, - @NonNull UAnnotation annotation) { - String signature = annotation.getFqName(); + @NonNull JavaContext context, + @NonNull PsiMethod method, + @NonNull UCallExpression call, + @NonNull PsiAnnotation annotation, + @NonNull PsiAnnotation[] allMethodAnnotations, + @NonNull PsiAnnotation[] allClassAnnotations) { + String signature = annotation.getQualifiedName(); if (signature == null) { return; } - if (CHECK_RESULT_ANNOTATION.equals(signature) - || signature.endsWith(".CheckReturnValue")) { // support findbugs annotation too - checkResult(context, node, annotation); + // support findbugs annotation too + || signature.endsWith(".CheckReturnValue")) { + checkResult(context, call, method, annotation); } else if (signature.equals(PERMISSION_ANNOTATION)) { - checkPermission(context, node, method, annotation); + PermissionRequirement requirement = PermissionRequirement.create(context, annotation); + checkPermission(context, call, method, null, requirement); } else if (signature.endsWith(THREAD_SUFFIX) && signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) { - checkThreading(context, node, method, signature); + checkThreading(context, call, method, signature, annotation, allMethodAnnotations, + allClassAnnotations); } } - private static void checkParameterAnnotation( - @NonNull UastAndroidContext context, - @NonNull UElement argument, - @NonNull UAnnotation annotation) { - String signature = annotation.getFqName(); - if (signature == null) { - return; - } + private void checkParameterAnnotations( + @NonNull JavaContext context, + @NonNull UExpression argument, + @NonNull UCallExpression call, + @NonNull PsiMethod method, + @NonNull PsiAnnotation[] annotations) { + boolean handledResourceTypes = false; + for (PsiAnnotation annotation : annotations) { + String signature = annotation.getQualifiedName(); + if (signature == null) { + continue; + } - if (COLOR_INT_ANNOTATION.equals(signature)) { - checkColor(context, argument); - } else if (signature.equals(INT_RANGE_ANNOTATION)) { - checkIntRange(context, annotation, argument); - } else if (signature.equals(FLOAT_RANGE_ANNOTATION)) { - checkFloatRange(context, annotation, argument); - } else if (signature.equals(SIZE_ANNOTATION)) { - checkSize(context, annotation, argument); - } else { - // We only run @IntDef, @StringDef and @Res checks if we're not - // running inside Android Studio / IntelliJ where there are already inspections - // covering the same warnings (using IntelliJ's own data flow analysis); we - // don't want to (a) create redundant warnings or (b) work harder than we - // have to - if (signature.equals(INT_DEF_ANNOTATION)) { - boolean flag = annotation.getValue(TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE; - checkTypeDefConstant(context, annotation, argument, null, flag); - } else if (signature.equals(STRING_DEF_ANNOTATION)) { - checkTypeDefConstant(context, annotation, argument, null, false); - } else if (signature.endsWith(RES_SUFFIX)) { - String typeString = signature.substring(SUPPORT_ANNOTATIONS_PREFIX.length(), - signature.length() - RES_SUFFIX.length()).toLowerCase(Locale.US); - ResourceType type = ResourceType.getEnum(typeString); - if (type != null) { - checkResourceType(context, argument, type); - } else if (typeString.equals("any")) { // @AnyRes - checkResourceType(context, argument, null); + if (COLOR_INT_ANNOTATION.equals(signature)) { + checkColor(context, argument); + } else if (signature.equals(PX_ANNOTATION)) { + checkPx(context, argument); + } else if (signature.equals(INT_RANGE_ANNOTATION)) { + checkIntRange(context, annotation, argument, annotations); + } else if (signature.equals(FLOAT_RANGE_ANNOTATION)) { + checkFloatRange(context, annotation, argument); + } else if (signature.equals(SIZE_ANNOTATION)) { + checkSize(context, annotation, argument); + } else if (signature.startsWith(PERMISSION_ANNOTATION)) { + // PERMISSION_ANNOTATION, PERMISSION_ANNOTATION_READ, PERMISSION_ANNOTATION_WRITE + // When specified on a parameter, that indicates that we're dealing with + // a permission requirement on this *method* which depends on the value + // supplied by this parameter + checkParameterPermission(context, signature, call, method, argument); + } else { + // We only run @IntDef, @StringDef and @Res checks if we're not + // running inside Android Studio / IntelliJ where there are already inspections + // covering the same warnings (using IntelliJ's own data flow analysis); we + // don't want to (a) create redundant warnings or (b) work harder than we + // have to + if (signature.equals(INT_DEF_ANNOTATION)) { + boolean flag = getAnnotationBooleanValue(annotation, TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE; + checkTypeDefConstant(context, annotation, argument, null, flag, + annotations); + } else if (signature.equals(STRING_DEF_ANNOTATION)) { + checkTypeDefConstant(context, annotation, argument, null, false, + annotations); + } else if (signature.endsWith(RES_SUFFIX)) { + if (handledResourceTypes) { + continue; + } + handledResourceTypes = true; + EnumSet types = null; + // Handle all resource type annotations in one go: there could be multiple + // resource type annotations specified on the same element; we need to + // know about them all up front. + for (PsiAnnotation a : annotations) { + String s = a.getQualifiedName(); + if (s != null && s.endsWith(RES_SUFFIX)) { + String typeString = s.substring(SUPPORT_ANNOTATIONS_PREFIX.length(), + s.length() - RES_SUFFIX.length()).toLowerCase(Locale.US); + ResourceType type = ResourceType.getEnum(typeString); + if (type != null) { + if (types == null) { + types = EnumSet.of(type); + } else { + types.add(type); + } + } else if (typeString.equals("any")) { // @AnyRes + types = getAnyRes(); + break; + } + } + } + + if (types != null) { + checkResourceType(context, argument, types, call, method); + } } } } } - private static void checkColor(@NonNull UastAndroidContext context, @NonNull UElement argument) { + private static EnumSet getAnyRes() { + EnumSet types = EnumSet.allOf(ResourceType.class); + types.remove(ResourceEvaluator.COLOR_INT_MARKER_TYPE); + types.remove(ResourceEvaluator.PX_MARKER_TYPE); + return types; + } + + private void checkParameterPermission( + @NonNull JavaContext context, + @NonNull String signature, + @NonNull UElement call, + @NonNull PsiMethod method, + @NonNull UExpression argument) { + Operation operation = null; + if (signature.equals(PERMISSION_ANNOTATION_READ)) { + operation = READ; + } else if (signature.equals(PERMISSION_ANNOTATION_WRITE)) { + operation = WRITE; + } else { + PsiType type = argument.getExpressionType(); + if (type != null && CLASS_INTENT.equals(type.getCanonicalText())) { + operation = ACTION; + } + } + if (operation == null) { + return; + } + Result result = PermissionFinder.findRequiredPermissions(operation, context, argument); + if (result != null) { + checkPermission(context, call, method, result, result.requirement); + } + } + + private static void checkColor(@NonNull JavaContext context, @NonNull UElement argument) { if (argument instanceof UIfExpression) { UIfExpression expression = (UIfExpression) argument; - if (expression.isTernary()) { - checkColor(context, expression.getThenBranch()); - checkColor(context, expression.getElseBranch()); + if (expression.getThenExpression() != null) { + checkColor(context, expression.getThenExpression()); + } + if (expression.getElseExpression() != null) { + checkColor(context, expression.getElseExpression()); } return; } - List types = getResourceTypes(context, argument); + EnumSet types = ResourceEvaluator.getResourceTypes(context, argument); - if (types != null && types.contains(ResourceType.COLOR)) { + if (types != null && types.contains(COLOR) + && !isIgnoredInIde(COLOR_USAGE, context, argument)) { String message = String.format( "Should pass resolved color instead of resource id here: " + - "`getResources().getColor(%1$s)`", argument.toString()); - context.report(COLOR_USAGE, argument, context.getLocation(argument), message); + "`getResources().getColor(%1$s)`", argument.asSourceString()); + context.report(COLOR_USAGE, argument, context.getUastLocation(argument), message); } } + private static void checkPx(@NonNull JavaContext context, @NonNull UElement argument) { + if (argument instanceof UIfExpression) { + UIfExpression expression = (UIfExpression) argument; + if (expression.getThenExpression() != null) { + checkPx(context, expression.getThenExpression()); + } + if (expression.getElseExpression() != null) { + checkPx(context, expression.getElseExpression()); + } + return; + } + + EnumSet types = ResourceEvaluator.getResourceTypes(context, argument); + + if (types != null && types.contains(DIMEN)) { + String message = String.format( + "Should pass resolved pixel dimension instead of resource id here: " + + "`getResources().getDimension*(%1$s)`", argument.asSourceString()); + context.report(COLOR_USAGE, argument, context.getUastLocation(argument), message); + } + } + + private static boolean isIgnoredInIde(@NonNull Issue issue, @NonNull JavaContext context, + @NonNull UElement node) { + // Historically, the IDE would treat *all* support annotation warnings as + // handled by the id "ResourceType", so look for that id too for issues + // deliberately suppressed prior to Android Studio 2.0. + Issue synonym = Issue.create("ResourceType", issue.getBriefDescription(TextFormat.RAW), + issue.getExplanation(TextFormat.RAW), issue.getCategory(), issue.getPriority(), + issue.getDefaultSeverity(), issue.getImplementation()); + return context.getDriver().isSuppressed(context, synonym, node); + } + private void checkPermission( - @NonNull UastAndroidContext context, - @NonNull UCallExpression node, - @NonNull UFunction method, - @NonNull UAnnotation annotation) { - PermissionRequirement requirement = PermissionRequirement.create(context, annotation); + @NonNull JavaContext context, + @NonNull UElement node, + @Nullable PsiMethod method, + @Nullable Result result, + @NonNull PermissionRequirement requirement) { if (requirement.isConditional()) { return; } - PermissionHolder permissions = getPermissions(context.getLintContext()); + PermissionHolder permissions = getPermissions(context); if (!requirement.isSatisfied(permissions)) { // See if it looks like we're holding the permission implicitly by @RequirePermission // annotations in the surrounding context permissions = addLocalPermissions(context, permissions, node); if (!requirement.isSatisfied(permissions)) { - UClass containingClass = UastUtils.getContainingClass(method); - if (containingClass != null) { - String name = containingClass.getName() + "." + method.getName(); - String message = getMissingPermissionMessage(requirement, name, permissions); - context.report(MISSING_PERMISSION, node, context.getLocation(node), message); + if (isIgnoredInIde(MISSING_PERMISSION, context, node)) { + return; } + Operation operation; + String name; + if (result != null) { + name = result.name; + operation = result.operation; + } else { + assert method != null; + PsiClass containingClass = method.getContainingClass(); + if (containingClass != null) { + name = containingClass.getName() + "." + method.getName(); + } else { + name = method.getName(); + } + operation = Operation.CALL; + } + String message = getMissingPermissionMessage(requirement, name, permissions, + operation); + context.report(MISSING_PERMISSION, node, context.getUastLocation(node), message); } } else if (requirement.isRevocable(permissions) && - context.getLintContext().getMainProject().getTargetSdkVersion().getFeatureLevel() >= 23) { - // Ensure that the caller is handling a security exception - // First check to see if we're inside a try/catch which catches a SecurityException - // (or some wider exception than that). Check for nested try/catches too. - boolean handlesMissingPermission = false; - UElement parent = node; - while (true) { - UTryExpression tryCatch = UastUtils.getParentOfType(parent, UTryExpression.class); - if (tryCatch == null) { - break; - } else { - for (UCatchClause aCatch : tryCatch.getCatchClauses()) { - for (UType catchType : aCatch.getTypes()) { - if (isSecurityException(catchType)) { - handlesMissingPermission = true; - break; - } - } - } - parent = tryCatch; - } - } + context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 23) { - // If not, check to see if the method itself declares that it throws a - // SecurityException or something wider. - if (!handlesMissingPermission) { - UFunction declaration = UastUtils.getParentOfType(parent, UFunction.class); - if (declaration instanceof JavaUFunction) { - List thrownExceptions = ((JavaUFunction)declaration).getThrownExceptions(); - for (UType typeReference : thrownExceptions) { - if (isSecurityException(typeReference)) { - handlesMissingPermission = true; - break; - } - } - } - } + boolean handlesMissingPermission = handlesSecurityException(node); // If not, check to see if the code is deliberately checking to see if the // given permission is available. if (!handlesMissingPermission) { - UFunction methodNode = UastUtils.getContainingFunction(node); + UMethod methodNode = UastUtils.getParentOfType(node, UMethod.class, true); if (methodNode != null) { CheckPermissionVisitor visitor = new CheckPermissionVisitor(node); methodNode.accept(visitor); @@ -392,38 +540,78 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } } - if (!handlesMissingPermission) { + if (!handlesMissingPermission && !isIgnoredInIde(MISSING_PERMISSION, context, node)) { String message = getUnhandledPermissionMessage(); - context.report(MISSING_PERMISSION, node, context.getLocation(node), message); + context.report(MISSING_PERMISSION, node, context.getUastLocation(node), message); } } } + private static boolean handlesSecurityException(@NonNull UElement node) { + // Ensure that the caller is handling a security exception + // First check to see if we're inside a try/catch which catches a SecurityException + // (or some wider exception than that). Check for nested try/catches too. + UElement parent = node; + while (true) { + UTryExpression tryCatch = UastUtils.getParentOfType(parent, UTryExpression.class, true); + if (tryCatch == null) { + break; + } else { + for (UCatchClause psiCatchSection : tryCatch.getCatchClauses()) { + if (isSecurityException(psiCatchSection.getTypes())) { + return true; + } + } + + parent = tryCatch; + } + } + + // If not, check to see if the method itself declares that it throws a + // SecurityException or something wider. + UMethod declaration = UastUtils.getParentOfType(parent, UMethod.class, false); + if (declaration != null) { + PsiClassType[] thrownTypes = declaration.getThrowsList().getReferencedTypes(); + if (isSecurityException(Arrays.asList(thrownTypes))) { + return true; + } + } + + return false; + } + @NonNull private static PermissionHolder addLocalPermissions( - @NonNull UastAndroidContext context, + @NonNull JavaContext context, @NonNull PermissionHolder permissions, @NonNull UElement node) { // Accumulate @RequirePermissions available in the local context - UFunction method = UastUtils.getContainingFunction(node); + UMethod method = UastUtils.getParentOfType(node, UMethod.class, true); if (method == null) { return permissions; } - UAnnotation annotation = UastUtils.findAnnotation(method, PERMISSION_ANNOTATION); - permissions = mergeAnnotationPermissions(context, permissions, annotation); - annotation = UastUtils.findAnnotation(UastUtils.getContainingClassOrEmpty(method), PERMISSION_ANNOTATION); + PsiAnnotation annotation = method.getModifierList().findAnnotation(PERMISSION_ANNOTATION); permissions = mergeAnnotationPermissions(context, permissions, annotation); + + PsiClass containingClass = method.getContainingClass(); + if (containingClass != null) { + PsiModifierList modifierList = containingClass.getModifierList(); + if (modifierList != null) { + annotation = modifierList.findAnnotation(PERMISSION_ANNOTATION); + permissions = mergeAnnotationPermissions(context, permissions, annotation); + } + } return permissions; } @NonNull private static PermissionHolder mergeAnnotationPermissions( - @NonNull UastAndroidContext context, + @NonNull JavaContext context, @NonNull PermissionHolder permissions, - @Nullable UAnnotation annotation) { + @Nullable PsiAnnotation annotation) { if (annotation != null) { PermissionRequirement requirement = PermissionRequirement.create(context, annotation); - permissions = PermissionHolder.SetPermissionLookup.join(permissions, requirement); + permissions = SetPermissionLookup.join(permissions, requirement); } return permissions; @@ -431,16 +619,17 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { /** Returns the error message shown when a given call is missing one or more permissions */ public static String getMissingPermissionMessage(@NonNull PermissionRequirement requirement, - @NonNull String callName, @NonNull PermissionHolder permissions) { - return String.format("Missing permissions required by %1$s: %2$s", callName, - requirement.describeMissingPermissions(permissions)); + @NonNull String callName, @NonNull PermissionHolder permissions, + @NonNull Operation operation) { + return String.format("Missing permissions required %1$s %2$s: %3$s", operation.prefix(), + callName, requirement.describeMissingPermissions(permissions)); } /** Returns the error message shown when a revocable permission call is not properly handled */ public static String getUnhandledPermissionMessage() { return "Call requires permission which may be rejected by user: code should explicitly " - + "check to see if permission is available (with `checkPermission`) or handle " - + "a potential `SecurityException`"; + + "check to see if permission is available (with `checkPermission`) or explicitly " + + "handle a potential `SecurityException`"; } /** @@ -465,27 +654,30 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } @Override - public boolean visitElement(@NotNull UElement node) { - return mDone; + public boolean visitElement(UElement node) { + return mDone || super.visitElement(node); } @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - if (node.getKind() == UastCallKind.FUNCTION_CALL) { - if (node == mTarget) { - mDone = true; - } + public boolean visitCallExpression(UCallExpression node) { + if (UastExpressionUtils.isMethodCall(node)) { + visitMethodCallExpression(node); + } + return super.visitCallExpression(node); + } - String name = node.getFunctionName(); - if (name != null && - (name.startsWith("check") || name.startsWith("enforce")) - && name.endsWith("Permission")) { - mChecksPermission = true; - mDone = true; - } + private void visitMethodCallExpression(UCallExpression node) { + if (node == mTarget) { + mDone = true; } - return super.visitCallExpression(node); + String name = node.getMethodName(); + if (name != null + && (name.startsWith("check") || name.startsWith("enforce")) + && name.endsWith("Permission")) { + mChecksPermission = true; + mDone = true; + } } public boolean checksPermission() { @@ -493,25 +685,33 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } } - private static boolean isSecurityException(@NonNull UType type) { - return type != null && (type.matchesFqName("java.lang.SecurityException") || - type.matchesFqName("java.lang.RuntimeException") || - type.matchesFqName("java.lang.Exception") || - type.matchesFqName("java.lang.Throwable")); + private static boolean isSecurityException( + @NonNull List types) { + for (PsiType type : types) { + if (type instanceof PsiClassType) { + PsiClass cls = ((PsiClassType) type).resolve(); + // In earlier versions we checked not just for java.lang.SecurityException but + // any super type as well, however that probably hides warnings in cases where + // users don't want that; see http://b.android.com/182165 + //return context.getEvaluator().extendsClass(cls, "java.lang.SecurityException", false); + return cls != null && "java.lang.SecurityException".equals(cls.getQualifiedName()); + } + } + + return false; } private PermissionHolder mPermissions; private PermissionHolder getPermissions( - @NonNull UastAndroidContext context) { + @NonNull JavaContext context) { if (mPermissions == null) { Set permissions = Sets.newHashSetWithExpectedSize(30); Set revocable = Sets.newHashSetWithExpectedSize(4); - JavaContext lintContext = context.getLintContext(); - LintClient client = lintContext.getClient(); + LintClient client = context.getClient(); // Gather permissions from all projects that contribute to the // main project. - Project mainProject = lintContext.getMainProject(); + Project mainProject = context.getMainProject(); for (File manifest : mainProject.getManifestFiles()) { addPermissions(client, permissions, revocable, manifest); } @@ -521,7 +721,10 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } } - mPermissions = new PermissionHolder.SetPermissionLookup(permissions, revocable); + AndroidVersion minSdkVersion = mainProject.getMinSdkVersion(); + AndroidVersion targetSdkVersion = mainProject.getTargetSdkVersion(); + mPermissions = new SetPermissionLookup(permissions, revocable, minSdkVersion, + targetSdkVersion); } return mPermissions; @@ -546,7 +749,9 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { continue; } String nodeName = item.getNodeName(); - if (nodeName.equals(TAG_USES_PERMISSION)) { + if (nodeName.equals(TAG_USES_PERMISSION) + || nodeName.equals(TAG_USES_PERMISSION_SDK_23) + || nodeName.equals(TAG_USES_PERMISSION_SDK_M)) { Element element = (Element)item; String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); if (!name.isEmpty()) { @@ -555,8 +760,8 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } else if (nodeName.equals(TAG_PERMISSION)) { Element element = (Element)item; String protectionLevel = element.getAttributeNS(ANDROID_URI, - PermissionRequirement.ATTR_PROTECTION_LEVEL); - if (PermissionRequirement.VALUE_DANGEROUS.equals(protectionLevel)) { + ATTR_PROTECTION_LEVEL); + if (VALUE_DANGEROUS.equals(protectionLevel)) { String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); if (!name.isEmpty()) { revocable.add(name); @@ -565,25 +770,27 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } } } - - private static void checkResult( - @NonNull UastAndroidContext context, - @NonNull UCallExpression node, - @NonNull UAnnotation annotation) { - if (node.getParent() instanceof UExpression) { - String methodName = node.getFunctionName(); - assert methodName != null; - Object suggested = annotation.getValue(ATTR_SUGGEST); + + private static void checkResult(@NonNull JavaContext context, @NonNull UCallExpression node, + @NonNull PsiMethod method, @NonNull PsiAnnotation annotation) { + if (context.getUastContext().isExpressionValueUsed(UastUtils.getQualifiedParentOrThis(node))) { + String methodName = JavaContext.getMethodName(node); + String suggested = getAnnotationStringValue(annotation, ATTR_SUGGEST); // Failing to check permissions is a potential security issue (and had an existing // dedicated issue id before which people may already have configured with a // custom severity in their LintOptions etc) so continue to use that issue // (which also has category Security rather than Correctness) for these: Issue issue = CHECK_RESULT; - if (methodName.startsWith("check") && methodName.contains("Permission")) { + if (methodName != null && methodName.startsWith("check") + && methodName.contains("Permission")) { issue = CHECK_PERMISSION; } + if (isIgnoredInIde(issue, context, node)) { + return; + } + String message = String.format("The result of `%1$s` is not used", methodName); if (suggested != null) { @@ -591,56 +798,187 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { // with "#" etc? message = String.format( "The result of `%1$s` is not used; did you mean to call `%2$s`?", - methodName, suggested.toString()); + methodName, suggested); + } else if ("intersect".equals(methodName) + && context.getEvaluator().isMemberInClass(method, "android.graphics.Rect")) { + message += ". If the rectangles do not intersect, no change is made and the " + + "original rectangle is not modified. These methods return false to " + + "indicate that this has happened."; } - context.report(issue, node, context.getLocation(node), message); + context.report(issue, node, context.getUastLocation(node), message); } } private static void checkThreading( - @NonNull UastAndroidContext context, - @NonNull UCallExpression node, - @NonNull UFunction method, - @NonNull String annotation) { - String threadContext = getThreadContext(context, node); - if (threadContext != null && !isCompatibleThread(threadContext, annotation)) { - String message = String.format("Method %1$s must be called from the `%2$s` thread, currently inferred thread is `%3$s` thread", - method.getName(), describeThread(annotation), describeThread(threadContext)); - context.report(THREAD, node, context.getLocation(node), message); + @NonNull JavaContext context, + @NonNull UElement node, + @NonNull PsiMethod method, + @NonNull String signature, + @NonNull PsiAnnotation annotation, + @NonNull PsiAnnotation[] allMethodAnnotations, + @NonNull PsiAnnotation[] allClassAnnotations) { + List threadContext = getThreadContext(context, node); + if (threadContext != null && !isCompatibleThread(threadContext, signature) + && !isIgnoredInIde(THREAD, context, node)) { + // If the annotation is specified on the class, ignore this requirement + // if there is another annotation specified on the method. + if (containsAnnotation(allClassAnnotations, annotation)) { + if (containsThreadingAnnotation(allMethodAnnotations)) { + return; + } + // Make sure ALL the other context annotations are acceptable! + } else { + assert containsAnnotation(allMethodAnnotations, annotation); + // See if any of the *other* annotations are compatible. + Boolean isFirst = null; + for (PsiAnnotation other : allMethodAnnotations) { + if (other == annotation) { + if (isFirst == null) { + isFirst = true; + } + continue; + } else if (!isThreadingAnnotation(other)) { + continue; + } + if (isFirst == null) { + // We'll be called for each annotation on the method. + // For each one we're checking *all* annotations on the target. + // Therefore, when we're seeing the second, third, etc annotation + // on the method we've already checked them, so return here. + return; + } + String s = other.getQualifiedName(); + if (s != null && isCompatibleThread(threadContext, s)) { + return; + } + } + } + + String name = method.getName(); + if ((name.startsWith("post") ) + && context.getEvaluator().isMemberInClass(method, CLASS_VIEW)) { + // The post()/postDelayed() methods are (currently) missing + // metadata (@AnyThread); they're on a class marked @UiThread + // but these specific methods are not @UiThread. + return; + } + + List targetThreads = getThreads(context, method); + if (targetThreads == null) { + targetThreads = Collections.singletonList(signature); + } + + String message = String.format( + "%1$s %2$s must be called from the `%3$s` thread, currently inferred thread is `%4$s` thread", + method.isConstructor() ? "Constructor" : "Method", + method.getName(), describeThreads(targetThreads, true), + describeThreads(threadContext, false)); + context.report(THREAD, node, context.getUastLocation(node), message); } } + public static boolean containsAnnotation( + @NonNull PsiAnnotation[] array, + @NonNull PsiAnnotation annotation) { + for (PsiAnnotation a : array) { + if (a == annotation) { + return true; + } + } + + return false; + } + + public static boolean containsThreadingAnnotation(@NonNull PsiAnnotation[] array) { + for (PsiAnnotation annotation : array) { + if (isThreadingAnnotation(annotation)) { + return true; + } + } + + return false; + } + + public static boolean isThreadingAnnotation(@NonNull PsiAnnotation annotation) { + String signature = annotation.getQualifiedName(); + return signature != null + && signature.endsWith(THREAD_SUFFIX) + && signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX); + } + + @NonNull + public static String describeThreads(@NonNull List annotations, boolean any) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < annotations.size(); i++) { + if (i > 0) { + if (i == annotations.size() - 1) { + if (any) { + sb.append(" or "); + } else { + sb.append(" and "); + } + } else { + sb.append(", "); + } + } + sb.append(describeThread(annotations.get(i))); + } + return sb.toString(); + } + @NonNull public static String describeThread(@NonNull String annotation) { - if (UI_THREAD_ANNOTATION.equals(annotation)) { + if (annotation.equals(UI_THREAD_ANNOTATION)) { return "UI"; } - else if (MAIN_THREAD_ANNOTATION.equals(annotation)) { + else if (annotation.equals(MAIN_THREAD_ANNOTATION)) { return "main"; } - else if (BINDER_THREAD_ANNOTATION.equals(annotation)) { + else if (annotation.equals(BINDER_THREAD_ANNOTATION)) { return "binder"; } - else if (WORKER_THREAD_ANNOTATION.equals(annotation)) { + else if (annotation.equals(WORKER_THREAD_ANNOTATION)) { return "worker"; - } else { + } + else if (annotation.equals(ANY_THREAD_ANNOTATION)) { + return "any"; + } + else { return "other"; } } /** returns true if the two threads are compatible */ - public static boolean isCompatibleThread(@NonNull String thread1, @NonNull String thread2) { - if (thread1.equals(thread2)) { + public static boolean isCompatibleThread(@NonNull List callers, + @NonNull String callee) { + // ALL calling contexts must be valid + assert !callers.isEmpty(); + for (String caller : callers) { + if (!isCompatibleThread(caller, callee)) { + return false; + } + } + + return true; + } + + /** returns true if the two threads are compatible */ + public static boolean isCompatibleThread(@NonNull String caller, @NonNull String callee) { + if (callee.equals(caller)) { + return true; + } + + if (callee.equals(ANY_THREAD_ANNOTATION)) { return true; } // Allow @UiThread and @MainThread to be combined - if (thread1.equals(UI_THREAD_ANNOTATION)) { - if (thread2.equals(MAIN_THREAD_ANNOTATION)) { + if (callee.equals(UI_THREAD_ANNOTATION)) { + if (caller.equals(MAIN_THREAD_ANNOTATION)) { return true; } - } else if (thread1.equals(MAIN_THREAD_ANNOTATION)) { - if (thread2.equals(UI_THREAD_ANNOTATION)) { + } else if (callee.equals(MAIN_THREAD_ANNOTATION)) { + if (caller.equals(UI_THREAD_ANNOTATION)) { return true; } } @@ -650,40 +988,62 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { /** Attempts to infer the current thread context at the site of the given method call */ @Nullable - private static String getThreadContext(@NonNull UastAndroidContext context, - @NonNull UCallExpression methodCall) { - UFunction method = UastUtils.getContainingFunction(methodCall); + private static List getThreadContext(@NonNull JavaContext context, + @NonNull UElement methodCall) { + //noinspection unchecked + PsiMethod method = UastUtils.getParentOfType(methodCall, UMethod.class, true, + UAnonymousClass.class); + return getThreads(context, method); + } + + /** Attempts to infer the current thread context at the site of the given method call */ + @Nullable + private static List getThreads(@NonNull JavaContext context, + @Nullable PsiMethod method) { if (method != null) { - UClass cls = UastUtils.getContainingClass(method); + List result = null; + PsiClass cls = method.getContainingClass(); while (method != null) { - for (UAnnotation annotation : method.getAnnotations()) { - String name = annotation.getFqName(); - if (name != null - && name.startsWith(SUPPORT_ANNOTATIONS_PREFIX) - && name.endsWith(THREAD_SUFFIX)) { - return name; + for (PsiAnnotation annotation : method.getModifierList().getAnnotations()) { + String name = annotation.getQualifiedName(); + if (name != null && name.startsWith(SUPPORT_ANNOTATIONS_PREFIX) + && name.endsWith(THREAD_SUFFIX)) { + if (result == null) { + result = new ArrayList(4); + } + result.add(name); } } - List superFunctions = method.getSuperFunctions(context); - if (superFunctions.isEmpty()) { - method = null; - } else { - method = superFunctions.get(0); + if (result != null) { + // We don't accumulate up the chain: one method replaces the requirements + // of its super methods. + return result; } + method = context.getEvaluator().getSuperMethod(method); } // See if we're extending a class with a known threading context while (cls != null) { - for (UAnnotation annotation : cls.getAnnotations()) { - String name = annotation.getFqName(); - if (name != null - && name.startsWith(SUPPORT_ANNOTATIONS_PREFIX) - && name.endsWith(THREAD_SUFFIX)) { - return name; + PsiModifierList modifierList = cls.getModifierList(); + if (modifierList != null) { + for (PsiAnnotation annotation : modifierList.getAnnotations()) { + String name = annotation.getQualifiedName(); + if (name != null && name.startsWith(SUPPORT_ANNOTATIONS_PREFIX) + && name.endsWith(THREAD_SUFFIX)) { + if (result == null) { + result = new ArrayList(4); + } + result.add(name); + } + } + if (result != null) { + // We don't accumulate up the chain: one class replaces the requirements + // of its super classes. + return result; } } - cls = cls.getSuperClass(context); + cls = cls.getSuperClass(); } } @@ -697,150 +1057,208 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } private static boolean isNumber(@NonNull UElement argument) { - return UastLiteralUtils.isIntegralLiteral(argument) || argument instanceof UPrefixExpression - && ((UPrefixExpression) argument).getOperator() == UastPrefixOperator.UNARY_MINUS - && UastLiteralUtils.isIntegralLiteral(((UPrefixExpression) argument).getOperand()); + if (argument instanceof ULiteralExpression) { + Object value = ((ULiteralExpression) argument).getValue(); + return value instanceof Number; + } else if (argument instanceof UPrefixExpression) { + UPrefixExpression expression = (UPrefixExpression) argument; + UExpression operand = expression.getOperand(); + return isNumber(operand); + } else { + return false; + } } private static boolean isZero(@NonNull UElement argument) { - return UastLiteralUtils.isIntegralLiteral(argument) - && UastLiteralUtils.getLongValue((ULiteralExpression) argument) == 0; + if (argument instanceof ULiteralExpression) { + Object value = ((ULiteralExpression) argument).getValue(); + return value instanceof Number && ((Number)value).intValue() == 0; + } + return false; } private static boolean isMinusOne(@NonNull UElement argument) { - return argument instanceof UUnaryExpression - && ((UUnaryExpression) argument).getOperator() == UastPrefixOperator.UNARY_MINUS - && UastLiteralUtils.isIntegralLiteral(((UUnaryExpression) argument).getOperand()) - && UastLiteralUtils.getLongValue((ULiteralExpression) ((UUnaryExpression) argument).getOperand()) - == 1; + if (argument instanceof UPrefixExpression) { + UPrefixExpression expression = (UPrefixExpression) argument; + UExpression operand = expression.getOperand(); + if (operand instanceof ULiteralExpression && + expression.getOperator() == UastPrefixOperator.UNARY_MINUS) { + Object value = ((ULiteralExpression) operand).getValue(); + return value instanceof Number && ((Number) value).intValue() == 1; + } else { + return false; + } + } else { + return false; + } } private static void checkResourceType( - @NonNull UastAndroidContext context, + @NonNull JavaContext context, @NonNull UElement argument, - @Nullable ResourceType expectedType) { - List actual = getResourceTypes(context, argument); + @NonNull EnumSet expectedType, + @NonNull UCallExpression call, + @NonNull PsiMethod calledMethod) { + EnumSet actual = ResourceEvaluator.getResourceTypes(context, argument); + if (actual == null && (!isNumber(argument) || isZero(argument) || isMinusOne(argument)) ) { return; - } else if (actual != null && (expectedType == null - || actual.contains(expectedType) - || expectedType == DRAWABLE && (actual.contains(COLOR) || actual.contains(MIPMAP)))) { + } else if (actual != null && (!Sets.intersection(actual, expectedType).isEmpty() + || expectedType.contains(DRAWABLE) + && (actual.contains(COLOR) || actual.contains(MIPMAP)))) { + return; + } + + if (isIgnoredInIde(RESOURCE_TYPE, context, argument)) { + return; + } + + if (expectedType.contains(ResourceType.STYLEABLE) && (expectedType.size() == 1) + && JavaEvaluator.isMemberInClass(calledMethod, + "android.content.res.TypedArray") + && typeArrayFromArrayLiteral(call.getReceiver(), context)) { + // You're generally supposed to provide a styleable to the TypedArray methods, + // but you're also allowed to supply an integer array return; } String message; - if (actual != null && actual.size() == 1 && actual.get(0) == COLOR_INT_MARKER_TYPE) { + if (actual != null && actual.size() == 1 && actual.contains( + ResourceEvaluator.COLOR_INT_MARKER_TYPE)) { message = "Expected a color resource id (`R.color.`) but received an RGB integer"; - } else if (expectedType == COLOR_INT_MARKER_TYPE) { + } else if (expectedType.contains(ResourceEvaluator.COLOR_INT_MARKER_TYPE)) { message = String.format("Should pass resolved color instead of resource id here: " + - "`getResources().getColor(%1$s)`", argument.toString()); - } else if (expectedType != null) { - message = String.format( - "Expected resource of type %1$s", expectedType.getName()); + "`getResources().getColor(%1$s)`", argument.asSourceString()); + } else if (actual != null && actual.size() == 1 && actual.contains( + ResourceEvaluator.PX_MARKER_TYPE)) { + message = "Expected a dimension resource id (`R.color.`) but received a pixel integer"; + } else if (expectedType.contains(ResourceEvaluator.PX_MARKER_TYPE)) { + message = String.format("Should pass resolved pixel size instead of resource id here: " + + "`getResources().getDimension*(%1$s)`", argument.asSourceString()); + } else if (expectedType.size() < ResourceType.getNames().length - 2) { // -2: marker types + message = String.format("Expected resource of type %1$s", + Joiner.on(" or ").join(expectedType)); } else { message = "Expected resource identifier (`R`.type.`name`)"; } - context.report(RESOURCE_TYPE, argument, context.getLocation(argument), message); + context.report(RESOURCE_TYPE, argument, context.getUastLocation(argument), message); } - @Nullable - private static List getResourceTypes(@NonNull UastAndroidContext context, - @NonNull UElement argument) { - if (argument instanceof UQualifiedExpression) { - UQualifiedExpression node = (UQualifiedExpression) argument; - if (node.getReceiver() instanceof UQualifiedExpression) { - UQualifiedExpression select = (UQualifiedExpression) node.getReceiver(); - if (select.getReceiver() instanceof UQualifiedExpression) { // android.R.... - UQualifiedExpression innerSelect = (UQualifiedExpression) select.getReceiver(); - if (UastUtils.matchesQualified(innerSelect.getSelector(), R_CLASS)) { - String typeName = select.getSelector().renderString(); - ResourceType type = ResourceType.getEnum(typeName); - return type != null ? Collections.singletonList(type) : null; - } - } - if (select.getReceiver() instanceof USimpleReferenceExpression) { - USimpleReferenceExpression reference = (USimpleReferenceExpression) select.getReceiver(); - if (reference.getIdentifier().equals(R_CLASS)) { - String typeName = select.getSelector().renderString(); - ResourceType type = ResourceType.getEnum(typeName); - return type != null ? Collections.singletonList(type) : null; - } - } - } - - // Arbitrary packages -- android.R.type.name, foo.bar.R.type.name - if (UastUtils.matchesQualified(node.getSelector(), R_CLASS)) { - UElement parent = node.getParent(); - if (parent instanceof UQualifiedExpression) { - UElement grandParent = parent.getParent(); - if (grandParent instanceof UQualifiedExpression) { - UQualifiedExpression select = (UQualifiedExpression) grandParent; - UExpression typeOperand = select.getReceiver(); - if (typeOperand instanceof UQualifiedExpression) { - UQualifiedExpression typeSelect = (UQualifiedExpression) typeOperand; - String typeName = typeSelect.getSelector().renderString(); - ResourceType type = ResourceType.getEnum(typeName); - return type != null ? Collections.singletonList(type) : null; - } - } - } - } - } else if (argument instanceof UCallExpression) { - UDeclaration resolved = ((UCallExpression)argument).resolve(context); - if (resolved != null) { - List annotations = ((UAnnotated) resolved).getAnnotations(); - for (UAnnotation annotation : annotations) { - String signature = annotation.getFqName(); - if (COLOR_INT_ANNOTATION.equals(signature)) { - return Collections.singletonList(COLOR_INT_MARKER_TYPE); - } - if (signature != null - && 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 Collections.singletonList(type); - } else if (typeString.equals("any")) { // @AnyRes - ResourceType[] types = ResourceType.values(); - List result = Lists.newArrayListWithExpectedSize( - types.length); - for (ResourceType t : types) { - if (t != COLOR_INT_MARKER_TYPE) { - result.add(t); - } + /** + * Returns true if the node is pointing to a TypedArray whose value was obtained + * from an array literal + */ + public static boolean typeArrayFromArrayLiteral( + @Nullable UElement node, @NonNull JavaContext context) { + if (UastExpressionUtils.isMethodCall(node)) { + UCallExpression expression = (UCallExpression) node; + assert expression != null; + String name = expression.getMethodName(); + if (name != null && "obtainStyledAttributes".equals(name)) { + List expressions = expression.getValueArguments(); + if (!expressions.isEmpty()) { + int arg; + if (expressions.size() == 1) { + // obtainStyledAttributes(int[] attrs) + arg = 0; + } else if (expressions.size() == 2) { + // obtainStyledAttributes(AttributeSet set, int[] attrs) + // obtainStyledAttributes(int resid, int[] attrs) + for (arg = 0; arg < expressions.size(); arg++) { + PsiType type = expressions.get(arg).getExpressionType(); + if (type instanceof PsiArrayType) { + break; } - - return result; } + if (arg == expressions.size()) { + return false; + } + } else if (expressions.size() == 4) { + // obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) + arg = 1; + } else { + return false; } + + return ConstantEvaluator.isArrayLiteral(expressions.get(arg), context); } } + return false; + } else if (node instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) node).resolve(); + if (resolved instanceof PsiVariable) { + PsiVariable variable = (PsiVariable) resolved; + UExpression lastAssignment = + UastLintUtils.findLastAssignment(variable, node, context); + + if (lastAssignment != null) { + return typeArrayFromArrayLiteral(lastAssignment, context); + } + } + } else if (UastExpressionUtils.isNewArrayWithInitializer(node)) { + return true; + } else if (UastExpressionUtils.isNewArrayWithDimensions(node)) { + return true; + } else if (node instanceof UParenthesizedExpression) { + UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) node; + UExpression expression = parenthesizedExpression.getExpression(); + return typeArrayFromArrayLiteral(expression, context); + } else if (UastExpressionUtils.isTypeCast(node)) { + UBinaryExpressionWithType castExpression = (UBinaryExpressionWithType) node; + assert castExpression != null; + UExpression operand = castExpression.getOperand(); + return typeArrayFromArrayLiteral(operand, context); } - return null; + return false; } private static void checkIntRange( - @NonNull UastAndroidContext context, - @NonNull UAnnotation annotation, - @NonNull UElement argument) { - Object object = null; - if (argument instanceof UExpression) { - object = ((UExpression) argument).evaluate(); + @NonNull JavaContext context, + @NonNull PsiAnnotation annotation, + @NonNull UElement argument, + @NonNull PsiAnnotation[] allAnnotations) { + String message = getIntRangeError(context, annotation, argument); + if (message != null) { + if (findIntDef(allAnnotations) != null) { + // Don't flag int range errors if there is an int def annotation there too; + // there could be a valid @IntDef constant. (The @IntDef check will + // perform range validation by calling getIntRange.) + return; + } + + if (isIgnoredInIde(RANGE, context, argument)) { + return; + } + + context.report(RANGE, argument, context.getUastLocation(argument), message); } + } + + @Nullable + private static String getIntRangeError( + @NonNull JavaContext context, + @NonNull PsiAnnotation annotation, + @NonNull UElement argument) { + if (UastExpressionUtils.isNewArrayWithInitializer(argument)) { + UCallExpression newExpression = (UCallExpression) argument; + for (UExpression expression : newExpression.getValueArguments()) { + String error = getIntRangeError(context, annotation, expression); + if (error != null) { + return error; + } + } + } + + Object object = ConstantEvaluator.evaluate(context, argument); if (!(object instanceof Number)) { - return; + return null; } long value = ((Number)object).longValue(); long from = getLongAttribute(annotation, ATTR_FROM, Long.MIN_VALUE); long to = getLongAttribute(annotation, ATTR_TO, Long.MAX_VALUE); - String message = getIntRangeError(value, from, to); - if (message != null) { - context.report(RANGE, argument, context.getLocation(argument), message); - } + return getIntRangeError(value, from, to); } /** @@ -866,13 +1284,10 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } private static void checkFloatRange( - @NonNull UastAndroidContext context, - @NonNull UAnnotation annotation, + @NonNull JavaContext context, + @NonNull PsiAnnotation annotation, @NonNull UElement argument) { - Object object = null; - if (argument instanceof UExpression) { - object = ((UExpression) argument).evaluate(); - } + Object object = ConstantEvaluator.evaluate(context, argument); if (!(object instanceof Number)) { return; } @@ -883,8 +1298,8 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { boolean toInclusive = getBoolean(annotation, ATTR_TO_INCLUSIVE, true); String message = getFloatRangeError(value, from, to, fromInclusive, toInclusive, argument); - if (message != null) { - context.report(RANGE, argument, context.getLocation(argument), message); + if (message != null && !isIgnoredInIde(RANGE, context, argument)) { + context.report(RANGE, argument, context.getUastLocation(argument), message); } } @@ -941,13 +1356,15 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { sb.append(Double.toString(to)); } sb.append(" (was "); - if (UastLiteralUtils.isNumberLiteral(node)) { + if (node instanceof ULiteralExpression) { // Use source text instead to avoid rounding errors involved in conversion, e.g // Error: Value must be > 2.5 (was 2.490000009536743) [Range] // printAtLeastExclusive(2.49f); // ERROR // ~~~~~ - //noinspection ConstantConditions - String str = ((ULiteralExpression)node).getValue().toString(); + String str = node.asSourceString(); + if (str.endsWith("f") || str.endsWith("F")) { + str = str.substring(0, str.length() - 1); + } sb.append(str); } else { sb.append(value); @@ -959,25 +1376,27 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } private static void checkSize( - @NonNull UastAndroidContext context, - @NonNull UAnnotation annotation, + @NonNull JavaContext context, + @NonNull PsiAnnotation annotation, @NonNull UElement argument) { int actual; - if (UastLiteralUtils.isStringLiteral(argument)) { - // Check string length - ULiteralExpression literal = (ULiteralExpression) argument; - String s = (String) literal.getValue(); - assert s != null; - actual = s.length(); - } else if (argument instanceof UCallExpression - && ((UCallExpression)argument).getKind() == UastCallKind.ARRAY_INITIALIZER) { - UCallExpression initializer = (UCallExpression) argument; - actual = initializer.getValueArgumentCount(); + boolean isString = false; + + // TODO: Collections syntax, e.g. Arrays.asList ⇒ param count, emptyList=0, singleton=1, etc + // TODO: Flow analysis + // No flow analysis for this check yet, only checking literals passed in as parameters + + if (UastExpressionUtils.isNewArrayWithInitializer(argument)) { + actual = ((UCallExpression) argument).getValueArgumentCount(); } else { - // TODO: Collections syntax, e.g. Arrays.asList => param count, emptyList=0, singleton=1, etc - // TODO: Flow analysis - // No flow analysis for this check yet, only checking literals passed in as parameters - return; + Object object = ConstantEvaluator.evaluate(context, argument); + // Check string length + if (object instanceof String) { + actual = ((String)object).length(); + isString = true; + } else { + return; + } } long exact = getLongAttribute(annotation, ATTR_VALUE, -1); long min = getLongAttribute(annotation, ATTR_MIN, Long.MIN_VALUE); @@ -985,15 +1404,14 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { long multiple = getLongAttribute(annotation, ATTR_MULTIPLE, 1); String unit; - boolean isString = argument instanceof ULiteralExpression; if (isString) { unit = "length"; } else { unit = "size"; } String message = getSizeError(actual, exact, min, max, multiple, unit); - if (message != null) { - context.report(RANGE, argument, context.getLocation(argument), message); + if (message != null && !isIgnoredInIde(RANGE, context, argument)) { + context.report(RANGE, argument, context.getUastLocation(argument), message); } } @@ -1030,98 +1448,235 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { return message; } - private static void checkTypeDefConstant( - @NonNull UastAndroidContext context, - @NonNull UAnnotation annotation, - @NonNull UElement argument, - @Nullable UElement errorNode, - boolean flag) { - if (UastLiteralUtils.isNullLiteral(argument)) { - // Accepted for @StringDef - return; + @Nullable + private static PsiAnnotation findIntRange( + @NonNull PsiAnnotation[] annotations) { + for (PsiAnnotation annotation : annotations) { + if (INT_RANGE_ANNOTATION.equals(annotation.getQualifiedName())) { + return annotation; + } } - if (UastLiteralUtils.isStringLiteral(argument)) { - ULiteralExpression string = (ULiteralExpression) argument; - checkTypeDefConstant(context, annotation, argument, errorNode, false, string.getValue()); - } else if (UastLiteralUtils.isIntegralLiteral(argument)) { - ULiteralExpression literal = (ULiteralExpression) argument; - long value = UastLiteralUtils.getLongValue(literal); - if (flag && value == 0) { - // Accepted for a flag @IntDef - return; + return null; + } + + @Nullable + static PsiAnnotation findIntDef(@NonNull PsiAnnotation[] annotations) { + for (PsiAnnotation annotation : annotations) { + if (INT_DEF_ANNOTATION.equals(annotation.getQualifiedName())) { + return annotation; + } + } + + return null; + } + + private static void checkTypeDefConstant( + @NonNull JavaContext context, + @NonNull PsiAnnotation annotation, + @Nullable UElement argument, + @Nullable UElement errorNode, + boolean flag, + @NonNull PsiAnnotation[] allAnnotations) { + if (argument == null) { + return; + } + if (argument instanceof ULiteralExpression) { + Object value = ((ULiteralExpression) argument).getValue(); + if (value == null) { + // Accepted for @StringDef + //noinspection UnnecessaryReturnStatement + return; + } else if (value instanceof String) { + String string = (String) value; + checkTypeDefConstant(context, annotation, argument, errorNode, false, string, + allAnnotations); + } else if (value instanceof Integer || value instanceof Long) { + long v = value instanceof Long ? ((Long) value) : ((Integer) value).longValue(); + if (flag && v == 0) { + // Accepted for a flag @IntDef + return; + } + + checkTypeDefConstant(context, annotation, argument, errorNode, flag, value, + allAnnotations); } - checkTypeDefConstant(context, annotation, argument, errorNode, flag, (int) value); } else if (isMinusOne(argument)) { // -1 is accepted unconditionally for flags if (!flag) { - reportTypeDef(context, annotation, argument, errorNode); + reportTypeDef(context, annotation, argument, errorNode, allAnnotations); + } + } else if (argument instanceof UPrefixExpression) { + UPrefixExpression expression = (UPrefixExpression) argument; + if (flag) { + checkTypeDefConstant(context, annotation, expression.getOperand(), + errorNode, true, allAnnotations); + } else { + UastOperator operator = expression.getOperator(); + if (operator == UastPrefixOperator.BITWISE_NOT) { + if (isIgnoredInIde(TYPE_DEF, context, expression)) { + return; + } + context.report(TYPE_DEF, expression, context.getUastLocation(expression), + "Flag not allowed here"); + } else if (operator == UastPrefixOperator.UNARY_MINUS) { + reportTypeDef(context, annotation, argument, errorNode, allAnnotations); + } + } + } else if (argument instanceof UParenthesizedExpression) { + UExpression expression = ((UParenthesizedExpression) argument).getExpression(); + if (expression != null) { + checkTypeDefConstant(context, annotation, expression, errorNode, flag, allAnnotations); } } else if (argument instanceof UIfExpression) { UIfExpression expression = (UIfExpression) argument; - if (expression.getThenBranch() != null) { - checkTypeDefConstant(context, annotation, expression.getThenBranch(), errorNode, flag); + if (expression.getThenExpression() != null) { + checkTypeDefConstant(context, annotation, expression.getThenExpression(), errorNode, flag, + allAnnotations); } - if (expression.getElseBranch() != null) { - checkTypeDefConstant(context, annotation, expression.getElseBranch(), errorNode, flag); - } - } else if (argument instanceof UUnaryExpression) { - UUnaryExpression expression = (UUnaryExpression) argument; - UastOperator operator = expression.getOperator(); - if (flag) { - checkTypeDefConstant(context, annotation, expression.getOperand(), errorNode, true); - } else if (operator == UastPrefixOperator.BITWISE_NOT) { - context.report(TYPE_DEF, expression, context.getLocation(expression), - "Flag not allowed here"); + if (expression.getElseExpression() != null) { + checkTypeDefConstant(context, annotation, expression.getElseExpression(), errorNode, flag, + allAnnotations); } } else if (argument instanceof UBinaryExpression) { + // If it's ?: then check both the if and else clauses UBinaryExpression expression = (UBinaryExpression) argument; if (flag) { - checkTypeDefConstant(context, annotation, expression.getLeftOperand(), errorNode, true); - checkTypeDefConstant(context, annotation, expression.getRightOperand(), errorNode, true); + checkTypeDefConstant(context, annotation, expression.getLeftOperand(), errorNode, true, + allAnnotations); + checkTypeDefConstant(context, annotation, expression.getRightOperand(), errorNode, true, + allAnnotations); } else { - UastOperator operator = expression.getOperator(); + UastBinaryOperator operator = expression.getOperator(); if (operator == UastBinaryOperator.BITWISE_AND || operator == UastBinaryOperator.BITWISE_OR || operator == UastBinaryOperator.BITWISE_XOR) { - context.report(TYPE_DEF, expression, context.getLocation(expression), + if (isIgnoredInIde(TYPE_DEF, context, expression)) { + return; + } + context.report(TYPE_DEF, expression, context.getUastLocation(expression), "Flag not allowed here"); } } - } else if (argument instanceof UResolvable) { - UDeclaration resolved = ((UResolvable)argument).resolve(context); - if (resolved instanceof UVariable) { - checkTypeDefConstant(context, annotation, argument, errorNode, flag, resolved); + } if (argument instanceof UReferenceExpression) { + PsiElement resolved = ((UReferenceExpression) argument).resolve(); + if (resolved instanceof PsiVariable) { + PsiVariable variable = (PsiVariable) resolved; + + if (variable.getType() instanceof PsiArrayType) { + // It's pointing to an array reference; we can't check these individual + // elements (because we can't jump from ResolvedNodes to AST elements; this + // is part of the motivation for the PSI change in lint 2.0), but we also + // don't want to flag it as invalid. + return; + } + + // If it's a constant (static/final) check that it's one of the allowed ones + if (variable.hasModifierProperty(PsiModifier.STATIC) + && variable.hasModifierProperty(PsiModifier.FINAL)) { + checkTypeDefConstant(context, annotation, argument, + errorNode != null ? errorNode : argument, + flag, resolved, allAnnotations); + } else { + UExpression lastAssignment = + UastLintUtils.findLastAssignment(variable, argument, context); + + if (lastAssignment != null) { + checkTypeDefConstant(context, annotation, + lastAssignment, + errorNode != null ? errorNode : argument, flag, + allAnnotations); + } + } + } + } else if (UastExpressionUtils.isNewArrayWithInitializer(argument)) { + UCallExpression arrayInitializer = (UCallExpression) argument; + PsiType type = arrayInitializer.getExpressionType(); + if (type != null) { + type = type.getDeepComponentType(); + } + if (PsiType.INT.equals(type) || PsiType.LONG.equals(type)) { + for (UExpression expression : arrayInitializer.getValueArguments()) { + checkTypeDefConstant(context, annotation, expression, errorNode, flag, + allAnnotations); + } } } } - private static void checkTypeDefConstant(@NonNull UastAndroidContext context, - @NonNull UAnnotation annotation, @NonNull UElement argument, - @Nullable UElement errorNode, boolean flag, Object value) { - for (UNamedExpression namedExpression : annotation.getValueArguments()) { - UExpression expression = namedExpression.getExpression(); - if (expression instanceof UCallExpression && - ((UCallExpression) expression).getKind() == UastCallKind.ARRAY_INITIALIZER) { - for (UExpression arg : ((UCallExpression) expression).getValueArguments()) { - if (value.equals(arg.evaluate())) { + private static void checkTypeDefConstant(@NonNull JavaContext context, + @NonNull PsiAnnotation annotation, @NonNull UElement argument, + @Nullable UElement errorNode, boolean flag, Object value, + @NonNull PsiAnnotation[] allAnnotations) { + PsiAnnotation rangeAnnotation = findIntRange(allAnnotations); + if (rangeAnnotation != null) { + // Allow @IntRange on this number + if (getIntRangeError(context, rangeAnnotation, argument) == null) { + return; + } + } + + PsiAnnotationMemberValue allowed = getAnnotationValue(annotation); + if (allowed == null) { + return; + } + + if (allowed instanceof PsiArrayInitializerMemberValue) { + PsiArrayInitializerMemberValue initializerExpression = + (PsiArrayInitializerMemberValue) allowed; + PsiAnnotationMemberValue[] initializers = initializerExpression.getInitializers(); + for (PsiAnnotationMemberValue expression : initializers) { + if (expression instanceof PsiLiteral) { + if (value.equals(((PsiLiteral)expression).getValue())) { + return; + } + } else if (expression instanceof PsiReference) { + PsiElement resolved = ((PsiReference) expression).resolve(); + if (resolved != null && resolved.equals(value)) { return; } } } + + if (value instanceof PsiField) { + PsiField astNode = (PsiField)value; + UExpression initializer = context.getUastContext().getInitializerBody(astNode); + if (initializer != null) { + checkTypeDefConstant(context, annotation, initializer, errorNode, + flag, allAnnotations); + return; + } + } + + reportTypeDef(context, argument, errorNode, flag, + initializers, allAnnotations); } - reportTypeDef(context, argument, errorNode, flag, annotation.getValues()); } - private static void reportTypeDef(@NonNull UastAndroidContext context, - @NonNull UAnnotation annotation, @NonNull UElement argument, - @Nullable UElement errorNode) { - List> allowed = annotation.getValues(); - reportTypeDef(context, argument, errorNode, false, allowed); + private static void reportTypeDef(@NonNull JavaContext context, + @NonNull PsiAnnotation annotation, @NonNull UElement argument, + @Nullable UElement errorNode, @NonNull PsiAnnotation[] allAnnotations) { + // reportTypeDef(context, argument, errorNode, false, allowedValues, allAnnotations); + PsiAnnotationMemberValue allowed = getAnnotationValue(annotation); + if (allowed instanceof PsiArrayInitializerMemberValue) { + PsiArrayInitializerMemberValue initializerExpression = + (PsiArrayInitializerMemberValue) allowed; + PsiAnnotationMemberValue[] initializers = initializerExpression.getInitializers(); + reportTypeDef(context, argument, errorNode, false, initializers, allAnnotations); + } } - private static void reportTypeDef(@NonNull UastAndroidContext context, @NonNull UElement node, - @Nullable UElement errorNode, boolean flag, @NonNull List> allowedValues) { + private static void reportTypeDef(@NonNull JavaContext context, @NonNull UElement node, + @Nullable UElement errorNode, boolean flag, + @NonNull PsiAnnotationMemberValue[] allowedValues, + @NonNull PsiAnnotation[] allAnnotations) { + if (errorNode == null) { + errorNode = node; + } + if (isIgnoredInIde(TYPE_DEF, context, errorNode)) { + return; + } + String values = listAllowedValues(allowedValues); String message; if (flag) { @@ -1129,37 +1684,49 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { } else { message = "Must be one of: " + values; } - if (errorNode == null) { - errorNode = node; + + PsiAnnotation rangeAnnotation = findIntRange(allAnnotations); + if (rangeAnnotation != null) { + // Allow @IntRange on this number + String rangeError = getIntRangeError(context, rangeAnnotation, node); + if (rangeError != null && !rangeError.isEmpty()) { + message += " or " + Character.toLowerCase(rangeError.charAt(0)) + + rangeError.substring(1); + } } - context.report(TYPE_DEF, errorNode, context.getLocation(errorNode), message); + + context.report(TYPE_DEF, errorNode, context.getUastLocation(errorNode), message); } - private static String listAllowedValues(@NonNull List> allowedValues) { + @Nullable + private static PsiAnnotationMemberValue getAnnotationValue(@NonNull PsiAnnotation annotation) { + PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes(); + for (PsiNameValuePair pair : attributes) { + if (pair.getName() == null || pair.getName().equals(ATTR_VALUE)) { + return pair.getValue(); + } + } + return null; + } + + private static String listAllowedValues(@NonNull PsiAnnotationMemberValue[] allowedValues) { StringBuilder sb = new StringBuilder(); - for (Pair namedValue : allowedValues) { - Object allowedValue = namedValue.getSecond(); - String s; - if (allowedValue instanceof Integer) { - s = allowedValue.toString(); - } else if (allowedValue instanceof UVariable) { - UVariable variable = (UVariable) allowedValue; - UClass containingClass = UastUtils.getContainingClassOrEmpty(variable); - String containingClassName = containingClass.getFqName(); - if (containingClassName == null) { - continue; + for (PsiAnnotationMemberValue allowedValue : allowedValues) { + String s = null; + if (allowedValue instanceof PsiReference) { + PsiElement resolved = ((PsiReference) allowedValue).resolve(); + if (resolved instanceof PsiField) { + PsiField field = (PsiField) resolved; + String containingClassName = field.getContainingClass() != null + ? field.getContainingClass().getName() : null; + if (containingClassName == null) { + continue; + } + s = containingClassName + "." + field.getName(); } - containingClassName = containingClassName.substring(containingClassName.lastIndexOf('.') + 1); - s = containingClassName + "." + variable.getName(); - } else if (allowedValue instanceof UFqNamed) { - String fqName = ((UFqNamed)allowedValue).getFqName(); - if (fqName != null) { - s = fqName; - } else { - continue; - } - } else { - continue; + } + if (s == null) { + s = allowedValue.getText(); } if (sb.length() > 0) { sb.append(", "); @@ -1169,145 +1736,206 @@ public class SupportAnnotationDetector extends Detector implements UastScanner { return sb.toString(); } - private static double getDoubleAttribute(@NonNull UAnnotation annotation, + static double getDoubleAttribute(@NonNull PsiAnnotation annotation, @NonNull String name, double defaultValue) { - Object value = annotation.getValue(name); - if (value instanceof Number) { - return ((Number) value).doubleValue(); + Double value = getAnnotationDoubleValue(annotation, name); + if (value != null) { + return value; } return defaultValue; } - private static long getLongAttribute(@NonNull UAnnotation annotation, + static long getLongAttribute(@NonNull PsiAnnotation annotation, @NonNull String name, long defaultValue) { - Object value = annotation.getValue(name); - if (value instanceof Number) { - return ((Number) value).longValue(); + Long value = getAnnotationLongValue(annotation, name); + if (value != null) { + return value; } return defaultValue; } - private static boolean getBoolean(@NonNull UAnnotation annotation, + static boolean getBoolean(@NonNull PsiAnnotation annotation, @NonNull String name, boolean defaultValue) { - Object value = annotation.getValue(name); - if (value instanceof Boolean) { - return ((Boolean) value); + Boolean value = getAnnotationBooleanValue(annotation, name); + if (value != null) { + return value; } return defaultValue; } - @Nullable - static UAnnotation getRelevantAnnotation( - @NonNull UAnnotation annotation, - @NonNull UastAndroidContext context - ) { - String signature = annotation.getFqName(); - if (signature == null) { - return null; + @NonNull + static PsiAnnotation[] filterRelevantAnnotations( + @NonNull PsiAnnotation[] annotations) { + List result = null; + int length = annotations.length; + if (length == 0) { + return annotations; } - - if (signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) { - // Bail on the nullness annotations early since they're the most commonly - // defined ones. They're not analyzed in lint yet. - if (signature.endsWith(".Nullable") || signature.endsWith(".NonNull")) { - return null; + for (PsiAnnotation annotation : annotations) { + String signature = annotation.getQualifiedName(); + if (signature == null || signature.startsWith("java.")) { + // @Override, @SuppressWarnings etc. Ignore + continue; } + if (signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) { + // Bail on the nullness annotations early since they're the most commonly + // defined ones. They're not analyzed in lint yet. + if (signature.endsWith(".Nullable") || signature.endsWith(".NonNull")) { + continue; + } - return annotation; - } + // Common case: there's just one annotation; no need to create a list copy + if (length == 1) { + return annotations; + } + if (result == null) { + result = new ArrayList(2); + } + result.add(annotation); + } - if (signature.startsWith("java.")) { - // @Override, @SuppressWarnings etc. Ignore - return null; - } - - // Special case @IntDef and @StringDef: These are used on annotations - // themselves. For example, you create a new annotation named @foo.bar.Baz, - // annotate it with @IntDef, and then use @foo.bar.Baz in your signatures. - // Here we want to map from @foo.bar.Baz to the corresponding int def. - // Don't need to compute this if performing @IntDef or @StringDef lookup - UClass type = annotation.resolve(context); - if (type != null) { - for (UAnnotation inner : type.getAnnotations()) { - if (inner.matchesFqName(INT_DEF_ANNOTATION) - || inner.matchesFqName(STRING_DEF_ANNOTATION) - || inner.matchesFqName(PERMISSION_ANNOTATION)) { - return inner; + // Special case @IntDef and @StringDef: These are used on annotations + // themselves. For example, you create a new annotation named @foo.bar.Baz, + // annotate it with @IntDef, and then use @foo.bar.Baz in your signatures. + // Here we want to map from @foo.bar.Baz to the corresponding int def. + // Don't need to compute this if performing @IntDef or @StringDef lookup + PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement(); + if (ref == null) { + continue; + } + PsiElement resolved = ref.resolve(); + if (!(resolved instanceof PsiClass) || !((PsiClass)resolved).isAnnotationType()) { + continue; + } + PsiClass cls = (PsiClass)resolved; + PsiModifierList modifierList = cls.getModifierList(); + if (modifierList != null) { + PsiAnnotation[] innerAnnotations = modifierList.getAnnotations(); + for (int j = 0; j < innerAnnotations.length; j++) { + PsiAnnotation inner = innerAnnotations[j]; + String a = inner.getQualifiedName(); + if (a == null) { + continue; + } + if (a.equals(INT_DEF_ANNOTATION) + || a.equals(PERMISSION_ANNOTATION) + || a.equals(INT_RANGE_ANNOTATION) + || a.equals(STRING_DEF_ANNOTATION)) { + if (length == 1 && j == innerAnnotations.length - 1) { + return innerAnnotations; + } + if (result == null) { + result = new ArrayList(2); + } + result.add(inner); + } } } } - return null; + return result != null + ? result.toArray(PsiAnnotation.EMPTY_ARRAY) : PsiAnnotation.EMPTY_ARRAY; } // ---- Implements UastScanner ---- + + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public List> getApplicableUastTypes() { + List> types = new ArrayList>(3); + types.add(UCallExpression.class); + //types.add(PsiMethodCallExpression.class); + //types.add(PsiNewExpression.class); + types.add(UField.class); + return types; + } + + @Nullable + @Override + public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new CallVisitor(context); } private class CallVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; - public CallVisitor(UastAndroidContext context) { + public CallVisitor(JavaContext context) { mContext = context; } @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - if (node.getKind() == UastCallKind.FUNCTION_CALL) { - visitFunctionInvocation(node); + public boolean visitCallExpression(UCallExpression call) { + PsiMethod method = call.resolve(); + if (method != null) { + checkCall(method, call); } - - return super.visitCallExpression(node); + return super.visitCallExpression(call); } - private boolean visitFunctionInvocation(@NonNull UCallExpression call) { - UFunction method = call.resolve(mContext); - if (method != null) { - List annotations = method.getAnnotations(); - for (UAnnotation annotation : annotations) { - annotation = getRelevantAnnotation(annotation, mContext); - if (annotation != null) { - checkMethodAnnotation(mContext, method, call, annotation); - } - } + @Override + public boolean visitVariable(UVariable node) { + if (node instanceof UEnumConstant) { + UEnumConstant constant = (UEnumConstant) node; + PsiMethod method = constant.resolveMethod(); + checkCall(method, constant); + } + return super.visitVariable(node); + } + + public void checkCall(PsiMethod method, UCallExpression call) { + JavaEvaluator evaluator = mContext.getEvaluator(); - // Look for annotations on the class as well: these trickle - // down to all the methods in the class - UClass containingClass = UastUtils.getContainingClass(method); - if (containingClass != null) { - annotations = containingClass.getAnnotations(); - for (UAnnotation annotation : annotations) { - annotation = getRelevantAnnotation(annotation, mContext); - if (annotation != null) { - checkMethodAnnotation(mContext, method, call, annotation); - } - } - } + PsiAnnotation[] methodAnnotations = evaluator.getAllAnnotations(method, true); + methodAnnotations = filterRelevantAnnotations(methodAnnotations); - Iterator arguments = call.getValueArguments().iterator(); - for (int i = 0, n = method.getValueParameterCount(); - i < n && arguments.hasNext(); - i++) { - UExpression argument = arguments.next(); + // Look for annotations on the class as well: these trickle + // down to all the methods in the class + PsiClass containingClass = method.getContainingClass(); + PsiAnnotation[] classAnnotations; + if (containingClass != null) { + classAnnotations = evaluator.getAllAnnotations(containingClass, true); + classAnnotations = filterRelevantAnnotations(classAnnotations); + } else { + classAnnotations = PsiAnnotation.EMPTY_ARRAY; + } - annotations = method.getValueParameters().get(i).getAnnotations(); - for (UAnnotation annotation : annotations) { - annotation = getRelevantAnnotation(annotation, mContext); - if (annotation != null) { - checkParameterAnnotation(mContext, argument, annotation); - } - } + for (PsiAnnotation annotation : methodAnnotations) { + checkMethodAnnotation(mContext, method, call, annotation, methodAnnotations, + classAnnotations); + } + + if (classAnnotations.length > 0) { + for (PsiAnnotation annotation : classAnnotations) { + checkMethodAnnotation(mContext, method, call, annotation, methodAnnotations, + classAnnotations); } } - return false; + List arguments = call.getValueArguments(); + PsiParameterList parameterList = method.getParameterList(); + PsiParameter[] parameters = parameterList.getParameters(); + PsiAnnotation[] annotations = null; + for (int i = 0, n = Math.min(parameters.length, arguments.size()); + i < n; + i++) { + UExpression argument = arguments.get(i); + PsiParameter parameter = parameters[i]; + annotations = evaluator.getAllAnnotations(parameter, true); + annotations = filterRelevantAnnotations(annotations); + checkParameterAnnotations(mContext, argument, call, method, annotations); + } + if (annotations != null) { + // last parameter is varargs (same parameter annotations) + for (int i = parameters.length; i < arguments.size(); i++) { + UExpression argument = arguments.get(i); + checkParameterAnnotations(mContext, argument, call, method, annotations); + } + } } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ToastDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ToastDetector.java index 9660cfad904..b68628be1cf 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ToastDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ToastDetector.java @@ -17,27 +17,31 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; -import com.android.tools.klint.client.api.UastLintUtils; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.ULiteralExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UReturnExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; -import java.io.File; import java.util.Collections; import java.util.List; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; - /** Detector looking for Toast.makeText() without a corresponding show() call */ -public class ToastDetector extends Detector implements UastScanner { +public class ToastDetector extends Detector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "ShowToast", //$NON-NLS-1$ @@ -51,64 +55,55 @@ public class ToastDetector extends Detector implements UastScanner { Severity.WARNING, new Implementation( ToastDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); /** Constructs a new {@link ToastDetector} check */ public ToastDetector() { } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - // ---- Implements UastScanner ---- @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("makeText"); //$NON-NLS-1$ } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - assert "makeText".equals(node.getFunctionName()); - - UElement qualifiedExpression = node.getParent(); - if (!(qualifiedExpression instanceof UQualifiedExpression)) { - return; - } - - if (!UastUtils.endsWithQualified(((UQualifiedExpression) qualifiedExpression).getReceiver(), "Toast")) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod uMethod) { + PsiMethod method = uMethod.getPsi(); + + if (!JavaEvaluator.isMemberInClass(method, "android.widget.Toast")) { return; } // Make sure you pass the right kind of duration: it's not a delay, it's // LENGTH_SHORT or LENGTH_LONG // (see http://code.google.com/p/android/issues/detail?id=3655) - List args = node.getValueArguments(); + List args = call.getValueArguments(); if (args.size() == 3) { UExpression duration = args.get(2); - if (duration instanceof ULiteralExpression && ((ULiteralExpression)duration).getValue() instanceof Number) { - context.report(ISSUE, duration, context.getLocation(duration), - "Expected duration `Toast.LENGTH_SHORT` or `Toast.LENGTH_LONG`, a custom " + - "duration value is not supported"); + if (duration instanceof ULiteralExpression) { + context.report(ISSUE, duration, context.getUastLocation(duration), + "Expected duration `Toast.LENGTH_SHORT` or `Toast.LENGTH_LONG`, a custom " + + "duration value is not supported"); } } - UFunction method = UastUtils.getContainingFunction(node.getParent()); - if (method == null) { + + UMethod surroundingMethod = UastUtils.getContainingUMethod(call); + if (surroundingMethod == null) { return; } - UExpression nodeWithPossibleQualifier = UastUtils.getQualifiedCallElement(node); - ShowFinder finder = new ShowFinder(nodeWithPossibleQualifier); - method.accept(finder); + ShowFinder finder = new ShowFinder(call); + surroundingMethod.getUastBody().accept(finder); if (!finder.isShowCalled()) { - context.report(ISSUE, node, context.getLocation(node), - "Toast created but not shown: did you forget to call `show()` ?"); + context.report(ISSUE, call, context.getUastNameLocation(call), + "Toast created but not shown: did you forget to call `show()` ?"); } + } private static class ShowFinder extends AbstractUastVisitor { @@ -119,43 +114,38 @@ public class ToastDetector extends Detector implements UastScanner { /** Whether we've seen the target makeText node yet */ private boolean mSeenTarget; - private ShowFinder(UExpression target) { - mTarget = target; + private ShowFinder(UCallExpression target) { + mTarget = UastUtils.getQualifiedParentOrThis(target); } @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { + public boolean visitCallExpression(UCallExpression node) { if (node.equals(mTarget)) { mSeenTarget = true; - } else if (mSeenTarget && node.matchesFunctionName("show")) { //$NON-NLS-1$ - // TODO: Do more flow analysis to see whether we're really calling show - // on the right type of object? - mFound = true; + } else { + if ((mTarget.equals(node.getReceiver())) + && "show".equals(node.getMethodName())) { + // TODO: Do more flow analysis to see whether we're really calling show + // on the right type of object? + mFound = true; + } } - + return super.visitCallExpression(node); } @Override - public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) { - if (node == mTarget) { - mSeenTarget = true; - } - - return super.visitQualifiedExpression(node); - } - - @Override - public boolean visitReturnExpression(@NotNull UReturnExpression node) { - if (UastLintUtils.isChildOfExpression(mTarget, node.getReturnExpression())) { + public boolean visitReturnExpression(UReturnExpression node) { + if (UastUtils.isChildOf(mTarget, node.getReturnExpression(), true)) { + // If you just do "return Toast.makeText(...) don't warn mFound = true; } - + return super.visitReturnExpression(node); } - boolean isShowCalled() { - return mFound && mSeenTarget; + private boolean isShowCalled() { + return mFound; } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/TrustAllX509TrustManagerDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/TrustAllX509TrustManagerDetector.java new file mode 100644 index 00000000000..0c054494445 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/TrustAllX509TrustManagerDetector.java @@ -0,0 +1,194 @@ +/* + * 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.checks; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +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.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.uast.UBlockExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UReturnExpression; +import org.jetbrains.uast.UastEmptyExpression; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodNode; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +public class TrustAllX509TrustManagerDetector extends Detector implements Detector.UastScanner, + ClassScanner { + + @SuppressWarnings("unchecked") + private static final Implementation IMPLEMENTATION = + new Implementation(TrustAllX509TrustManagerDetector.class, + EnumSet.of(Scope.JAVA_LIBRARIES, Scope.JAVA_FILE), + Scope.JAVA_FILE_SCOPE); + + public static final Issue ISSUE = Issue.create("TrustAllX509TrustManager", + "Insecure TLS/SSL trust manager", + "This check looks for X509TrustManager implementations whose `checkServerTrusted` or " + + "`checkClientTrusted` methods do nothing (thus trusting any certificate chain) " + + "which could result in insecure network traffic caused by trusting arbitrary " + + "TLS/SSL certificates presented by peers.", + Category.SECURITY, + 6, + Severity.WARNING, + IMPLEMENTATION); + + public TrustAllX509TrustManagerDetector() { + } + + // ---- Implements UastScanner ---- + + @Nullable + @Override + public List applicableSuperClasses() { + return Collections.singletonList("javax.net.ssl.X509TrustManager"); + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass cls) { + checkMethod(context, cls, "checkServerTrusted"); + checkMethod(context, cls, "checkClientTrusted"); + } + + private static void checkMethod(@NonNull JavaContext context, + @NonNull UClass cls, + @NonNull String methodName) { + JavaEvaluator evaluator = context.getEvaluator(); + for (PsiMethod method : cls.findMethodsByName(methodName, true)) { + if (evaluator.isAbstract(method)) { + continue; + } + + // For now very simple; only checks if nothing is done. + // Future work: Improve this check to be less sensitive to irrelevant + // instructions/statements/invocations (e.g. System.out.println) by + // looking for calls that could lead to a CertificateException being + // thrown, e.g. throw statement within the method itself or invocation + // of another method that may throw a CertificateException, and only + // reporting an issue if none of these calls are found. ControlFlowGraph + // may be useful here. + + UExpression body = context.getUastContext().getMethodBody(method); + + ComplexBodyVisitor visitor = new ComplexBodyVisitor(); + body.accept(visitor); + + if (!visitor.isComplex()) { + Location location = context.getNameLocation(method); + String message = getErrorMessage(methodName); + context.report(ISSUE, method, location, message); + } + } + } + + @NonNull + private static String getErrorMessage(String methodName) { + return "`" + methodName + "` is empty, which could cause " + + "insecure network traffic due to trusting arbitrary TLS/SSL " + + "certificates presented by peers"; + } + + private static class ComplexBodyVisitor extends AbstractUastVisitor { + private boolean isComplex = false; + + @Override + public boolean visitElement(@NotNull UElement node) { + if (node instanceof UExpression && + !(node instanceof UReturnExpression + || node instanceof UBlockExpression + || node instanceof UastEmptyExpression)) { + isComplex = true; + } + + return isComplex || super.visitElement(node); + } + + boolean isComplex() { + return isComplex; + } + } + + // ---- Implements ClassScanner ---- + // Only used for libraries where we have to analyze bytecode + + @Override + @SuppressWarnings("rawtypes") + public void checkClass(@NonNull final ClassContext context, + @NonNull ClassNode classNode) { + if (!context.isFromClassLibrary()) { + // Non-library code checked at the AST level + return; + } + if (!classNode.interfaces.contains("javax/net/ssl/X509TrustManager")) { + return; + } + List methodList = classNode.methods; + for (Object m : methodList) { + MethodNode method = (MethodNode) m; + if ("checkServerTrusted".equals(method.name) || + "checkClientTrusted".equals(method.name)) { + InsnList nodes = method.instructions; + boolean emptyMethod = true; // Stays true if method doesn't perform any "real" + // operations + for (int i = 0, n = nodes.size(); i < n; i++) { + // Future work: Improve this check to be less sensitive to irrelevant + // instructions/statements/invocations (e.g. System.out.println) by + // looking for calls that could lead to a CertificateException being + // thrown, e.g. throw statement within the method itself or invocation + // of another method that may throw a CertificateException, and only + // reporting an issue if none of these calls are found. ControlFlowGraph + // may be useful here. + AbstractInsnNode instruction = nodes.get(i); + int type = instruction.getType(); + if (type != AbstractInsnNode.LABEL && type != AbstractInsnNode.LINE && + !(type == AbstractInsnNode.INSN && + instruction.getOpcode() == Opcodes.RETURN)) { + emptyMethod = false; + break; + } + } + if (emptyMethod) { + Location location = context.getLocation(method, classNode); + context.report(ISSUE, location, getErrorMessage(method.name)); + } + } + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/TypoLookup.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/TypoLookup.java new file mode 100644 index 00000000000..98fa7421d06 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/TypoLookup.java @@ -0,0 +1,776 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import static com.android.SdkConstants.DOT_XML; +import static com.android.tools.klint.detector.api.LintUtils.assertionsEnabled; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.tools.klint.client.api.LintClient; +import com.android.tools.klint.detector.api.LintUtils; +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.google.common.io.ByteSink; +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel.MapMode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.WeakHashMap; + +/** + * Database of common typos / misspellings. + */ +public class TypoLookup { + private static final TypoLookup NONE = new TypoLookup(); + + /** String separating misspellings and suggested replacements in the text file */ + private static final String WORD_SEPARATOR = "->"; //$NON-NLS-1$ + + /** Relative path to the typos database file within the Lint installation */ + private static final String XML_FILE_PATH = "tools/support/typos-%1$s.txt"; //$NON-NLS-1$ + private static final String FILE_HEADER = "Typo database used by Android lint\000"; + private static final int BINARY_FORMAT_VERSION = 2; + private static final boolean DEBUG_FORCE_REGENERATE_BINARY = false; + private static final boolean DEBUG_SEARCH = false; + private static final boolean WRITE_STATS = false; + /** Default size to reserve for each API entry when creating byte buffer to build up data */ + private static final int BYTES_PER_ENTRY = 28; + + private byte[] mData; + private int[] mIndices; + private int mWordCount; + + private static final WeakHashMap sInstanceMap = + new WeakHashMap(); + + /** + * Returns an instance of the Typo database for the given locale + * + * @param client the client to associate with this database - used only for + * logging. The database object may be shared among repeated + * invocations, and in that case client used will be the one + * originally passed in. In other words, this parameter may be + * ignored if the client created is not new. + * @param locale the locale to look up a typo database for (should be a + * language code (ISO 639-1, two lowercase character names) + * @param region the region to look up a typo database for (should be a two + * letter ISO 3166-1 alpha-2 country code in upper case) language + * code + * @return a (possibly shared) instance of the typo database, or null if its + * data can't be found + */ + @Nullable + public static TypoLookup get(@NonNull LintClient client, @NonNull String locale, + @Nullable String region) { + synchronized (TypoLookup.class) { + String key = locale; + + if (region != null && region.length() == 2) { // skip BCP-47 regions + // Allow for region-specific dictionaries. See for example + // http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences + assert region.length() == 2 + && Character.isUpperCase(region.charAt(0)) + && Character.isUpperCase(region.charAt(1)) : region; + // Look for typos-en-rUS.txt etc + key = locale + 'r' + region; + } + + TypoLookup db = sInstanceMap.get(key); + if (db == null) { + String path = String.format(XML_FILE_PATH, key); + File file = client.findResource(path); + if (file == null) { + // AOSP build environment? + String build = System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ + if (build != null) { + file = new File(build, ("sdk/files/" //$NON-NLS-1$ + + path.substring(path.lastIndexOf('/') + 1)) + .replace('/', File.separatorChar)); + } + } + + if (file == null || !file.exists()) { + //noinspection VariableNotUsedInsideIf + if (region != null) { + // Fall back to the generic locale (non-region-specific) database + return get(client, locale, null); + } + db = NONE; + } else { + db = get(client, file); + assert db != null : file; + } + sInstanceMap.put(key, db); + } + + if (db == NONE) { + return null; + } else { + return db; + } + } + } + + /** + * Returns an instance of the typo database + * + * @param client the client to associate with this database - used only for + * logging + * @param xmlFile the XML file containing configuration data to use for this + * database + * @return a (possibly shared) instance of the typo database, or null + * if its data can't be found + */ + @Nullable + private static TypoLookup get(LintClient client, File xmlFile) { + if (!xmlFile.exists()) { + client.log(null, "The typo database file %1$s does not exist", xmlFile); + return null; + } + + String name = xmlFile.getName(); + if (LintUtils.endsWith(name, DOT_XML)) { + name = name.substring(0, name.length() - DOT_XML.length()); + } + File cacheDir = client.getCacheDir(true/*create*/); + if (cacheDir == null) { + cacheDir = xmlFile.getParentFile(); + } + + File binaryData = new File(cacheDir, name + // Incorporate version number in the filename to avoid upgrade filename + // conflicts on Windows (such as issue #26663) + + '-' + BINARY_FORMAT_VERSION + ".bin"); //$NON-NLS-1$ + + if (DEBUG_FORCE_REGENERATE_BINARY) { + System.err.println("\nTemporarily regenerating binary data unconditionally \nfrom " + + xmlFile + "\nto " + binaryData); + if (!createCache(client, xmlFile, binaryData)) { + return null; + } + } else if (!binaryData.exists() || binaryData.lastModified() < xmlFile.lastModified()) { + if (!createCache(client, xmlFile, binaryData)) { + return null; + } + } + + if (!binaryData.exists()) { + client.log(null, "The typo database file %1$s does not exist", binaryData); + return null; + } + + return new TypoLookup(client, xmlFile, binaryData); + } + + private static boolean createCache(LintClient client, File xmlFile, File binaryData) { + long begin = 0; + if (WRITE_STATS) { + begin = System.currentTimeMillis(); + } + + // Read in data + List lines; + try { + lines = Files.readLines(xmlFile, Charsets.UTF_8); + } catch (IOException e) { + client.log(e, "Can't read typo database file"); + return false; + } + + if (WRITE_STATS) { + long end = System.currentTimeMillis(); + System.out.println("Reading data structures took " + (end - begin) + " ms)"); + } + + try { + writeDatabase(binaryData, lines); + return true; + } catch (IOException ioe) { + client.log(ioe, "Can't write typo cache file"); + } + + return false; + } + + /** Use one of the {@link #get} factory methods instead */ + private TypoLookup( + @NonNull LintClient client, + @NonNull File xmlFile, + @Nullable File binaryFile) { + if (binaryFile != null) { + readData(client, xmlFile, binaryFile); + } + } + + private TypoLookup() { + } + + private void readData(@NonNull LintClient client, @NonNull File xmlFile, + @NonNull File binaryFile) { + if (!binaryFile.exists()) { + client.log(null, "%1$s does not exist", binaryFile); + return; + } + long start = System.currentTimeMillis(); + try { + MappedByteBuffer buffer = Files.map(binaryFile, MapMode.READ_ONLY); + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + // First skip the header + byte[] expectedHeader = FILE_HEADER.getBytes(Charsets.US_ASCII); + buffer.rewind(); + for (byte anExpectedHeader : expectedHeader) { + if (anExpectedHeader != buffer.get()) { + client.log(null, "Incorrect file header: not an typo database cache " + + "file, or a corrupt cache file"); + return; + } + } + + // Read in the format number + if (buffer.get() != BINARY_FORMAT_VERSION) { + // Force regeneration of new binary data with up to date format + if (createCache(client, xmlFile, binaryFile)) { + readData(client, xmlFile, binaryFile); // Recurse + } + + return; + } + + mWordCount = buffer.getInt(); + + // Read in the word table indices; + int count = mWordCount; + int[] offsets = new int[count]; + + // Another idea: I can just store the DELTAS in the file (and add them up + // when reading back in) such that it takes just ONE byte instead of four! + + for (int i = 0; i < count; i++) { + offsets[i] = buffer.getInt(); + } + + // No need to read in the rest -- we'll just keep the whole byte array in memory + // TODO: Make this code smarter/more efficient. + int size = buffer.limit(); + byte[] b = new byte[size]; + buffer.rewind(); + buffer.get(b); + mData = b; + mIndices = offsets; + + // TODO: We only need to keep the data portion here since we've initialized + // the offset array separately. + // TODO: Investigate (profile) accessing the byte buffer directly instead of + // accessing a byte array. + } catch (IOException e) { + client.log(e, null); + } + if (WRITE_STATS) { + long end = System.currentTimeMillis(); + System.out.println("\nRead typo database in " + (end - start) + + " milliseconds."); + System.out.println("Size of data table: " + mData.length + " bytes (" + + Integer.toString(mData.length/1024) + "k)\n"); + } + } + + /** See the {@link #readData(LintClient,File,File)} for documentation on the data format. */ + private static void writeDatabase(File file, List lines) throws IOException { + /* + * 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded + * as ASCII characters. The purpose of the header is to identify what the file + * is for, for anyone attempting to open the file. + * 2. A file version number. If the binary file does not match the reader's expected + * version, it can ignore it (and regenerate the cache from XML). + */ + + // Drop comments etc + List words = new ArrayList(lines.size()); + for (String line : lines) { + if (!line.isEmpty() && Character.isLetter(line.charAt(0))) { + int end = line.indexOf(WORD_SEPARATOR); + if (end == -1) { + end = line.trim().length(); + } + String typo = line.substring(0, end).trim(); + String replacements = line.substring(end + WORD_SEPARATOR.length()).trim(); + if (replacements.isEmpty()) { + // We don't support empty replacements + continue; + } + String combined = typo + (char) 0 + replacements; + + words.add(combined); + } + } + + byte[][] wordArrays = new byte[words.size()][]; + for (int i = 0, n = words.size(); i < n; i++) { + String word = words.get(i); + wordArrays[i] = word.getBytes(Charsets.UTF_8); + } + // Sort words, using our own comparator to ensure that it matches the + // binary search in getTypos() + Comparator comparator = new Comparator() { + @Override + public int compare(byte[] o1, byte[] o2) { + return TypoLookup.compare(o1, 0, (byte) 0, o2, 0, o2.length); + } + }; + Arrays.sort(wordArrays, comparator); + + byte[] headerBytes = FILE_HEADER.getBytes(Charsets.US_ASCII); + int entryCount = wordArrays.length; + int capacity = entryCount * BYTES_PER_ENTRY + headerBytes.length + 5; + ByteBuffer buffer = ByteBuffer.allocate(capacity); + buffer.order(ByteOrder.BIG_ENDIAN); + // 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded + // as ASCII characters. The purpose of the header is to identify what the file + // is for, for anyone attempting to open the file. + buffer.put(headerBytes); + + // 2. A file version number. If the binary file does not match the reader's expected + // version, it can ignore it (and regenerate the cache from XML). + buffer.put((byte) BINARY_FORMAT_VERSION); + + // 3. The number of words [1 int] + buffer.putInt(entryCount); + + // 4. Word offset table (one integer per word, pointing to the byte offset in the + // file (relative to the beginning of the file) where each word begins. + // The words are always sorted alphabetically. + int wordOffsetTable = buffer.position(); + + // Reserve enough room for the offset table here: we will backfill it with pointers + // as we're writing out the data structures below + for (int i = 0, n = entryCount; i < n; i++) { + buffer.putInt(0); + } + + int nextEntry = buffer.position(); + int nextOffset = wordOffsetTable; + + // 7. Word entry table. Each word entry consists of the word, followed by the byte 0 + // as a terminator, followed by a comma separated list of suggestions (which + // may be empty), or a final 0. + for (byte[] word : wordArrays) { + buffer.position(nextOffset); + buffer.putInt(nextEntry); + nextOffset = buffer.position(); + buffer.position(nextEntry); + + buffer.put(word); // already embeds 0 to separate typo from words + buffer.put((byte)0); + + nextEntry = buffer.position(); + } + + int size = buffer.position(); + assert size <= buffer.limit(); + buffer.mark(); + + if (WRITE_STATS) { + System.out.println("Wrote " + words.size() + " word entries"); + System.out.print("Actual binary size: " + size + " bytes"); + System.out.println(String.format(" (%.1fM)", size/(1024*1024.f))); + + System.out.println("Allocated size: " + (entryCount * BYTES_PER_ENTRY) + " bytes"); + System.out.println("Required bytes per entry: " + (size/ entryCount) + " bytes"); + } + + // Now dump this out as a file + // There's probably an API to do this more efficiently; TODO: Look into this. + byte[] b = new byte[size]; + buffer.rewind(); + buffer.get(b); + ByteSink sink = Files.asByteSink(file); + sink.write(b); + } + + // For debugging only + private String dumpEntry(int offset) { + if (DEBUG_SEARCH) { + int end = offset; + while (mData[end] != 0) { + end++; + } + return new String(mData, offset, end - offset, Charsets.UTF_8); + } else { + return ""; //$NON-NLS-1$ + } + } + + /** Comparison function: *only* used for ASCII strings */ + @VisibleForTesting + static int compare(byte[] data, int offset, byte terminator, CharSequence s, + int begin, int end) { + int i = offset; + int j = begin; + for (; ; i++, j++) { + byte b = data[i]; + if (b == ' ') { + // We've matched up to the space in a split-word typo, such as + // in German all zu⇒allzu; here we've matched just past "all". + // Rather than terminating, attempt to continue in the buffer. + if (j == end) { + int max = s.length(); + if (end < max && s.charAt(end) == ' ') { + // Find next word + for (; end < max; end++) { + char c = s.charAt(end); + if (!Character.isLetter(c)) { + if (c == ' ' && end == j) { + continue; + } + break; + } + } + } + } + } + + if (j == end) { + break; + } + + if (b == '*') { + // Glob match (only supported at the end) + return 0; + } + char c = s.charAt(j); + byte cb = (byte) c; + int delta = b - cb; + if (delta != 0) { + cb = (byte) Character.toLowerCase(c); + if (b != cb) { + // Ensure that it has the right sign + b = (byte) Character.toLowerCase(b); + delta = b - cb; + if (delta != 0) { + return delta; + } + } + } + } + + return data[i] - terminator; + } + + /** Comparison function used for general UTF-8 encoded strings */ + @VisibleForTesting + static int compare(byte[] data, int offset, byte terminator, byte[] s, + int begin, int end) { + int i = offset; + int j = begin; + for (; ; i++, j++) { + byte b = data[i]; + if (b == ' ') { + // We've matched up to the space in a split-word typo, such as + // in German all zu⇒allzu; here we've matched just past "all". + // Rather than terminating, attempt to continue in the buffer. + // We've matched up to the space in a split-word typo, such as + // in German all zu⇒allzu; here we've matched just past "all". + // Rather than terminating, attempt to continue in the buffer. + if (j == end) { + int max = s.length; + if (end < max && s[end] == ' ') { + // Find next word + for (; end < max; end++) { + byte cb = s[end]; + if (!isLetter(cb)) { + if (cb == ' ' && end == j) { + continue; + } + break; + } + } + } + } + } + + if (j == end) { + break; + } + if (b == '*') { + // Glob match (only supported at the end) + return 0; + } + byte cb = s[j]; + int delta = b - cb; + if (delta != 0) { + cb = toLowerCase(cb); + b = toLowerCase(b); + delta = b - cb; + if (delta != 0) { + return delta; + } + } + + if (b == terminator || cb == terminator) { + return delta; + } + } + + return data[i] - terminator; + } + + /** + * Look up whether this word is a typo, and if so, return the typo itself + * and one or more likely meanings + * + * @param text the string containing the word + * @param begin the index of the first character in the word + * @param end the index of the first character after the word. Note that the + * search may extend beyond this index, if for example the + * word matches a multi-word typo in the dictionary + * @return a list of the typo itself followed by the replacement strings if + * the word represents a typo, and null otherwise + */ + @Nullable + public List getTypos(@NonNull CharSequence text, int begin, int end) { + assert end <= text.length(); + + if (assertionsEnabled()) { + for (int i = begin; i < end; i++) { + char c = text.charAt(i); + if (c >= 128) { + assert false : "Call the UTF-8 version of this method instead"; + return null; + } + } + } + + int low = 0; + int high = mWordCount - 1; + while (low <= high) { + int middle = (low + high) >>> 1; + int offset = mIndices[middle]; + + if (DEBUG_SEARCH) { + System.out.println("Comparing string " + text +" with entry at " + offset + + ": " + dumpEntry(offset)); + } + + // Compare the word at the given index. + int compare = compare(mData, offset, (byte) 0, text, begin, end); + + if (compare == 0) { + offset = mIndices[middle]; + + // Don't allow matching uncapitalized words, such as "enlish", when + // the dictionary word is capitalized, "Enlish". + if (mData[offset] != text.charAt(begin) + && Character.isLowerCase(text.charAt(begin))) { + return null; + } + + // Make sure there is a case match; we only want to allow + // matching capitalized words to capitalized typos or uncapitalized typos + // (e.g. "Teh" and "teh" to "the"), but not uncapitalized words to capitalized + // typos (e.g. "enlish" to "Enlish"). + String glob = null; + for (int i = begin; ; i++) { + byte b = mData[offset++]; + if (b == 0) { + offset--; + break; + } else if (b == '*') { + int globEnd = i; + while (globEnd < text.length() + && Character.isLetter(text.charAt(globEnd))) { + globEnd++; + } + glob = text.subSequence(i, globEnd).toString(); + break; + } + char c = text.charAt(i); + byte cb = (byte) c; + if (b != cb && i > begin) { + return null; + } + } + + return computeSuggestions(mIndices[middle], offset, glob); + } + + if (compare < 0) { + low = middle + 1; + } else if (compare > 0) { + high = middle - 1; + } else { + assert false; // compare == 0 already handled above + return null; + } + } + + return null; + } + + /** + * Look up whether this word is a typo, and if so, return the typo itself + * and one or more likely meanings + * + * @param utf8Text the string containing the word, encoded as UTF-8 + * @param begin the index of the first character in the word + * @param end the index of the first character after the word. Note that the + * search may extend beyond this index, if for example the + * word matches a multi-word typo in the dictionary + * @return a list of the typo itself followed by the replacement strings if + * the word represents a typo, and null otherwise + */ + @Nullable + public List getTypos(@NonNull byte[] utf8Text, int begin, int end) { + assert end <= utf8Text.length; + + int low = 0; + int high = mWordCount - 1; + while (low <= high) { + int middle = (low + high) >>> 1; + int offset = mIndices[middle]; + + if (DEBUG_SEARCH) { + String s = new String(Arrays.copyOfRange(utf8Text, begin, end), Charsets.UTF_8); + System.out.println("Comparing string " + s +" with entry at " + offset + + ": " + dumpEntry(offset)); + System.out.println(" middle=" + middle + ", low=" + low + ", high=" + high); + } + + // Compare the word at the given index. + int compare = compare(mData, offset, (byte) 0, utf8Text, begin, end); + + if (DEBUG_SEARCH) { + System.out.println(" signum=" + (int)Math.signum(compare) + ", delta=" + compare); + } + + if (compare == 0) { + offset = mIndices[middle]; + + // Don't allow matching uncapitalized words, such as "enlish", when + // the dictionary word is capitalized, "Enlish". + if (mData[offset] != utf8Text[begin] && isUpperCase(mData[offset])) { + return null; + } + + // Make sure there is a case match; we only want to allow + // matching capitalized words to capitalized typos or uncapitalized typos + // (e.g. "Teh" and "teh" to "the"), but not uncapitalized words to capitalized + // typos (e.g. "enlish" to "Enlish"). + String glob = null; + for (int i = begin; ; i++) { + byte b = mData[offset++]; + if (b == 0) { + offset--; + break; + } else if (b == '*') { + int globEnd = i; + while (globEnd < utf8Text.length && isLetter(utf8Text[globEnd])) { + globEnd++; + } + glob = new String(utf8Text, i, globEnd - i, Charsets.UTF_8); + break; + } + byte cb = utf8Text[i]; + if (b != cb && i > begin) { + return null; + } + } + + return computeSuggestions(mIndices[middle], offset, glob); + } + + if (compare < 0) { + low = middle + 1; + } else if (compare > 0) { + high = middle - 1; + } else { + assert false; // compare == 0 already handled above + return null; + } + } + + return null; + } + + private List computeSuggestions(int begin, int offset, String glob) { + String typo = new String(mData, begin, offset - begin, Charsets.UTF_8); + + if (glob != null) { + typo = typo.replaceAll("\\*", glob); //$NON-NLS-1$ + } + + assert mData[offset] == 0; + offset++; + int replacementEnd = offset; + while (mData[replacementEnd] != 0) { + replacementEnd++; + } + String replacements = new String(mData, offset, replacementEnd - offset, Charsets.UTF_8); + List words = new ArrayList(); + words.add(typo); + + // The first entry should be the typo itself. We need to pass this back since due + // to multi-match words and globbing it could extend beyond the initial word range + + for (String s : Splitter.on(',').omitEmptyStrings().trimResults().split(replacements)) { + if (glob != null) { + // Need to append the glob string to each result + words.add(s.replaceAll("\\*", glob)); //$NON-NLS-1$ + } else { + words.add(s); + } + } + + return words; + } + + // "Character" handling for bytes. This assumes that the bytes correspond to Unicode + // characters in the ISO 8859-1 range, which is are encoded the same way in UTF-8. + // This obviously won't work to for example uppercase to lowercase conversions for + // multi byte characters, which means we simply won't catch typos if the dictionaries + // contain these. None of the currently included dictionaries do. However, it does + // help us properly deal with punctuation and spacing characters. + + static boolean isUpperCase(byte b) { + return Character.isUpperCase((char) b); + } + + static byte toLowerCase(byte b) { + return (byte) Character.toLowerCase((char) b); + } + + static boolean isSpace(byte b) { + return Character.isWhitespace((char) b); + } + + static boolean isLetter(byte b) { + // Assume that multi byte characters represent letters in other languages. + // Obviously, it could be unusual punctuation etc but letters are more likely + // in this context. + return Character.isLetter((char) b) || (b & 0x80) != 0; + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnsafeBroadcastReceiverDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnsafeBroadcastReceiverDetector.java new file mode 100644 index 00000000000..f9e7fe2ca9b --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnsafeBroadcastReceiverDetector.java @@ -0,0 +1,569 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_PERMISSION; +import static com.android.SdkConstants.CLASS_BROADCASTRECEIVER; +import static com.android.SdkConstants.CLASS_CONTEXT; +import static com.android.SdkConstants.CLASS_INTENT; +import static com.android.SdkConstants.TAG_INTENT_FILTER; +import static com.android.SdkConstants.TAG_RECEIVER; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ClassContext; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; +import com.android.tools.klint.detector.api.Detector.XmlScanner; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LintUtils; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.XmlContext; +import com.google.common.collect.Sets; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiReferenceExpression; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UClass; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.USimpleNameReferenceExpression; +import org.jetbrains.uast.util.UastExpressionUtils; +import org.jetbrains.uast.visitor.AbstractUastVisitor; +import org.jetbrains.uast.visitor.UastVisitor; +import org.objectweb.asm.tree.ClassNode; +import org.w3c.dom.Element; + +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class UnsafeBroadcastReceiverDetector extends Detector + implements Detector.UastScanner, XmlScanner { + + /* Description of check implementations: + * + * UnsafeProtectedBroadcastReceiver check + * + * If a receiver is declared in the application manifest that has an intent-filter + * with an action string that matches a protected-broadcast action string, + * then if that receiver has an onReceive method, ensure that the method calls + * getAction at least once. + * + * With this check alone, false positives will occur if the onReceive method + * passes the received intent to another method that calls getAction. + * We look for any calls to aload_2 within the method bytecode, which could + * indicate loading the inputted intent onto the stack to use in a call + * to another method. In those cases, still report the issue, but + * report in the description that the finding may be a false positive. + * An alternative implementation option would be to omit reporting the issue + * at all when a call to aload_2 exists. + * + * UnprotectedSMSBroadcastReceiver check + * + * If a receiver is declared in AndroidManifest that has an intent-filter + * with action string SMS_DELIVER or SMS_RECEIVED, ensure that the + * receiver requires callers to have the BROADCAST_SMS permission. + * + * It is possible that the receiver may check the sender's permission by + * calling checkCallingPermission, which could cause a false positive. + * However, application developers should still be encouraged to declare + * the permission requirement in the manifest where it can be easily + * audited. + * + * Future work: Add checks for other action strings that should require + * particular permissions be checked, such as + * android.provider.Telephony.WAP_PUSH_DELIVER + * + * Note that neither of these checks address receivers dynamically created at runtime, + * only ones that are declared in the application manifest. + */ + + public static final Issue ACTION_STRING = Issue.create( + "UnsafeProtectedBroadcastReceiver", + "Unsafe Protected BroadcastReceiver", + "BroadcastReceivers that declare an intent-filter for a protected-broadcast action " + + "string must check that the received intent's action string matches the expected " + + "value, otherwise it is possible for malicious actors to spoof intents.", + Category.SECURITY, + 6, + Severity.WARNING, + new Implementation(UnsafeBroadcastReceiverDetector.class, + EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE))); + + public static final Issue BROADCAST_SMS = Issue.create( + "UnprotectedSMSBroadcastReceiver", + "Unprotected SMS BroadcastReceiver", + "BroadcastReceivers that declare an intent-filter for SMS_DELIVER or " + + "SMS_RECEIVED must ensure that the caller has the BROADCAST_SMS permission, " + + "otherwise it is possible for malicious actors to spoof intents.", + Category.SECURITY, + 6, + Severity.WARNING, + new Implementation(UnsafeBroadcastReceiverDetector.class, + Scope.MANIFEST_SCOPE)); + + /* List of protected broadcast strings. This list must be sorted alphabetically. + * Protected broadcast strings are defined by entries in the + * manifest of system-level components or applications. + * The below list is copied from frameworks/base/core/res/AndroidManifest.xml + * and packages/services/Telephony/AndroidManifest.xml . + * It should be periodically updated. This list will likely not be complete, since + * protected-broadcast entries can be defined elsewhere, but should address + * most situations. + */ + @VisibleForTesting + static final String[] PROTECTED_BROADCASTS = new String[] { + "android.app.action.DEVICE_OWNER_CHANGED", + "android.app.action.ENTER_CAR_MODE", + "android.app.action.ENTER_DESK_MODE", + "android.app.action.EXIT_CAR_MODE", + "android.app.action.EXIT_DESK_MODE", + "android.app.action.NEXT_ALARM_CLOCK_CHANGED", + "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED", + "android.appwidget.action.APPWIDGET_DELETED", + "android.appwidget.action.APPWIDGET_DISABLED", + "android.appwidget.action.APPWIDGET_ENABLED", + "android.appwidget.action.APPWIDGET_HOST_RESTORED", + "android.appwidget.action.APPWIDGET_RESTORED", + "android.appwidget.action.APPWIDGET_UPDATE_OPTIONS", + "android.backup.intent.CLEAR", + "android.backup.intent.INIT", + "android.backup.intent.RUN", + "android.bluetooth.a2dp-sink.profile.action.AUDIO_CONFIG_CHANGED", + "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.a2dp-sink.profile.action.PLAYING_STATE_CHANGED", + "android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED", + "android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.adapter.action.DISCOVERY_FINISHED", + "android.bluetooth.adapter.action.DISCOVERY_STARTED", + "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED", + "android.bluetooth.adapter.action.SCAN_MODE_CHANGED", + "android.bluetooth.adapter.action.STATE_CHANGED", + "android.bluetooth.avrcp-controller.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.device.action.ACL_CONNECTED", + "android.bluetooth.device.action.ACL_DISCONNECTED", + "android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED", + "android.bluetooth.device.action.ALIAS_CHANGED", + "android.bluetooth.device.action.BOND_STATE_CHANGED", + "android.bluetooth.device.action.CLASS_CHANGED", + "android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL", + "android.bluetooth.device.action.CONNECTION_ACCESS_REPLY", + "android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST", + "android.bluetooth.device.action.DISAPPEARED", + "android.bluetooth.device.action.FOUND", + "android.bluetooth.device.action.MAS_INSTANCE", + "android.bluetooth.device.action.NAME_CHANGED", + "android.bluetooth.device.action.NAME_FAILED", + "android.bluetooth.device.action.PAIRING_CANCEL", + "android.bluetooth.device.action.PAIRING_REQUEST", + "android.bluetooth.device.action.UUID", + "android.bluetooth.devicepicker.action.DEVICE_SELECTED", + "android.bluetooth.devicepicker.action.LAUNCH", + "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT", + "android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED", + "android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.headsetclient.profile.action.AG_CALL_CHANGED", + "android.bluetooth.headsetclient.profile.action.AG_EVENT", + "android.bluetooth.headsetclient.profile.action.AUDIO_STATE_CHANGED", + "android.bluetooth.headsetclient.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.headsetclient.profile.action.LAST_VTAG", + "android.bluetooth.headsetclient.profile.action.RESULT", + "android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.input.profile.action.PROTOCOL_MODE_CHANGED", + "android.bluetooth.input.profile.action.VIRTUAL_UNPLUG_STATUS", + "android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.pbap.intent.action.PBAP_STATE_CHANGED", + "android.btopp.intent.action.CONFIRM", + "android.btopp.intent.action.HIDE", + "android.btopp.intent.action.HIDE_COMPLETE", + "android.btopp.intent.action.INCOMING_FILE_NOTIFICATION", + "android.btopp.intent.action.LIST", + "android.btopp.intent.action.OPEN", + "android.btopp.intent.action.OPEN_INBOUND", + "android.btopp.intent.action.OPEN_OUTBOUND", + "android.btopp.intent.action.RETRY", + "android.btopp.intent.action.USER_CONFIRMATION_TIMEOUT", + "android.hardware.display.action.WIFI_DISPLAY_STATUS_CHANGED", + "android.hardware.usb.action.USB_ACCESSORY_ATTACHED", + "android.hardware.usb.action.USB_ACCESSORY_DETACHED", + "android.hardware.usb.action.USB_DEVICE_ATTACHED", + "android.hardware.usb.action.USB_DEVICE_DETACHED", + "android.hardware.usb.action.USB_PORT_CHANGED", + "android.hardware.usb.action.USB_STATE", + "android.intent.action.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED", + "android.intent.action.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED", + "android.intent.action.ACTION_DEFAULT_SUBSCRIPTION_CHANGED", + "android.intent.action.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED", + "android.intent.action.ACTION_IDLE_MAINTENANCE_END", + "android.intent.action.ACTION_IDLE_MAINTENANCE_START", + "android.intent.action.ACTION_POWER_CONNECTED", + "android.intent.action.ACTION_POWER_DISCONNECTED", + "android.intent.action.ACTION_SET_RADIO_CAPABILITY_DONE", + "android.intent.action.ACTION_SET_RADIO_CAPABILITY_FAILED", + "android.intent.action.ACTION_SHUTDOWN", + "android.intent.action.ACTION_SUBINFO_CONTENT_CHANGE", + "android.intent.action.ACTION_SUBINFO_RECORD_UPDATED", + "android.intent.action.ADVANCED_SETTINGS", + "android.intent.action.AIRPLANE_MODE", + "android.intent.action.ANY_DATA_STATE", + "android.intent.action.APPLICATION_RESTRICTIONS_CHANGED", + "android.intent.action.BATTERY_CHANGED", + "android.intent.action.BATTERY_LOW", + "android.intent.action.BATTERY_OKAY", + "android.intent.action.BOOT_COMPLETED", + "android.intent.action.BUGREPORT_FINISHED", + "android.intent.action.CHARGING", + "android.intent.action.CLEAR_DNS_CACHE", + "android.intent.action.CONFIGURATION_CHANGED", + "android.intent.action.DATE_CHANGED", + "android.intent.action.DEVICE_STORAGE_FULL", + "android.intent.action.DEVICE_STORAGE_LOW", + "android.intent.action.DEVICE_STORAGE_NOT_FULL", + "android.intent.action.DEVICE_STORAGE_OK", + "android.intent.action.DISCHARGING", + "android.intent.action.DOCK_EVENT", + "android.intent.action.DREAMING_STARTED", + "android.intent.action.DREAMING_STOPPED", + "android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE", + "android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE", + "android.intent.action.HDMI_PLUGGED", + "android.intent.action.HEADSET_PLUG", + "android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION", + "android.intent.action.LOCALE_CHANGED", + "android.intent.action.MASTER_CLEAR_NOTIFICATION", + "android.intent.action.MEDIA_BAD_REMOVAL", + "android.intent.action.MEDIA_CHECKING", + "android.intent.action.MEDIA_EJECT", + "android.intent.action.MEDIA_MOUNTED", + "android.intent.action.MEDIA_NOFS", + "android.intent.action.MEDIA_REMOVED", + "android.intent.action.MEDIA_SHARED", + "android.intent.action.MEDIA_UNMOUNTABLE", + "android.intent.action.MEDIA_UNMOUNTED", + "android.intent.action.MEDIA_UNSHARED", + "android.intent.action.MY_PACKAGE_REPLACED", + "android.intent.action.NEW_OUTGOING_CALL", + "android.intent.action.PACKAGE_ADDED", + "android.intent.action.PACKAGE_CHANGED", + "android.intent.action.PACKAGE_DATA_CLEARED", + "android.intent.action.PACKAGE_FIRST_LAUNCH", + "android.intent.action.PACKAGE_FULLY_REMOVED", + "android.intent.action.PACKAGE_INSTALL", + "android.intent.action.PACKAGE_NEEDS_VERIFICATION", + "android.intent.action.PACKAGE_REMOVED", + "android.intent.action.PACKAGE_REPLACED", + "android.intent.action.PACKAGE_RESTARTED", + "android.intent.action.PACKAGE_VERIFIED", + "android.intent.action.PERMISSION_RESPONSE_RECEIVED", + "android.intent.action.PHONE_STATE", + "android.intent.action.PROXY_CHANGE", + "android.intent.action.QUERY_PACKAGE_RESTART", + "android.intent.action.REBOOT", + "android.intent.action.REQUEST_PERMISSION", + "android.intent.action.SCREEN_OFF", + "android.intent.action.SCREEN_ON", + "android.intent.action.SUB_DEFAULT_CHANGED", + "android.intent.action.THERMAL_EVENT", + "android.intent.action.TIMEZONE_CHANGED", + "android.intent.action.TIME_SET", + "android.intent.action.TIME_TICK", + "android.intent.action.UID_REMOVED", + "android.intent.action.USER_ADDED", + "android.intent.action.USER_BACKGROUND", + "android.intent.action.USER_FOREGROUND", + "android.intent.action.USER_PRESENT", + "android.intent.action.USER_REMOVED", + "android.intent.action.USER_STARTED", + "android.intent.action.USER_STARTING", + "android.intent.action.USER_STOPPED", + "android.intent.action.USER_STOPPING", + "android.intent.action.USER_SWITCHED", + "android.internal.policy.action.BURN_IN_PROTECTION", + "android.location.GPS_ENABLED_CHANGE", + "android.location.GPS_FIX_CHANGE", + "android.location.MODE_CHANGED", + "android.location.PROVIDERS_CHANGED", + "android.media.ACTION_SCO_AUDIO_STATE_UPDATED", + "android.media.AUDIO_BECOMING_NOISY", + "android.media.MASTER_MUTE_CHANGED_ACTION", + "android.media.MASTER_VOLUME_CHANGED_ACTION", + "android.media.RINGER_MODE_CHANGED", + "android.media.SCO_AUDIO_STATE_CHANGED", + "android.media.VIBRATE_SETTING_CHANGED", + "android.media.VOLUME_CHANGED_ACTION", + "android.media.action.HDMI_AUDIO_PLUG", + "android.net.ConnectivityService.action.PKT_CNT_SAMPLE_INTERVAL_ELAPSED", + "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED", + "android.net.conn.CAPTIVE_PORTAL", + "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED", + "android.net.conn.CONNECTIVITY_CHANGE", + "android.net.conn.CONNECTIVITY_CHANGE_IMMEDIATE", + "android.net.conn.DATA_ACTIVITY_CHANGE", + "android.net.conn.INET_CONDITION_ACTION", + "android.net.conn.NETWORK_CONDITIONS_MEASURED", + "android.net.conn.TETHER_STATE_CHANGED", + "android.net.nsd.STATE_CHANGED", + "android.net.proxy.PAC_REFRESH", + "android.net.scoring.SCORER_CHANGED", + "android.net.scoring.SCORE_NETWORKS", + "android.net.wifi.CONFIGURED_NETWORKS_CHANGE", + "android.net.wifi.LINK_CONFIGURATION_CHANGED", + "android.net.wifi.RSSI_CHANGED", + "android.net.wifi.SCAN_RESULTS", + "android.net.wifi.STATE_CHANGE", + "android.net.wifi.WIFI_AP_STATE_CHANGED", + "android.net.wifi.WIFI_CREDENTIAL_CHANGED", + "android.net.wifi.WIFI_SCAN_AVAILABLE", + "android.net.wifi.WIFI_STATE_CHANGED", + "android.net.wifi.p2p.CONNECTION_STATE_CHANGE", + "android.net.wifi.p2p.DISCOVERY_STATE_CHANGE", + "android.net.wifi.p2p.PEERS_CHANGED", + "android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED", + "android.net.wifi.p2p.STATE_CHANGED", + "android.net.wifi.p2p.THIS_DEVICE_CHANGED", + "android.net.wifi.supplicant.CONNECTION_CHANGE", + "android.net.wifi.supplicant.STATE_CHANGE", + "android.nfc.action.LLCP_LINK_STATE_CHANGED", + "android.nfc.action.TRANSACTION_DETECTED", + "android.nfc.handover.intent.action.HANDOVER_STARTED", + "android.nfc.handover.intent.action.TRANSFER_DONE", + "android.nfc.handover.intent.action.TRANSFER_PROGRESS", + "android.os.UpdateLock.UPDATE_LOCK_CHANGED", + "android.os.action.DEVICE_IDLE_MODE_CHANGED", + "android.os.action.POWER_SAVE_MODE_CHANGED", + "android.os.action.POWER_SAVE_MODE_CHANGING", + "android.os.action.POWER_SAVE_TEMP_WHITELIST_CHANGED", + "android.os.action.POWER_SAVE_WHITELIST_CHANGED", + "android.os.action.SCREEN_BRIGHTNESS_BOOST_CHANGED", + "android.os.action.SETTING_RESTORED", + "android.telecom.action.DEFAULT_DIALER_CHANGED", + "com.android.bluetooth.pbap.authcancelled", + "com.android.bluetooth.pbap.authchall", + "com.android.bluetooth.pbap.authresponse", + "com.android.bluetooth.pbap.userconfirmtimeout", + "com.android.nfc_extras.action.AID_SELECTED", + "com.android.nfc_extras.action.RF_FIELD_OFF_DETECTED", + "com.android.nfc_extras.action.RF_FIELD_ON_DETECTED", + "com.android.server.WifiManager.action.DELAYED_DRIVER_STOP", + "com.android.server.WifiManager.action.START_PNO", + "com.android.server.WifiManager.action.START_SCAN", + "com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION", + }; + + private static final Set PROTECTED_BROADCAST_SET = + Sets.newHashSet(PROTECTED_BROADCASTS); + + private final Set mReceiversWithProtectedBroadcastIntentFilter = new HashSet(); + + public UnsafeBroadcastReceiverDetector() { + } + + // ---- Implements XmlScanner ---- + + @Override + public Collection getApplicableElements() { + return Collections.singletonList(TAG_RECEIVER); + } + + @Override + public void visitElement(@NonNull XmlContext context, + @NonNull Element element) { + String tag = element.getTagName(); + if (TAG_RECEIVER.equals(tag)) { + String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); + String permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); + // If no permission attribute, then if any exists at the application + // element, it applies + if (permission == null || permission.isEmpty()) { + Element parent = (Element) element.getParentNode(); + permission = parent.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); + } + List children = LintUtils.getChildren(element); + for (Element child : children) { + String tagName = child.getTagName(); + if (TAG_INTENT_FILTER.equals(tagName)) { + if (name.startsWith(".")) { + name = context.getProject().getPackage() + name; + } + name = name.replace('$', '.'); + List children2 = LintUtils.getChildren(child); + for (Element child2 : children2) { + if ("action".equals(child2.getTagName())) { + String actionName = child2.getAttributeNS( + ANDROID_URI, ATTR_NAME); + if (("android.provider.Telephony.SMS_DELIVER".equals(actionName) || + "android.provider.Telephony.SMS_RECEIVED". + equals(actionName)) && + !"android.permission.BROADCAST_SMS".equals(permission)) { + context.report( + BROADCAST_SMS, + element, + context.getLocation(element), + "BroadcastReceivers that declare an intent-filter for " + + "SMS_DELIVER or SMS_RECEIVED must ensure that the " + + "caller has the BROADCAST_SMS permission, otherwise it " + + "is possible for malicious actors to spoof intents."); + } + else if (PROTECTED_BROADCAST_SET.contains(actionName)) { + mReceiversWithProtectedBroadcastIntentFilter.add(name); + } + } + } + break; + } + } + } + } + + // ---- Implements JavaScanner ---- + + @Nullable + @Override + public List applicableSuperClasses() { + return mReceiversWithProtectedBroadcastIntentFilter.isEmpty() + ? null : Collections.singletonList(CLASS_BROADCASTRECEIVER); + } + + @Override + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + String name = declaration.getName(); + if (name == null) { + // anonymous classes can't be the ones referenced in the manifest + return; + } + String qualifiedName = declaration.getQualifiedName(); + if (qualifiedName == null) { + return; + } + if (!mReceiversWithProtectedBroadcastIntentFilter.contains(qualifiedName)) { + return; + } + JavaEvaluator evaluator = context.getEvaluator(); + for (PsiMethod method : declaration.findMethodsByName("onReceive", false)) { + if (evaluator.parametersMatch(method, CLASS_CONTEXT, CLASS_INTENT)) { + checkOnReceive(context, method); + } + } + } + + private static void checkOnReceive(@NonNull JavaContext context, + @NonNull PsiMethod method) { + // Search for call to getAction but also search for references to aload_2, + // which indicates that the method is making use of the received intent in + // some way. + // + // If the onReceive method doesn't call getAction but does make use of + // the received intent, it is possible that it is passing it to another + // method that might be performing the getAction check, so we warn that the + // finding may be a false positive. (An alternative option would be to not + // report a finding at all in this case.) + PsiParameter parameter = method.getParameterList().getParameters()[1]; + OnReceiveVisitor visitor = new OnReceiveVisitor(context.getEvaluator(), parameter); + context.getUastContext().getMethodBody(method).accept(visitor); + if (!visitor.getCallsGetAction()) { + String report; + if (!visitor.getUsesIntent()) { + report = "This broadcast receiver declares an intent-filter for a protected " + + "broadcast action string, which can only be sent by the system, " + + "not third-party applications. However, the receiver's onReceive " + + "method does not appear to call getAction to ensure that the " + + "received Intent's action string matches the expected value, " + + "potentially making it possible for another actor to send a " + + "spoofed intent with no action string or a different action " + + "string and cause undesired behavior."; + } else { + // An alternative implementation option is to not report a finding at all in + // this case, if we are worried about false positives causing confusion or + // resulting in developers ignoring other lint warnings. + report = "This broadcast receiver declares an intent-filter for a protected " + + "broadcast action string, which can only be sent by the system, " + + "not third-party applications. However, the receiver's onReceive " + + "method does not appear to call getAction to ensure that the " + + "received Intent's action string matches the expected value, " + + "potentially making it possible for another actor to send a " + + "spoofed intent with no action string or a different action " + + "string and cause undesired behavior. In this case, it is " + + "possible that the onReceive method passed the received Intent " + + "to another method that checked the action string. If so, this " + + "finding can safely be ignored."; + } + Location location = context.getNameLocation(method); + context.report(ACTION_STRING, method, location, report); + } + } + + private static class OnReceiveVisitor extends AbstractUastVisitor { + @NonNull private final JavaEvaluator mEvaluator; + @Nullable private final PsiParameter mParameter; + private boolean mCallsGetAction; + private boolean mUsesIntent; + + public OnReceiveVisitor(@NonNull JavaEvaluator context, @Nullable PsiParameter parameter) { + mEvaluator = context; + mParameter = parameter; + } + + public boolean getCallsGetAction() { + return mCallsGetAction; + } + + public boolean getUsesIntent() { + return mUsesIntent; + } + + @Override + public boolean visitCallExpression(UCallExpression node) { + if (!mCallsGetAction && UastExpressionUtils.isMethodCall(node)) { + PsiMethod method = node.resolve(); + if (method != null && "getAction".equals(method.getName()) && + mEvaluator.isMemberInSubClassOf(method, CLASS_INTENT, false)) { + mCallsGetAction = true; + } + } + return super.visitCallExpression(node); + } + + @Override + public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { + if (!mUsesIntent && mParameter != null) { + PsiElement resolved = node.resolve(); + if (mParameter.equals(resolved)) { + mUsesIntent = true; + } + } + return super.visitSimpleNameReferenceExpression(node); + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnsafeNativeCodeDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnsafeNativeCodeDetector.java new file mode 100644 index 00000000000..f64cb6f7c66 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnsafeNativeCodeDetector.java @@ -0,0 +1,214 @@ +/* + * 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.checks; + +import static com.android.SdkConstants.DOT_NATIVE_LIBS; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.LintUtils; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Project; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.Speed; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class UnsafeNativeCodeDetector extends Detector implements Detector.UastScanner { + + private static final Implementation IMPLEMENTATION = new Implementation( + UnsafeNativeCodeDetector.class, + Scope.JAVA_FILE_SCOPE); + + public static final Issue LOAD = Issue.create( + "UnsafeDynamicallyLoadedCode", + "`load` used to dynamically load code", + "Dynamically loading code from locations other than the application's library " + + "directory or the Android platform's built-in library directories is dangerous, " + + "as there is an increased risk that the code could have been tampered with. " + + "Applications should use `loadLibrary` when possible, which provides increased " + + "assurance that libraries are loaded from one of these safer locations. " + + "Application developers should use the features of their development " + + "environment to place application native libraries into the lib directory " + + "of their compiled APKs.", + Category.SECURITY, + 4, + Severity.WARNING, + IMPLEMENTATION); + + public static final Issue UNSAFE_NATIVE_CODE_LOCATION = Issue.create( + "UnsafeNativeCodeLocation", //$NON-NLS-1$ + "Native code outside library directory", + "In general, application native code should only be placed in the application's " + + "library directory, not in other locations such as the res or assets directories. " + + "Placing the code in the library directory provides increased assurance that the " + + "code will not be tampered with after application installation. Application " + + "developers should use the features of their development environment to place " + + "application native libraries into the lib directory of their compiled " + + "APKs. Embedding non-shared library native executables into applications should " + + "be avoided when possible.", + Category.SECURITY, + 4, + Severity.WARNING, + IMPLEMENTATION); + + private static final String RUNTIME_CLASS = "java.lang.Runtime"; //$NON-NLS-1$ + private static final String SYSTEM_CLASS = "java.lang.System"; //$NON-NLS-1$ + + private static final byte[] ELF_MAGIC_VALUE = { (byte) 0x7F, (byte) 0x45, (byte) 0x4C, (byte) 0x46 }; + + @NonNull + @Override + public Speed getSpeed() { + return Speed.NORMAL; + } + + // ---- Implements Detector.UastScanner ---- + + @Override + public List getApplicableMethodNames() { + // Identify calls to Runtime.load() and System.load() + return Collections.singletonList("load"); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + // Report calls to Runtime.load() and System.load() + if ("load".equals(method.getName())) { + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.isMemberInSubClassOf(method, RUNTIME_CLASS, false) || + evaluator.isMemberInSubClassOf(method, SYSTEM_CLASS, false)) { + context.report(LOAD, call, context.getUastLocation(call), + "Dynamically loading code using `load` is risky, please use " + + "`loadLibrary` instead when possible"); + } + } + } + + // ---- Look for code in resource and asset directories ---- + + @Override + public void afterCheckLibraryProject(@NonNull Context context) { + if (!context.getProject().getReportIssues()) { + // If this is a library project not being analyzed, ignore it + return; + } + + checkResourceFolders(context, context.getProject()); + } + + @Override + public void afterCheckProject(@NonNull Context context) { + if (!context.getProject().getReportIssues()) { + // If this is a library project not being analyzed, ignore it + return; + } + + checkResourceFolders(context, context.getProject()); + } + + private static boolean isNativeCode(File file) { + if (!file.isFile()) { + return false; + } + + try { + FileInputStream fis = new FileInputStream(file); + try { + byte[] bytes = new byte[4]; + int length = fis.read(bytes); + return (length == 4) && (Arrays.equals(ELF_MAGIC_VALUE, bytes)); + } finally { + fis.close(); + } + } catch (IOException ex) { + return false; + } + } + + private static void checkResourceFolders(Context context, @NonNull Project project) { + if (!context.getScope().contains(Scope.RESOURCE_FOLDER)) { + // Don't do work when doing in-editor analysis of Java files + return; + } + List resourceFolders = project.getResourceFolders(); + for (File res : resourceFolders) { + File[] folders = res.listFiles(); + if (folders != null) { + for (File typeFolder : folders) { + if (typeFolder.getName().startsWith(SdkConstants.FD_RES_RAW)) { + File[] rawFiles = typeFolder.listFiles(); + if (rawFiles != null) { + for (File rawFile : rawFiles) { + checkFile(context, rawFile); + } + } + } + } + } + } + + List assetFolders = project.getAssetFolders(); + for (File assetFolder : assetFolders) { + File[] assets = assetFolder.listFiles(); + if (assets != null) { + for (File asset : assets) { + checkFile(context, asset); + } + } + } + } + + private static void checkFile(@NonNull Context context, @NonNull File file) { + if (isNativeCode(file)) { + if (LintUtils.endsWith(file.getPath(), DOT_NATIVE_LIBS)) { + context.report(UNSAFE_NATIVE_CODE_LOCATION, Location.create(file), + "Shared libraries should not be placed in the res or assets " + + "directories. Please use the features of your development " + + "environment to place shared libraries in the lib directory of " + + "the compiled APK."); + } else { + context.report(UNSAFE_NATIVE_CODE_LOCATION, Location.create(file), + "Embedding non-shared library native executables into applications " + + "should be avoided when possible, as there is an increased risk that " + + "the executables could be tampered with after installation. Instead, " + + "native code should be placed in a shared library, and the features of " + + "the development environment should be used to place the shared library " + + "in the lib directory of the compiled APK."); + } + } + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnusedResourceDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnusedResourceDetector.java deleted file mode 100644 index 7ba0759741c..00000000000 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnusedResourceDetector.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tools.klint.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_REF_PREFIX; -import static com.android.SdkConstants.ATTR_TYPE; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.RESOURCE_CLR_STYLEABLE; -import static com.android.SdkConstants.RESOURCE_CLZ_ARRAY; -import static com.android.SdkConstants.RESOURCE_CLZ_ID; -import static com.android.SdkConstants.R_ATTR_PREFIX; -import static com.android.SdkConstants.R_CLASS; -import static com.android.SdkConstants.R_ID_PREFIX; -import static com.android.SdkConstants.R_PREFIX; -import static com.android.SdkConstants.TAG_ARRAY; -import static com.android.SdkConstants.TAG_INTEGER_ARRAY; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_PLURALS; -import static com.android.SdkConstants.TAG_RESOURCES; -import static com.android.SdkConstants.TAG_STRING_ARRAY; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.utils.SdkUtils.getResourceFieldName; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceType; -import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Context; -import com.android.tools.klint.detector.api.Implementation; -import com.android.tools.klint.detector.api.Issue; -import com.android.tools.klint.detector.api.LintUtils; -import com.android.tools.klint.detector.api.Location; -import com.android.tools.klint.detector.api.Project; -import com.android.tools.klint.detector.api.ResourceXmlDetector; -import com.android.tools.klint.detector.api.Scope; -import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; -import com.android.tools.klint.detector.api.XmlContext; -import com.google.common.collect.Lists; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UDeclaration; -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UVariable; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; -import org.jetbrains.uast.visitor.AbstractUastVisitor; -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Finds unused resources. - *

- * Note: This detector currently performs *string* analysis to check Java files. - * The Lint API needs an official Java AST API (or map to an existing one like - * BCEL for bytecode analysis etc) and once it does this should be updated to - * use it. - */ -public class UnusedResourceDetector extends ResourceXmlDetector implements UastScanner { - - private static final Implementation IMPLEMENTATION = new Implementation( - UnusedResourceDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.ALL_RESOURCE_FILES, Scope.ALL_SOURCE_FILES, - Scope.TEST_SOURCES)); - - /** Unused resources (other than ids). */ - public static final Issue ISSUE = Issue.create( - "UnusedResources", //$NON-NLS-1$ - "Unused resources", - "Unused resources make applications larger and slow down builds.", - Category.PERFORMANCE, - 3, - Severity.WARNING, - IMPLEMENTATION); - - /** Unused id's */ - public static final Issue ISSUE_IDS = Issue.create( - "UnusedIds", //$NON-NLS-1$ - "Unused id", - "This resource id definition appears not to be needed since it is not referenced " + - "from anywhere. Having id definitions, even if unused, is not necessarily a bad " + - "idea since they make working on layouts and menus easier, so there is not a " + - "strong reason to delete these.", - Category.PERFORMANCE, - 1, - Severity.WARNING, - IMPLEMENTATION) - .setEnabledByDefault(false); - - private Set mDeclarations; - private Set mReferences; - private Map mUnused; - - /** - * Constructs a new {@link UnusedResourceDetector} - */ - public UnusedResourceDetector() { - } - - @Override - public void run(@NonNull Context context) { - assert false; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - mDeclarations = new HashSet(300); - mReferences = new HashSet(300); - } - } - - // ---- Implements UastScanner ---- - - @Override - public void beforeCheckFile(@NonNull Context context) { - File file = context.file; - - boolean isXmlFile = LintUtils.isXmlFile(file); - if (isXmlFile || LintUtils.isBitmapFile(file)) { - String fileName = file.getName(); - String parentName = file.getParentFile().getName(); - int dash = parentName.indexOf('-'); - String typeName = parentName.substring(0, dash == -1 ? parentName.length() : dash); - ResourceType type = ResourceType.getEnum(typeName); - if (type != null && LintUtils.isFileBasedResourceType(type)) { - String baseName = fileName.substring(0, fileName.length() - DOT_XML.length()); - String resource = R_PREFIX + typeName + '.' + baseName; - if (context.getPhase() == 1) { - mDeclarations.add(resource); - } else { - assert context.getPhase() == 2; - if (mUnused.containsKey(resource)) { - // Check whether this is an XML document that has a tools:ignore attribute - // on the document element: if so don't record it as a declaration. - if (isXmlFile && context instanceof XmlContext) { - XmlContext xmlContext = (XmlContext) context; - if (xmlContext.document != null - && xmlContext.document.getDocumentElement() != null) { - Element root = xmlContext.document.getDocumentElement(); - if (xmlContext.getDriver().isSuppressed(xmlContext, ISSUE, root)) { - // Also remove it from consideration such that even the - // presence of this field in the R file is ignored. - mUnused.remove(resource); - return; - } - } - } - - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - mUnused.remove(resource); - return; - } - - recordLocation(resource, Location.create(file)); - } - } - } - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - mDeclarations.removeAll(mReferences); - Set unused = mDeclarations; - mReferences = null; - mDeclarations = null; - - // Remove styles and attributes: they may be used, analysis isn't complete for these - List styles = new ArrayList(); - for (String resource : unused) { - // R.style.x, R.styleable.x, R.attr - if (resource.startsWith("R.style") //$NON-NLS-1$ - || resource.startsWith("R.attr")) { //$NON-NLS-1$ - styles.add(resource); - } - } - unused.removeAll(styles); - - // Remove id's if the user has disabled reporting issue ids - if (!unused.isEmpty() && !context.isEnabled(ISSUE_IDS)) { - // Remove all R.id references - List ids = new ArrayList(); - for (String resource : unused) { - if (resource.startsWith(R_ID_PREFIX)) { - ids.add(resource); - } - } - unused.removeAll(ids); - } - - if (!unused.isEmpty() && !context.getDriver().hasParserErrors()) { - mUnused = new HashMap(unused.size()); - for (String resource : unused) { - mUnused.put(resource, null); - } - - // Request another pass, and in the second pass we'll gather location - // information for all declaration locations we've found - context.requestRepeat(this, Scope.ALL_RESOURCES_SCOPE); - } - } else { - assert context.getPhase() == 2; - - // Report any resources that we (for some reason) could not find a declaration - // location for - if (!mUnused.isEmpty()) { - // Fill in locations for files that we didn't encounter in other ways - for (Map.Entry entry : mUnused.entrySet()) { - String resource = entry.getKey(); - Location location = entry.getValue(); - //noinspection VariableNotUsedInsideIf - if (location != null) { - continue; - } - - // Try to figure out the file if it's a file based resource (such as R.layout) -- - // in that case we can figure out the filename since it has a simple mapping - // from the resource name (though the presence of qualifiers like -land etc - // makes it a little tricky if there's no base file provided) - int secondDot = resource.indexOf('.', 2); - String typeName = resource.substring(2, secondDot); // 2: Skip R. - ResourceType type = ResourceType.getEnum(typeName); - if (type != null && LintUtils.isFileBasedResourceType(type)) { - String name = resource.substring(secondDot + 1); - - List folders = Lists.newArrayList(); - List resourceFolders = context.getProject().getResourceFolders(); - for (File res : resourceFolders) { - File[] f = res.listFiles(); - if (f != null) { - folders.addAll(Arrays.asList(f)); - } - } - if (folders != null) { - // Process folders in alphabetical order such that we process - // based folders first: we want the locations in base folder - // order - Collections.sort(folders, new Comparator() { - @Override - public int compare(File file1, File file2) { - return file1.getName().compareTo(file2.getName()); - } - }); - for (File folder : folders) { - if (folder.getName().startsWith(typeName)) { - File[] files = folder.listFiles(); - if (files != null) { - Arrays.sort(files); - for (File file : files) { - String fileName = file.getName(); - if (fileName.startsWith(name) - && fileName.startsWith(".", //$NON-NLS-1$ - name.length())) { - recordLocation(resource, Location.create(file)); - } - } - } - } - } - } - } - } - - List sorted = new ArrayList(mUnused.keySet()); - Collections.sort(sorted); - - Boolean skippedLibraries = null; - - for (String resource : sorted) { - Location location = mUnused.get(resource); - if (location != null) { - // We were prepending locations, but we want to prefer the base folders - location = Location.reverse(location); - } - - if (location == null) { - if (skippedLibraries == null) { - skippedLibraries = false; - for (Project project : context.getDriver().getProjects()) { - if (!project.getReportIssues()) { - skippedLibraries = true; - break; - } - } - } - if (skippedLibraries) { - // Skip this resource if we don't have a location, and one or - // more library projects were skipped; the resource was very - // probably defined in that library project and only encountered - // in the main project's java R file - continue; - } - } - - String message = String.format("The resource `%1$s` appears to be unused", - resource); - Issue issue = getIssue(resource); - // TODO: Compute applicable node scope - context.report(issue, location, message); - } - } - } - } - - private static Issue getIssue(String resource) { - return resource.startsWith(R_ID_PREFIX) ? ISSUE_IDS : ISSUE; - } - - private void recordLocation(String resource, Location location) { - Location oldLocation = mUnused.get(resource); - if (oldLocation != null) { - location.setSecondary(oldLocation); - } - mUnused.put(resource, location); - } - - @Override - public Collection getApplicableAttributes() { - return ALL; - } - - @Override - public Collection getApplicableElements() { - return Arrays.asList( - TAG_STYLE, - TAG_RESOURCES, - TAG_ARRAY, - TAG_STRING_ARRAY, - TAG_INTEGER_ARRAY, - TAG_PLURALS - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (TAG_RESOURCES.equals(element.getTagName())) { - for (Element item : LintUtils.getChildren(element)) { - Attr nameAttribute = item.getAttributeNode(ATTR_NAME); - if (nameAttribute != null) { - String name = getResourceFieldName(nameAttribute.getValue()); - String type = item.getTagName(); - if (type.equals(TAG_ITEM)) { - type = item.getAttribute(ATTR_TYPE); - if (type == null || type.isEmpty()) { - type = RESOURCE_CLZ_ID; - } - } else if (type.equals("declare-styleable")) { //$NON-NLS-1$ - type = RESOURCE_CLR_STYLEABLE; - } else if (type.contains("array")) { //$NON-NLS-1$ - // etc - type = RESOURCE_CLZ_ARRAY; - } - String resource = R_PREFIX + type + '.' + name; - - if (context.getPhase() == 1) { - mDeclarations.add(resource); - checkChildRefs(item); - } else { - assert context.getPhase() == 2; - if (mUnused.containsKey(resource)) { - if (context.getDriver().isSuppressed(context, getIssue(resource), - item)) { - mUnused.remove(resource); - continue; - } - if (!context.getProject().getReportIssues()) { - mUnused.remove(resource); - continue; - } - if (isAnalyticsFile(context)) { - mUnused.remove(resource); - continue; - } - - recordLocation(resource, context.getLocation(nameAttribute)); - } - } - } - } - } else //noinspection VariableNotUsedInsideIf - if (mReferences != null) { - assert TAG_STYLE.equals(element.getTagName()) - || TAG_ARRAY.equals(element.getTagName()) - || TAG_PLURALS.equals(element.getTagName()) - || TAG_INTEGER_ARRAY.equals(element.getTagName()) - || TAG_STRING_ARRAY.equals(element.getTagName()); - for (Element item : LintUtils.getChildren(element)) { - checkChildRefs(item); - } - } - } - - private static final String ANALYTICS_FILE = "analytics.xml"; //$NON-NLS-1$ - - /** - * Returns true if this XML file corresponds to an Analytics configuration file; - * these contain some attributes read by the library which won't be flagged as - * used by the application - * - * @param context the context used for scanning - * @return true if the file represents an analytics file - */ - public static boolean isAnalyticsFile(Context context) { - File file = context.file; - return file.getPath().endsWith(ANALYTICS_FILE) && file.getName().equals(ANALYTICS_FILE); - } - - private void checkChildRefs(Element item) { - // Look for ?attr/ and @dimen/foo etc references in the item children - NodeList childNodes = item.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.TEXT_NODE) { - String text = child.getNodeValue(); - - int index = text.indexOf(ATTR_REF_PREFIX); - if (index != -1) { - String name = text.substring(index + ATTR_REF_PREFIX.length()).trim(); - mReferences.add(R_ATTR_PREFIX + name); - } else { - index = text.indexOf('@'); - if (index != -1 && text.indexOf('/', index) != -1 - && !text.startsWith("@android:", index)) { //$NON-NLS-1$ - // Compute R-string, e.g. @string/foo => R.string.foo - String token = text.substring(index + 1).trim().replace('/', '.'); - String r = R_PREFIX + token; - mReferences.add(r); - } - } - } - } - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String value = attribute.getValue(); - - if (value.startsWith("@+") && !value.startsWith("@+android")) { //$NON-NLS-1$ //$NON-NLS-2$ - String resource = R_PREFIX + value.substring(2).replace('/', '.'); - // We already have the declarations when we scan the R file, but we're tracking - // these here to get attributes for position info - - if (context.getPhase() == 1) { - mDeclarations.add(resource); - } else if (mUnused.containsKey(resource)) { - if (context.getDriver().isSuppressed(context, getIssue(resource), attribute)) { - mUnused.remove(resource); - return; - } - if (!context.getProject().getReportIssues()) { - mUnused.remove(resource); - return; - } - recordLocation(resource, context.getLocation(attribute)); - return; - } - } else if (mReferences != null) { - if (value.startsWith("@") //$NON-NLS-1$ - && !value.startsWith("@android:")) { //$NON-NLS-1$ - // Compute R-string, e.g. @string/foo => R.string.foo - String r = R_PREFIX + value.substring(1).replace('/', '.'); - mReferences.add(r); - } else if (value.startsWith(ATTR_REF_PREFIX)) { - mReferences.add(R_ATTR_PREFIX + value.substring(ATTR_REF_PREFIX.length())); - } - } - - if (attribute.getNamespaceURI() != null - && !ANDROID_URI.equals(attribute.getNamespaceURI()) && mReferences != null) { - mReferences.add(R_ATTR_PREFIX + attribute.getLocalName()); - } - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; - } - - @Override - public boolean appliesToResourceRefs() { - return true; - } - - @Override - public void visitResourceReference(UastAndroidContext context, UElement element, String type, String name, boolean isFramework) { - if (mReferences != null && !isFramework) { - String reference = R_PREFIX + type + '.' + name; - mReferences.add(reference); - } - } - - @Override - public AbstractUastVisitor createUastVisitor(@NonNull UastAndroidContext context) { - if (mReferences != null) { - return new UnusedResourceVisitor(); - } else { - // Second pass, computing resource declaration locations: No need to look at Java - return null; - } - } - - // Look for references and declarations - private class UnusedResourceVisitor extends AbstractUastVisitor { - @Override - public boolean visitClass(@NotNull UClass node) { - // Look for declarations of R class fields and store them in - // mDeclarations - String description = node.getName(); - if (description.equals(R_CLASS)) { - for (UDeclaration subclass : node.getNestedClasses()) { - for (UVariable variable : node.getProperties()) { - String resource = R_PREFIX + subclass.getName() + '.' + variable.getName(); - mDeclarations.add(resource); - } - } - - return true; - } - - return false; - } - } -} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewConstructorDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewConstructorDetector.java index 74ea0e86961..05e60495a71 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewConstructorDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewConstructorDetector.java @@ -22,32 +22,40 @@ import static com.android.SdkConstants.CLASS_CONTEXT; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.ClassContext; import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiAnonymousClass; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiParameterList; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UClass; +import org.objectweb.asm.tree.ClassNode; import java.util.Collections; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Looks for custom views that do not define the view constructors needed by UI builders */ -public class ViewConstructorDetector extends Detector implements UastScanner { +public class ViewConstructorDetector extends Detector implements Detector.UastScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "ViewConstructor", //$NON-NLS-1$ "Missing View constructors for XML inflation", - "Some layout tools (such as the Android layout editor for Studio & Eclipse) needs to " + + "Some layout tools (such as the Android layout editor) need to " + "find a constructor with one of the following signatures:\n" + "* `View(Context context)`\n" + "* `View(Context context, AttributeSet attrs)`\n" + @@ -63,70 +71,68 @@ public class ViewConstructorDetector extends Detector implements UastScanner { Severity.WARNING, new Implementation( ViewConstructorDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); /** Constructs a new {@link ViewConstructorDetector} check */ public ViewConstructorDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } + // ---- Implements JavaScanner ---- - // ---- Implements UastScanner ---- - - private static boolean isXmlConstructor(UFunction method) { + private static boolean isXmlConstructor( + @NonNull JavaEvaluator evaluator, + @NonNull PsiMethod method) { // Accept // android.content.Context // android.content.Context,android.util.AttributeSet // android.content.Context,android.util.AttributeSet,int - List valueParameters = method.getValueParameters(); - int valueParameterCount = valueParameters.size(); - if (valueParameterCount == 0 || valueParameterCount > 3) { + PsiParameterList parameterList = method.getParameterList(); + int argumentCount = parameterList.getParametersCount(); + if (argumentCount == 0 || argumentCount > 3) { return false; } - - if (!valueParameters.get(0).getType().matchesFqName(CLASS_CONTEXT)) { + PsiParameter[] parameters = parameterList.getParameters(); + if (!evaluator.typeMatches(parameters[0].getType(), CLASS_CONTEXT)) { return false; } - if (valueParameterCount == 1) { + if (argumentCount == 1) { return true; } - if (!valueParameters.get(1).getType().matchesFqName(CLASS_ATTRIBUTE_SET)) { + if (!evaluator.typeMatches(parameters[1].getType(), CLASS_ATTRIBUTE_SET)) { return false; } //noinspection SimplifiableIfStatement - if (valueParameterCount == 2) { + if (argumentCount == 2) { return true; } - return valueParameters.get(2).getType().isInt(); + return PsiType.INT.equals(parameters[2].getType()); } @Nullable @Override - public List getApplicableSuperClasses() { + public List applicableSuperClasses() { return Collections.singletonList(SdkConstants.CLASS_VIEW); } @Override - public void visitClass(UastAndroidContext context, UClass node) { - // Only applies to concrete and not abstract classes - UastClassKind kind = node.getKind(); - if (kind != UastClassKind.CLASS || node.hasModifier(UastModifier.ABSTRACT)) { + public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { + // Only applies to concrete classes + JavaEvaluator evaluator = context.getEvaluator(); + if (evaluator.isAbstract(declaration) || declaration instanceof PsiAnonymousClass) { + // Ignore abstract classes return; } - if (UastUtils.getContainingClass(node) != null && !node.hasModifier(UastModifier.STATIC)) { + if (declaration.getContainingClass() != null && + !evaluator.isStatic(declaration)) { // Ignore inner classes that aren't static: we can't create these // anyway since we'd need the outer instance return; } boolean found = false; - for (UFunction constructor : node.getConstructors()) { - if (isXmlConstructor(constructor)) { + for (PsiMethod constructor : declaration.getConstructors()) { + if (isXmlConstructor(evaluator, constructor)) { found = true; break; } @@ -134,12 +140,12 @@ public class ViewConstructorDetector extends Detector implements UastScanner { if (!found) { String message = String.format( - "Custom view `%1$s` is missing constructor used by tools: " - + "`(Context)` or `(Context,AttributeSet)` " - + "or `(Context,AttributeSet,int)`", - node.getFqName()); - Location location = context.getLocation(node.getNameElement()); - context.report(ISSUE, node, location, message /*data*/); + "Custom view `%1$s` is missing constructor used by tools: " + + "`(Context)` or `(Context,AttributeSet)` " + + "or `(Context,AttributeSet,int)`", + declaration.getName()); + Location location = context.getUastNameLocation(declaration); + context.reportUast(ISSUE, declaration, location, message /*data*/); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewHolderDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewHolderDetector.java index c31da96815e..94fc4986f69 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewHolderDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewHolderDetector.java @@ -16,35 +16,45 @@ package com.android.tools.klint.checks; -import static com.android.SdkConstants.*; +import static com.android.SdkConstants.CLASS_VIEW; +import static com.android.SdkConstants.CLASS_VIEWGROUP; +import static com.android.tools.klint.client.api.JavaParser.TYPE_INT; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.google.common.collect.Lists; +import com.intellij.psi.PsiMethod; -import java.util.List; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UIfExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.USwitchExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.util.UastExpressionUtils; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; +import java.util.Collections; +import java.util.List; + /** * Looks for ListView scrolling performance: should use view holder pattern */ -public class ViewHolderDetector extends Detector implements UastScanner { +public class ViewHolderDetector extends Detector implements Detector.UastScanner { private static final Implementation IMPLEMENTATION = new Implementation( ViewHolderDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); /** Using a view inflater unconditionally in an AdapterView */ public static final Issue ISSUE = Issue.create( @@ -70,84 +80,78 @@ public class ViewHolderDetector extends Detector implements UastScanner { public ViewHolderDetector() { } - @NonNull + // ---- Implements JavaScanner ---- + + @Nullable @Override - public Speed getSpeed() { - return Speed.NORMAL; + public List> getApplicableUastTypes() { + return Collections.>singletonList(UMethod.class); } - // ---- Implements UastScanner ---- - + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new ViewAdapterVisitor(context); } private static class ViewAdapterVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; - public ViewAdapterVisitor(UastAndroidContext context) { + public ViewAdapterVisitor(JavaContext context) { mContext = context; } @Override - public boolean visitFunction(@NotNull UFunction node) { - if (isViewAdapterMethod(node)) { + public boolean visitMethod(UMethod method) { + if (isViewAdapterMethod(mContext, method)) { InflationVisitor visitor = new InflationVisitor(mContext); - node.accept(visitor); + method.accept(visitor); visitor.finish(); } - return false; + return super.visitMethod(method); } /** * Returns true if this method looks like it's overriding android.widget.Adapter's getView * method: getView(int position, View convertView, ViewGroup parent) */ - private static boolean isViewAdapterMethod(UFunction node) { - if (GET_VIEW.equals(node.getName())) { - List parameters = node.getValueParameters(); - if (parameters.size() == 3) { - //noinspection RedundantIfStatement - if (!parameters.get(2).getType().matchesFqName(CLASS_VIEWGROUP)) { - return false; - } - - return true; - } - } - - return false; + private static boolean isViewAdapterMethod(JavaContext context, PsiMethod node) { + JavaEvaluator evaluator = context.getEvaluator(); + return GET_VIEW.equals(node.getName()) && evaluator.parametersMatch(node, + TYPE_INT, CLASS_VIEW, CLASS_VIEWGROUP); } } private static class InflationVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; private List mNodes; private boolean mHaveConditional; - public InflationVisitor(UastAndroidContext context) { + public InflationVisitor(JavaContext context) { mContext = context; } @Override - public boolean visitCallExpression(@NotNull UCallExpression node) { - UElement parent = node.getParent(); - if (parent instanceof UQualifiedExpression) { - String methodName = node.getFunctionName(); - if (INFLATE.equals(methodName) && node.getValueArgumentCount() >= 1) { + public boolean visitCallExpression(UCallExpression node) { + if (UastExpressionUtils.isMethodCall(node)) { + checkMethodCall(node); + } + return super.visitCallExpression(node); + } + + private void checkMethodCall(UCallExpression node) { + UExpression receiver = node.getReceiver(); + if (receiver != null) { + String methodName = node.getMethodName(); + if (INFLATE.equals(methodName) + && node.getValueArgumentCount() >= 1) { // See if we're inside a conditional boolean insideIf = false; - UElement p = parent.getParent(); - while (p != null) { - if (p instanceof UIfExpression || p instanceof USwitchExpression) { - insideIf = true; - mHaveConditional = true; - break; - } else if (p == node) { - break; - } - p = p.getParent(); + //noinspection unchecked + if (UastUtils.getParentOfType(node, true, UIfExpression.class, + USwitchExpression.class) != null) { + insideIf = true; + mHaveConditional = true; } if (!insideIf) { // Rather than reporting immediately, we only report if we didn't @@ -166,8 +170,6 @@ public class ViewHolderDetector extends Detector implements UastScanner { } } } - - return false; } public void finish() { @@ -177,7 +179,7 @@ public class ViewHolderDetector extends Detector implements UastScanner { + "Should use View Holder pattern (use recycled view passed " + "into this method as the second parameter) for smoother " + "scrolling"; - mContext.report(ISSUE, node, mContext.getLocation(node), message); + mContext.report(ISSUE, node, mContext.getUastLocation(node), message); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTagDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTagDetector.java new file mode 100644 index 00000000000..b9e7abf2ec2 --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTagDetector.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.klint.checks; + +import static com.android.SdkConstants.CLASS_VIEW; +import static com.android.tools.klint.checks.CleanupDetector.CURSOR_CLS; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.klint.client.api.JavaEvaluator; +import com.android.tools.klint.detector.api.Category; +import com.android.tools.klint.detector.api.Detector; +import com.android.tools.klint.detector.api.Implementation; +import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.Severity; +import com.android.tools.klint.detector.api.TypeEvaluator; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiType; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.visitor.UastVisitor; + +import java.util.Collections; +import java.util.List; + +/** + * Checks for missing view tag detectors + */ +public class ViewTagDetector extends Detector implements Detector.UastScanner { + /** Using setTag and leaking memory */ + public static final Issue ISSUE = Issue.create( + "ViewTag", //$NON-NLS-1$ + "Tagged object leaks", + + "Prior to Android 4.0, the implementation of `View.setTag(int, Object)` would " + + "store the objects in a static map, where the values were strongly referenced. " + + "This means that if the object contains any references pointing back to the " + + "context, the context (which points to pretty much everything else) will leak. " + + "If you pass a view, the view provides a reference to the context " + + "that created it. Similarly, view holders typically contain a view, and cursors " + + "are sometimes also associated with views.", + + Category.PERFORMANCE, + 6, + Severity.WARNING, + new Implementation( + ViewTagDetector.class, + Scope.JAVA_FILE_SCOPE)); + + /** Constructs a new {@link ViewTagDetector} */ + public ViewTagDetector() { + } + + // ---- Implements JavaScanner ---- + + @Nullable + @Override + public List getApplicableMethodNames() { + return Collections.singletonList("setTag"); + } + + @Override + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + // The leak behavior is fixed in ICS: + // http://code.google.com/p/android/issues/detail?id=18273 + if (context.getMainProject().getMinSdk() >= 14) { + return; + } + + JavaEvaluator evaluator = context.getEvaluator(); + if (!evaluator.isMemberInSubClassOf(method, CLASS_VIEW, false)) { + return; + } + + List arguments = call.getValueArguments(); + if (arguments.size() != 2) { + return; + } + UExpression tagArgument = arguments.get(1); + if (tagArgument == null) { + return; + } + + PsiType type = TypeEvaluator.evaluate(context, tagArgument); + if ((!(type instanceof PsiClassType))) { + return; + } + PsiClass typeClass = ((PsiClassType) type).resolve(); + if (typeClass == null) { + return; + } + + String objectType; + if (evaluator.extendsClass(typeClass, CLASS_VIEW, false)) { + objectType = "views"; + } else if (evaluator.implementsInterface(typeClass, CURSOR_CLS, false)) { + objectType = "cursors"; + } else if (typeClass.getName() != null && typeClass.getName().endsWith("ViewHolder")) { + objectType = "view holders"; + } else { + return; + } + String message = String.format("Avoid setting %1$s as values for `setTag`: " + + "Can lead to memory leaks in versions older than Android 4.0", + objectType); + + context.report(ISSUE, call, context.getUastLocation(tagArgument), message); + } +} diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTypeDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTypeDetector.java index e9cdca6e703..40f5a524bbe 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTypeDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTypeDetector.java @@ -23,22 +23,24 @@ import static com.android.SdkConstants.DOT_XML; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.VIEW_TAG; -import static org.jetbrains.uast.UastBinaryExpressionWithTypeUtils.*; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.res2.ResourceFile; import com.android.ide.common.res2.ResourceItem; +import com.android.ide.common.resources.ResourceUrl; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.tools.klint.client.api.LintClient; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; +import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LintUtils; +import com.android.tools.klint.detector.api.ResourceEvaluator; import com.android.tools.klint.detector.api.ResourceXmlDetector; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; @@ -51,10 +53,16 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiType; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; +import org.jetbrains.uast.UBinaryExpressionWithType; +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.UParenthesizedExpression; +import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -83,7 +91,7 @@ import java.util.Set; * check its name or class attributes to make sure the cast is compatible with * the named fragment class! */ -public class ViewTypeDetector extends ResourceXmlDetector implements UastScanner { +public class ViewTypeDetector extends ResourceXmlDetector implements Detector.UastScanner { /** Mismatched view types */ @SuppressWarnings("unchecked") public static final Issue ISSUE = Issue.create( @@ -96,8 +104,8 @@ public class ViewTypeDetector extends ResourceXmlDetector implements UastScanner Severity.FATAL, new Implementation( ViewTypeDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.ALL_SOURCE_FILES), - Scope.SOURCE_FILE_SCOPE)); + EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES), + Scope.JAVA_FILE_SCOPE)); /** Flag used to do no work if we're running in incremental mode in a .java file without * a client supporting project resources */ @@ -159,101 +167,88 @@ public class ViewTypeDetector extends ResourceXmlDetector implements UastScanner } } - // ---- Implements Detector.UastScanner ---- + // ---- Implements Detector.JavaScanner ---- @Override - public List getApplicableFunctionNames() { + public List getApplicableMethodNames() { return Collections.singletonList("findViewById"); //$NON-NLS-1$ } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { - JavaContext lintContext = context.getLintContext(); - LintClient client = lintContext.getClient(); + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression call, @NonNull UMethod method) { + LintClient client = context.getClient(); if (mIgnore == Boolean.TRUE) { return; } else if (mIgnore == null) { - mIgnore = !lintContext.getScope().contains(Scope.ALL_RESOURCE_FILES) && - !client.supportsProjectResources(); + mIgnore = !context.getScope().contains(Scope.ALL_RESOURCE_FILES) && + !client.supportsProjectResources(); if (mIgnore) { return; } } - assert "findViewById".equals(node.getFunctionName()); - UBinaryExpressionWithType cast = findContainingTypeCast(node.getParent()); - if (cast == null) { - return; + assert method.getName().equals("findViewById"); + UElement node = LintUtils.skipParentheses(call); + while (node != null && node.getContainingElement() instanceof UParenthesizedExpression) { + node = node.getContainingElement(); } + if (node.getContainingElement() instanceof UBinaryExpressionWithType) { + UBinaryExpressionWithType cast = (UBinaryExpressionWithType) node.getContainingElement(); + PsiType type = cast.getType(); + String castType = null; + if (type instanceof PsiClassType) { + castType = type.getCanonicalText(); + } + if (castType == null) { + return; + } - String castType = cast.getType().getFqName(); - List args = node.getValueArguments(); - if (args.size() == 1) { - UExpression first = args.get(0); - // TODO: Do flow analysis as in the StringFormatDetector in order - // to handle variable references too - if (first instanceof UQualifiedExpression) { - if (UastUtils.startsWithQualified(first, "R.id")) { //$NON-NLS-1$ - String id = ((UQualifiedExpression) first).getSelectorAsIdentifier(); + List args = call.getValueArguments(); + if (args.size() == 1) { + UExpression first = args.get(0); + ResourceUrl resourceUrl = ResourceEvaluator.getResource(context, first); + if (resourceUrl != null && resourceUrl.type == ResourceType.ID && + !resourceUrl.framework) { + String id = resourceUrl.name; - if (id != null) { - if (client.supportsProjectResources()) { - AbstractResourceRepository resources = client - .getProjectResources(lintContext.getMainProject(), true); - if (resources == null) { - return; - } + if (client.supportsProjectResources()) { + AbstractResourceRepository resources = client + .getProjectResources(context.getMainProject(), true); + if (resources == null) { + return; + } - List items = resources.getResourceItem(ResourceType.ID, id); - if (items != null && !items.isEmpty()) { - Set compatible = Sets.newHashSet(); - for (ResourceItem item : items) { - Collection tags = getViewTags(lintContext, item); - if (tags != null) { - compatible.addAll(tags); - } - } - if (!compatible.isEmpty()) { - ArrayList layoutTypes = Lists.newArrayList(compatible); - checkCompatible(lintContext, castType, null, layoutTypes, cast); + List items = resources.getResourceItem(ResourceType.ID, + id); + if (items != null && !items.isEmpty()) { + Set compatible = Sets.newHashSet(); + for (ResourceItem item : items) { + Collection tags = getViewTags(context, item); + if (tags != null) { + compatible.addAll(tags); } } - } else { - Object types = mIdToViewTag.get(id); - if (types instanceof String) { - String layoutType = (String) types; - checkCompatible(lintContext, castType, layoutType, null, cast); - } else if (types instanceof List) { - @SuppressWarnings("unchecked") - List layoutTypes = (List) types; - checkCompatible(lintContext, castType, null, layoutTypes, cast); + if (!compatible.isEmpty()) { + ArrayList layoutTypes = Lists.newArrayList(compatible); + checkCompatible(context, castType, null, layoutTypes, cast); } } + } else { + Object types = mIdToViewTag.get(id); + if (types instanceof String) { + String layoutType = (String) types; + checkCompatible(context, castType, layoutType, null, cast); + } else if (types instanceof List) { + @SuppressWarnings("unchecked") + List layoutTypes = (List) types; + checkCompatible(context, castType, null, layoutTypes, cast); + } } } } } } - private UBinaryExpressionWithType findContainingTypeCast(UElement expression) { - if (expression == null) { - return null; - } - - if (isTypeCast(expression)) { - return (UBinaryExpressionWithType) expression; - } else if (expression instanceof UQualifiedExpression) { - UElement parent = expression.getParent(); - if (expression.equals(expression.getParent())) { - return null; - } - return findContainingTypeCast(parent); - } else if (expression instanceof UParenthesizedExpression) { - return findContainingTypeCast(expression.getParent()); - } else { - return null; - } - } - @Nullable protected Collection getViewTags( @NonNull Context context, @@ -345,8 +340,8 @@ public class ViewTypeDetector extends ResourceXmlDetector implements UastScanner } String message = String.format( "Unexpected cast to `%1$s`: layout tag was `%2$s`", - castType, layoutType); - context.report(ISSUE, node, context.getLocation(node), message); + castType.substring(castType.lastIndexOf('.') + 1), layoutType); + context.report(ISSUE, node, context.getUastLocation(node), message); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongCallDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongCallDetector.java index 09675c315f8..65a2209973e 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongCallDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongCallDetector.java @@ -27,23 +27,28 @@ import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; import com.android.tools.klint.detector.api.TextFormat; +import com.intellij.psi.PsiMethod; + +import org.jetbrains.uast.UCallExpression; +import org.jetbrains.uast.UElement; +import org.jetbrains.uast.UExpression; +import org.jetbrains.uast.UMethod; +import org.jetbrains.uast.USuperExpression; +import org.jetbrains.uast.UastUtils; +import org.jetbrains.uast.visitor.UastVisitor; import java.util.Arrays; import java.util.List; -import org.jetbrains.uast.*; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; - /** * Checks for cases where the wrong call is being made */ -public class WrongCallDetector extends Detector implements UastScanner { +public class WrongCallDetector extends Detector implements Detector.UastScanner { /** Calling the wrong method */ public static final Issue ISSUE = Issue.create( "WrongCall", //$NON-NLS-1$ @@ -57,79 +62,61 @@ public class WrongCallDetector extends Detector implements UastScanner { Severity.FATAL, new Implementation( WrongCallDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); /** Constructs a new {@link WrongCallDetector} */ public WrongCallDetector() { } - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - // ---- Implements UastScanner ---- - @Override - public List getApplicableFunctionNames() { + @Nullable + public List getApplicableMethodNames() { return Arrays.asList( - ON_DRAW, - ON_MEASURE, - ON_LAYOUT + ON_DRAW, + ON_MEASURE, + ON_LAYOUT ); } @Override - public void visitCall(UastAndroidContext context, UCallExpression node) { + public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor, + @NonNull UCallExpression node, @NonNull UMethod calledMethod) { // Call is only allowed if it is both only called on the super class (invoke special) // as well as within the same overriding method (e.g. you can't call super.onLayout // from the onMeasure method) - if (node.getKind() != UastCallKind.FUNCTION_CALL) { + UExpression operand = node.getReceiver(); + if (!(operand instanceof USuperExpression)) { + report(context, node, calledMethod); return; } - UElement referenceExpression = node.getParent(); - if (referenceExpression instanceof UQualifiedExpression) { - UExpression operand = ((UQualifiedExpression)referenceExpression).getReceiver(); - if (!(operand instanceof USuperExpression)) { - report(context, node); - return; + PsiMethod method = UastUtils.getParentOfType(node, UMethod.class, true); + if (method != null) { + String callName = node.getMethodName(); + if (callName != null && !callName.equals(method.getName())) { + report(context, node, calledMethod); } } - - UFunction containingFunction = UastUtils.getContainingFunction(node); - if (containingFunction == null) { - return; - } - - if (containingFunction.getKind() == UastFunctionKind.CONSTRUCTOR || !containingFunction.matchesName(node.getFunctionName())) { - report(context, node); - } } - private static void report(UastAndroidContext context, UCallExpression node) { + private static void report( + @NonNull JavaContext context, + @NonNull UCallExpression node, + @NonNull PsiMethod method) { // Make sure the call is on a view - UFunction resolved = node.resolve(context); - if (resolved != null && resolved.getKind() != UastFunctionKind.CONSTRUCTOR) { - UClass containingClass = UastUtils.getContainingClass(resolved); - if (containingClass == null || !containingClass.isSubclassOf(CLASS_VIEW)) { - return; - } - } - - String name = node.getFunctionName(); - if (name == null) { + if (!context.getEvaluator().isMemberInSubClassOf(method, CLASS_VIEW, false)) { return; } + String name = method.getName(); String suggestion = Character.toLowerCase(name.charAt(2)) + name.substring(3); String message = String.format( // Keep in sync with {@link #getOldValue} and {@link #getNewValue} below! "Suspicious method call; should probably call \"`%1$s`\" rather than \"`%2$s`\"", suggestion, name); - context.report(ISSUE, node, context.getLocation(node.getFunctionReference()), message); + context.report(ISSUE, node, context.getUastNameLocation(node), message); } /** diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java index 6a93c321ac6..6d3f85facc9 100644 --- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java @@ -17,22 +17,26 @@ package com.android.tools.klint.checks; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; +import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.Speed; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.uast.UElement; import org.jetbrains.uast.UImportStatement; -import org.jetbrains.uast.check.UastAndroidContext; -import org.jetbrains.uast.check.UastScanner; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; +import java.util.Collections; +import java.util.List; + /** * Checks for "import android.R", which seems to be a common source of confusion * (see for example http://stackoverflow.com/questions/885009/r-cannot-be-resolved-android-error @@ -45,7 +49,7 @@ import org.jetbrains.uast.visitor.UastVisitor; * break. Look out for these erroneous import statements and delete them. * */ -public class WrongImportDetector extends Detector implements UastScanner { +public class WrongImportDetector extends Detector implements Detector.UastScanner { /** Is android.R being imported? */ public static final Issue ISSUE = Issue.create( "SuspiciousImport", //$NON-NLS-1$ @@ -62,42 +66,49 @@ public class WrongImportDetector extends Detector implements UastScanner { Severity.WARNING, new Implementation( WrongImportDetector.class, - Scope.SOURCE_FILE_SCOPE)); + Scope.JAVA_FILE_SCOPE)); /** Constructs a new {@link WrongImportDetector} check */ public WrongImportDetector() { } - @NonNull + // ---- Implements JavaScanner ---- + + + @Nullable @Override - public Speed getSpeed() { - return Speed.FAST; + public List> getApplicableUastTypes() { + return Collections.>singletonList(UImportStatement.class); } - // ---- Implements Detector.UastScanner ---- - + @Nullable @Override - public UastVisitor createUastVisitor(UastAndroidContext context) { + public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new ImportVisitor(context); } private static class ImportVisitor extends AbstractUastVisitor { - private final UastAndroidContext mContext; + private final JavaContext mContext; - public ImportVisitor(UastAndroidContext context) { + private ImportVisitor(JavaContext context) { + super(); mContext = context; } @Override - public boolean visitImportStatement(@NotNull UImportStatement node) { - String fqn = node.getFqNameToImport(); - if (fqn != null && fqn.equals("android.R")) { //$NON-NLS-1$ - Location location = mContext.getLocation(node); - mContext.report(ISSUE, node, location, - "Don't include `android.R` here; use a fully qualified name for " - + "each usage instead"); + public boolean visitImportStatement(UImportStatement statement) { + PsiElement resolved = statement.resolve(); + if (resolved instanceof PsiClass) { + String qualifiedName = ((PsiClass) resolved).getQualifiedName(); + if ("android.R".equals(qualifiedName)) { + Location location = mContext.getUastLocation(statement); + mContext.report(ISSUE, statement, location, + "Don't include `android.R` here; use a fully qualified name for " + + "each usage instead"); + } } - return false; + + return super.visitImportStatement(statement); } } } diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/api-versions-support-library.xml b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/api-versions-support-library.xml new file mode 100644 index 00000000000..faa8970f88b --- /dev/null +++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/api-versions-support-library.xml @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/lint/lint-idea/lint-idea.iml b/plugins/lint/lint-idea/lint-idea.iml index 7e41a5fdcaa..da20043eccd 100644 --- a/plugins/lint/lint-idea/lint-idea.iml +++ b/plugins/lint/lint-idea/lint-idea.iml @@ -12,9 +12,9 @@ - + \ No newline at end of file diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintExternalAnnotator.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintExternalAnnotator.java index 231e8ac7405..9ce1e7a88f7 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintExternalAnnotator.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintExternalAnnotator.java @@ -1,12 +1,13 @@ package org.jetbrains.android.inspections.klint; import com.android.SdkConstants; +import com.android.tools.idea.gradle.util.Projects; import com.android.tools.klint.client.api.IssueRegistry; import com.android.tools.klint.client.api.LintDriver; -import com.android.tools.klint.client.api.LintLanguageExtension; import com.android.tools.klint.client.api.LintRequest; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.Scope; +import com.android.utils.SdkUtils; import com.intellij.codeHighlighting.HighlightDisplayLevel; import com.intellij.codeInsight.FileModificationService; import com.intellij.codeInsight.daemon.DaemonBundle; @@ -46,11 +47,9 @@ import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.android.util.AndroidCommonUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.uast.UastConverterUtils; +import org.jetbrains.kotlin.idea.KotlinFileType; import javax.swing.*; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -60,6 +59,9 @@ import static com.android.SdkConstants.*; import static com.android.tools.klint.detector.api.TextFormat.HTML; import static com.android.tools.klint.detector.api.TextFormat.RAW; +/** + * @author Eugene.Kudelevsky + */ public class AndroidLintExternalAnnotator extends ExternalAnnotator { static final boolean INCLUDE_IDEA_SUPPRESS_ACTIONS = false; @@ -88,22 +90,14 @@ public class AndroidLintExternalAnnotator extends ExternalAnnotator issues = getIssuesFromInspections(file.getProject(), file); @@ -129,20 +123,16 @@ public class AndroidLintExternalAnnotator extends ExternalAnnotator" + DaemonBundle.message("inspection.extended.description") - + " " + getShowMoreShortCut(); - String tooltip = XmlStringUtil.wrapInHtml(RAW.convertTo(message, HTML) + link); + String link = " " + DaemonBundle.message("inspection.extended.description") + +" " + getShowMoreShortCut(); + String tooltip = XmlStringUtil.wrapInHtml(RAW.convertTo(message, HTML) + link); - try { - return (Annotation)createHtmlAnnotation.invoke(holder, severity, range, message, tooltip); - } - catch (IllegalAccessException ignored) { - ourCreateHtmlAnnotationMethod = null; - //noinspection AssignmentToStaticFieldFromInstanceMethod - ourCreateHtmlAnnotationMethodFailed = true; - } - catch (InvocationTargetException e) { - ourCreateHtmlAnnotationMethod = null; - //noinspection AssignmentToStaticFieldFromInstanceMethod - ourCreateHtmlAnnotationMethodFailed = true; - } - } - - return holder.createAnnotation(severity, range, message); - } - - private static boolean ourCreateHtmlAnnotationMethodFailed; - private static Method ourCreateHtmlAnnotationMethod; - - @Nullable - private static Method getCreateHtmlAnnotation() { - if (ourCreateHtmlAnnotationMethod != null) { - return ourCreateHtmlAnnotationMethod; - } - if (ourCreateHtmlAnnotationMethodFailed) { - return null; - } else { - ourCreateHtmlAnnotationMethodFailed = true; - try { - ourCreateHtmlAnnotationMethod = AnnotationHolder.class.getMethod("createAnnotation", HighlightSeverity.class, - TextRange.class, String.class, String.class); - } - catch (NoSuchMethodException ignore) { - } - return ourCreateHtmlAnnotationMethod; - } + return holder.createAnnotation(severity, range, message, tooltip); } // Based on similar code in the LocalInspectionsPass constructor diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintGlobalInspectionContext.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintGlobalInspectionContext.java index 2984e790220..f88fd5312b1 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintGlobalInspectionContext.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintGlobalInspectionContext.java @@ -34,13 +34,13 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; +import java.util.*; +/** + * @author Eugene.Kudelevsky + */ class AndroidLintGlobalInspectionContext implements GlobalInspectionContextExtension { - static final Key ID = Key.create("AndroidKlintGlobalInspectionContext"); + static final Key ID = Key.create("AndroidLintGlobalInspectionContext"); private Map>> myResults; @NotNull diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionBase.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionBase.java index 60a077f60c4..8a7424a7435 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionBase.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionBase.java @@ -1,11 +1,7 @@ package org.jetbrains.android.inspections.klint; import com.android.annotations.concurrency.GuardedBy; -import com.android.tools.klint.detector.api.Category; -import com.android.tools.klint.detector.api.Implementation; -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.tools.klint.detector.api.*; import com.intellij.analysis.AnalysisScope; import com.intellij.codeHighlighting.HighlightDisplayLevel; import com.intellij.codeInsight.daemon.HighlightDisplayKey; @@ -16,7 +12,6 @@ import com.intellij.lang.annotation.ProblemGroup; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.colors.TextAttributesKey; -import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.LocalFileSystem; @@ -35,13 +30,12 @@ import org.jetbrains.annotations.TestOnly; import java.io.File; import java.util.*; -import static com.android.tools.klint.detector.api.TextFormat.HTML; -import static com.android.tools.klint.detector.api.TextFormat.RAW; +import static com.android.tools.klint.detector.api.TextFormat.*; import static com.intellij.xml.CommonXmlStrings.HTML_END; import static com.intellij.xml.CommonXmlStrings.HTML_START; public abstract class AndroidLintInspectionBase extends GlobalInspectionTool { - private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.inspections.klint.AndroidLintInspectionBase"); + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.inspections.lint.AndroidLintInspectionBase"); private static final Object ISSUE_MAP_LOCK = new Object(); @@ -56,12 +50,9 @@ public abstract class AndroidLintInspectionBase extends GlobalInspectionTool { myIssue = issue; final Category category = issue.getCategory(); - final String[] categoryNames = category != null - ? computeAllNames(category) - : ArrayUtil.EMPTY_STRING_ARRAY; myGroupPath = ArrayUtil.mergeArrays(new String[]{AndroidBundle.message("android.inspections.group.name"), - AndroidBundle.message("android.lint.inspections.subgroup.name")}, categoryNames); + AndroidBundle.message("android.lint.inspections.subgroup.name")}, computeAllNames(category)); myDisplayName = displayName; } @@ -157,8 +148,6 @@ public abstract class AndroidLintInspectionBase extends GlobalInspectionTool { private ProblemDescriptor[] computeProblemDescriptors(@NotNull PsiElement psiFile, @NotNull InspectionManager manager, @NotNull List problems) { - ProgressManager.checkCanceled(); - final List result = new ArrayList(); for (ProblemData problemData : problems) { @@ -167,7 +156,12 @@ public abstract class AndroidLintInspectionBase extends GlobalInspectionTool { // We need to have explicit and tags around the text; inspection infrastructure // such as the {@link com.intellij.codeInspection.ex.DescriptorComposer} will call // {@link com.intellij.xml.util.XmlStringUtil.isWrappedInHtml}. See issue 177283 for uses. - final String formattedMessage = HTML_START + RAW.convertTo(originalMessage, HTML) + HTML_END; + // Note that we also need to use HTML with unicode characters here, since the HTML display + // in the inspections view does not appear to support numeric code character entities. + String formattedMessage = HTML_START + RAW.convertTo(originalMessage, HTML_WITH_UNICODE) + HTML_END; + + // The inspections UI does not correctly handle + final TextRange range = problemData.getTextRange(); if (range.getStartOffset() == range.getEndOffset()) { @@ -198,52 +192,17 @@ public abstract class AndroidLintInspectionBase extends GlobalInspectionTool { @NotNull @Override public SuppressQuickFix[] getBatchSuppressActions(@Nullable PsiElement element) { - SuppressLintQuickFix suppressLintQuickFix = new SuppressLintQuickFix(myIssue); if (AndroidLintExternalAnnotator.INCLUDE_IDEA_SUPPRESS_ACTIONS) { final List result = new ArrayList(); - result.add(suppressLintQuickFix); result.addAll(Arrays.asList(BatchSuppressManager.SERVICE.getInstance().createBatchSuppressActions(HighlightDisplayKey.find(getShortName())))); result.addAll(Arrays.asList(new XmlSuppressableInspectionTool.SuppressTagStatic(getShortName()), new XmlSuppressableInspectionTool.SuppressForFile(getShortName()))); return result.toArray(new SuppressQuickFix[result.size()]); } else { - return new SuppressQuickFix[] { suppressLintQuickFix }; + return new SuppressQuickFix[0]; } } - private static class SuppressLintQuickFix implements SuppressQuickFix { - private Issue myIssue; - - private SuppressLintQuickFix(Issue issue) { - myIssue = issue; - } - - @Override - public boolean isAvailable(@NotNull Project project, @NotNull PsiElement context) { - return true; - } - - @Override - public boolean isSuppressAll() { - return false; - } - - @NotNull - @Override - public String getName() { - return "Suppress with @SuppressLint (Java) or tools:ignore (XML)"; - } - - @NotNull - @Override - public String getFamilyName() { - return "Suppress"; - } - - @Override - public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {} - } - @TestOnly public static void invalidateInspectionShortName2IssueMap() { ourIssue2InspectionShortName = null; @@ -389,7 +348,7 @@ public abstract class AndroidLintInspectionBase extends GlobalInspectionTool { return false; } final Scope scope = scopes.iterator().next(); - return scope == Scope.SOURCE_FILE || scope == Scope.RESOURCE_FILE || scope == Scope.MANIFEST + return scope == Scope.JAVA_FILE || scope == Scope.RESOURCE_FILE || scope == Scope.MANIFEST || scope == Scope.PROGUARD_FILE || scope == Scope.OTHER; } diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionToolProvider.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionToolProvider.java index 1de28f8c1f4..458a7db555b 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionToolProvider.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionToolProvider.java @@ -1,14 +1,67 @@ package org.jetbrains.android.inspections.klint; +import com.android.SdkConstants; +import com.android.ide.common.repository.GradleCoordinate; +import com.android.ide.common.resources.ResourceUrl; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.VersionQualifier; +import com.android.resources.ResourceFolderType; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkVersionInfo; +import com.android.tools.idea.actions.OverrideResourceAction; +import com.android.tools.idea.templates.RepositoryUrlManager; import com.android.tools.klint.checks.*; +import com.android.tools.klint.detector.api.Issue; +import com.google.common.collect.Lists; +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlAttributeValue; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.sdk.AndroidSdkData; import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidResourceUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.android.SdkConstants.*; +import static com.android.tools.klint.checks.ApiDetector.REQUIRES_API_ANNOTATION; import static com.android.tools.klint.checks.FragmentDetector.ISSUE; +import static com.android.tools.klint.detector.api.TextFormat.RAW; +import static com.android.xml.AndroidManifest.*; /** * Registrations for all the various Lint rules as local IDE inspections, along with quickfixes for many of them */ public class AndroidLintInspectionToolProvider { + public static class AndroidKLintCustomErrorInspection extends AndroidLintInspectionBase { + public AndroidKLintCustomErrorInspection() { + super("Error from Custom Lint Check", IntellijLintIssueRegistry.CUSTOM_ERROR); + } + } + + public static class AndroidKLintCustomWarningInspection extends AndroidLintInspectionBase { + public AndroidKLintCustomWarningInspection() { + super("Warning from Custom Lint Check", IntellijLintIssueRegistry.CUSTOM_WARNING); + } + + } + public static class AndroidKLintInconsistentLayoutInspection extends AndroidLintInspectionBase { public AndroidKLintInconsistentLayoutInspection() { super(AndroidBundle.message("android.lint.inspections.inconsistent.layout"), LayoutConsistencyDetector.INCONSISTENT_IDS); @@ -51,6 +104,12 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintFloatMathInspection extends AndroidLintInspectionBase { + public AndroidKLintFloatMathInspection() { + super("Using FloatMath instead of Math", MathDetector.ISSUE); + } + } + public static class AndroidKLintGetInstanceInspection extends AndroidLintInspectionBase { public AndroidKLintGetInstanceInspection() { super(AndroidBundle.message("android.lint.inspections.get.instance"), CipherGetInstanceDetector.ISSUE); @@ -93,28 +152,53 @@ public class AndroidLintInspectionToolProvider { } } - public static class AndroidKLintUnusedResourcesInspection extends AndroidLintInspectionBase { - public AndroidKLintUnusedResourcesInspection() { - super(AndroidBundle.message("android.lint.inspections.unused.resources"), UnusedResourceDetector.ISSUE); + public static class AndroidKLintUnprotectedSMSBroadcastReceiverInspection extends AndroidLintInspectionBase { + public AndroidKLintUnprotectedSMSBroadcastReceiverInspection() { + super(AndroidBundle.message("android.lint.inspections.unprotected.smsbroadcast.receiver"), UnsafeBroadcastReceiverDetector.BROADCAST_SMS); } + } - public static class AndroidKLintUnusedIdsInspection extends AndroidLintInspectionBase { - public AndroidKLintUnusedIdsInspection() { - super(AndroidBundle.message("android.lint.inspections.unused.ids"), UnusedResourceDetector.ISSUE_IDS); + public static class AndroidKLintUnusedAttributeInspection extends AndroidLintInspectionBase { + public AndroidKLintUnusedAttributeInspection() { + super(AndroidBundle.message("android.lint.inspections.unused.attribute"), ApiDetector.UNUSED); } + } public static class AndroidKLintAlwaysShowActionInspection extends AndroidLintInspectionBase { public AndroidKLintAlwaysShowActionInspection() { super(AndroidBundle.message("android.lint.inspections.always.show.action"), AlwaysShowActionDetector.ISSUE); } + } public static class AndroidKLintAppCompatMethodInspection extends AndroidLintInspectionBase { public AndroidKLintAppCompatMethodInspection() { super(AndroidBundle.message("android.lint.inspections.app.compat.method"), AppCompatCallDetector.ISSUE); } + + } + + public static class AndroidKLintGoogleAppIndexingUrlErrorInspection extends AndroidLintInspectionBase { + public AndroidKLintGoogleAppIndexingUrlErrorInspection() { + super("URL not supported by app for Google App Indexing", AppIndexingApiDetector.ISSUE_URL_ERROR); + } + + } + + public static class AndroidKLintGoogleAppIndexingWarningInspection extends AndroidLintInspectionBase { + public AndroidKLintGoogleAppIndexingWarningInspection() { + super("Missing support for Google App Indexing", AppIndexingApiDetector.ISSUE_APP_INDEXING); + } + + } + + public static class AndroidKLintGoogleAppIndexingApiWarningInspection extends AndroidLintInspectionBase { + public AndroidKLintGoogleAppIndexingApiWarningInspection() { + super("Missing support for Google App Indexing Api", AppIndexingApiDetector.ISSUE_APP_INDEXING_API); + } + } public static class AndroidKLintStringFormatCountInspection extends AndroidLintInspectionBase { @@ -134,7 +218,7 @@ public class AndroidLintInspectionToolProvider { super(AndroidBundle.message("android.lint.inspections.string.format.invalid"), StringFormatDetector.INVALID); } } - + public static class AndroidKLintWrongViewCastInspection extends AndroidLintInspectionBase { public AndroidKLintWrongViewCastInspection() { super(AndroidBundle.message("android.lint.inspections.wrong.view.cast"), ViewTypeDetector.ISSUE); @@ -146,6 +230,18 @@ public class AndroidLintInspectionToolProvider { super(AndroidBundle.message("android.lint.inspections.commit.transaction"), CleanupDetector.COMMIT_FRAGMENT); } } + + public static class AndroidKLintBadHostnameVerifierInspection extends AndroidLintInspectionBase { + public AndroidKLintBadHostnameVerifierInspection() { + super("Insecure HostnameVerifier", BadHostnameVerifierDetector.ISSUE); + } + } + + public static class AndroidKLintBatteryLifeInspection extends AndroidLintInspectionBase { + public AndroidKLintBatteryLifeInspection() { + super("Battery Life Issues", BatteryDetector.ISSUE); + } + } public static class AndroidKLintHandlerLeakInspection extends AndroidLintInspectionBase { public AndroidKLintHandlerLeakInspection() { @@ -169,6 +265,7 @@ public class AndroidLintInspectionToolProvider { public AndroidKLintUseValueOfInspection() { super(AndroidBundle.message("android.lint.inspections.use.value.of"), JavaPerformanceDetector.USE_VALUE_OF); } + } public static class AndroidKLintPackageManagerGetSignaturesInspection extends AndroidLintInspectionBase { @@ -177,10 +274,18 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintParcelClassLoaderInspection extends AndroidLintInspectionBase { + public AndroidKLintParcelClassLoaderInspection() { + super("Default Parcel Class Loader", ReadParcelableDetector.ISSUE); + } + + } + public static class AndroidKLintParcelCreatorInspection extends AndroidLintInspectionBase { public AndroidKLintParcelCreatorInspection() { super(AndroidBundle.message("android.lint.inspections.parcel.creator"), ParcelDetector.ISSUE); } + } public static class AndroidKLintPluralsCandidateInspection extends AndroidLintInspectionBase { @@ -213,6 +318,12 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintDefaultLocaleInspection extends AndroidLintInspectionBase { + public AndroidKLintDefaultLocaleInspection() { + super("Implied default locale in case conversion", LocaleDetector.STRING_LOCALE); + } + } + public static class AndroidKLintValidFragmentInspection extends AndroidLintInspectionBase { public AndroidKLintValidFragmentInspection() { super(AndroidBundle.message("android.lint.inspections.valid.fragment"), ISSUE); @@ -231,6 +342,12 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintViewTagInspection extends AndroidLintInspectionBase { + public AndroidKLintViewTagInspection() { + super("Tagged object leaks", ViewTagDetector.ISSUE); + } + } + public static class AndroidKLintMergeRootFrameInspection extends AndroidLintInspectionBase { public AndroidKLintMergeRootFrameInspection() { super(AndroidBundle.message("android.lint.inspections.merge.root.frame"), MergeRootFrameLayoutDetector.ISSUE); @@ -241,6 +358,7 @@ public class AndroidLintInspectionToolProvider { public AndroidKLintExportedServiceInspection() { super(AndroidBundle.message("android.lint.inspections.exported.service"), SecurityDetector.EXPORTED_SERVICE); } + } public static class AndroidKLintGrantAllUrisInspection extends AndroidLintInspectionBase { @@ -255,28 +373,67 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintSSLCertificateSocketFactoryCreateSocketInspection extends AndroidLintInspectionBase { + public AndroidKLintSSLCertificateSocketFactoryCreateSocketInspection() { + super(AndroidBundle.message("android.lint.inspections.sslcertificate.socket.factory.create.socket"), SslCertificateSocketFactoryDetector.CREATE_SOCKET); + } + } + + public static class AndroidKLintSSLCertificateSocketFactoryGetInsecureInspection extends AndroidLintInspectionBase { + public AndroidKLintSSLCertificateSocketFactoryGetInsecureInspection() { + super(AndroidBundle.message("android.lint.inspections.sslcertificate.socket.factory.get.insecure"), SslCertificateSocketFactoryDetector.GET_INSECURE); + } + } + + public static class AndroidKLintSwitchIntDefInspection extends AndroidLintInspectionBase { + public AndroidKLintSwitchIntDefInspection() { + super("Missing @IntDef in Switch", AnnotationDetector.SWITCH_TYPE_DEF); + } + + } + + public static class AndroidKLintTrustAllX509TrustManagerInspection extends AndroidLintInspectionBase { + public AndroidKLintTrustAllX509TrustManagerInspection() { + super("Insecure TLS/SSL trust manager", TrustAllX509TrustManagerDetector.ISSUE); + } + } + + private abstract static class AndroidKLintTypographyInspectionBase extends AndroidLintInspectionBase { + public AndroidKLintTypographyInspectionBase(String displayName, Issue issue) { + super(displayName, issue); + } + + } + public static class AndroidKLintNewApiInspection extends AndroidLintInspectionBase { public AndroidKLintNewApiInspection() { super(AndroidBundle.message("android.lint.inspections.new.api"), ApiDetector.UNSUPPORTED); } + } public static class AndroidKLintInlinedApiInspection extends AndroidLintInspectionBase { public AndroidKLintInlinedApiInspection() { super(AndroidBundle.message("android.lint.inspections.inlined.api"), ApiDetector.INLINED); } + } public static class AndroidKLintOverrideInspection extends AndroidLintInspectionBase { public AndroidKLintOverrideInspection() { super(AndroidBundle.message("android.lint.inspections.override"), ApiDetector.OVERRIDE); } + } + private static final Pattern QUOTED_PARAMETER = Pattern.compile("`.+:(.+)=\"(.*)\"`"); + public static class AndroidKLintRtlCompatInspection extends AndroidLintInspectionBase { public AndroidKLintRtlCompatInspection() { super(AndroidBundle.message("android.lint.inspections.rtl.compat"), RtlDetector.COMPAT); } + + } public static class AndroidKLintRtlEnabledInspection extends AndroidLintInspectionBase { public AndroidKLintRtlEnabledInspection() { @@ -307,10 +464,17 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintAllowAllHostnameVerifierInspection extends AndroidLintInspectionBase { + public AndroidKLintAllowAllHostnameVerifierInspection() { + super("Insecure HostnameVerifier", AllowAllHostnameVerifierDetector.ISSUE); + } + } + public static class AndroidKLintCommitPrefEditsInspection extends AndroidLintInspectionBase { public AndroidKLintCommitPrefEditsInspection() { - super(AndroidBundle.message("android.lint.inspections.commit.pref.edits"), SharedPrefsDetector.ISSUE); + super(AndroidBundle.message("android.lint.inspections.commit.pref.edits"), CleanupDetector.SHARED_PREF); } + } public static class AndroidKLintCustomViewStyleableInspection extends AndroidLintInspectionBase { @@ -333,6 +497,7 @@ public class AndroidLintInspectionToolProvider { public AndroidKLintExportedContentProviderInspection() { super(AndroidBundle.message("android.lint.inspections.exported.content.provider"), SecurityDetector.EXPORTED_PROVIDER); } + } public static class AndroidKLintExportedPreferenceActivityInspection extends AndroidLintInspectionBase { public AndroidKLintExportedPreferenceActivityInspection() { @@ -343,6 +508,7 @@ public class AndroidLintInspectionToolProvider { public AndroidKLintExportedReceiverInspection() { super(AndroidBundle.message("android.lint.inspections.exported.receiver"), SecurityDetector.EXPORTED_RECEIVER); } + } public static class AndroidKLintIconColorsInspection extends AndroidLintInspectionBase { public AndroidKLintIconColorsInspection() { @@ -364,12 +530,24 @@ public class AndroidLintInspectionToolProvider { super(AndroidBundle.message("android.lint.inspections.icon.xml.and.png"), IconDetector.ICON_XML_AND_PNG); } } + + public static class AndroidKLintAuthLeakInspection extends AndroidLintInspectionBase { + public AndroidKLintAuthLeakInspection() { + super("Code could contain an credential leak", StringAuthLeakDetector.AUTH_LEAK); + } + } public static class AndroidKLintInflateParamsInspection extends AndroidLintInspectionBase { public AndroidKLintInflateParamsInspection() { super(AndroidBundle.message("android.lint.inspections.inflate.params"), LayoutInflationDetector.ISSUE); } } + + public static class AndroidKLintInvalidUsesTagAttributeInspection extends AndroidLintInspectionBase { + public AndroidKLintInvalidUsesTagAttributeInspection() { + super(AndroidBundle.message("android.lint.inspections.invalid.uses.tag.attribute"), AndroidAutoDetector.INVALID_USES_TAG_ISSUE); + } + } public static class AndroidKLintJavascriptInterfaceInspection extends AndroidLintInspectionBase { public AndroidKLintJavascriptInterfaceInspection() { @@ -379,7 +557,7 @@ public class AndroidLintInspectionToolProvider { public static class AndroidKLintLocalSuppressInspection extends AndroidLintInspectionBase { public AndroidKLintLocalSuppressInspection() { - super(AndroidBundle.message("android.lint.inspections.local.suppress"), AnnotationDetector.ISSUE); + super(AndroidBundle.message("android.lint.inspections.local.suppress"), AnnotationDetector.INSIDE_METHOD); } } @@ -401,6 +579,27 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintMissingIntentFilterForMediaSearchInspection extends AndroidLintInspectionBase { + public AndroidKLintMissingIntentFilterForMediaSearchInspection() { + super(AndroidBundle.message("android.lint.inspections.missing.intent.filter.for.media.search"), + AndroidAutoDetector.MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH); + } + } + + public static class AndroidKLintMissingMediaBrowserServiceIntentFilterInspection extends AndroidLintInspectionBase { + public AndroidKLintMissingMediaBrowserServiceIntentFilterInspection() { + super(AndroidBundle.message("android.lint.inspections.missing.media.browser.service.intent.filter"), + AndroidAutoDetector.MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE); + } + } + + public static class AndroidKLintMissingOnPlayFromSearchInspection extends AndroidLintInspectionBase { + public AndroidKLintMissingOnPlayFromSearchInspection() { + super(AndroidBundle.message("android.lint.inspections.missing.on.play.from.search"), + AndroidAutoDetector.MISSING_ON_PLAY_FROM_SEARCH); + } + } + public static class AndroidKLintOverrideAbstractInspection extends AndroidLintInspectionBase { public AndroidKLintOverrideAbstractInspection() { super(AndroidBundle.message("android.lint.inspections.override.abstract"), OverrideConcreteDetector.ISSUE); @@ -413,11 +612,28 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintRecyclerViewInspection extends AndroidLintInspectionBase { + public AndroidKLintRecyclerViewInspection() { + super("RecyclerView Problems", RecyclerViewDetector.FIXED_POSITION); + } + } + + public static class AndroidKLintRegisteredInspection extends AndroidLintInspectionBase { + public AndroidKLintRegisteredInspection() { + super(AndroidBundle.message("android.lint.inspections.registered"), RegistrationDetector.ISSUE); + } + } + public static class AndroidKLintRequiredSizeInspection extends AndroidLintInspectionBase { public AndroidKLintRequiredSizeInspection() { super(AndroidBundle.message("android.lint.inspections.required.size"), RequiredAttributeDetector.ISSUE); } } + public static class AndroidKLintSecureRandomInspection extends AndroidLintInspectionBase { + public AndroidKLintSecureRandomInspection() { + super("Using a fixed seed with SecureRandom", SecureRandomDetector.ISSUE); + } + } public static class AndroidKLintServiceCastInspection extends AndroidLintInspectionBase { public AndroidKLintServiceCastInspection() { @@ -430,6 +646,30 @@ public class AndroidLintInspectionToolProvider { } } + public static class AndroidKLintSetTextI18nInspection extends AndroidLintInspectionBase { + public AndroidKLintSetTextI18nInspection() { + super(AndroidBundle.message("android.lint.inspections.set.text.i18n"), SetTextDetector.SET_TEXT_I18N); + } + } + + public static class AndroidKLintSetWorldReadableInspection extends AndroidLintInspectionBase { + public AndroidKLintSetWorldReadableInspection() { + super(AndroidBundle.message("android.lint.inspections.set.world.readable"), SecurityDetector.SET_READABLE); + } + } + + public static class AndroidKLintSetWorldWritableInspection extends AndroidLintInspectionBase { + public AndroidKLintSetWorldWritableInspection() { + super(AndroidBundle.message("android.lint.inspections.set.world.writable"), SecurityDetector.SET_WRITABLE); + } + } + + public static class AndroidKLintShiftFlagsInspection extends AndroidLintInspectionBase { + public AndroidKLintShiftFlagsInspection() { + super(AndroidBundle.message("android.lint.inspections.shift.flags"), AnnotationDetector.FLAG_STYLE); + } + } + public static class AndroidKLintShortAlarmInspection extends AndroidLintInspectionBase { public AndroidKLintShortAlarmInspection() { super(AndroidBundle.message("android.lint.inspections.short.alarm"), AlarmDetector.ISSUE); @@ -441,7 +681,7 @@ public class AndroidLintInspectionToolProvider { super(AndroidBundle.message("android.lint.inspections.show.toast"), ToastDetector.ISSUE); } } - + public static class AndroidKLintSimpleDateFormatInspection extends AndroidLintInspectionBase { public AndroidKLintSimpleDateFormatInspection() { super(AndroidBundle.message("android.lint.inspections.simple.date.format"), DateFormatDetector.DATE_FORMAT); @@ -453,6 +693,19 @@ public class AndroidLintInspectionToolProvider { public AndroidKLintStopShipInspection() { super(AndroidBundle.message("android.lint.inspections.stop.ship"), CommentDetector.STOP_SHIP); } + + } + + public static class AndroidKLintSupportAnnotationUsageInspection extends AndroidLintInspectionBase { + public AndroidKLintSupportAnnotationUsageInspection() { + super("Incorrect support annotation usage", AnnotationDetector.ANNOTATION_USAGE); + } + } + + public static class AndroidKLintUniqueConstantsInspection extends AndroidLintInspectionBase { + public AndroidKLintUniqueConstantsInspection() { + super(AndroidBundle.message("android.lint.inspections.unique.constants"), AnnotationDetector.UNIQUE); + } } public static class AndroidKLintUnlocalizedSmsInspection extends AndroidLintInspectionBase { @@ -469,5 +722,30 @@ public class AndroidLintInspectionToolProvider { public AndroidKLintWrongCallInspection() { super(AndroidBundle.message("android.lint.inspections.wrong.call"), WrongCallDetector.ISSUE); } + + } + + public static class AndroidKLintPendingBindingsInspection extends AndroidLintInspectionBase { + public AndroidKLintPendingBindingsInspection() { + super("Missing Pending Bindings", RecyclerViewDetector.DATA_BINDER); + } + } + + public static class AndroidKLintUnsafeDynamicallyLoadedCodeInspection extends AndroidLintInspectionBase { + public AndroidKLintUnsafeDynamicallyLoadedCodeInspection() { + super("load used to dynamically load code", UnsafeNativeCodeDetector.LOAD); + } + } + + public static class AndroidKLintUnsafeNativeCodeLocationInspection extends AndroidLintInspectionBase { + public AndroidKLintUnsafeNativeCodeLocationInspection() { + super("Native code outside library directory", UnsafeNativeCodeDetector.UNSAFE_NATIVE_CODE_LOCATION); + } + } + + public static class AndroidKLintUnsafeProtectedBroadcastReceiverInspection extends AndroidLintInspectionBase { + public AndroidKLintUnsafeProtectedBroadcastReceiverInspection() { + super("Unsafe Protected BroadcastReceiver", UnsafeBroadcastReceiverDetector.ACTION_STRING); + } } } diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintQuickFix.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintQuickFix.java index d5690b77072..0b45766b945 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintQuickFix.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintQuickFix.java @@ -1,11 +1,18 @@ package org.jetbrains.android.inspections.klint; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +/** + * @author Eugene.Kudelevsky + */ public interface AndroidLintQuickFix { AndroidLintQuickFix[] EMPTY_ARRAY = new AndroidLintQuickFix[0]; - + void apply(@NotNull PsiElement startElement, @NotNull PsiElement endElement, @NotNull AndroidQuickfixContexts.Context context); boolean isApplicable(@NotNull PsiElement startElement, @NotNull PsiElement endElement, @NotNull AndroidQuickfixContexts.ContextType contextType); diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintUtil.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintUtil.java index 4682520d9e7..a8bf5c6e6d2 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintUtil.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintUtil.java @@ -12,6 +12,9 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * @author Eugene.Kudelevsky + */ public class AndroidLintUtil { @NonNls static final String ATTR_VALUE_VERTICAL = "vertical"; @NonNls static final String ATTR_VALUE_WRAP_CONTENT = "wrap_content"; @@ -38,7 +41,14 @@ public class AndroidLintUtil { final InspectionProfile profile = InspectionProjectProfileManager.getInstance(context.getProject()).getInspectionProfile(); if (!profile.isToolEnabled(key, context)) { - return null; + if (!issue.isEnabledByDefault()) { + // Lint will skip issues (and not report them) for issues that have been disabled, + // except for those issues that are explicitly enabled via Gradle. Therefore, if + // we get this far, lint has found this issue to be explicitly enabled, so we let + // that setting override a local enabled/disabled state in the IDE profile. + } else { + return null; + } } final AndroidLintInspectionBase inspection = (AndroidLintInspectionBase)profile.getUnwrappedTool(inspectionShortName, context); diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidQuickfixContexts.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidQuickfixContexts.java index a83fd52060a..6e7871f6c77 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidQuickfixContexts.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidQuickfixContexts.java @@ -3,70 +3,73 @@ package org.jetbrains.android.inspections.klint; import com.intellij.openapi.editor.Editor; import org.jetbrains.annotations.NotNull; +/** + * @author Eugene.Kudelevsky + */ public class AndroidQuickfixContexts { - public static abstract class Context { - private final ContextType myType; + public static abstract class Context { + private final ContextType myType; - private Context(@NotNull ContextType type) { - myType = type; - } - - @NotNull - public ContextType getType() { - return myType; - } + private Context(@NotNull ContextType type) { + myType = type; } - public static class ContextType { - private ContextType() { - } + @NotNull + public ContextType getType() { + return myType; + } + } + + public static class ContextType { + private ContextType() { + } + } + + public static class BatchContext extends Context { + public static final ContextType TYPE = new ContextType(); + private static final BatchContext INSTANCE = new BatchContext(); + + private BatchContext() { + super(TYPE); } - public static class BatchContext extends Context { - public static final ContextType TYPE = new ContextType(); - private static final BatchContext INSTANCE = new BatchContext(); + @NotNull + public static BatchContext getInstance() { + return INSTANCE; + } + } - private BatchContext() { - super(TYPE); - } + public static class EditorContext extends Context { + public static final ContextType TYPE = new ContextType(); + private final Editor myEditor; - @NotNull - public static BatchContext getInstance() { - return INSTANCE; - } + private EditorContext(@NotNull Editor editor) { + super(TYPE); + myEditor = editor; } - public static class EditorContext extends Context { - public static final ContextType TYPE = new ContextType(); - private final Editor myEditor; - - private EditorContext(@NotNull Editor editor) { - super(TYPE); - myEditor = editor; - } - - @NotNull - public Editor getEditor() { - return myEditor; - } - - @NotNull - public static EditorContext getInstance(@NotNull Editor editor) { - return new EditorContext(editor); - } + @NotNull + public Editor getEditor() { + return myEditor; } - public static class DesignerContext extends Context { - public static final ContextType TYPE = new ContextType(); - private static final DesignerContext INSTANCE = new DesignerContext(); - - private DesignerContext() { - super(TYPE); - } - - @NotNull - public static DesignerContext getInstance() { - return INSTANCE; - } + @NotNull + public static EditorContext getInstance(@NotNull Editor editor) { + return new EditorContext(editor); } -} \ No newline at end of file + } + + public static class DesignerContext extends Context { + public static final ContextType TYPE = new ContextType(); + private static final DesignerContext INSTANCE = new DesignerContext(); + + private DesignerContext() { + super(TYPE); + } + + @NotNull + public static DesignerContext getInstance() { + return INSTANCE; + } + } +} diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IdeaJavaParser.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IdeaJavaParser.java new file mode 100644 index 00000000000..3636b8e4fef --- /dev/null +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IdeaJavaParser.java @@ -0,0 +1,209 @@ +/* + * 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 org.jetbrains.android.inspections.klint; + +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.detector.api.JavaContext; +import com.android.tools.klint.detector.api.Location; +import com.android.tools.klint.detector.api.Severity; +import com.google.common.collect.Sets; +import com.intellij.codeInsight.AnnotationUtil; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.InheritanceUtil; +import lombok.ast.Node; +import lombok.ast.Position; +import org.jetbrains.uast.UastContext; + +import java.io.File; +import java.util.List; + +public class IdeaJavaParser extends JavaParser { + private final IntellijLintClient myClient; + private final Project myProject; + private final UastContext myContext; + private final JavaEvaluator myEvaluator; + + public IdeaJavaParser(IntellijLintClient client, Project myProject) { + this.myClient = client; + this.myProject = myProject; + this.myEvaluator = new MyJavaEvaluator(myProject); + + myContext = ServiceManager.getService(myProject, UastContext.class); + } + + @Override + public UastContext getUastContext() { + return myContext; + } + + @Override + public void prepareJavaParse(@NonNull List contexts) { + + } + + @Override + public PsiJavaFile parseJavaToPsi(@NonNull JavaContext context) { + PsiFile psiFile = IntellijLintUtils.getPsiFile(context); + if (!(psiFile instanceof PsiJavaFile)) { + return null; + } + return (PsiJavaFile)psiFile; + } + + @Override + public JavaEvaluator getEvaluator() { + return myEvaluator; + } + + @Override + public Project getIdeaProject() { + return myProject; + } + + @Override + public Location getRangeLocation( + @NonNull JavaContext context, @NonNull Node from, int fromDelta, @NonNull Node to, int toDelta + ) { + Position position1 = from.getPosition(); + Position position2 = to.getPosition(); + if (position1 == null) { + return getLocation(context, to); + } + else if (position2 == null) { + return getLocation(context, from); + } + + int start = Math.max(0, from.getPosition().getStart() + fromDelta); + int end = to.getPosition().getEnd() + toDelta; + return Location.create(context.file, null, start, end); + } + + @Override + public Location.Handle createLocationHandle(@NonNull JavaContext context, @NonNull Node node) { + return new LocationHandle(context.file, node); + } + + @Override + public void runReadAction(@NonNull Runnable runnable) { + ApplicationManager.getApplication().runReadAction(runnable); + } + + /* Handle for creating positions cheaply and returning full fledged locations later */ + private class LocationHandle implements Location.Handle { + private final File myFile; + private final Node myNode; + private Object mClientData; + + public LocationHandle(File file, Node node) { + myFile = file; + myNode = node; + } + + @NonNull + @Override + public Location resolve() { + Position pos = myNode.getPosition(); + if (pos == null) { + myClient.log(Severity.WARNING, null, "No position data found for node %1$s", myNode); + return Location.create(myFile); + } + return Location.create(myFile, null /*contents*/, pos.getStart(), pos.getEnd()); + } + + @Override + public void setClientData(@Nullable Object clientData) { + mClientData = clientData; + } + + @Override + @Nullable + public Object getClientData() { + return mClientData; + } + } + + private static class MyJavaEvaluator extends JavaEvaluator { + private final Project myProject; + + public MyJavaEvaluator(Project project) { + myProject = project; + } + + @Override + public boolean extendsClass(@Nullable PsiClass cls, @NonNull String className, boolean strict) { + // TODO: This checks interfaces too. Let's find a cheaper method which only checks direct super classes! + return InheritanceUtil.isInheritor(cls, strict, className); + } + + @Override + public boolean implementsInterface(@NonNull PsiClass cls, @NonNull String interfaceName, boolean strict) { + // TODO: This checks superclasses too. Let's find a cheaper method which only checks interfaces. + return false; + } + + @Override + public boolean inheritsFrom(@NonNull PsiClass cls, @NonNull String className, boolean strict) { + return InheritanceUtil.isInheritor(cls, strict, className); + } + + @Nullable + @Override + public PsiClass findClass(@NonNull String qualifiedName) { + return JavaPsiFacade.getInstance(myProject).findClass(qualifiedName, GlobalSearchScope.allScope(myProject)); + } + + @Nullable + @Override + public PsiClassType getClassType(@Nullable PsiClass cls) { + return cls != null ? JavaPsiFacade.getElementFactory(myProject).createType(cls) : null; + } + + @NonNull + @Override + public PsiAnnotation[] getAllAnnotations(@NonNull PsiModifierListOwner owner, boolean inHierarchy) { + return AnnotationUtil.getAllAnnotations(owner, inHierarchy, null, true); + } + + @Nullable + @Override + public PsiAnnotation findAnnotationInHierarchy(@NonNull PsiModifierListOwner listOwner, @NonNull String... annotationNames) { + return AnnotationUtil.findAnnotationInHierarchy(listOwner, Sets.newHashSet(annotationNames)); + } + + @Nullable + @Override + public PsiAnnotation findAnnotation(@Nullable PsiModifierListOwner listOwner, @NonNull String... annotationNames) { + return AnnotationUtil.findAnnotation(listOwner, false, annotationNames); + } + + @Nullable + @Override + public File getFile(@NonNull PsiFile file) { + VirtualFile virtualFile = file.getVirtualFile(); + return virtualFile != null ? VfsUtilCore.virtualToIoFile(virtualFile) : null; + } + } +} diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java index 102010772fb..33ea6355c2a 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java @@ -7,18 +7,16 @@ import com.android.ide.common.repository.ResourceVisibilityLookup; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.res2.ResourceFile; import com.android.ide.common.res2.ResourceItem; +import com.android.tools.idea.gradle.util.Projects; import com.android.tools.idea.rendering.AppResourceRepository; import com.android.tools.idea.rendering.LocalResourceRepository; import com.android.tools.idea.sdk.IdeSdks; -import com.android.tools.klint.client.api.*; import com.android.tools.klint.checks.ApiLookup; -import com.android.tools.klint.detector.api.Context; -import com.android.tools.klint.detector.api.DefaultPosition; -import com.android.tools.klint.detector.api.Issue; -import com.android.tools.klint.detector.api.Location; -import com.android.tools.klint.detector.api.Position; -import com.android.tools.klint.detector.api.Severity; -import com.android.tools.klint.detector.api.TextFormat; +import com.android.tools.klint.client.api.*; +import com.android.tools.klint.detector.api.*; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.io.Files; import com.intellij.analysis.AnalysisScope; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; @@ -45,24 +43,24 @@ import com.intellij.psi.xml.XmlElement; import com.intellij.psi.xml.XmlTag; import com.intellij.util.PathUtil; import com.intellij.util.containers.HashMap; +import com.intellij.util.lang.UrlClassLoader; import com.intellij.util.net.HttpConfigurable; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidRootUtil; +import org.jetbrains.android.sdk.AndroidSdkData; import org.jetbrains.android.sdk.AndroidSdkType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.uast.UastLanguagePlugin; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.android.tools.klint.detector.api.TextFormat.RAW; +import static org.jetbrains.android.inspections.klint.IntellijLintIssueRegistry.CUSTOM_ERROR; +import static org.jetbrains.android.inspections.klint.IntellijLintIssueRegistry.CUSTOM_WARNING; /** * Implementation of the {@linkplain LintClient} API for executing lint within the IDE: @@ -74,12 +72,9 @@ public class IntellijLintClient extends LintClient implements Disposable { @NonNull protected Project myProject; @Nullable protected Map myModuleMap; - @NonNull - private List myPlugins; - - protected IntellijLintClient(@NonNull Project project) { + public IntellijLintClient(@NonNull Project project) { + super(CLIENT_STUDIO); myProject = project; - myPlugins = LintLanguageExtension.getPlugins(project); } /** Creates a lint client for batch inspections */ @@ -126,8 +121,9 @@ public class IntellijLintClient extends LintClient implements Disposable { myModuleMap = moduleMap; } + @NonNull @Override - public Configuration getConfiguration(@NonNull com.android.tools.klint.detector.api.Project project) { + public Configuration getConfiguration(@NonNull com.android.tools.klint.detector.api.Project project, @Nullable final LintDriver driver) { if (project.isGradleProject() && project.isAndroidProject() && !project.isLibrary()) { AndroidProject model = project.getGradleProjectModel(); if (model != null) { @@ -157,7 +153,7 @@ public class IntellijLintClient extends LintClient implements Disposable { } // This is a LIST lookup. I should make this faster! - if (!getIssues().contains(issue)) { + if (!getIssues().contains(issue) && (driver == null || !driver.isCustomIssue(issue))) { return Severity.IGNORE; } @@ -173,7 +169,11 @@ public class IntellijLintClient extends LintClient implements Disposable { return new DefaultConfiguration(this, project, null) { @Override public boolean isEnabled(@NonNull Issue issue) { - return getIssues().contains(issue) && super.isEnabled(issue); + if (getIssues().contains(issue) && super.isEnabled(issue)) { + return true; + } + + return driver != null && driver.isCustomIssue(issue); } }; } @@ -182,7 +182,7 @@ public class IntellijLintClient extends LintClient implements Disposable { public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity severity, - @Nullable Location location, + @NonNull Location location, @NonNull String message, @NonNull TextFormat format) { assert false : message; @@ -241,6 +241,12 @@ public class IntellijLintClient extends LintClient implements Disposable { return new DomPsiParser(this); } + @Nullable + @Override + public JavaParser getJavaParser(@Nullable com.android.tools.klint.detector.api.Project project) { + return new IdeaJavaParser(this, myProject); + } + @NonNull @Override public List getJavaClassFolders(@NonNull com.android.tools.klint.detector.api.Project project) { @@ -250,7 +256,7 @@ public class IntellijLintClient extends LintClient implements Disposable { @NonNull @Override - public List getJavaLibraries(@NonNull com.android.tools.klint.detector.api.Project project) { + public List getJavaLibraries(@NonNull com.android.tools.klint.detector.api.Project project, boolean includeProvided) { // todo: implement return Collections.emptyList(); } @@ -290,7 +296,7 @@ public class IntellijLintClient extends LintClient implements Disposable { Module module = getModule(); if (module != null) { Sdk moduleSdk = ModuleRootManager.getInstance(module).getSdk(); - if (moduleSdk != null) { + if (moduleSdk != null && moduleSdk.getSdkType() instanceof AndroidSdkType) { String path = moduleSdk.getHomePath(); if (path != null) { File home = new File(path); @@ -367,19 +373,9 @@ public class IntellijLintClient extends LintClient implements Disposable { Module module = getModule(); if (module != null) { AndroidFacet facet = AndroidFacet.getInstance(module); - return facet != null && IntellijLintUtils.isGradleModule(facet); + return facet != null && facet.requiresAndroidModel(); } - return false; - } - - @Override - public Project getProject() { - return myProject; - } - - @Override - public List getLanguagePlugins() { - return myPlugins; + return Projects.requiresAndroidModel(myProject); } // Overridden such that lint doesn't complain about missing a bin dir property in the event @@ -416,6 +412,37 @@ public class IntellijLintClient extends LintClient implements Disposable { return new File(dir, Project.DIRECTORY_STORE_FOLDER).exists(); } + private static List ourReportedCustomIssues; + + private static void recordCustomIssue(@NonNull Issue issue) { + if (ourReportedCustomIssues == null) { + ourReportedCustomIssues = Lists.newArrayList(); + } else if (ourReportedCustomIssues.contains(issue)) { + return; + } + ourReportedCustomIssues.add(issue); + } + + @Nullable + public static Issue findCustomIssue(@NonNull String errorMessage) { + if (ourReportedCustomIssues != null) { + // We stash the original id into the error message such that we can + // find it later + int begin = errorMessage.lastIndexOf('['); + int end = errorMessage.lastIndexOf(']'); + if (begin < end && begin != -1) { + String id = errorMessage.substring(begin + 1, end); + for (Issue issue : ourReportedCustomIssues) { + if (id.equals(issue.getId())) { + return issue; + } + } + } + } + + return null; + } + /** * A lint client used for in-editor single file lint analysis (e.g. background checking while user is editing.) *

@@ -446,13 +473,21 @@ public class IntellijLintClient extends LintClient implements Disposable { public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity severity, - @Nullable Location location, + @NonNull Location location, @NonNull String message, @NonNull TextFormat format) { if (location != null) { final File file = location.getFile(); final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file); + if (context.getDriver().isCustomIssue(issue)) { + // Record original issue id in the message (such that we can find + // it later, in #findCustomIssue) + message += " [" + issue.getId() + "]"; + recordCustomIssue(issue); + issue = Severity.WARNING.compareTo(severity) <= 0 ? CUSTOM_WARNING : CUSTOM_ERROR; + } + if (myState.getMainFile().equals(vFile)) { final Position start = location.getStart(); final Position end = location.getEnd(); @@ -479,8 +514,12 @@ public class IntellijLintClient extends LintClient implements Disposable { final VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file); if (vFile == null) { - LOG.debug("Cannot find file " + file.getPath() + " in the VFS"); - return ""; + try { + return Files.toString(file, Charsets.UTF_8); + } catch (IOException ioe) { + LOG.debug("Cannot find file " + file.getPath() + " in the VFS"); + return ""; + } } final String content = getFileContent(vFile); @@ -503,6 +542,10 @@ public class IntellijLintClient extends LintClient implements Disposable { public String compute() { final Module module = myState.getModule(); final Project project = module.getProject(); + if (project.isDisposed()) { + return null; + } + final PsiFile psiFile = PsiManager.getInstance(project).findFile(vFile); if (psiFile == null) { @@ -584,7 +627,7 @@ public class IntellijLintClient extends LintClient implements Disposable { public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity severity, - @Nullable Location location, + @NonNull Location location, @NonNull String message, @NonNull TextFormat format) { VirtualFile vFile = null; @@ -616,7 +659,7 @@ public class IntellijLintClient extends LintClient implements Disposable { if (myScope.getScopeType() == AnalysisScope.PROJECT) { inScope = true; } else if (myScope.getScopeType() == AnalysisScope.MODULE || - myScope.getScopeType() == AnalysisScope.MODULES) { + myScope.getScopeType() == AnalysisScope.MODULES) { final Module module = findModuleForLintProject(myProject, context.getProject()); if (module != null && myScope.containsModule(module)) { inScope = true; @@ -625,6 +668,14 @@ public class IntellijLintClient extends LintClient implements Disposable { } if (inScope) { + if (context.getDriver().isCustomIssue(issue)) { + // Record original issue id in the message (such that we can find + // it later, in #findCustomIssue) + message += " [" + issue.getId() + "]"; + recordCustomIssue(issue); + issue = Severity.WARNING.compareTo(severity) <= 0 ? CUSTOM_WARNING : CUSTOM_ERROR; + } + file = new File(PathUtil.getCanonicalPath(file.getPath())); Map> file2ProblemList = myProblemMap.get(issue); @@ -719,6 +770,11 @@ public class IntellijLintClient extends LintClient implements Disposable { return HttpConfigurable.getInstance().openConnection(url.toExternalForm()); } + @Override + public ClassLoader createUrlClassLoader(@NonNull URL[] urls, @NonNull ClassLoader parent) { + return UrlClassLoader.build().parent(parent).urls(urls).get(); + } + @NonNull @Override public Location.Handle createResourceItemHandle(@NonNull ResourceItem item) { diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java index 0f415867b71..934104f1fe8 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java @@ -17,15 +17,14 @@ package org.jetbrains.android.inspections.klint; import com.android.annotations.NonNull; import com.android.tools.klint.checks.*; -import com.android.tools.klint.detector.api.Detector; -import com.android.tools.klint.detector.api.Implementation; -import com.android.tools.klint.detector.api.Issue; -import com.android.tools.klint.detector.api.Scope; +import com.android.tools.klint.detector.api.*; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import static org.jetbrains.android.inspections.klint.IntellijLintProject.*; + /** * Custom version of the {@link BuiltinIssueRegistry}. This * variation will filter the default issues and remove @@ -34,6 +33,26 @@ import java.util.List; * of some issues with IDEA specific ones. */ public class IntellijLintIssueRegistry extends BuiltinIssueRegistry { + private static final Implementation DUMMY_IMPLEMENTATION = new Implementation(Detector.class, + EnumSet.noneOf(Scope.class)); + + private static final String CUSTOM_EXPLANATION = + "When custom (third-party) lint rules are integrated in the IDE, they are not available as native IDE inspections, " + + "so the explanation text (which must be statically registered by a plugin) is not available. As a workaround, run the " + + "lint target in Gradle instead; the HTML report will include full explanations."; + + /** + * Issue reported by a custom rule (3rd party detector). We need a placeholder issue to reference for inspections, which + * have to be registered statically (can't load these on the fly from custom jars the way lint does) + */ + @NonNull + public static final Issue CUSTOM_WARNING = Issue.create( + "CustomWarning", "Warning from Custom Rule", CUSTOM_EXPLANATION, Category.CORRECTNESS, 5, Severity.WARNING, DUMMY_IMPLEMENTATION); + + @NonNull + public static final Issue CUSTOM_ERROR = Issue.create( + "CustomError", "Error from Custom Rule", CUSTOM_EXPLANATION, Category.CORRECTNESS, 5, Severity.ERROR, DUMMY_IMPLEMENTATION); + private static List ourFilteredIssues; public IntellijLintIssueRegistry() { @@ -56,7 +75,7 @@ public class IntellijLintIssueRegistry extends BuiltinIssueRegistry { scope.contains(Scope.ALL_CLASS_FILES) || scope.contains(Scope.JAVA_LIBRARIES)) { //noinspection ConstantConditions - assert !IntellijLintProject.SUPPORT_CLASS_FILES; // When enabled, adjust this to include class detector based issues + assert !SUPPORT_CLASS_FILES; // When enabled, adjust this to include class detector based issues boolean isOk = false; for (EnumSet analysisScope : implementation.getAnalysisScopes()) { diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java index 4c1281b350e..7ae4ee2b5be 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java @@ -19,9 +19,10 @@ import com.android.annotations.NonNull; import com.android.builder.model.*; import com.android.sdklib.AndroidTargetHash; import com.android.sdklib.AndroidVersion; +import com.android.tools.idea.gradle.AndroidGradleModel; import com.android.tools.idea.gradle.util.GradleUtil; +import com.android.tools.idea.model.AndroidModel; import com.android.tools.idea.model.AndroidModuleInfo; -import com.android.tools.idea.model.ManifestInfo; import com.android.tools.klint.client.api.LintClient; import com.android.tools.klint.detector.api.Project; import com.google.common.collect.Lists; @@ -42,6 +43,7 @@ import com.intellij.util.graph.Graph; import org.jetbrains.android.compiler.AndroidDexCompiler; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidRootUtil; +import org.jetbrains.android.facet.IdeaSourceProvider; import org.jetbrains.android.sdk.AndroidPlatform; import org.jetbrains.android.util.AndroidCommonUtils; import org.jetbrains.android.util.AndroidUtils; @@ -53,7 +55,6 @@ import java.util.*; import static com.android.SdkConstants.APPCOMPAT_LIB_ARTIFACT; import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT; -import static org.jetbrains.android.inspections.klint.IntellijLintUtils.*; /** * An {@linkplain IntellijLintProject} represents a lint project, which typically corresponds to a {@link Module}, @@ -146,7 +147,7 @@ class IntellijLintProject extends Project { client.setModuleMap(projectMap); //noinspection ConstantConditions - return Pair.create(project, main); + return Pair.create(project,main); } /** Find an Android module that depends on this module; prefer app modules over library modules */ @@ -156,10 +157,18 @@ class IntellijLintProject extends Project { Graph graph = ApplicationManager.getApplication().runReadAction(new Computable>() { @Override public Graph compute() { - return ModuleManager.getInstance(module.getProject()).moduleGraph(); + com.intellij.openapi.project.Project project = module.getProject(); + if (project.isDisposed()) { + return null; + } + return ModuleManager.getInstance(project).moduleGraph(); } }); + if (graph == null) { + return null; + } + Set facets = Sets.newHashSet(); HashSet seen = Sets.newHashSet(); seen.add(module); @@ -234,6 +243,8 @@ class IntellijLintProject extends Project { } List dependencies = Lists.newArrayList(); + // No, this shouldn't use getAllAndroidDependencies; we may have non-Android dependencies that this won't include + // (e.g. Java-only modules) List dependentFacets = AndroidUtils.getAllAndroidDependencies(module, true); for (AndroidFacet dependentFacet : dependentFacets) { Project p = moduleMap.get(dependentFacet.getModule()); @@ -245,8 +256,11 @@ class IntellijLintProject extends Project { } AndroidFacet facet = AndroidFacet.getInstance(module); - if (facet != null && isGradleModule(facet)) { - addGradleLibraryProjects(client, files, libraryMap, projects, facet, project, projectMap, dependencies); + if (facet != null) { + AndroidGradleModel androidGradleModel = AndroidGradleModel.get(facet); + if (androidGradleModel != null) { + addGradleLibraryProjects(client, files, libraryMap, projects, facet, androidGradleModel, project, projectMap, dependencies); + } } project.setDirectLibraries(dependencies); @@ -296,21 +310,28 @@ class IntellijLintProject extends Project { } dir = new File(FileUtil.toSystemDependentName(moduleDirPath)); } - LintModuleProject project; + LintModuleProject project = null; if (facet == null) { project = new LintModuleProject(client, dir, dir, module); AndroidFacet f = findAndroidFacetInProject(module.getProject()); if (f != null) { - project.mGradleProject = isGradleModule(f); + project.mGradleProject = f.requiresAndroidModel(); } } - else if (isGradleModule((facet))) { - project = new LintGradleProject(client, dir, dir, facet); + else if (facet.requiresAndroidModel()) { + AndroidModel androidModel = facet.getAndroidModel(); + if (androidModel instanceof AndroidGradleModel) { + project = new LintGradleProject(client, dir, dir, facet, (AndroidGradleModel)androidModel); + } else { + project = new LintAndroidModelProject(client, dir, dir, facet, androidModel); + } } else { project = new LintAndroidProject(client, dir, dir, facet); } - client.registerProject(dir, project); + if (project != null) { + client.registerProject(dir, project); + } return project; } @@ -337,42 +358,38 @@ class IntellijLintProject extends Project { @NonNull Map libraryMap, @NonNull List projects, @NonNull AndroidFacet facet, + @NonNull AndroidGradleModel androidGradleModel, @NonNull LintModuleProject project, @NonNull Map projectMap, @NonNull List dependencies) { - File dir; - AndroidModelFacade androidModelFacade = getModelFacade(facet); - Variant selectedVariant = androidModelFacade.getSelectedVariant(); - if (selectedVariant != null) { - Collection libraries = selectedVariant.getMainArtifact().getDependencies().getLibraries(); - for (AndroidLibrary library : libraries) { - Project p = libraryMap.get(library); - if (p == null) { - dir = library.getFolder(); - p = new LintGradleLibraryProject(client, dir, dir, library); - libraryMap.put(library, p); - projectMap.put(p, facet.getModule()); - projects.add(p); + Collection libraries = androidGradleModel.getMainArtifact().getDependencies().getLibraries(); + for (AndroidLibrary library : libraries) { + Project p = libraryMap.get(library); + if (p == null) { + File dir = library.getFolder(); + p = new LintGradleLibraryProject(client, dir, dir, library); + libraryMap.put(library, p); + projectMap.put(p, facet.getModule()); + projects.add(p); - if (files != null) { - VirtualFile libraryDir = LocalFileSystem.getInstance().findFileByIoFile(dir); - if (libraryDir != null) { - ListIterator iterator = files.listIterator(); - while (iterator.hasNext()) { - VirtualFile file = iterator.next(); - if (VfsUtilCore.isAncestor(libraryDir, file, false)) { - project.addFile(VfsUtilCore.virtualToIoFile(file)); - iterator.remove(); - } + if (files != null) { + VirtualFile libraryDir = LocalFileSystem.getInstance().findFileByIoFile(dir); + if (libraryDir != null) { + ListIterator iterator = files.listIterator(); + while (iterator.hasNext()) { + VirtualFile file = iterator.next(); + if (VfsUtilCore.isAncestor(libraryDir, file, false)) { + project.addFile(VfsUtilCore.virtualToIoFile(file)); + iterator.remove(); } } - if (files.isEmpty()) { - files = null; // No more work in other modules - } + } + if (files.isEmpty()) { + files = null; // No more work in other modules } } - dependencies.add(p); } + dependencies.add(p); } } @@ -465,7 +482,7 @@ class IntellijLintProject extends Project { @NonNull @Override - public List getJavaLibraries() { + public List getJavaLibraries(boolean includeProvided) { if (SUPPORT_CLASS_FILES) { if (mJavaLibraries == null) { mJavaLibraries = Lists.newArrayList(); @@ -542,7 +559,6 @@ class IntellijLintProject extends Project { @Override public List getProguardFiles() { if (mProguardFiles == null) { - assert !isGradleModule(myFacet); // Should be overridden to read from gradle state final JpsAndroidModuleProperties properties = myFacet.getProperties(); if (properties.RUN_PROGUARD) { @@ -611,44 +627,99 @@ class IntellijLintProject extends Project { } return mSupportLib; } else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) { - if (mAppCompat == null) { - mAppCompat = false; + if (mSupportLib == null) { final OrderEntry[] entries = ModuleRootManager.getInstance(myFacet.getModule()).getOrderEntries(); + libraries: for (int i = entries.length - 1; i >= 0; i--) { final OrderEntry orderEntry = entries[i]; - if (orderEntry instanceof ModuleOrderEntry) { - ModuleOrderEntry moduleOrderEntry = (ModuleOrderEntry)orderEntry; - Module module = moduleOrderEntry.getModule(); - if (module == null || module == myFacet.getModule()) { - continue; - } - AndroidFacet facet = AndroidFacet.getInstance(module); - if (facet == null) { - continue; - } - ManifestInfo manifestInfo = ManifestInfo.get(module, false); - if ("android.support.v7.appcompat".equals(manifestInfo.getPackage())) { - mAppCompat = true; - break; + if (orderEntry instanceof LibraryOrderEntry) { + LibraryOrderEntry libraryOrderEntry = (LibraryOrderEntry)orderEntry; + VirtualFile[] classes = libraryOrderEntry.getRootFiles(OrderRootType.CLASSES); + if (classes != null) { + for (VirtualFile file : classes) { + if (file.getName().equals("appcompat-v7.jar")) { + mSupportLib = true; + break libraries; + + } + } } } } + if (mSupportLib == null) { + mSupportLib = depsDependsOn(this, artifact); + } } - return mAppCompat; + return mSupportLib; } else { return super.dependsOn(artifact); } } } - private static class LintGradleProject extends LintAndroidProject { + private static class LintAndroidModelProject extends LintAndroidProject { + private final AndroidModel myAndroidModel; + + private LintAndroidModelProject( + @NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir, + @NonNull AndroidFacet facet, + @NonNull AndroidModel androidModel) { + super(client, dir, referenceDir, facet); + myAndroidModel = androidModel; + } + + @Nullable + @Override + public String getPackage() { + String manifestPackage = super.getPackage(); + // For now, lint only needs the manifest package; not the potentially variant specific + // package. As part of the Gradle work on the Lint API we should make two separate + // package lookup methods -- one for the manifest package, one for the build package + if (manifestPackage != null) { + return manifestPackage; + } + + return myAndroidModel.getApplicationId(); + } + + @NonNull + @Override + public AndroidVersion getMinSdkVersion() { + if (mMinSdkVersion == null) { + mMinSdkVersion = AndroidModuleInfo.get(myFacet).getMinSdkVersion(); + } + return mMinSdkVersion; + } + + @NonNull + @Override + public AndroidVersion getTargetSdkVersion() { + if (mTargetSdkVersion == null) { + mTargetSdkVersion = AndroidModuleInfo.get(myFacet).getTargetSdkVersion(); + } + + return mTargetSdkVersion; + } + } + + private static class LintGradleProject extends LintAndroidModelProject { + private final AndroidGradleModel myAndroidGradleModel; + /** * Creates a new Project. Use one of the factory methods to create. */ - private LintGradleProject(@NonNull LintClient client, @NonNull File dir, @NonNull File referenceDir, @NonNull AndroidFacet facet) { - super(client, dir, referenceDir, facet); + private LintGradleProject( + @NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir, + @NonNull AndroidFacet facet, + @NonNull AndroidGradleModel androidGradleModel) { + super(client, dir, referenceDir, facet, androidGradleModel); mGradleProject = true; mMergeManifests = true; + myAndroidGradleModel = androidGradleModel; } @NonNull @@ -661,7 +732,7 @@ class IntellijLintProject extends Project { mManifestFiles.add(mainManifest); } - List flavorSourceProviders = getModelFacade(myFacet).getFlavorSourceProviders(); + List flavorSourceProviders = myAndroidGradleModel.getFlavorSourceProviders(); if (flavorSourceProviders != null) { for (SourceProvider provider : flavorSourceProviders) { File manifestFile = provider.getManifestFile(); @@ -671,7 +742,7 @@ class IntellijLintProject extends Project { } } - SourceProvider multiProvider = getModelFacade(myFacet).getMultiFlavorSourceProvider(); + SourceProvider multiProvider = myAndroidGradleModel.getMultiFlavorSourceProvider(); if (multiProvider != null) { File manifestFile = multiProvider.getManifestFile(); if (manifestFile.exists()) { @@ -679,7 +750,7 @@ class IntellijLintProject extends Project { } } - SourceProvider buildTypeSourceProvider = getModelFacade(myFacet).getBuildTypeSourceProvider(); + SourceProvider buildTypeSourceProvider = myAndroidGradleModel.getBuildTypeSourceProvider(); if (buildTypeSourceProvider != null) { File manifestFile = buildTypeSourceProvider.getManifestFile(); if (manifestFile.exists()) { @@ -687,7 +758,7 @@ class IntellijLintProject extends Project { } } - SourceProvider variantProvider = getModelFacade(myFacet).getVariantSourceProvider(); + SourceProvider variantProvider = myAndroidGradleModel.getVariantSourceProvider(); if (variantProvider != null) { File manifestFile = variantProvider.getManifestFile(); if (manifestFile.exists()) { @@ -699,15 +770,33 @@ class IntellijLintProject extends Project { return mManifestFiles; } + @NonNull + @Override + public List getAssetFolders() { + if (mAssetFolders == null) { + mAssetFolders = Lists.newArrayList(); + for (SourceProvider provider : IdeaSourceProvider.getAllSourceProviders(myFacet)) { + Collection dirs = provider.getAssetsDirectories(); + for (File dir : dirs) { + if (dir.exists()) { // model returns path whether or not it exists + mAssetFolders.add(dir); + } + } + } + } + + return mAssetFolders; + } + @NonNull @Override public List getProguardFiles() { if (mProguardFiles == null) { - if (isGradleModule(myFacet)) { - AndroidModelFacade androidModelFacade = getModelFacade(myFacet); - AndroidProject androidProject = androidModelFacade.getAndroidProject(); - if (androidProject != null) { - ProductFlavor flavor = androidProject.getDefaultConfig().getProductFlavor(); + if (myFacet.requiresAndroidModel()) { + // TODO: b/22928250 + AndroidGradleModel androidModel = AndroidGradleModel.get(myFacet); + if (androidModel != null) { + ProductFlavor flavor = androidModel.getAndroidProject().getDefaultConfig().getProductFlavor(); mProguardFiles = Lists.newArrayList(); for (File file : flavor.getProguardFiles()) { if (file.exists()) { @@ -744,14 +833,7 @@ class IntellijLintProject extends Project { // Overridden because we don't synchronize the gradle output directory to // the AndroidDexCompiler settings the way java source roots are mapped into // the module content root settings - File dir = null; - if (isGradleModule(myFacet)) { - AndroidModelFacade androidModelFacade = getModelFacade(myFacet); - Variant variant = androidModelFacade.getSelectedVariant(); - if (variant != null) { - dir = variant.getMainArtifact().getClassesFolder(); - } - } + File dir = myAndroidGradleModel.getMainArtifact().getClassesFolder(); if (dir != null) { mJavaClassFolders = Collections.singletonList(dir); } else { @@ -765,24 +847,40 @@ class IntellijLintProject extends Project { return Collections.emptyList(); } + private static boolean sProvidedAvailable = true; + @NonNull @Override - public List getJavaLibraries() { + public List getJavaLibraries(boolean includeProvided) { if (SUPPORT_CLASS_FILES) { if (mJavaLibraries == null) { - AndroidModelFacade androidModelFacade = getModelFacade(myFacet); - Variant selectedVariant = androidModelFacade.getSelectedVariant(); - if (isGradleModule(myFacet) && selectedVariant != null) { - Collection libs = selectedVariant.getMainArtifact().getDependencies().getJavaLibraries(); + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) { + Collection libs = myAndroidGradleModel.getMainArtifact().getDependencies().getJavaLibraries(); mJavaLibraries = Lists.newArrayListWithExpectedSize(libs.size()); for (JavaLibrary lib : libs) { + if (!includeProvided) { + if (sProvidedAvailable) { + // Method added in 1.4-rc1; gracefully handle running with + // older plugins + try { + if (lib.isProvided()) { + continue; + } + } + catch (Throwable t) { + //noinspection AssignmentToStaticFieldFromInstanceMethod + sProvidedAvailable = false; // don't try again + } + } + } + File jar = lib.getJarFile(); if (jar.exists()) { mJavaLibraries.add(jar); } } } else { - mJavaLibraries = super.getJavaLibraries(); + mJavaLibraries = super.getJavaLibraries(includeProvided); } } return mJavaLibraries; @@ -791,55 +889,21 @@ class IntellijLintProject extends Project { return Collections.emptyList(); } - @Nullable - @Override - public String getPackage() { - String manifestPackage = super.getPackage(); - // For now, lint only needs the manifest package; not the potentially variant specific - // package. As part of the Gradle work on the Lint API we should make two separate - // package lookup methods -- one for the manifest package, one for the build package - if (manifestPackage != null) { - return manifestPackage; - } - - return getModelFacade(myFacet).computePackageName(); - } - - @NonNull - @Override - public AndroidVersion getMinSdkVersion() { - if (mMinSdkVersion == null) { - mMinSdkVersion = AndroidModuleInfo.get(myFacet).getMinSdkVersion(); - } - - return mMinSdkVersion; - } - - @NonNull - @Override - public AndroidVersion getTargetSdkVersion() { - if (mTargetSdkVersion == null) { - mTargetSdkVersion = AndroidModuleInfo.get(myFacet).getTargetSdkVersion(); - } - - return mTargetSdkVersion; - } - @Override public int getBuildSdk() { - AndroidModelFacade androidModelFacade = getModelFacade(myFacet); - AndroidProject androidProject = androidModelFacade.getAndroidProject(); - if (androidProject != null) { - String compileTarget = androidProject.getCompileTarget(); + // TODO: b/22928250 + AndroidGradleModel androidModel = AndroidGradleModel.get(myFacet); + if (androidModel != null) { + String compileTarget = androidModel.getAndroidProject().getCompileTarget(); AndroidVersion version = AndroidTargetHash.getPlatformVersion(compileTarget); if (version != null) { - return version.getApiLevel(); + return version.getFeatureLevel(); } } AndroidPlatform platform = AndroidPlatform.getInstance(myFacet.getModule()); if (platform != null) { - return platform.getApiLevel(); + return platform.getApiVersion().getFeatureLevel(); } return super.getBuildSdk(); @@ -848,14 +912,25 @@ class IntellijLintProject extends Project { @Nullable @Override public AndroidProject getGradleProjectModel() { - AndroidModelFacade androidModelFacade = getModelFacade(myFacet); - return androidModelFacade.getAndroidProject(); + // TODO: b/22928250 + AndroidGradleModel androidModel = AndroidGradleModel.get(myFacet); + if (androidModel != null) { + return androidModel.getAndroidProject(); + } + + return null; } @Nullable @Override public Variant getCurrentVariant() { - return getModelFacade(myFacet).getSelectedVariant(); + // TODO: b/22928250 + AndroidGradleModel androidModel = AndroidGradleModel.get(myFacet); + if (androidModel != null) { + return androidModel.getSelectedVariant(); + } + + return null; } @Nullable @@ -867,11 +942,13 @@ class IntellijLintProject extends Project { @Nullable @Override public Boolean dependsOn(@NonNull String artifact) { + // TODO: b/22928250 + AndroidGradleModel androidModel = AndroidGradleModel.get(myFacet); + if (SUPPORT_LIB_ARTIFACT.equals(artifact)) { if (mSupportLib == null) { - AndroidModelFacade facade = getModelFacade(myFacet); - if (isGradleModule(myFacet) && facade.isModelReady()) { - mSupportLib = facade.getDependsOn(artifact); + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) { + mSupportLib = GradleUtil.dependsOn(androidModel, artifact); } else { mSupportLib = depsDependsOn(this, artifact); } @@ -879,9 +956,8 @@ class IntellijLintProject extends Project { return mSupportLib; } else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) { if (mAppCompat == null) { - AndroidModelFacade facade = getModelFacade(myFacet); - if (isGradleModule(myFacet) && facade.isModelReady()) { - mAppCompat = facade.getDependsOn(artifact); + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) { + mAppCompat = GradleUtil.dependsOn(androidModel, artifact); } else { mAppCompat = depsDependsOn(this, artifact); } @@ -889,9 +965,8 @@ class IntellijLintProject extends Project { return mAppCompat; } else { // Some other (not yet directly cached result) - AndroidModelFacade facade = getModelFacade(myFacet); - if (isGradleModule(myFacet) && facade.isModelReady() - && facade.getDependsOn(artifact)) { + if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null + && GradleUtil.dependsOn(androidModel, artifact)) { return true; } @@ -974,10 +1049,28 @@ class IntellijLintProject extends Project { return Collections.emptyList(); } + private static boolean sOptionalAvailable = true; + @NonNull @Override - public List getJavaLibraries() { + public List getJavaLibraries(boolean includeProvided) { if (SUPPORT_CLASS_FILES) { + if (!includeProvided) { + if (sOptionalAvailable) { + // Method added in 1.4-rc1; gracefully handle running with + // older plugins + try { + if (myLibrary.isOptional()) { + return Collections.emptyList(); + } + } + catch (Throwable t) { + //noinspection AssignmentToStaticFieldFromInstanceMethod + sOptionalAvailable = false; // don't try again + } + } + } + if (mJavaLibraries == null) { mJavaLibraries = Lists.newArrayList(); File jarFile = myLibrary.getJarFile(); diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintRequest.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintRequest.java index 497db06767d..13b340d72e6 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintRequest.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintRequest.java @@ -92,7 +92,7 @@ public class IntellijLintRequest extends LintRequest { public Collection getProjects() { if (mProjects == null) { if (myIncremental && myFileList != null && myFileList.size() == 1 && myModules.size() == 1) { - Pair pair = + Pair pair = IntellijLintProject.createForSingleFile(mLintClient, myFileList.get(0), myModules.get(0)); mProjects = pair.first != null ? Collections.singletonList(pair.first) : Collections.emptyList(); diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintUtils.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintUtils.java index bdcf145c93d..b06496db8d8 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintUtils.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintUtils.java @@ -19,20 +19,13 @@ import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.builder.model.SourceProvider; +import com.android.tools.idea.AndroidPsiUtils; +import com.android.tools.idea.model.AndroidModel; import com.android.tools.klint.client.api.LintRequest; -import com.android.tools.klint.detector.api.ClassContext; -import com.android.tools.klint.detector.api.Context; -import com.android.tools.klint.detector.api.DefaultPosition; -import com.android.tools.klint.detector.api.Issue; -import com.android.tools.klint.detector.api.Location; -import com.android.tools.klint.detector.api.Position; +import com.android.tools.klint.detector.api.*; import com.google.common.base.Splitter; import com.intellij.debugger.engine.JVMNameUtil; -import com.intellij.facet.Facet; import com.intellij.ide.util.JavaAnonymousClassesHelper; -import com.intellij.openapi.externalSystem.model.ProjectSystemId; -import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil; -import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.io.FileUtil; @@ -42,17 +35,18 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.util.ClassUtil; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.TypeConversionUtil; import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UFile; -import org.jetbrains.uast.java.JavaUClass; import java.io.File; import java.util.ArrayList; import java.util.List; +import static com.android.SdkConstants.CONSTRUCTOR_NAME; +import static com.android.SdkConstants.SUPPRESS_ALL; + /** * Common utilities for handling lint within IntelliJ * TODO: Merge with {@link AndroidLintUtil} @@ -61,7 +55,10 @@ public class IntellijLintUtils { private IntellijLintUtils() { } - private static final ProjectSystemId GRADLE_ID = new ProjectSystemId("GRADLE"); + @NonNls + public static final String SUPPRESS_LINT_FQCN = "android.annotation.SuppressLint"; + @NonNls + public static final String SUPPRESS_WARNINGS_FQCN = "java.lang.SuppressWarnings"; /** * Gets the location of the given element @@ -108,13 +105,113 @@ public class IntellijLintUtils { if (project.isDisposed()) { return null; } - return PsiManager.getInstance(project).findFile(file); + return AndroidPsiUtils.getPsiFileSafely(project, file); } - public static boolean isSuppressed(@NonNull UElement element, @NonNull UFile file, @NonNull Issue issue) { + /** + * Returns true if the given issue is suppressed at the given element within the given file + * + * @param element the element to check + * @param file the file containing the element + * @param issue the issue to check + * @return true if the given issue is suppressed + */ + public static boolean isSuppressed(@NonNull PsiElement element, @NonNull PsiFile file, @NonNull Issue issue) { + // Search upwards for suppress lint and suppress warnings annotations + //noinspection ConstantConditions + while (element != null && element != file) { // otherwise it will keep going into directories! + if (element instanceof PsiModifierListOwner) { + PsiModifierListOwner owner = (PsiModifierListOwner)element; + PsiModifierList modifierList = owner.getModifierList(); + if (modifierList != null) { + for (PsiAnnotation annotation : modifierList.getAnnotations()) { + String fqcn = annotation.getQualifiedName(); + if (fqcn != null && (fqcn.equals(SUPPRESS_LINT_FQCN) || fqcn.equals(SUPPRESS_WARNINGS_FQCN))) { + PsiAnnotationParameterList parameterList = annotation.getParameterList(); + for (PsiNameValuePair pair : parameterList.getAttributes()) { + PsiAnnotationMemberValue v = pair.getValue(); + if (v instanceof PsiLiteral) { + PsiLiteral literal = (PsiLiteral)v; + Object value = literal.getValue(); + if (value instanceof String) { + if (isSuppressed(issue, (String) value)) { + return true; + } + } + } else if (v instanceof PsiArrayInitializerMemberValue) { + PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue)v; + for (PsiAnnotationMemberValue mmv : mv.getInitializers()) { + if (mmv instanceof PsiLiteral) { + PsiLiteral literal = (PsiLiteral) mmv; + Object value = literal.getValue(); + if (value instanceof String) { + if (isSuppressed(issue, (String) value)) { + return true; + } + } + } + } + } else if (v != null) { + // This shouldn't be necessary + String text = v.getText().trim(); // UGH! Find better way to access value! + if (!text.isEmpty() && isSuppressed(issue, text)) { + return true; + } + } + } + } + } + } + } + element = element.getParent(); + } + return false; } + /** + * Returns true if the given issue is suppressed by the given suppress string; this + * is typically the same as the issue id, but is allowed to not match case sensitively, + * and is allowed to be a comma separated list, and can be the string "all" + * + * @param issue the issue id to match + * @param string the suppress string -- typically the id, or "all", or a comma separated list of ids + * @return true if the issue is suppressed by the given string + */ + private static boolean isSuppressed(@NonNull Issue issue, @NonNull String string) { + for (String id : Splitter.on(',').trimResults().split(string)) { + if (id.equals(issue.getId()) || id.equals(SUPPRESS_ALL)) { + return true; + } + } + + return false; + } + + /** Returns the internal method name */ + @NonNull + public static String getInternalMethodName(@NonNull PsiMethod method) { + if (method.isConstructor()) { + return SdkConstants.CONSTRUCTOR_NAME; + } + else { + return method.getName(); + } + } + + @Nullable + public static PsiElement getCallName(@NonNull PsiCallExpression expression) { + PsiElement firstChild = expression.getFirstChild(); + if (firstChild != null) { + PsiElement lastChild = firstChild.getLastChild(); + if (lastChild instanceof PsiIdentifier) { + return lastChild; + } + } + return null; + } + + /** * Computes the internal class name of the given class. * For example, for PsiClass foo.bar.Foo.Bar it returns foo/bar/Foo$Bar. @@ -150,54 +247,145 @@ public class IntellijLintUtils { return sig; } - public static String getInternalName(@NotNull UClass clazz) { - if (clazz instanceof JavaUClass) { - return getInternalName(((JavaUClass) clazz).getPsi()); - } else { - return null; + /** + * Computes the internal class name of the given class type. + * For example, for PsiClassType foo.bar.Foo.Bar it returns foo/bar/Foo$Bar. + * + * @param psiClassType the class type to look up the internal name for + * @return the internal class name + * @see ClassContext#getInternalName(String) + */ + @Nullable + public static String getInternalName(@NonNull PsiClassType psiClassType) { + PsiClass resolved = psiClassType.resolve(); + if (resolved != null) { + return getInternalName(resolved); } + + String className = psiClassType.getClassName(); + if (className != null) { + return ClassContext.getInternalName(className); + } + + return null; + } + + /** + * Computes the internal JVM description of the given method. This is in the same + * format as the ASM desc fields for methods; meaning that a method named foo which for example takes an + * int and a String and returns a void will have description {@code foo(ILjava/lang/String;):V}. + * + * @param method the method to look up the description for + * @param includeName whether the name should be included + * @param includeReturn whether the return type should be included + * @return the internal JVM description for this method + */ + @Nullable + public static String getInternalDescription(@NonNull PsiMethod method, boolean includeName, boolean includeReturn) { + assert !includeName; // not yet tested + assert !includeReturn; // not yet tested + + StringBuilder signature = new StringBuilder(); + + if (includeName) { + if (method.isConstructor()) { + final PsiClass declaringClass = method.getContainingClass(); + if (declaringClass != null) { + final PsiClass outerClass = declaringClass.getContainingClass(); + if (outerClass != null) { + // declaring class is an inner class + if (!declaringClass.hasModifierProperty(PsiModifier.STATIC)) { + if (!appendJvmTypeName(signature, outerClass)) { + return null; + } + } + } + } + signature.append(CONSTRUCTOR_NAME); + } else { + signature.append(method.getName()); + } + } + + signature.append('('); + + for (PsiParameter psiParameter : method.getParameterList().getParameters()) { + if (!appendJvmSignature(signature, psiParameter.getType())) { + return null; + } + } + signature.append(')'); + if (includeReturn) { + if (!method.isConstructor()) { + if (!appendJvmSignature(signature, method.getReturnType())) { + return null; + } + } + else { + signature.append('V'); + } + } + return signature.toString(); + } + + private static boolean appendJvmTypeName(@NonNull StringBuilder signature, @NonNull PsiClass outerClass) { + String className = getInternalName(outerClass); + if (className == null) { + return false; + } + signature.append('L').append(className.replace('.', '/')).append(';'); + return true; } public static AndroidModelFacade getModelFacade(AndroidFacet facet) { return new AndroidModelFacade(facet); } - public static boolean isGradleModule(Facet facet) { - Module module = facet.getModule(); - return ExternalSystemApiUtil.isExternalSystemAwareModule(GRADLE_ID, module); + private static boolean appendJvmSignature(@NonNull StringBuilder buffer, @Nullable PsiType type) { + if (type == null) { + return false; + } + final PsiType psiType = TypeConversionUtil.erasure(type); + if (psiType instanceof PsiArrayType) { + buffer.append('['); + appendJvmSignature(buffer, ((PsiArrayType)psiType).getComponentType()); + } + else if (psiType instanceof PsiClassType) { + PsiClass resolved = ((PsiClassType)psiType).resolve(); + if (resolved == null) { + return false; + } + if (!appendJvmTypeName(buffer, resolved)) { + return false; + } + } + else if (psiType instanceof PsiPrimitiveType) { + buffer.append(JVMNameUtil.getPrimitiveSignature(psiType.getCanonicalText())); + } + else { + return false; + } + return true; } /** Returns the resource directories to use for the given module */ @NotNull public static List getResourceDirectories(@NotNull AndroidFacet facet) { - if (isGradleModule(facet)) { - List resDirectories = new ArrayList(); - resDirectories.addAll(facet.getMainSourceProvider().getResDirectories()); - List flavorSourceProviders = getModelFacade(facet).getFlavorSourceProviders(); - if (flavorSourceProviders != null) { - for (SourceProvider provider : flavorSourceProviders) { + if (facet.requiresAndroidModel()) { + AndroidModel androidModel = facet.getAndroidModel(); + if (androidModel != null) { + List resDirectories = new ArrayList(); + List sourceProviders = androidModel.getActiveSourceProviders(); + for (SourceProvider provider : sourceProviders) { for (File file : provider.getResDirectories()) { if (file.isDirectory()) { resDirectories.add(file); } } } + return resDirectories; } - - SourceProvider buildTypeSourceProvider = getModelFacade(facet).getBuildTypeSourceProvider(); - if (buildTypeSourceProvider != null) { - for (File file : buildTypeSourceProvider.getResDirectories()) { - if (file.isDirectory()) { - resDirectories.add(file); - } - } - } - - return resDirectories; - } else { - return new ArrayList(facet.getMainSourceProvider().getResDirectories()); } + return new ArrayList(facet.getMainSourceProvider().getResDirectories()); } - - } diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijViewTypeDetector.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijViewTypeDetector.java index 6bb95d42253..fdd8f32f31e 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijViewTypeDetector.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijViewTypeDetector.java @@ -31,7 +31,7 @@ import java.util.Collections; public class IntellijViewTypeDetector extends ViewTypeDetector { static final Implementation IMPLEMENTATION = new Implementation( IntellijViewTypeDetector.class, - Scope.SOURCE_FILE_SCOPE); + Scope.JAVA_FILE_SCOPE); @Nullable @Override diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/KotlinLintLanguageExtension.kt b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/KotlinLintLanguageExtension.kt deleted file mode 100644 index e8269d99d9f..00000000000 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/KotlinLintLanguageExtension.kt +++ /dev/null @@ -1,25 +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 org.jetbrains.android.inspections.klint - -import com.android.tools.klint.client.api.LintLanguageExtension -import org.jetbrains.kotlin.uast.KotlinUastLanguagePlugin - -class KotlinLintLanguageExtension : LintLanguageExtension() { - override val converter = KotlinUastLanguagePlugin.converter - override val visitorExtensions = KotlinUastLanguagePlugin.visitorExtensions -} \ No newline at end of file diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/LintInspectionDescriptionLinkHandler.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/LintInspectionDescriptionLinkHandler.java index a9782bd7fcf..dd910a3ff8c 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/LintInspectionDescriptionLinkHandler.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/LintInspectionDescriptionLinkHandler.java @@ -19,16 +19,19 @@ import com.android.tools.klint.checks.BuiltinIssueRegistry; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.TextFormat; import com.intellij.codeInsight.highlighting.TooltipLinkHandler; +import com.intellij.codeInspection.InspectionProfile; import com.intellij.codeInspection.InspectionsBundle; +import com.intellij.codeInspection.ex.InspectionToolWrapper; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; +import com.intellij.profile.codeInspection.InspectionProfileManager; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.NotNull; public class LintInspectionDescriptionLinkHandler extends TooltipLinkHandler { - private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.inspections.klint.LintInspectionDescriptionLinkHandler"); + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.inspections.lint.LintInspectionDescriptionLinkHandler"); @Override public String getDescription(@NotNull final String refSuffix, @NotNull final Editor editor) { diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/ProblemData.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/ProblemData.java index 33abc54e177..ad5fd896dc6 100644 --- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/ProblemData.java +++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/ProblemData.java @@ -6,6 +6,9 @@ import com.intellij.openapi.util.TextRange; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * @author Eugene.Kudelevsky + */ public class ProblemData { private final Issue myIssue; private final String myMessage; diff --git a/plugins/lint/uast-android/src/org/jetbrains/uast/check/UastChecker.kt b/plugins/lint/uast-android/src/org/jetbrains/uast/check/UastChecker.kt deleted file mode 100644 index 4aeebe67372..00000000000 --- a/plugins/lint/uast-android/src/org/jetbrains/uast/check/UastChecker.kt +++ /dev/null @@ -1,147 +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 org.jetbrains.uast.check - -import com.android.tools.klint.detector.api.Issue -import com.android.tools.klint.detector.api.JavaContext -import com.android.tools.klint.detector.api.Location -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.project.DumbService -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.psi.PsiJavaFile -import com.intellij.psi.PsiManager -import org.jetbrains.uast.* -import org.jetbrains.uast.UastCallKind.Companion.CONSTRUCTOR_CALL -import org.jetbrains.uast.UastCallKind.Companion.FUNCTION_CALL -import org.jetbrains.uast.java.JavaUastLanguagePlugin -import org.jetbrains.uast.visitor.AbstractUastVisitor -import org.jetbrains.uast.visitor.UastExtendableVisitor -import org.jetbrains.uast.visitor.UastVisitor -import java.io.File - -interface UastAndroidContext : UastContext { - val lintContext: JavaContext - fun report(issue: Issue, element: UElement, location: Location?, message: String) - - fun getLocation(element: UElement?) = element?.getLocation() -} - -object UastChecker { - fun check(project: Project, file: File, context: UastAndroidContext, visitor: UastVisitor) { - ProgressManager.checkCanceled() - - val vfile = VirtualFileManager.getInstance().findFileByUrl("file://" + file.absolutePath) ?: return - - val plugins = context.languagePlugins - val extendableVisitor = UastExtendableVisitor(visitor, context, plugins.flatMap { it.visitorExtensions }) - - val instance = DumbService.getInstance(project) - // Do not check anything in dumb mode - if (instance.isDumb) return - - instance.runReadActionInSmartMode { - val psiFile = PsiManager.getInstance(project).findFile(vfile) - - if (psiFile != null) { - when (psiFile) { - is PsiJavaFile -> { - val ufile = JavaUastLanguagePlugin.converter.convertWithParent(psiFile) - ufile?.accept(extendableVisitor) - } - else -> for (plugin in plugins) { - val ufile = plugin.converter.convertWithParent(psiFile) - if (ufile != null) { - ufile.accept(extendableVisitor) - break - } - } - } - } - } - } - - fun check(project: Project, file: File, scanner: UastScanner, context: UastAndroidContext) { - val applicableFunctionNames = scanner.applicableFunctionNames ?: emptyList() - val applicableSuperClasses = scanner.applicableSuperClasses ?: emptyList() - val applicableConstructorTypes = scanner.applicableConstructorTypes ?: emptyList() - - val appliesToResourcesRefs = scanner.appliesToResourceRefs() - - val visitor = object : AbstractUastVisitor() { - override fun visitCallExpression(node: UCallExpression): Boolean { - ProgressManager.checkCanceled() - if (applicableFunctionNames.isNotEmpty()) { - if (node.kind == FUNCTION_CALL && node.functionName in applicableFunctionNames) { - scanner.visitCall(context, node) - } - } - - if (applicableConstructorTypes.isNotEmpty()) { - if (node.kind == CONSTRUCTOR_CALL) { - node.resolve(context)?.let { constructor -> - if (constructor.getContainingClass()?.fqName in applicableConstructorTypes) { - scanner.visitConstructor(context, node, constructor) - } - } - } - } - - return false - } - - override fun visitClass(node: UClass): Boolean { - ProgressManager.checkCanceled() - if (applicableSuperClasses.isNotEmpty()) { - if (applicableSuperClasses.any { node.isSubclassOf(it) }) { - scanner.visitClass(context, node) - } - } - - return false - } - - override fun visitQualifiedExpression(node: UQualifiedExpression): Boolean { - ProgressManager.checkCanceled() - if (appliesToResourcesRefs && node.receiver is UQualifiedExpression) { - val parentQualifiedExpr = node.receiver as UQualifiedExpression - val resourceName = node.selector - val resourceType = parentQualifiedExpr.selector - val receiver = parentQualifiedExpr.receiver - - val receiverIsResourceClass = when (receiver) { - is USimpleReferenceExpression -> receiver.identifier == "R" - is UQualifiedExpression -> receiver.selectorMatches("R") - else -> false - } - - if (resourceName is USimpleReferenceExpression && resourceType is USimpleReferenceExpression - && receiverIsResourceClass && receiver is UResolvable) { - val resolvedReceiver = receiver.resolve(context) - val isFramework = (resolvedReceiver as? UClass)?.matchesFqName("android.R") ?: false - - scanner.visitResourceReference(context, node, resourceType.identifier, resourceName.identifier, isFramework) - } - } - - return false - } - } - - check(project, file, context, visitor) - } - -} \ No newline at end of file diff --git a/plugins/lint/uast-android/src/org/jetbrains/uast/check/UastScanner.java b/plugins/lint/uast-android/src/org/jetbrains/uast/check/UastScanner.java deleted file mode 100644 index 1940452596d..00000000000 --- a/plugins/lint/uast-android/src/org/jetbrains/uast/check/UastScanner.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2000-2015 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 org.jetbrains.uast.check; - -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UFunction; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.visitor.UastVisitor; - -import java.util.List; - -public interface UastScanner { - UastVisitor createUastVisitor(UastAndroidContext context); - - List getApplicableFunctionNames(); - List getApplicableSuperClasses(); - List getApplicableConstructorTypes(); - - void visitCall(UastAndroidContext context, UCallExpression node); - void visitClass(UastAndroidContext context, UClass node); - void visitConstructor(UastAndroidContext context, UCallExpression functionCall, UFunction constructor); - - boolean appliesToResourceRefs(); - void visitResourceReference(UastAndroidContext context, UElement element, String type, String name, boolean isFramework); -} diff --git a/plugins/lint/uast-android/src/org/jetbrains/uast/check/androidUtils.kt b/plugins/lint/uast-android/src/org/jetbrains/uast/check/androidUtils.kt deleted file mode 100644 index 8817ea00fb6..00000000000 --- a/plugins/lint/uast-android/src/org/jetbrains/uast/check/androidUtils.kt +++ /dev/null @@ -1,32 +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. - */ -@file:JvmName("UastAndroidUtils") -package org.jetbrains.uast.check - -import com.android.tools.klint.detector.api.Location -import com.intellij.openapi.vfs.VfsUtilCore -import org.jetbrains.uast.UElement -import org.jetbrains.uast.psi.PsiElementBacked - -fun UElement?.getLocation(): Location? { - val psiElementBacked = this as? PsiElementBacked ?: return null - val psiElement = psiElementBacked.psi - val psiFile = psiElement?.containingFile ?: return null - val vfile = psiFile.virtualFile - val file = VfsUtilCore.virtualToIoFile(vfile) - val range = psiElement.textRange ?: return null - return Location.create(file, psiFile.text, range.startOffset, range.endOffset) -} \ No newline at end of file diff --git a/plugins/lint/uast-android/uast-android.iml b/plugins/lint/uast-android/uast-android.iml deleted file mode 100644 index 9bd119efcd1..00000000000 --- a/plugins/lint/uast-android/uast-android.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file