Remove 173 bunch files

This commit is contained in:
Vyacheslav Gerasimov
2019-01-14 15:34:41 +03:00
parent 96c21297b6
commit 818910267e
475 changed files with 0 additions and 82220 deletions
@@ -1,77 +0,0 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.android.model.impl
import com.android.builder.model.SourceProvider
import com.android.tools.idea.gradle.project.GradleProjectInfo
import com.android.tools.idea.gradle.project.model.AndroidModuleModel
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.android.facet.AndroidFacet
import org.jetbrains.kotlin.android.model.AndroidModuleInfoProvider
import java.io.File
class AndroidModuleInfoProviderImpl(override val module: Module) : AndroidModuleInfoProvider {
private val androidFacet: AndroidFacet?
get() = AndroidFacet.getInstance(module)
private val androidModuleModel: AndroidModuleModel?
get() = AndroidModuleModel.get(module)
override fun isAndroidModule() = androidFacet != null
override fun isGradleModule() = GradleProjectInfo.getInstance(module.project).isBuildWithGradle
override fun getAllResourceDirectories(): List<VirtualFile> {
return androidFacet?.allResourceDirectories ?: emptyList()
}
override fun getApplicationPackage() = androidFacet?.manifest?.`package`?.toString()
override fun getMainSourceProvider(): AndroidModuleInfoProvider.SourceProviderMirror? {
return androidFacet?.mainSourceProvider?.let(::SourceProviderMirrorImpl)
}
override fun getApplicationResourceDirectories(createIfNecessary: Boolean): Collection<VirtualFile> {
return androidFacet?.getAppResources(createIfNecessary)?.resourceDirs ?: emptyList()
}
override fun getAllSourceProviders(): List<AndroidModuleInfoProvider.SourceProviderMirror> {
val androidModuleModel = this.androidModuleModel ?: return emptyList()
return androidModuleModel.allSourceProviders.map(::SourceProviderMirrorImpl)
}
override fun getActiveSourceProviders(): List<AndroidModuleInfoProvider.SourceProviderMirror> {
val androidModuleModel = this.androidModuleModel ?: return emptyList()
return androidModuleModel.activeSourceProviders.map(::SourceProviderMirrorImpl)
}
override fun getFlavorSourceProviders(): List<AndroidModuleInfoProvider.SourceProviderMirror> {
val androidModuleModel = this.androidModuleModel ?: return emptyList()
val getFlavorSourceProvidersMethod = try {
AndroidFacet::class.java.getMethod("getFlavorSourceProviders")
} catch (e: NoSuchMethodException) {
null
}
return if (getFlavorSourceProvidersMethod != null) {
@Suppress("UNCHECKED_CAST")
val sourceProviders = getFlavorSourceProvidersMethod.invoke(androidFacet) as? List<SourceProvider>
sourceProviders?.map(::SourceProviderMirrorImpl) ?: emptyList()
} else {
androidModuleModel.flavorSourceProviders.map(::SourceProviderMirrorImpl)
}
}
private class SourceProviderMirrorImpl(val sourceProvider: SourceProvider) :
AndroidModuleInfoProvider.SourceProviderMirror {
override val name: String
get() = sourceProvider.name
override val resDirectories: Collection<File>
get() = sourceProvider.resDirectories
}
}
@@ -1,35 +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.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
/**
* Denotes that a parameter, field or method return value can never be null.
* <p/>
* This is a marker annotation and it has no specific attributes.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({METHOD,PARAMETER,LOCAL_VARIABLE,FIELD})
public @interface NonNull {
}
@@ -1,47 +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.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.TYPE;
/**
* Denotes that all parameters, fields or methods within a class or method by
* default can not be null. This can be overridden by adding specific
* {@link com.android.annotations.Nullable} annotations on fields, parameters or
* methods that should not use the default.
* <p/>
* NOTE: Eclipse does not yet handle defaults well (in particular, if
* you add this on a class which implements Comparable, then it will insist
* that your compare method is changing the nullness of the compare parameter,
* so you'll need to add @Nullable on it, which also is not right (since
* the method should have implied @NonNull and you do not need to check
* the parameter.). For now, it's best to individually annotate methods,
* parameters and fields.
* <p/>
* This is a marker annotation and it has no specific attributes.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({PACKAGE, TYPE})
public @interface NonNullByDefault {
}
@@ -1,46 +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.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
/**
* Denotes that a parameter, field or method return value can be null.
* <b>Note</b>: this is the default assumption for most Java APIs and the
* default assumption made by most static code checking tools, so usually you
* don't need to use this annotation; its primary use is to override a default
* wider annotation like {@link NonNullByDefault}.
* <p/>
* When decorating a method call parameter, this denotes the parameter can
* legitimately be null and the method will gracefully deal with it. Typically
* used on optional parameters.
* <p/>
* When decorating a method, this denotes the method might legitimately return
* null.
* <p/>
* This is a marker annotation and it has no specific attributes.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({METHOD, PARAMETER, LOCAL_VARIABLE, FIELD})
public @interface Nullable {
}
@@ -1,50 +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.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Denotes that the class, method or field has its visibility relaxed so
* that unit tests can access it.
* <p/>
* The <code>visibility</code> argument can be used to specific what the original
* visibility should have been if it had not been made public or package-private for testing.
* The default is to consider the element private.
*/
@Retention(RetentionPolicy.SOURCE)
public @interface VisibleForTesting {
/**
* Intended visibility if the element had not been made public or package-private for
* testing.
*/
enum Visibility {
/** The element should be considered protected. */
PROTECTED,
/** The element should be considered package-private. */
PACKAGE,
/** The element should be considered private. */
PRIVATE
}
/**
* Intended visibility if the element had not been made public or package-private for testing.
* If not specified, one should assume the element originally intended to be private.
*/
Visibility visibility() default Visibility.PRIVATE;
}
@@ -1,30 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.annotations.concurrency;
import java.lang.annotation.*;
/**
* Indicates that the target field or method should only be accessed
* with the specified lock being held.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface GuardedBy {
String value();
}
@@ -1,29 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.annotations.concurrency;
import java.lang.annotation.*;
/**
* Indicates that the target class to which this annotation is applied
* is immutable.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Immutable {
}
@@ -1,65 +0,0 @@
/*
* 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;
}
}
@@ -1,202 +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.client.api;
import com.android.annotations.NonNull;
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.AbstractInsnNode;
import org.jetbrains.org.objectweb.asm.tree.ClassNode;
import org.jetbrains.org.objectweb.asm.tree.InsnList;
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode;
import org.jetbrains.org.objectweb.asm.tree.MethodNode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Specialized visitor for running detectors on a class object model.
* <p>
* It operates in two phases:
* <ol>
* <li> First, it computes a set of maps where it generates a map from each
* significant method name to a list of detectors to consult for that method
* name. The set of method names that a detector is interested in is provided
* by the detectors themselves.
* <li> Second, it iterates over the DOM a single time. For each method call it finds,
* it dispatches to any check that has registered interest in that method name.
* <li> Finally, it runs a full check on those class scanners that do not register
* specific method names to be checked. This is intended for those detectors
* that do custom work, not related specifically to method calls.
* </ol>
* It also notifies all the detectors before and after the document is processed
* such that they can do pre- and post-processing.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
class AsmVisitor {
/**
* Number of distinct node types specified in {@link AbstractInsnNode}. Sadly
* there isn't a max-constant there, so update this along with ASM library
* updates.
*/
private static final int TYPE_COUNT = AbstractInsnNode.LINE + 1;
private final Map<String, List<ClassScanner>> mMethodNameToChecks =
new HashMap<String, List<ClassScanner>>();
private final Map<String, List<ClassScanner>> mMethodOwnerToChecks =
new HashMap<String, List<ClassScanner>>();
private final List<Detector> mFullClassChecks = new ArrayList<Detector>();
private final List<? extends Detector> mAllDetectors;
private List<ClassScanner>[] mNodeTypeDetectors;
// Really want this:
//<T extends List<Detector> & Detector.ClassScanner> ClassVisitor(T xmlDetectors) {
// but it makes client code tricky and ugly.
@SuppressWarnings("unchecked")
AsmVisitor(@NonNull LintClient client, @NonNull List<? extends Detector> classDetectors) {
mAllDetectors = classDetectors;
// TODO: Check appliesTo() for files, and find a quick way to enable/disable
// rules when running through a full project!
for (Detector detector : classDetectors) {
Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
boolean checkFullClass = true;
Collection<String> names = scanner.getApplicableCallNames();
if (names != null) {
checkFullClass = false;
for (String element : names) {
List<Detector.ClassScanner> list = mMethodNameToChecks.get(element);
if (list == null) {
list = new ArrayList<Detector.ClassScanner>();
mMethodNameToChecks.put(element, list);
}
list.add(scanner);
}
}
Collection<String> owners = scanner.getApplicableCallOwners();
if (owners != null) {
checkFullClass = false;
for (String element : owners) {
List<Detector.ClassScanner> list = mMethodOwnerToChecks.get(element);
if (list == null) {
list = new ArrayList<Detector.ClassScanner>();
mMethodOwnerToChecks.put(element, list);
}
list.add(scanner);
}
}
int[] types = scanner.getApplicableAsmNodeTypes();
if (types != null) {
checkFullClass = false;
for (int type : types) {
if (type < 0 || type >= TYPE_COUNT) {
// Can't support this node type: looks like ASM wasn't updated correctly.
client.log(null, "Out of range node type %1$d from detector %2$s",
type, scanner);
continue;
}
if (mNodeTypeDetectors == null) {
mNodeTypeDetectors = new List[TYPE_COUNT];
}
List<ClassScanner> checks = mNodeTypeDetectors[type];
if (checks == null) {
checks = new ArrayList<ClassScanner>();
mNodeTypeDetectors[type] = checks;
}
checks.add(scanner);
}
}
if (checkFullClass) {
mFullClassChecks.add(detector);
}
}
}
@SuppressWarnings("rawtypes") // ASM API uses raw types
void runClassDetectors(ClassContext context) {
ClassNode classNode = context.getClassNode();
for (Detector detector : mAllDetectors) {
detector.beforeCheckFile(context);
}
for (Detector detector : mFullClassChecks) {
Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
scanner.checkClass(context, classNode);
detector.afterCheckFile(context);
}
if (!mMethodNameToChecks.isEmpty() || !mMethodOwnerToChecks.isEmpty() ||
mNodeTypeDetectors != null && mNodeTypeDetectors.length > 0) {
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
InsnList nodes = method.instructions;
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
int type = instruction.getType();
if (type == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode call = (MethodInsnNode) instruction;
String owner = call.owner;
List<ClassScanner> scanners = mMethodOwnerToChecks.get(owner);
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkCall(context, classNode, method, call);
}
}
String name = call.name;
scanners = mMethodNameToChecks.get(name);
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkCall(context, classNode, method, call);
}
}
}
if (mNodeTypeDetectors != null && type < mNodeTypeDetectors.length) {
List<ClassScanner> scanners = mNodeTypeDetectors[type];
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkInstruction(context, classNode, method, instruction);
}
}
}
}
}
}
for (Detector detector : mAllDetectors) {
detector.afterCheckFile(context);
}
}
}
@@ -1,81 +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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.google.common.annotations.Beta;
/**
* Exception thrown when there is a circular dependency, such as a circular dependency
* of library mProject references
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class CircularDependencyException extends RuntimeException {
@Nullable
private Project mProject;
@Nullable
private Location mLocation;
CircularDependencyException(@NonNull String message) {
super(message);
}
/**
* Returns the associated project, if any
*
* @return the associated project, if any
*/
@Nullable
public Project getProject() {
return mProject;
}
/**
* Sets the associated project, if any
*
* @param project the associated project, if any
*/
public void setProject(@Nullable Project project) {
mProject = project;
}
/**
* Returns the associated location, if any
*
* @return the associated location, if any
*/
@Nullable
public Location getLocation() {
return mLocation;
}
/**
* Sets the associated location, if any
*
* @param location the associated location, if any
*/
public void setLocation(@Nullable Location location) {
mLocation = location;
}
}
@@ -1,335 +0,0 @@
/*
* 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.client.api;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static org.jetbrains.org.objectweb.asm.Opcodes.API_VERSION;
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 java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/** A class, present either as a .class file on disk, or inside a .jar file. */
@VisibleForTesting
class ClassEntry implements Comparable<ClassEntry> {
public final File file;
public final File jarFile;
public final File binDir;
public final byte[] bytes;
@VisibleForTesting
ClassEntry(
@NonNull File file,
@Nullable File jarFile,
@NonNull File binDir,
@NonNull byte[] bytes) {
super();
this.file = file;
this.jarFile = jarFile;
this.binDir = binDir;
this.bytes = bytes;
}
@NonNull
public String path() {
if (jarFile != null) {
return jarFile.getPath() + ':' + file.getPath();
} else {
return file.getPath();
}
}
@Override
public int compareTo(@NonNull ClassEntry other) {
String p1 = file.getPath();
String p2 = other.file.getPath();
int m1 = p1.length();
int m2 = p2.length();
if (m1 == m2 && p1.equals(p2)) {
return 0;
}
int m = Math.min(m1, m2);
for (int i = 0; i < m; i++) {
char c1 = p1.charAt(i);
char c2 = p2.charAt(i);
if (c1 != c2) {
// Sort Foo$Bar.class *after* Foo.class, even though $ < .
if (c1 == '.' && c2 == '$') {
return -1;
}
if (c1 == '$' && c2 == '.') {
return 1;
}
return c1 - c2;
}
}
return (m == m1) ? -1 : 1;
}
@Override
public String toString() {
return file.getPath();
}
/**
* Creates a list of class entries from the given class path.
*
* @param client the client to report errors to and to use to read files
* @param classPath the class path (directories and jar files) to scan
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassPath(
@NonNull LintClient client,
@NonNull List<File> classPath,
boolean sort) {
if (!classPath.isEmpty()) {
List<ClassEntry> libraryEntries = new ArrayList<ClassEntry>(64);
addEntries(client, libraryEntries, classPath);
if (sort) {
Collections.sort(libraryEntries);
}
return libraryEntries;
} else {
return Collections.emptyList();
}
}
/**
* Creates a list of class entries from the given class path and specific set of
* files within it.
*
* @param client the client to report errors to and to use to read files
* @param classFiles the specific set of class files to look for
* @param classFolders the list of class folders to look in (to determine the
* package root)
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassFiles(
@NonNull LintClient client,
@NonNull List<File> classFiles, @NonNull List<File> classFolders,
boolean sort) {
List<ClassEntry> entries = new ArrayList<ClassEntry>(classFiles.size());
if (!classFolders.isEmpty()) {
for (File file : classFiles) {
String path = file.getPath();
if (file.isFile() && path.endsWith(DOT_CLASS)) {
try {
byte[] bytes = client.readBytes(file);
for (File dir : classFolders) {
if (path.startsWith(dir.getPath())) {
entries.add(new ClassEntry(file, null /* jarFile*/, dir,
bytes));
break;
}
}
} catch (IOException e) {
client.log(e, null);
}
}
}
if (sort && !entries.isEmpty()) {
Collections.sort(entries);
}
}
return entries;
}
/**
* Given a classpath, add all the class files found within the directories and inside jar files
*/
private static void addEntries(
@NonNull LintClient client,
@NonNull List<ClassEntry> entries,
@NonNull List<File> classPath) {
for (File classPathEntry : classPath) {
if (classPathEntry.getName().endsWith(DOT_JAR)) {
//noinspection UnnecessaryLocalVariable
File jarFile = classPathEntry;
if (!jarFile.exists()) {
continue;
}
ZipInputStream zis = null;
try {
FileInputStream fis = new FileInputStream(jarFile);
try {
zis = new ZipInputStream(fis);
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
String name = entry.getName();
if (name.endsWith(DOT_CLASS)) {
try {
byte[] bytes = ByteStreams.toByteArray(zis);
if (bytes != null) {
File file = new File(entry.getName());
entries.add(new ClassEntry(file, jarFile, jarFile, bytes));
}
} catch (Exception e) {
client.log(e, null);
continue;
}
}
entry = zis.getNextEntry();
}
} finally {
Closeables.close(fis, true);
}
} catch (IOException e) {
client.log(e, "Could not read jar file contents from %1$s", jarFile);
} finally {
try {
Closeables.close(zis, true);
} catch (IOException e) {
// cannot happen
}
}
} else if (classPathEntry.isDirectory()) {
//noinspection UnnecessaryLocalVariable
File binDir = classPathEntry;
List<File> classFiles = new ArrayList<File>();
addClassFiles(binDir, classFiles);
for (File file : classFiles) {
try {
byte[] bytes = client.readBytes(file);
entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes));
} catch (IOException e) {
client.log(e, null);
}
}
} else {
client.log(null, "Ignoring class path entry %1$s", classPathEntry);
}
}
}
/** Adds in all the .class files found recursively in the given directory */
private static void addClassFiles(@NonNull File dir, @NonNull List<File> classFiles) {
// Process the resource folder
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(DOT_CLASS)) {
classFiles.add(file);
} else if (file.isDirectory()) {
// Recurse
addClassFiles(file, classFiles);
}
}
}
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param libraryEntries the set of library entries to consult
* @param classEntries the set of class entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client,
@NonNull List<ClassEntry> libraryEntries,
@NonNull List<ClassEntry> classEntries) {
int size = libraryEntries.size() + classEntries.size();
Map<String, String> map = Maps.newHashMapWithExpectedSize(size);
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, libraryEntries);
addSuperClasses(client, visitor, classEntries);
return map;
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param entries the set of library entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client,
@NonNull List<ClassEntry> entries) {
Map<String, String> map = Maps.newHashMapWithExpectedSize(entries.size());
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, entries);
return map;
}
/** Adds in all the super classes found for the given class entries into the given map */
private static void addSuperClasses(
@NonNull LintClient client,
@NonNull SuperclassVisitor visitor,
@NonNull List<ClassEntry> entries) {
for (ClassEntry entry : entries) {
try {
ClassReader reader = new ClassReader(entry.bytes);
int flags = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG
| ClassReader.SKIP_FRAMES;
reader.accept(visitor, flags);
} catch (Throwable t) {
client.log(null, "Error processing %1$s: broken class file?", entry.path());
}
}
}
/** Visitor skimming classes and initializing a map of super classes */
private static class SuperclassVisitor extends ClassVisitor {
private final Map<String, String> mMap;
public SuperclassVisitor(Map<String, String> map) {
super(API_VERSION);
mMap = map;
}
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
// Record super class in the map (but don't waste space on java.lang.Object)
if (superName != null && !"java/lang/Object".equals(superName)) {
mMap.put(name, superName);
}
}
}
}
@@ -1,52 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.client.api;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Issue;
import com.google.common.collect.Lists;
import java.util.List;
/**
* Registry which merges many issue registries into one, and presents a unified list
* of issues.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
class CompositeIssueRegistry extends IssueRegistry {
private final List<IssueRegistry> myRegistries;
private List<Issue> myIssues;
public CompositeIssueRegistry(@NonNull List<IssueRegistry> registries) {
myRegistries = registries;
}
@NonNull
@Override
public List<Issue> getIssues() {
if (myIssues == null) {
List<Issue> issues = Lists.newArrayListWithExpectedSize(200);
for (IssueRegistry registry : myRegistries) {
issues.addAll(registry.getIssues());
}
myIssues = issues;
}
return myIssues;
}
}
@@ -1,124 +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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Severity;
import com.google.common.annotations.Beta;
/**
* Lint configuration for an Android project such as which specific rules to include,
* which specific rules to exclude, and which specific errors to ignore.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class Configuration {
/**
* Checks whether this issue should be ignored because the user has already
* suppressed the error? Note that this refers to individual issues being
* suppressed/ignored, not a whole detector being disabled via something
* like {@link #isEnabled(Issue)}.
*
* @param context the context used by the detector when the issue was found
* @param issue the issue that was found
* @param location the location of the issue
* @param message the associated user message
* @return true if this issue should be suppressed
*/
public boolean isIgnored(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
return false;
}
/**
* Returns false if the given issue has been disabled. This is just
* a convenience method for {@code getSeverity(issue) != Severity.IGNORE}.
*
* @param issue the issue to check
* @return false if the issue has been disabled
*/
public boolean isEnabled(@NonNull Issue issue) {
return getSeverity(issue) != Severity.IGNORE;
}
/**
* Returns the severity for a given issue. This is the same as the
* {@link Issue#getDefaultSeverity()} unless the user has selected a custom
* severity (which is tool context dependent).
*
* @param issue the issue to look up the severity from
* @return the severity use for issues for the given detector
*/
public Severity getSeverity(@NonNull Issue issue) {
return issue.getDefaultSeverity();
}
// Editing configurations
/**
* Marks the given warning as "ignored".
*
* @param context The scanning context
* @param issue the issue to be ignored
* @param location The location to ignore the warning at, if any
* @param message The message for the warning
*/
public abstract void ignore(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message);
/**
* Sets the severity to be used for this issue.
*
* @param issue the issue to set the severity for
* @param severity the severity to associate with this issue, or null to
* reset the severity to the default
*/
public abstract void setSeverity(@NonNull Issue issue, @Nullable Severity severity);
// Bulk editing support
/**
* Marks the beginning of a "bulk" editing operation with repeated calls to
* {@link #setSeverity} or {@link #ignore}. After all the values have been
* set, the client <b>must</b> call {@link #finishBulkEditing()}. This
* allows configurations to avoid doing expensive I/O (such as writing out a
* config XML file) for each and every editing operation when they are
* applied in bulk, such as from a configuration dialog's "Apply" action.
*/
public void startBulkEditing() {
}
/**
* Marks the end of a "bulk" editing operation, where values should be
* committed to persistent storage. See {@link #startBulkEditing()} for
* details.
*/
public void finishBulkEditing() {
}
}
@@ -1,608 +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.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;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
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.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXParseException;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Default implementation of a {@link Configuration} which reads and writes
* configuration data into {@code lint.xml} in the project directory.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class DefaultConfiguration extends Configuration {
private final LintClient mClient;
/** Default name of the configuration file */
public static final String CONFIG_FILE_NAME = "lint.xml"; //$NON-NLS-1$
// Lint XML File
@NonNull
private static final String TAG_ISSUE = "issue"; //$NON-NLS-1$
@NonNull
private static final String ATTR_ID = "id"; //$NON-NLS-1$
@NonNull
private static final String ATTR_SEVERITY = "severity"; //$NON-NLS-1$
@NonNull
private static final String ATTR_PATH = "path"; //$NON-NLS-1$
@NonNull
private static final String ATTR_REGEXP = "regexp"; //$NON-NLS-1$
@NonNull
private static final String TAG_IGNORE = "ignore"; //$NON-NLS-1$
@NonNull
private static final String VALUE_ALL = "all"; //$NON-NLS-1$
private final Configuration mParent;
private final Project mProject;
private final File mConfigFile;
private boolean mBulkEditing;
/** Map from id to list of project-relative paths for suppressed warnings */
private Map<String, List<String>> mSuppressed;
/** Map from id to regular expressions. */
@Nullable
private Map<String, List<Pattern>> mRegexps;
/**
* Map from id to custom {@link Severity} override
*/
private Map<String, Severity> mSeverity;
protected DefaultConfiguration(
@NonNull LintClient client,
@Nullable Project project,
@Nullable Configuration parent,
@NonNull File configFile) {
mClient = client;
mProject = project;
mParent = parent;
mConfigFile = configFile;
}
protected DefaultConfiguration(
@NonNull LintClient client,
@NonNull Project project,
@Nullable Configuration parent) {
this(client, project, parent, new File(project.getDir(), CONFIG_FILE_NAME));
}
/**
* Creates a new {@link DefaultConfiguration}
*
* @param client the client to report errors to etc
* @param project the associated project
* @param parent the parent/fallback configuration or null
* @return a new configuration
*/
@NonNull
public static DefaultConfiguration create(
@NonNull LintClient client,
@NonNull Project project,
@Nullable Configuration parent) {
return new DefaultConfiguration(client, project, parent);
}
/**
* Creates a new {@link DefaultConfiguration} for the given lint config
* file, not affiliated with a project. This is used for global
* configurations.
*
* @param client the client to report errors to etc
* @param lintFile the lint file containing the configuration
* @return a new configuration
*/
@NonNull
public static DefaultConfiguration create(@NonNull LintClient client, @NonNull File lintFile) {
return new DefaultConfiguration(client, null /*project*/, null /*parent*/, lintFile);
}
@Override
public boolean isIgnored(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
ensureInitialized();
String id = issue.getId();
List<String> paths = mSuppressed.get(id);
if (paths == null) {
paths = mSuppressed.get(VALUE_ALL);
}
if (paths != null && location != null) {
File file = location.getFile();
String relativePath = context.getProject().getRelativePath(file);
for (String suppressedPath : paths) {
if (suppressedPath.equals(relativePath)) {
return true;
}
// Also allow a prefix
if (relativePath.startsWith(suppressedPath)) {
return true;
}
}
}
if (mRegexps != null) {
List<Pattern> regexps = mRegexps.get(id);
if (regexps == null) {
regexps = mRegexps.get(VALUE_ALL);
}
if (regexps != null && location != null) {
// Check message
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(message);
if (matcher.find()) {
return true;
}
}
// Check location
File file = location.getFile();
String relativePath = context.getProject().getRelativePath(file);
boolean checkUnixPath = false;
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(relativePath);
if (matcher.find()) {
return true;
} else if (regexp.pattern().indexOf('/') != -1) {
checkUnixPath = true;
}
}
if (checkUnixPath && CURRENT_PLATFORM == PLATFORM_WINDOWS) {
relativePath = relativePath.replace('\\', '/');
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(relativePath);
if (matcher.find()) {
return true;
}
}
}
}
}
return mParent != null && mParent.isIgnored(context, issue, location, message);
}
@NonNull
protected Severity getDefaultSeverity(@NonNull Issue issue) {
if (!issue.isEnabledByDefault()) {
return Severity.IGNORE;
}
return issue.getDefaultSeverity();
}
@Override
@NonNull
public Severity getSeverity(@NonNull Issue issue) {
ensureInitialized();
Severity severity = mSeverity.get(issue.getId());
if (severity == null) {
severity = mSeverity.get(VALUE_ALL);
}
if (severity != null) {
return severity;
}
if (mParent != null) {
return mParent.getSeverity(issue);
}
return getDefaultSeverity(issue);
}
private void ensureInitialized() {
if (mSuppressed == null) {
readConfig();
}
}
private void formatError(String message, Object... args) {
if (args != null && args.length > 0) {
message = String.format(message, args);
}
message = "Failed to parse `lint.xml` configuration file: " + message;
LintDriver driver = new LintDriver(new IssueRegistry() {
@Override @NonNull public List<Issue> getIssues() {
return Collections.emptyList();
}
}, mClient);
mClient.report(new Context(driver, mProject, mProject, mConfigFile),
IssueRegistry.LINT_ERROR,
mProject.getConfiguration(driver).getSeverity(IssueRegistry.LINT_ERROR),
Location.create(mConfigFile), message, TextFormat.RAW);
}
private void readConfig() {
mSuppressed = new HashMap<String, List<String>>();
mSeverity = new HashMap<String, Severity>();
if (!mConfigFile.exists()) {
return;
}
try {
Document document = XmlUtils.parseUtfXmlFile(mConfigFile, false);
NodeList issues = document.getElementsByTagName(TAG_ISSUE);
Splitter splitter = Splitter.on(',').trimResults().omitEmptyStrings();
for (int i = 0, count = issues.getLength(); i < count; i++) {
Node node = issues.item(i);
Element element = (Element) node;
String idList = element.getAttribute(ATTR_ID);
if (idList.isEmpty()) {
formatError("Invalid lint config file: Missing required issue id attribute");
continue;
}
Iterable<String> ids = splitter.split(idList);
NamedNodeMap attributes = node.getAttributes();
for (int j = 0, n = attributes.getLength(); j < n; j++) {
Node attribute = attributes.item(j);
String name = attribute.getNodeName();
String value = attribute.getNodeValue();
if (ATTR_ID.equals(name)) {
// already handled
} else if (ATTR_SEVERITY.equals(name)) {
for (Severity severity : Severity.values()) {
if (value.equalsIgnoreCase(severity.name())) {
for (String id : ids) {
mSeverity.put(id, severity);
}
break;
}
}
} else {
formatError("Unexpected attribute \"%1$s\"", name);
}
}
// Look up ignored errors
NodeList childNodes = element.getChildNodes();
if (childNodes.getLength() > 0) {
for (int j = 0, n = childNodes.getLength(); j < n; j++) {
Node child = childNodes.item(j);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element ignore = (Element) child;
String path = ignore.getAttribute(ATTR_PATH);
if (path.isEmpty()) {
String regexp = ignore.getAttribute(ATTR_REGEXP);
if (regexp.isEmpty()) {
formatError("Missing required attribute %1$s or %2$s under %3$s",
ATTR_PATH, ATTR_REGEXP, idList);
} else {
addRegexp(idList, ids, n, regexp, false);
}
} else {
// Normalize path format to File.separator. Also
// handle the file format containing / or \.
if (File.separatorChar == '/') {
path = path.replace('\\', '/');
} else {
path = path.replace('/', File.separatorChar);
}
if (path.indexOf('*') != -1) {
String regexp = globToRegexp(path);
addRegexp(idList, ids, n, regexp, false);
} else {
for (String id : ids) {
List<String> paths = mSuppressed.get(id);
if (paths == null) {
paths = new ArrayList<String>(n / 2 + 1);
mSuppressed.put(id, paths);
}
paths.add(path);
}
}
}
}
}
}
}
} catch (SAXParseException e) {
formatError(e.getMessage());
} catch (Exception e) {
mClient.log(e, null);
}
}
@NonNull
public static String globToRegexp(@NonNull String glob) {
StringBuilder sb = new StringBuilder(glob.length() * 2);
int begin = 0;
sb.append('^');
for (int i = 0, n = glob.length(); i < n; i++) {
char c = glob.charAt(i);
if (c == '*') {
begin = appendQuoted(sb, glob, begin, i) + 1;
if (i < n - 1 && glob.charAt(i + 1) == '*') {
i++;
begin++;
}
sb.append(".*?");
} else if (c == '?') {
begin = appendQuoted(sb, glob, begin, i) + 1;
sb.append(".?");
}
}
appendQuoted(sb, glob, begin, glob.length());
sb.append('$');
return sb.toString();
}
private static int appendQuoted(StringBuilder sb, String s, int from, int to) {
if (to > from) {
boolean isSimple = true;
for (int i = from; i < to; i++) {
char c = s.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '/' && c != ' ') {
isSimple = false;
break;
}
}
if (isSimple) {
for (int i = from; i < to; i++) {
sb.append(s.charAt(i));
}
return to;
}
sb.append(Pattern.quote(s.substring(from, to)));
}
return to;
}
private void addRegexp(@NonNull String idList, @NonNull Iterable<String> ids, int n,
@NonNull String regexp, boolean silent) {
try {
if (mRegexps == null) {
mRegexps = new HashMap<String, List<Pattern>>();
}
Pattern pattern = Pattern.compile(regexp);
for (String id : ids) {
List<Pattern> paths = mRegexps.get(id);
if (paths == null) {
paths = new ArrayList<Pattern>(n / 2 + 1);
mRegexps.put(id, paths);
}
paths.add(pattern);
}
} catch (PatternSyntaxException e) {
if (!silent) {
formatError("Invalid pattern %1$s under %2$s: %3$s",
regexp, idList, e.getDescription());
}
}
}
private void writeConfig() {
try {
// Write the contents to a new file first such that we don't clobber the
// existing file if some I/O error occurs.
File file = new File(mConfigFile.getParentFile(),
mConfigFile.getName() + ".new"); //$NON-NLS-1$
Writer writer = new BufferedWriter(new FileWriter(file));
writer.write(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //$NON-NLS-1$
"<lint>\n"); //$NON-NLS-1$
if (!mSuppressed.isEmpty() || !mSeverity.isEmpty()) {
// Process the maps in a stable sorted order such that if the
// files are checked into version control with the project,
// there are no random diffs just because hashing algorithms
// differ:
Set<String> idSet = new HashSet<String>();
for (String id : mSuppressed.keySet()) {
idSet.add(id);
}
for (String id : mSeverity.keySet()) {
idSet.add(id);
}
List<String> ids = new ArrayList<String>(idSet);
Collections.sort(ids);
for (String id : ids) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_ISSUE);
writeAttribute(writer, ATTR_ID, id);
Severity severity = mSeverity.get(id);
if (severity != null) {
writeAttribute(writer, ATTR_SEVERITY,
severity.name().toLowerCase(Locale.US));
}
List<Pattern> regexps = mRegexps != null ? mRegexps.get(id) : null;
List<String> paths = mSuppressed.get(id);
if (paths != null && !paths.isEmpty()
|| regexps != null && !regexps.isEmpty()) {
writer.write('>');
writer.write('\n');
// The paths are already kept in sorted order when they are modified
// by ignore(...)
if (paths != null) {
for (String path : paths) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_IGNORE);
writeAttribute(writer, ATTR_PATH, path.replace('\\', '/'));
writer.write(" />\n"); //$NON-NLS-1$
}
}
if (regexps != null) {
for (Pattern regexp : regexps) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_IGNORE);
writeAttribute(writer, ATTR_REGEXP, regexp.pattern());
writer.write(" />\n"); //$NON-NLS-1$
}
}
writer.write(" </"); //$NON-NLS-1$
writer.write(TAG_ISSUE);
writer.write('>');
writer.write('\n');
} else {
writer.write(" />\n"); //$NON-NLS-1$
}
}
}
writer.write("</lint>"); //$NON-NLS-1$
writer.close();
// Move file into place: move current version to lint.xml~ (removing the old ~ file
// if it exists), then move the new version to lint.xml.
File oldFile = new File(mConfigFile.getParentFile(),
mConfigFile.getName() + '~'); //$NON-NLS-1$
if (oldFile.exists()) {
oldFile.delete();
}
if (mConfigFile.exists()) {
mConfigFile.renameTo(oldFile);
}
boolean ok = file.renameTo(mConfigFile);
if (ok && oldFile.exists()) {
oldFile.delete();
}
} catch (Exception e) {
mClient.log(e, null);
}
}
private static void writeAttribute(
@NonNull Writer writer, @NonNull String name, @NonNull String value)
throws IOException {
writer.write(' ');
writer.write(name);
writer.write('=');
writer.write('"');
writer.write(value);
writer.write('"');
}
@Override
public void ignore(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
// This configuration only supports suppressing warnings on a per-file basis
if (location != null) {
ignore(issue, location.getFile());
}
}
/**
* Marks the given issue and file combination as being ignored.
*
* @param issue the issue to be ignored in the given file
* @param file the file to ignore the issue in
*/
public void ignore(@NonNull Issue issue, @NonNull File file) {
ensureInitialized();
String path = mProject != null ? mProject.getRelativePath(file) : file.getPath();
List<String> paths = mSuppressed.get(issue.getId());
if (paths == null) {
paths = new ArrayList<String>();
mSuppressed.put(issue.getId(), paths);
}
paths.add(path);
// Keep paths sorted alphabetically; makes XML output stable
Collections.sort(paths);
if (!mBulkEditing) {
writeConfig();
}
}
@Override
public void setSeverity(@NonNull Issue issue, @Nullable Severity severity) {
ensureInitialized();
String id = issue.getId();
if (severity == null) {
mSeverity.remove(id);
} else {
mSeverity.put(id, severity);
}
if (!mBulkEditing) {
writeConfig();
}
}
@Override
public void startBulkEditing() {
mBulkEditing = true;
}
@Override
public void finishBulkEditing() {
mBulkEditing = false;
writeConfig();
}
@VisibleForTesting
File getConfigFile() {
return mConfigFile;
}
}
@@ -1,288 +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.client.api;
import static com.android.SdkConstants.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ABS_LIST_VIEW;
import static com.android.SdkConstants.ABS_SEEK_BAR;
import static com.android.SdkConstants.ABS_SPINNER;
import static com.android.SdkConstants.ADAPTER_VIEW;
import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECKABLE;
import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.COMPOUND_BUTTON;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.GALLERY;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.IMAGE_BUTTON;
import static com.android.SdkConstants.IMAGE_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.PROGRESS_BAR;
import static com.android.SdkConstants.RADIO_BUTTON;
import static com.android.SdkConstants.RADIO_GROUP;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.android.SdkConstants.SEEK_BAR;
import static com.android.SdkConstants.SPINNER;
import static com.android.SdkConstants.SURFACE_VIEW;
import static com.android.SdkConstants.SWITCH;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAB_HOST;
import static com.android.SdkConstants.TAB_WIDGET;
import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.SdkConstants.TOGGLE_BUTTON;
import static com.android.SdkConstants.VIEW;
import static com.android.SdkConstants.VIEW_ANIMATOR;
import static com.android.SdkConstants.VIEW_GROUP;
import static com.android.SdkConstants.VIEW_PKG_PREFIX;
import static com.android.SdkConstants.VIEW_STUB;
import static com.android.SdkConstants.VIEW_SWITCHER;
import static com.android.SdkConstants.WEB_VIEW;
import static com.android.SdkConstants.WIDGET_PKG_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Default simple implementation of an {@link SdkInfo}
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
class DefaultSdkInfo extends SdkInfo {
@Override
@Nullable
public String getParentViewName(@NonNull String name) {
name = getRawType(name);
return PARENTS.get(name);
}
@Override
@Nullable
public String getParentViewClass(@NonNull String fqcn) {
int index = fqcn.lastIndexOf('.');
if (index != -1) {
fqcn = fqcn.substring(index + 1);
}
String parent = PARENTS.get(fqcn);
if (parent == null) {
return null;
}
// The map only stores class names internally; correct for full package paths
if (parent.equals(VIEW) || parent.equals(VIEW_GROUP) || parent.equals(SURFACE_VIEW)) {
return VIEW_PKG_PREFIX + parent;
} else {
return WIDGET_PKG_PREFIX + parent;
}
}
@Override
public boolean isSubViewOf(@NonNull String parentType, @NonNull String childType) {
String parent = getRawType(parentType);
String child = getRawType(childType);
// Do analysis just on non-fqcn paths
if (parent.indexOf('.') != -1) {
parent = parent.substring(parent.lastIndexOf('.') + 1);
}
if (child.indexOf('.') != -1) {
child = child.substring(child.lastIndexOf('.') + 1);
}
if (parent.equals(VIEW)) {
return true;
}
while (!child.equals(VIEW)) {
if (parent.equals(child)) {
return true;
}
if (implementsInterface(child, parent)) {
return true;
}
child = PARENTS.get(child);
if (child == null) {
// Unknown view - err on the side of caution
return true;
}
}
return false;
}
private static boolean implementsInterface(String className, String interfaceName) {
return interfaceName.equals(INTERFACES.get(className));
}
// Strip off type parameters, e.g. AdapterView<?> ⇒ AdapterView
private static String getRawType(String type) {
if (type != null) {
int index = type.indexOf('<');
if (index != -1) {
type = type.substring(0, index);
}
}
return type;
}
@Override
public boolean isLayout(@NonNull String tag) {
// TODO: Read in widgets.txt from the platform install area to look up this information
// dynamically instead!
if (super.isLayout(tag)) {
return true;
}
return LAYOUTS.contains(tag);
}
private static final int CLASS_COUNT = 59;
private static final int LAYOUT_COUNT = 20;
private static final Map<String,String> PARENTS = Maps.newHashMapWithExpectedSize(CLASS_COUNT);
private static final Set<String> LAYOUTS = Sets.newHashSetWithExpectedSize(CLASS_COUNT);
static {
PARENTS.put(COMPOUND_BUTTON, BUTTON);
PARENTS.put(ABS_SPINNER, ADAPTER_VIEW);
PARENTS.put(ABS_LIST_VIEW, ADAPTER_VIEW);
PARENTS.put(ABS_SEEK_BAR, ADAPTER_VIEW);
PARENTS.put(ADAPTER_VIEW, VIEW_GROUP);
PARENTS.put(VIEW_GROUP, VIEW);
PARENTS.put(TEXT_VIEW, VIEW);
PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW);
PARENTS.put(RADIO_BUTTON, COMPOUND_BUTTON);
PARENTS.put(SPINNER, ABS_SPINNER);
PARENTS.put(IMAGE_BUTTON, IMAGE_VIEW);
PARENTS.put(IMAGE_VIEW, VIEW);
PARENTS.put(EDIT_TEXT, TEXT_VIEW);
PARENTS.put(PROGRESS_BAR, VIEW);
PARENTS.put(TOGGLE_BUTTON, COMPOUND_BUTTON);
PARENTS.put(VIEW_STUB, VIEW);
PARENTS.put(BUTTON, TEXT_VIEW);
PARENTS.put(SEEK_BAR, ABS_SEEK_BAR);
PARENTS.put(CHECK_BOX, COMPOUND_BUTTON);
PARENTS.put(SWITCH, COMPOUND_BUTTON);
PARENTS.put(GALLERY, ABS_SPINNER);
PARENTS.put(SURFACE_VIEW, VIEW);
PARENTS.put(ABSOLUTE_LAYOUT, VIEW_GROUP);
PARENTS.put(LINEAR_LAYOUT, VIEW_GROUP);
PARENTS.put(RELATIVE_LAYOUT, VIEW_GROUP);
PARENTS.put(LIST_VIEW, ABS_LIST_VIEW);
PARENTS.put(VIEW_SWITCHER, VIEW_ANIMATOR);
PARENTS.put(FRAME_LAYOUT, VIEW_GROUP);
PARENTS.put(HORIZONTAL_SCROLL_VIEW, FRAME_LAYOUT);
PARENTS.put(VIEW_ANIMATOR, FRAME_LAYOUT);
PARENTS.put(TAB_HOST, FRAME_LAYOUT);
PARENTS.put(TABLE_ROW, LINEAR_LAYOUT);
PARENTS.put(RADIO_GROUP, LINEAR_LAYOUT);
PARENTS.put(TAB_WIDGET, LINEAR_LAYOUT);
PARENTS.put(EXPANDABLE_LIST_VIEW, LIST_VIEW);
PARENTS.put(TABLE_LAYOUT, LINEAR_LAYOUT);
PARENTS.put(SCROLL_VIEW, FRAME_LAYOUT);
PARENTS.put(GRID_VIEW, ABS_LIST_VIEW);
PARENTS.put(WEB_VIEW, ABSOLUTE_LAYOUT);
PARENTS.put(AUTO_COMPLETE_TEXT_VIEW, EDIT_TEXT);
PARENTS.put(MULTI_AUTO_COMPLETE_TEXT_VIEW, AUTO_COMPLETE_TEXT_VIEW);
PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW);
PARENTS.put("MediaController", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("SlidingDrawer", VIEW_GROUP); //$NON-NLS-1$
PARENTS.put("DialerFilter", RELATIVE_LAYOUT); //$NON-NLS-1$
PARENTS.put("DigitalClock", TEXT_VIEW); //$NON-NLS-1$
PARENTS.put("Chronometer", TEXT_VIEW); //$NON-NLS-1$
PARENTS.put("ImageSwitcher", VIEW_SWITCHER); //$NON-NLS-1$
PARENTS.put("TextSwitcher", VIEW_SWITCHER); //$NON-NLS-1$
PARENTS.put("AnalogClock", VIEW); //$NON-NLS-1$
PARENTS.put("TwoLineListItem", RELATIVE_LAYOUT); //$NON-NLS-1$
PARENTS.put("ZoomControls", LINEAR_LAYOUT); //$NON-NLS-1$
PARENTS.put("DatePicker", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("TimePicker", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("VideoView", SURFACE_VIEW); //$NON-NLS-1$
PARENTS.put("ZoomButton", IMAGE_BUTTON); //$NON-NLS-1$
PARENTS.put("RatingBar", ABS_SEEK_BAR); //$NON-NLS-1$
PARENTS.put("ViewFlipper", VIEW_ANIMATOR); //$NON-NLS-1$
PARENTS.put("NumberPicker", LINEAR_LAYOUT); //$NON-NLS-1$
assert PARENTS.size() <= CLASS_COUNT : PARENTS.size();
/*
// Check that all widgets lead to the root view
if (LintUtils.assertionsEnabled()) {
for (String key : PARENTS.keySet()) {
String parent = PARENTS.get(key);
if (!parent.equals(VIEW)) {
String grandParent = PARENTS.get(parent);
assert grandParent != null : parent;
}
}
}
*/
LAYOUTS.add(TAB_HOST);
LAYOUTS.add(HORIZONTAL_SCROLL_VIEW);
LAYOUTS.add(VIEW_SWITCHER);
LAYOUTS.add(TAB_WIDGET);
LAYOUTS.add(VIEW_ANIMATOR);
LAYOUTS.add(SCROLL_VIEW);
LAYOUTS.add(GRID_VIEW);
LAYOUTS.add(TABLE_ROW);
LAYOUTS.add(RADIO_GROUP);
LAYOUTS.add(LIST_VIEW);
LAYOUTS.add(EXPANDABLE_LIST_VIEW);
LAYOUTS.add("MediaController"); //$NON-NLS-1$
LAYOUTS.add("DialerFilter"); //$NON-NLS-1$
LAYOUTS.add("ViewFlipper"); //$NON-NLS-1$
LAYOUTS.add("SlidingDrawer"); //$NON-NLS-1$
LAYOUTS.add("StackView"); //$NON-NLS-1$
LAYOUTS.add("SearchView"); //$NON-NLS-1$
LAYOUTS.add("TextSwitcher"); //$NON-NLS-1$
LAYOUTS.add("AdapterViewFlipper"); //$NON-NLS-1$
LAYOUTS.add("ImageSwitcher"); //$NON-NLS-1$
assert LAYOUTS.size() <= LAYOUT_COUNT : LAYOUTS.size();
}
// Currently using a map; this should really be a list, but using a map until we actually
// start adding more than one item
@NonNull
private static final Map<String, String> INTERFACES = new HashMap<String, String>(2);
static {
INTERFACES.put(CHECKED_TEXT_VIEW, CHECKABLE);
INTERFACES.put(COMPOUND_BUTTON, CHECKABLE);
}
}
@@ -1,25 +0,0 @@
/*
* 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.Nullable;
import com.intellij.psi.PsiElement;
public interface ExternalReferenceExpression {
@Nullable
PsiElement resolve(PsiElement context);
}
@@ -1,346 +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.client.api;
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.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.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Registry which provides a list of checks to be performed on an Android project
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class IssueRegistry {
private static volatile List<Category> sCategories;
private static volatile Map<String, Issue> sIdToIssue;
private static Map<EnumSet<Scope>, List<Issue>> sScopeIssues = Maps.newHashMap();
/**
* Creates a new {@linkplain IssueRegistry}
*/
protected IssueRegistry() {
}
private static final Implementation DUMMY_IMPLEMENTATION = new Implementation(Detector.class,
EnumSet.noneOf(Scope.class));
/**
* Issue reported by lint (not a specific detector) when it cannot even
* parse an XML file prior to analysis
*/
@NonNull
public static final Issue PARSER_ERROR = Issue.create(
"ParserError", //$NON-NLS-1$
"Parser Errors",
"Lint will ignore any files that contain fatal parsing errors. These may contain " +
"other errors, or contain code which affects issues in other files.",
Category.CORRECTNESS,
10,
Severity.ERROR,
DUMMY_IMPLEMENTATION);
/**
* Issue reported by lint for various other issues which prevents lint from
* running normally when it's not necessarily an error in the user's code base.
*/
@NonNull
public static final Issue LINT_ERROR = Issue.create(
"LintError", //$NON-NLS-1$
"Lint Failure",
"This issue type represents a problem running lint itself. Examples include " +
"failure to find bytecode for source files (which means certain detectors " +
"could not be run), parsing errors in lint configuration files, etc." +
"\n" +
"These errors are not errors in your own code, but they are shown to make " +
"it clear that some checks were not completed.",
Category.LINT,
10,
Severity.ERROR,
DUMMY_IMPLEMENTATION);
/**
* Issue reported when lint is canceled
*/
@NonNull
public static final Issue CANCELLED = Issue.create(
"LintCanceled", //$NON-NLS-1$
"Lint Canceled",
"Lint canceled by user; the issue report may not be complete.",
Category.LINT,
0,
Severity.INFORMATIONAL,
DUMMY_IMPLEMENTATION);
/**
* Returns the list of issues that can be found by all known detectors.
*
* @return the list of issues to be checked (including those that may be
* disabled!)
*/
@NonNull
public abstract List<Issue> getIssues();
/**
* Get an approximate issue count for a given scope. This is just an optimization,
* so the number does not have to be accurate.
*
* @param scope the scope set
* @return an approximate ceiling of the number of issues expected for a given scope set
*/
protected int getIssueCapacity(@NonNull EnumSet<Scope> scope) {
return 20;
}
/**
* Returns all available issues of a given scope (regardless of whether
* they are actually enabled for a given configuration etc)
*
* @param scope the applicable scope set
* @return a list of issues
*/
@NonNull
protected List<Issue> getIssuesForScope(@NonNull EnumSet<Scope> scope) {
List<Issue> list = sScopeIssues.get(scope);
if (list == null) {
List<Issue> issues = getIssues();
if (scope.equals(Scope.ALL)) {
list = issues;
} else {
list = new ArrayList<Issue>(getIssueCapacity(scope));
for (Issue issue : issues) {
// Determine if the scope matches
if (issue.getImplementation().isAdequate(scope)) {
list.add(issue);
}
}
}
sScopeIssues.put(scope, list);
}
return list;
}
/**
* Creates a list of detectors applicable to the given scope, and with the
* given configuration.
*
* @param client the client to report errors to
* @param configuration the configuration to look up which issues are
* enabled etc from
* @param scope the scope for the analysis, to filter out detectors that
* require wider analysis than is currently being performed
* @param scopeToDetectors an optional map which (if not null) will be
* filled by this method to contain mappings from each scope to
* the applicable detectors for that scope
* @return a list of new detector instances
*/
@NonNull
final List<? extends Detector> createDetectors(
@NonNull LintClient client,
@NonNull Configuration configuration,
@NonNull EnumSet<Scope> scope,
@Nullable Map<Scope, List<Detector>> scopeToDetectors) {
List<Issue> issues = getIssuesForScope(scope);
if (issues.isEmpty()) {
return Collections.emptyList();
}
Set<Class<? extends Detector>> detectorClasses = new HashSet<Class<? extends Detector>>();
Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope =
new HashMap<Class<? extends Detector>, EnumSet<Scope>>();
for (Issue issue : issues) {
Implementation implementation = issue.getImplementation();
Class<? extends Detector> detectorClass = implementation.getDetectorClass();
EnumSet<Scope> issueScope = implementation.getScope();
if (!detectorClasses.contains(detectorClass)) {
// Determine if the issue is enabled
if (!configuration.isEnabled(issue)) {
continue;
}
assert implementation.isAdequate(scope); // Ensured by getIssuesForScope above
detectorClass = client.replaceDetector(detectorClass);
assert detectorClass != null : issue.getId();
detectorClasses.add(detectorClass);
}
if (scopeToDetectors != null) {
EnumSet<Scope> s = detectorToScope.get(detectorClass);
if (s == null) {
detectorToScope.put(detectorClass, issueScope);
} else if (!s.containsAll(issueScope)) {
EnumSet<Scope> union = EnumSet.copyOf(s);
union.addAll(issueScope);
detectorToScope.put(detectorClass, union);
}
}
}
List<Detector> detectors = new ArrayList<Detector>(detectorClasses.size());
for (Class<? extends Detector> clz : detectorClasses) {
try {
Detector detector = clz.newInstance();
detectors.add(detector);
if (scopeToDetectors != null) {
EnumSet<Scope> union = detectorToScope.get(clz);
for (Scope s : union) {
List<Detector> list = scopeToDetectors.get(s);
if (list == null) {
list = new ArrayList<Detector>();
scopeToDetectors.put(s, list);
}
list.add(detector);
}
}
} catch (Throwable t) {
client.log(t, "Can't initialize detector %1$s", clz.getName()); //$NON-NLS-1$
}
}
return detectors;
}
/**
* Returns true if the given id represents a valid issue id
*
* @param id the id to be checked
* @return true if the given id is valid
*/
public final boolean isIssueId(@NonNull String id) {
return getIssue(id) != null;
}
/**
* Returns true if the given category is a valid category
*
* @param name the category name to be checked
* @return true if the given string is a valid category
*/
public final boolean isCategoryName(@NonNull String name) {
for (Category category : getCategories()) {
if (category.getName().equals(name) || category.getFullName().equals(name)) {
return true;
}
}
return false;
}
/**
* Returns the available categories
*
* @return an iterator for all the categories, never null
*/
@SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
@NonNull
public List<Category> getCategories() {
List<Category> categories = sCategories;
if (categories == null) {
synchronized (IssueRegistry.class) {
categories = sCategories;
if (categories == null) {
sCategories = categories = Collections.unmodifiableList(createCategoryList());
}
}
}
return categories;
}
@NonNull
private List<Category> createCategoryList() {
Set<Category> categorySet = Sets.newHashSetWithExpectedSize(20);
for (Issue issue : getIssues()) {
categorySet.add(issue.getCategory());
}
List<Category> sorted = new ArrayList<Category>(categorySet);
Collections.sort(sorted);
return sorted;
}
/**
* Returns the issue for the given id, or null if it's not a valid id
*
* @param id the id to be checked
* @return the corresponding issue, or null
*/
@SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
@Nullable
public final Issue getIssue(@NonNull String id) {
Map<String, Issue> map = sIdToIssue;
if (map == null) {
synchronized (IssueRegistry.class) {
map = sIdToIssue;
if (map == null) {
map = createIdToIssueMap();
sIdToIssue = map;
}
}
}
return map.get(id);
}
@NonNull
private Map<String, Issue> createIdToIssueMap() {
List<Issue> issues = getIssues();
Map<String, Issue> map = Maps.newHashMapWithExpectedSize(issues.size() + 2);
for (Issue issue : issues) {
map.put(issue.getId(), issue);
}
map.put(PARSER_ERROR.getId(), PARSER_ERROR);
map.put(LINT_ERROR.getId(), LINT_ERROR);
return map;
}
/**
* Reset the registry such that it recomputes its available issues.
*/
protected static void reset() {
sIdToIssue = null;
sCategories = null;
sScopeIssues = Maps.newHashMap();
}
}
@@ -1,236 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.DOT_CLASS;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* <p> An {@link IssueRegistry} for a custom lint rule jar file. The rule jar should provide a
* manifest entry with the key {@code Lint-Registry} and the value of the fully qualified name of an
* implementation of {@link IssueRegistry} (with a default constructor). </p>
*
* <p> NOTE: The custom issue registry should not extend this file; it should be a plain
* IssueRegistry! This file is used internally to wrap the given issue registry.</p>
*/
class JarFileIssueRegistry extends IssueRegistry {
/**
* Manifest constant for declaring an issue provider. Example: Lint-Registry:
* foo.bar.CustomIssueRegistry
*/
private static final String MF_LINT_REGISTRY_OLD = "Lint-Registry"; //$NON-NLS-1$
private static final String MF_LINT_REGISTRY = "Lint-Registry-v2"; //$NON-NLS-1$
private static Map<File, SoftReference<JarFileIssueRegistry>> sCache;
private final List<Issue> myIssues;
private boolean mHasLegacyDetectors;
/** True if one or more java detectors were found that use the old Lombok-based API */
public boolean hasLegacyDetectors() {
return mHasLegacyDetectors;
}
@NonNull
static JarFileIssueRegistry get(@NonNull LintClient client, @NonNull File jarFile)
throws IOException, ClassNotFoundException, IllegalAccessException,
InstantiationException {
if (sCache == null) {
sCache = new HashMap<File, SoftReference<JarFileIssueRegistry>>();
} else {
SoftReference<JarFileIssueRegistry> reference = sCache.get(jarFile);
if (reference != null) {
JarFileIssueRegistry registry = reference.get();
if (registry != null) {
return registry;
}
}
}
// Ensure that the scope-to-detector map doesn't return stale results
IssueRegistry.reset();
JarFileIssueRegistry registry = new JarFileIssueRegistry(client, jarFile);
sCache.put(jarFile, new SoftReference<JarFileIssueRegistry>(registry));
return registry;
}
private JarFileIssueRegistry(@NonNull LintClient client, @NonNull File file)
throws IOException, ClassNotFoundException, IllegalAccessException,
InstantiationException {
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);
ClassLoader loader = client.createUrlClassLoader(new URL[]{url},
JarFileIssueRegistry.class.getClassLoader());
Class<?> registryClass = Class.forName(className, true, loader);
IssueRegistry registry = (IssueRegistry) registryClass.newInstance();
myIssues.addAll(registry.getIssues());
if (isLegacy) {
// If it's an old registry, look through the issues to see if it
// provides Java scanning and if so create the old style visitors
for (Issue issue : myIssues) {
EnumSet<Scope> scope = issue.getImplementation().getScope();
if (scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.JAVA_LIBRARIES)
|| scope.contains(Scope.ALL_JAVA_FILES)) {
mHasLegacyDetectors = true;
break;
}
}
}
if (loader instanceof URLClassLoader) {
loadAndCloseURLClassLoader(client, file, (URLClassLoader)loader);
}
} else {
client.log(Severity.ERROR, null,
"Custom lint rule jar %1$s does not contain a valid registry manifest key " +
"(%2$s).\n" +
"Either the custom jar is invalid, or it uses an outdated API not supported " +
"this lint client", file.getPath(), MF_LINT_REGISTRY);
}
} finally {
if (jarFile != null) {
jarFile.close();
}
}
}
/**
* Work around http://bugs.java.com/bugdatabase/view_bug.do?bug_id=5041014 :
* URLClassLoader, on Windows, locks the .jar file forever.
* As of Java 7, there's a workaround: you can call close() when you're "done"
* with the file. We'll do that here. However, the whole point of the
* {@linkplain JarFileIssueRegistry} is that when lint is run over and over again
* as the user is editing in the IDE and we're background checking the code, we
* don't to keep loading the custom view classes over and over again: we want to
* cache them. Therefore, just closing the URLClassLoader right away isn't great
* either. However, it turns out it's safe to close the URLClassLoader once you've
* loaded the classes you need, since the URLClassLoader will continue to serve
* those classes even after its close() methods has been called.
* <p>
* Therefore, if we can call close() on this URLClassLoader, we'll proactively load
* all class files we find in the .jar file, then close it.
*
* @param client the client to report errors to
* @param file the .jar file
* @param loader the URLClassLoader we should close
*/
private static void loadAndCloseURLClassLoader(
@NonNull LintClient client,
@NonNull File file,
@NonNull URLClassLoader loader) {
try {
// Proactively close out the .jar file. This is only available on Java 7.
Method closeMethod = loader.getClass().getDeclaredMethod("close");
// But first, proactively load all classes:
try {
InputStream inputStream = new FileInputStream(file);
try {
JarInputStream jarInputStream = new JarInputStream(inputStream);
try {
ZipEntry entry = jarInputStream.getNextEntry();
while (entry != null) {
String name = entry.getName();
// Load non-inner-classes
if (name.endsWith(DOT_CLASS)) {
// Strip .class suffix and change .jar file path (/)
// to class name (.'s).
name = name.substring(0,
name.length() - DOT_CLASS.length());
name = name.replace('/', '.');
try {
Class.forName(name, true, loader);
} catch (Throwable e) {
client.log(Severity.ERROR, e,
"Failed to prefetch " + name + " from " + file);
}
}
entry = jarInputStream.getNextEntry();
}
} finally {
jarInputStream.close();
}
} finally {
inputStream.close();
}
} catch (Throwable ignore) {
} finally {
// Finally close the URL class loader
try {
closeMethod.invoke(loader);
} catch (Throwable ignore) {
// Couldn't close. This is unlikely.
}
}
} catch (NoSuchMethodException ignore) {
// No close method - we're on 1.6
}
}
@NonNull
@Override
public List<Issue> getIssues() {
return myIssues;
}
}
@@ -1,244 +0,0 @@
/*
* 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 com.intellij.psi.util.InheritanceUtil;
import java.io.File;
@SuppressWarnings("MethodMayBeStatic") // Some of these methods may be overridden by LintClients
public abstract class JavaEvaluator {
public boolean isMemberInSubClassOf(
@NonNull PsiMember method,
@NonNull String className,
boolean strict) {
PsiClass containingClass = method.getContainingClass();
return containingClass != null && InheritanceUtil.isInheritor(containingClass, strict, className);
}
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();
}
/**
* 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);
@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);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,70 +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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Context;
import com.google.common.annotations.Beta;
/**
* Interface implemented by listeners to be notified of lint events
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public interface LintListener {
/** The various types of events provided to lint listeners */
enum EventType {
/** A lint check is about to begin */
STARTING,
/** Lint is about to check the given project, see {@link Context#getProject()} */
SCANNING_PROJECT,
/** Lint is about to check the given library project, see {@link Context#getProject()} */
SCANNING_LIBRARY_PROJECT,
/** Lint is about to check the given file, see {@link Context#file} */
SCANNING_FILE,
/** A new pass was initiated */
NEW_PHASE,
/** The lint check was canceled */
CANCELED,
/** The lint check is done */
COMPLETED,
}
/**
* Notifies listeners that the event of the given type has occurred.
* Additional information, such as the file being scanned, or the project
* being scanned, is available in the {@link Context} object (except for the
* {@link EventType#STARTING}, {@link EventType#CANCELED} or
* {@link EventType#COMPLETED} events which are fired outside of project
* contexts.)
*
* @param driver the driver running through the checks
* @param type the type of event that occurred
* @param context the context providing additional information
*/
void update(@NonNull LintDriver driver, @NonNull EventType type,
@Nullable Context context);
}
@@ -1,180 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* Information about a request to run lint
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class LintRequest {
@NonNull
protected final LintClient mClient;
@NonNull
protected final List<File> mFiles;
@Nullable
protected EnumSet<Scope> mScope;
@Nullable
protected Boolean mReleaseMode;
@Nullable
protected Collection<Project> mProjects;
/**
* Creates a new {@linkplain LintRequest}, to be passed to a {@link LintDriver}
*
* @param client the tool wrapping the analyzer, such as an IDE or a CLI
* @param files the set of files to check with lint. This can reference Android projects,
* or directories containing Android projects, or individual XML or Java files
* (typically for incremental IDE analysis).
*/
public LintRequest(@NonNull LintClient client, @NonNull List<File> files) {
mClient = client;
mFiles = files;
}
/**
* Returns the lint client requesting the lint check
*
* @return the client, never null
*/
@NonNull
public LintClient getClient() {
return mClient;
}
/**
* Returns the set of files to check with lint. This can reference Android projects,
* or directories containing Android projects, or individual XML or Java files
* (typically for incremental IDE analysis).
*
* @return the set of files to check, should not be empty
*/
@NonNull
public List<File> getFiles() {
return mFiles;
}
/**
* Sets the scope to use; lint checks which require a wider scope set
* will be ignored
*
* @return the scope to use, or null to use the default
*/
@Nullable
public EnumSet<Scope> getScope() {
return mScope;
}
/**
* Sets the scope to use; lint checks which require a wider scope set
* will be ignored
*
* @param scope the scope
* @return this, for constructor chaining
*/
@NonNull
public LintRequest setScope(@Nullable EnumSet<Scope> scope) {
mScope = scope;
return this;
}
/**
* Returns {@code true} if lint is invoked as part of a release mode build,
* {@code false} if it is part of a debug mode build, and {@code null} if
* the release mode is not known
*
* @return true if this lint is running in release mode, null if not known
*/
@Nullable
public Boolean isReleaseMode() {
return mReleaseMode;
}
/**
* Sets the release mode. Use {@code true} if lint is invoked as part of a
* release mode build, {@code false} if it is part of a debug mode build,
* and {@code null} if the release mode is not known
*
* @param releaseMode true if this lint is running in release mode, null if not known
* @return this, for constructor chaining
*/
@NonNull
public LintRequest setReleaseMode(@Nullable Boolean releaseMode) {
mReleaseMode = releaseMode;
return this;
}
/**
* Gets the projects for the lint requests. This is optional; if not provided lint will search
* the {@link #getFiles()} directories and look for projects via {@link
* LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to
* set up all the projects ahead of time, and associate those projects with native resources
* (in an IDE for example, each lint project can be associated with the corresponding IDE
* project).
*
* @return a collection of projects, or null
*/
@Nullable
public Collection<Project> getProjects() {
return mProjects;
}
/**
* Sets the projects for the lint requests. This is optional; if not provided lint will search
* the {@link #getFiles()} directories and look for projects via {@link
* LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to
* set up all the projects ahead of time, and associate those projects with native resources
* (in an IDE for example, each lint project can be associated with the corresponding IDE
* project).
*
* @param projects a collection of projects, or null
*/
public void setProjects(@Nullable Collection<Project> projects) {
mProjects = projects;
}
/**
* Returns the project to be used as the main project during analysis. This is
* usually the project itself, but when you are for example analyzing a library project,
* it can be the app project using the library.
*
* @param project the project to look up the main project for
* @return the main project
*/
@SuppressWarnings("MethodMayBeStatic")
@NonNull
public Project getMainProject(@NonNull Project project) {
return project;
}
}
@@ -1,207 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.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;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Visitor for "other" files: files that aren't java sources,
* XML sources, etc -- or which should have custom handling in some
* other way.
*/
class OtherFileVisitor {
@NonNull
private final List<Detector> mDetectors;
@NonNull
private Map<Scope, List<File>> mFiles = new EnumMap<Scope, List<File>>(Scope.class);
OtherFileVisitor(@NonNull List<Detector> detectors) {
mDetectors = detectors;
}
/** Analyze other files in the given project */
void scan(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main) {
// Collect all project files
File projectFolder = project.getDir();
EnumSet<Scope> scopes = EnumSet.noneOf(Scope.class);
for (Detector detector : mDetectors) {
OtherFileScanner fileScanner = (OtherFileScanner) detector;
EnumSet<Scope> applicable = fileScanner.getApplicableFiles();
if (applicable.contains(Scope.OTHER)) {
scopes = Scope.ALL;
break;
}
scopes.addAll(applicable);
}
List<File> subset = project.getSubset();
if (scopes.contains(Scope.RESOURCE_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (SdkUtils.endsWith(file.getPath(), DOT_XML) &&
!file.getName().equals(ANDROID_MANIFEST_XML)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.RESOURCE_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File res : project.getResourceFolders()) {
collectFiles(files, res);
}
File assets = new File(projectFolder, FD_ASSETS);
if (assets.exists()) {
collectFiles(files, assets);
}
if (!files.isEmpty()) {
mFiles.put(Scope.RESOURCE_FILE, files);
}
}
}
if (scopes.contains(Scope.JAVA_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(".kt")) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.JAVA_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File srcFolder : project.getJavaSourceFolders()) {
collectFiles(files, srcFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.JAVA_FILE, files);
}
}
}
if (scopes.contains(Scope.CLASS_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(DOT_CLASS)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.CLASS_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File classFolder : project.getJavaClassFolders()) {
collectFiles(files, classFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.CLASS_FILE, files);
}
}
}
if (scopes.contains(Scope.MANIFEST)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getName().equals(ANDROID_MANIFEST_XML)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.MANIFEST, files);
}
} else {
List<File> manifestFiles = project.getManifestFiles();
if (manifestFiles != null) {
mFiles.put(Scope.MANIFEST, manifestFiles);
}
}
}
for (Map.Entry<Scope, List<File>> entry : mFiles.entrySet()) {
Scope scope = entry.getKey();
List<File> files = entry.getValue();
List<Detector> applicable = new ArrayList<Detector>(mDetectors.size());
for (Detector detector : mDetectors) {
OtherFileScanner fileScanner = (OtherFileScanner) detector;
EnumSet<Scope> appliesTo = fileScanner.getApplicableFiles();
if (appliesTo.contains(Scope.OTHER) || appliesTo.contains(scope)) {
applicable.add(detector);
}
}
if (!applicable.isEmpty()) {
for (File file : files) {
Context context = new Context(driver, project, main, file);
for (Detector detector : applicable) {
detector.beforeCheckFile(context);
detector.run(context);
detector.afterCheckFile(context);
}
if (driver.isCanceled()) {
return;
}
}
}
}
}
private static void collectFiles(List<File> files, File file) {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
collectFiles(files, child);
}
}
} else {
files.add(file);
}
}
}
@@ -1,245 +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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.XmlScanner;
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.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
/**
* Specialized visitor for running detectors on resources: typically XML documents,
* but also binary resources.
* <p>
* It operates in two phases:
* <ol>
* <li> First, it computes a set of maps where it generates a map from each
* significant element name, and each significant attribute name, to a list
* of detectors to consult for that element or attribute name.
* The set of element names or attribute names (or both) that a detector
* is interested in is provided by the detectors themselves.
* <li> Second, it iterates over the document a single time. For each element and
* attribute it looks up the list of interested detectors, and runs them.
* </ol>
* It also notifies all the detectors before and after the document is processed
* such that they can do pre- and post-processing.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
class ResourceVisitor {
private final Map<String, List<Detector.XmlScanner>> mElementToCheck =
new HashMap<String, List<Detector.XmlScanner>>();
private final Map<String, List<Detector.XmlScanner>> mAttributeToCheck =
new HashMap<String, List<Detector.XmlScanner>>();
private final List<Detector.XmlScanner> mDocumentDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<Detector.XmlScanner> mAllElementDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<Detector.XmlScanner> mAllAttributeDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<? extends Detector> mAllDetectors;
private final List<? extends Detector> mBinaryDetectors;
private final XmlParser mParser;
// Really want this:
//<T extends List<Detector> & Detector.XmlScanner> XmlVisitor(IDomParser parser,
// T xmlDetectors) {
// but it makes client code tricky and ugly.
ResourceVisitor(
@NonNull XmlParser parser,
@NonNull List<? extends Detector> xmlDetectors,
@Nullable List<Detector> binaryDetectors) {
mParser = parser;
mAllDetectors = xmlDetectors;
mBinaryDetectors = binaryDetectors;
// TODO: Check appliesTo() for files, and find a quick way to enable/disable
// rules when running through a full project!
for (Detector detector : xmlDetectors) {
Detector.XmlScanner xmlDetector = (XmlScanner) detector;
Collection<String> attributes = xmlDetector.getApplicableAttributes();
if (attributes == XmlScanner.ALL) {
mAllAttributeDetectors.add(xmlDetector);
} else if (attributes != null) {
for (String attribute : attributes) {
List<Detector.XmlScanner> list = mAttributeToCheck.get(attribute);
if (list == null) {
list = new ArrayList<Detector.XmlScanner>();
mAttributeToCheck.put(attribute, list);
}
list.add(xmlDetector);
}
}
Collection<String> elements = xmlDetector.getApplicableElements();
if (elements == XmlScanner.ALL) {
mAllElementDetectors.add(xmlDetector);
} else if (elements != null) {
for (String element : elements) {
List<Detector.XmlScanner> list = mElementToCheck.get(element);
if (list == null) {
list = new ArrayList<Detector.XmlScanner>();
mElementToCheck.put(element, list);
}
list.add(xmlDetector);
}
}
if ((attributes == null || (attributes.isEmpty()
&& attributes != XmlScanner.ALL))
&& (elements == null || (elements.isEmpty()
&& elements != XmlScanner.ALL))) {
mDocumentDetectors.add(xmlDetector);
}
}
}
void visitFile(@NonNull XmlContext context, @NonNull File file) {
assert LintUtils.isXmlFile(file);
try {
if (context.document == null) {
context.document = mParser.parseXml(context);
if (context.document == null) {
// No need to log this; the parser should be reporting
// a full warning (such as IssueRegistry#PARSER_ERROR)
// with details, location, etc.
return;
}
if (context.document.getDocumentElement() == null) {
// Ignore empty documents
return;
}
}
for (Detector check : mAllDetectors) {
check.beforeCheckFile(context);
}
for (Detector.XmlScanner check : mDocumentDetectors) {
check.visitDocument(context, context.document);
}
if (!mElementToCheck.isEmpty() || !mAttributeToCheck.isEmpty()
|| !mAllAttributeDetectors.isEmpty() || !mAllElementDetectors.isEmpty()) {
visitElement(context, context.document.getDocumentElement());
}
for (Detector check : mAllDetectors) {
check.afterCheckFile(context);
}
} finally {
if (context.document != null) {
mParser.dispose(context, context.document);
context.document = null;
}
}
}
private void visitElement(@NonNull XmlContext context, @NonNull Element element) {
List<Detector.XmlScanner> elementChecks = mElementToCheck.get(element.getTagName());
if (elementChecks != null) {
assert elementChecks instanceof RandomAccess;
for (XmlScanner check : elementChecks) {
check.visitElement(context, element);
}
}
if (!mAllElementDetectors.isEmpty()) {
for (XmlScanner check : mAllElementDetectors) {
check.visitElement(context, element);
}
}
if (!mAttributeToCheck.isEmpty() || !mAllAttributeDetectors.isEmpty()) {
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
String name = attribute.getLocalName();
if (name == null) {
name = attribute.getName();
}
List<Detector.XmlScanner> list = mAttributeToCheck.get(name);
if (list != null) {
for (XmlScanner check : list) {
check.visitAttribute(context, attribute);
}
}
if (!mAllAttributeDetectors.isEmpty()) {
for (XmlScanner check : mAllAttributeDetectors) {
check.visitAttribute(context, attribute);
}
}
}
}
// Visit children
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
visitElement(context, (Element) child);
}
}
// Post hooks
if (elementChecks != null) {
for (XmlScanner check : elementChecks) {
check.visitElementAfter(context, element);
}
}
if (!mAllElementDetectors.isEmpty()) {
for (XmlScanner check : mAllElementDetectors) {
check.visitElementAfter(context, element);
}
}
}
@NonNull
public XmlParser getParser() {
return mParser;
}
public void visitBinaryResource(@NonNull ResourceContext context) {
if (mBinaryDetectors == null) {
return;
}
for (Detector check : mBinaryDetectors) {
check.beforeCheckFile(context);
check.checkBinaryResource(context);
check.afterCheckFile(context);
}
}
}
@@ -1,90 +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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* Information about SDKs
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class SdkInfo {
/**
* Returns true if the given child view is the same class or a sub class of
* the given parent view class
*
* @param parentViewFqcn the fully qualified class name of the parent view
* @param childViewFqcn the fully qualified class name of the child view
* @return true if the child view is a sub view of (or the same class as)
* the parent view
*/
public boolean isSubViewOf(@NonNull String parentViewFqcn, @NonNull String childViewFqcn) {
while (!childViewFqcn.equals("android.view.View")) { //$NON-NLS-1$
if (parentViewFqcn.equals(childViewFqcn)) {
return true;
}
String parent = getParentViewClass(childViewFqcn);
if (parent == null) {
// Unknown view - err on the side of caution
return true;
}
childViewFqcn = parent;
}
return false;
}
/**
* Returns the fully qualified name of the parent view, or null if the view
* is the root android.view.View class.
*
* @param fqcn the fully qualified class name of the view
* @return the fully qualified class name of the parent view, or null
*/
@Nullable
public abstract String getParentViewClass(@NonNull String fqcn);
/**
* Returns the class name of the parent view, or null if the view is the
* root android.view.View class. This is the same as the
* {@link #getParentViewClass(String)} but without the package.
*
* @param name the view class name to look up the parent for (not including
* package)
* @return the view name of the parent
*/
@Nullable
public abstract String getParentViewName(@NonNull String name);
/**
* Returns true if the given widget name is a layout
*
* @param tag the XML tag for the view
* @return true if the given tag corresponds to a layout
*/
public boolean isLayout(@NonNull String tag) {
return tag.endsWith("Layout"); //$NON-NLS-1$
}
// TODO: Add access to resource resolution here.
}
@@ -1,338 +0,0 @@
/*
* 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.SdkConstants;
import com.android.annotations.NonNull;
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.*;
import org.jetbrains.uast.*;
import org.jetbrains.uast.UReferenceExpression;
import org.jetbrains.uast.java.JavaAbstractUExpression;
import org.jetbrains.uast.java.JavaUDeclarationsExpression;
import java.util.Collections;
import java.util.List;
public class UastLintUtils {
@Nullable
public static String getQualifiedName(PsiElement element) {
if (element instanceof PsiClass) {
return ((PsiClass) element).getQualifiedName();
} else if (element instanceof PsiMethod) {
PsiClass containingClass = ((PsiMethod) element).getContainingClass();
if (containingClass == null) {
return null;
}
String containingClassFqName = getQualifiedName(containingClass);
if (containingClassFqName == null) {
return null;
}
return containingClassFqName + "." + ((PsiMethod) element).getName();
} else if (element instanceof PsiField) {
PsiClass containingClass = ((PsiField) element).getContainingClass();
if (containingClass == null) {
return null;
}
String containingClassFqName = getQualifiedName(containingClass);
if (containingClassFqName == null) {
return null;
}
return containingClassFqName + "." + ((PsiField) element).getName();
} else {
return null;
}
}
@Nullable
public static PsiElement resolve(ExternalReferenceExpression expression, UElement context) {
UDeclaration declaration = UastUtils.getParentOfType(context, UDeclaration.class);
if (declaration == null) {
return null;
}
return expression.resolve(declaration.getPsi());
}
@NonNull
public static String getClassName(PsiClassType type) {
PsiClass psiClass = type.resolve();
if (psiClass == null) {
return type.getClassName();
} else {
return getClassName(psiClass);
}
}
@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, -1);
containingFunction.accept(finder);
lastAssignment = finder.getLastAssignment();
}
} else {
lastAssignment = context.getUastContext().getInitializerBody(variable);
}
if (lastAssignment instanceof UExpression) {
return (UExpression) lastAssignment;
}
return null;
}
@Nullable
public static String getReferenceName(UReferenceExpression expression) {
if (expression instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) expression).getIdentifier();
} else if (expression instanceof UQualifiedReferenceExpression) {
UExpression selector = ((UQualifiedReferenceExpression) expression).getSelector();
if (selector instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) selector).getIdentifier();
}
}
return null;
}
@Nullable
public static Object findLastValue(
@NonNull PsiVariable variable,
@NonNull UElement call,
@NonNull JavaContext context,
@NonNull ConstantEvaluator evaluator) {
Object value = null;
if (!variable.hasModifierProperty(PsiModifier.FINAL) &&
(variable instanceof PsiLocalVariable || variable instanceof PsiParameter)) {
UMethod containingFunction = UastUtils.getContainingUMethod(call);
if (containingFunction != null) {
ConstantEvaluator.LastAssignmentFinder
finder = new ConstantEvaluator.LastAssignmentFinder(
variable, call, context, evaluator, 1);
containingFunction.getUastBody().accept(finder);
value = finder.getCurrentValue();
}
} else {
UExpression initializer = context.getUastContext().getInitializerBody(variable);
if (initializer != null) {
value = initializer.evaluate();
}
}
return value;
}
@Nullable
private static AndroidReference toAndroidReference(UQualifiedReferenceExpression expression) {
List<String> path = UastUtils.asQualifiedPath(expression);
String packageNameFromResolved = null;
PsiClass containingClass = UastUtils.getContainingClass(expression.resolve());
if (containingClass != null) {
String containingClassFqName = containingClass.getQualifiedName();
if (containingClassFqName != null) {
int i = containingClassFqName.lastIndexOf(".R.");
if (i >= 0) {
packageNameFromResolved = containingClassFqName.substring(0, i);
}
}
}
if (path == null) {
return null;
}
int size = path.size();
if (size < 3) {
return null;
}
String r = path.get(size - 3);
if (!r.equals(SdkConstants.R_CLASS)) {
return null;
}
String packageName = packageNameFromResolved != null
? packageNameFromResolved
: Joiner.on('.').join(path.subList(0, size - 3));
String type = path.get(size - 2);
String name = path.get(size - 1);
ResourceType resourceType = null;
for (ResourceType value : ResourceType.values()) {
if (value.getName().equals(type)) {
resourceType = value;
break;
}
}
if (resourceType == null) {
return null;
}
return new AndroidReference(expression, packageName, resourceType, name);
}
@Nullable
public static AndroidReference toAndroidReferenceViaResolve(UElement element) {
if (element instanceof UQualifiedReferenceExpression
&& element instanceof JavaAbstractUExpression) {
AndroidReference ref = toAndroidReference((UQualifiedReferenceExpression) element);
if (ref != null) {
return ref;
}
}
PsiElement declaration;
if (element instanceof UVariable) {
declaration = ((UVariable) element).getPsi();
} else if (element instanceof UResolvable) {
declaration = ((UResolvable) element).resolve();
} else {
return null;
}
if (declaration == null && element instanceof USimpleNameReferenceExpression
&& element instanceof JavaAbstractUExpression) {
// R class can't be resolved in tests so we need to use heuristics to calc the reference
UExpression maybeQualified = UastUtils.getQualifiedParentOrThis((UExpression) element);
if (maybeQualified instanceof UQualifiedReferenceExpression) {
AndroidReference ref = toAndroidReference(
(UQualifiedReferenceExpression) maybeQualified);
if (ref != null) {
return ref;
}
}
}
if (!(declaration instanceof PsiVariable)) {
return null;
}
PsiVariable variable = (PsiVariable) declaration;
if (!(variable instanceof PsiField)
|| variable.getType() != PsiType.INT
|| !variable.hasModifierProperty(PsiModifier.STATIC)
|| !variable.hasModifierProperty(PsiModifier.FINAL)) {
return null;
}
PsiClass resTypeClass = ((PsiField) variable).getContainingClass();
if (resTypeClass == null || !resTypeClass.hasModifierProperty(PsiModifier.STATIC)) {
return null;
}
PsiClass rClass = resTypeClass.getContainingClass();
if (rClass == null || rClass.getContainingClass() != null || !"R".equals(rClass.getName())) {
return null;
}
String packageName = ((PsiJavaFile) rClass.getContainingFile()).getPackageName();
if (packageName.isEmpty()) {
return null;
}
String resourceTypeName = resTypeClass.getName();
ResourceType resourceType = null;
for (ResourceType value : ResourceType.values()) {
if (value.getName().equals(resourceTypeName)) {
resourceType = value;
break;
}
}
if (resourceType == null) {
return null;
}
String resourceName = variable.getName();
UExpression node;
if (element instanceof UExpression) {
node = (UExpression) element;
} else if (element instanceof UVariable) {
node = new JavaUDeclarationsExpression(
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;
}
}
@@ -1,137 +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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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;
/**
* A wrapper for an XML parser. This allows tools integrating lint to map directly
* to builtin services, such as already-parsed data structures in XML editors.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class XmlParser {
/**
* Parse the file pointed to by the given context and return as a Document
*
* @param context the context pointing to the file to be parsed, typically
* via {@link Context#getContents()} but the file handle (
* {@link Context#file} can also be used to map to an existing
* editor buffer in the surrounding tool, etc)
* @return the parsed DOM document, or null if parsing fails
*/
@Nullable
public abstract Document parseXml(@NonNull XmlContext context);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns a {@link Location} for the given DOM node. Like
* {@link #getLocation(XmlContext, Node)}, but allows a position range that
* is a subset of the node range.
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @param start the starting position within the node, inclusive
* @param end the ending position within the node, exclusive
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node,
int start, int end);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getNameLocation(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getValueLocation(@NonNull XmlContext context, @NonNull Attr node);
/**
* Creates a light-weight handle to a location for the given node. It can be
* turned into a full fledged location by
* {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}.
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return a location handle
*/
@NonNull
public abstract Location.Handle createLocationHandle(@NonNull XmlContext context,
@NonNull Node node);
/**
* Dispose any data structures held for the given context.
* @param context information about the file previously parsed
* @param document the document that was parsed and is now being disposed
*/
public void dispose(@NonNull XmlContext context, @NonNull Document document) {
}
/**
* Returns the start offset of the given node, or -1 if not known
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return the start offset, or -1 if not known
*/
public abstract int getNodeStartOffset(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns the end offset of the given node, or -1 if not known
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return the end offset, or -1 if not known
*/
public abstract int getNodeEndOffset(@NonNull XmlContext context, @NonNull Node node);
}
@@ -1,190 +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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* A category is a container for related issues.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public final class Category implements Comparable<Category> {
private final String mName;
private final int mPriority;
private final Category mParent;
/**
* Creates a new {@link Category}.
*
* @param parent the name of a parent category, or null
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
*/
private Category(
@Nullable Category parent,
@NonNull String name,
int priority) {
mParent = parent;
mName = name;
mPriority = priority;
}
/**
* Creates a new top level {@link Category} with the given sorting priority.
*
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
* @return a new category
*/
@NonNull
public static Category create(@NonNull String name, int priority) {
return new Category(null, name, priority);
}
/**
* Creates a new top level {@link Category} with the given sorting priority.
*
* @param parent the name of a parent category, or null
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
* @return a new category
*/
@NonNull
public static Category create(@Nullable Category parent, @NonNull String name, int priority) {
return new Category(parent, name, priority);
}
/**
* Returns the parent category, or null if this is a top level category
*
* @return the parent category, or null if this is a top level category
*/
public Category getParent() {
return mParent;
}
/**
* Returns the name of this category
*
* @return the name of this category
*/
public String getName() {
return mName;
}
/**
* Returns a full name for this category. For a top level category, this is just
* the {@link #getName()} value, but for nested categories it will include the parent
* names as well.
*
* @return a full name for this category
*/
public String getFullName() {
if (mParent != null) {
return mParent.getFullName() + ':' + mName;
} else {
return mName;
}
}
@Override
public 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;
} else if (other.mParent == this) {
return -1;
}
}
int delta = other.mPriority - mPriority;
if (delta != 0) {
return delta;
}
return mName.compareTo(other.mName);
}
/** Issues related to running lint itself */
public static final Category LINT = create("Lint", 110);
/** Issues related to correctness */
public static final Category CORRECTNESS = create("Correctness", 100);
/** Issues related to security */
public static final Category SECURITY = create("Security", 90);
/** Issues related to performance */
public static final Category PERFORMANCE = create("Performance", 80);
/** Issues related to usability */
public static final Category USABILITY = create("Usability", 70);
/** Issues related to accessibility */
public static final Category A11Y = create("Accessibility", 60);
/** Issues related to internationalization */
public static final Category I18N = create("Internationalization", 50);
// Sub categories
/** Issues related to icons */
public static final Category ICONS = create(USABILITY, "Icons", 73);
/** Issues related to typography */
public static final Category TYPOGRAPHY = create(USABILITY, "Typography", 76);
/** Issues related to messages/strings */
public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95);
/** Issues related to right to left and bidirectional text support */
public static final Category RTL = create(I18N, "Bidirectional Text", 40);
}
@@ -1,725 +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.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.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import org.jetbrains.org.objectweb.asm.Type;
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 org.jetbrains.org.objectweb.asm.tree.LineNumberNode;
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode;
import org.jetbrains.org.objectweb.asm.tree.MethodNode;
import java.io.File;
import java.util.List;
/**
* A {@link Context} used when checking .class files.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class ClassContext extends Context {
private final File mBinDir;
/** The class file DOM root node */
private final ClassNode mClassNode;
/** The class file byte data */
private final byte[] mBytes;
/** The source file, if known/found */
private File mSourceFile;
/** The contents of the source file, if source file is known/found */
private String mSourceContents;
/** Whether we've searched for the source file (used to avoid repeated failed searches) */
private boolean mSearchedForSource;
/** If the file is a relative path within a jar file, this is the jar file, otherwise null */
private final File mJarFile;
/** Whether this class is part of a library (rather than corresponding to one of the
* source files in this project */
private final boolean mFromLibrary;
/**
* Construct a new {@link ClassContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is the
* root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param jarFile If the file is a relative path within a jar file, this is
* the jar file, otherwise null
* @param binDir the root binary directory containing this .class file.
* @param bytes the bytecode raw data
* @param classNode the bytecode object model
* @param fromLibrary whether this class is from a library rather than part
* of this project
* @param sourceContents initial contents of the Java source, if known, or
* null
*/
public ClassContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable File jarFile,
@NonNull File binDir,
@NonNull byte[] bytes,
@NonNull ClassNode classNode,
boolean fromLibrary,
@Nullable String sourceContents) {
super(driver, project, main, file);
mJarFile = jarFile;
mBinDir = binDir;
mBytes = bytes;
mClassNode = classNode;
mFromLibrary = fromLibrary;
mSourceContents = sourceContents;
}
/**
* Returns the raw bytecode data for this class file
*
* @return the byte array containing the bytecode data
*/
@NonNull
public byte[] getBytecode() {
return mBytes;
}
/**
* Returns the bytecode object model
*
* @return the bytecode object model, never null
*/
@NonNull
public ClassNode getClassNode() {
return mClassNode;
}
/**
* Returns the jar file, if any. If this is null, the .class file is a real file
* on disk, otherwise it represents a relative path within the jar file.
*
* @return the jar file, or null
*/
@Nullable
public File getJarFile() {
return mJarFile;
}
/**
* Returns whether this class is part of a library (not this project).
*
* @return true if this class is part of a library
*/
public boolean isFromClassLibrary() {
return mFromLibrary;
}
/**
* Returns the source file for this class file, if possible.
*
* @return the source file, or null
*/
@Nullable
public File getSourceFile() {
if (mSourceFile == null && !mSearchedForSource) {
mSearchedForSource = true;
String source = mClassNode.sourceFile;
if (source == null) {
source = file.getName();
if (source.endsWith(DOT_CLASS)) {
source = source.substring(0, source.length() - DOT_CLASS.length()) + ".kt";
}
int index = source.indexOf('$');
if (index != -1) {
source = source.substring(0, index) + ".kt";
}
}
if (source != null) {
if (mJarFile != null) {
String relative = file.getParent() + File.separator + source;
List<File> sources = getProject().getJavaSourceFolders();
for (File dir : sources) {
File sourceFile = new File(dir, relative);
if (sourceFile.exists()) {
mSourceFile = sourceFile;
break;
}
}
} else {
// Determine package
String topPath = mBinDir.getPath();
String parentPath = file.getParentFile().getPath();
if (parentPath.startsWith(topPath)) {
int start = topPath.length() + 1;
String relative = start > parentPath.length() ? // default package?
"" : parentPath.substring(start);
List<File> sources = getProject().getJavaSourceFolders();
for (File dir : sources) {
File sourceFile = new File(dir, relative + File.separator + source);
if (sourceFile.exists()) {
mSourceFile = sourceFile;
break;
}
}
}
}
}
}
return mSourceFile;
}
/**
* Returns the contents of the source file for this class file, if found.
*
* @return the source contents, or ""
*/
@NonNull
public String getSourceContents() {
if (mSourceContents == null) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
mSourceContents = getClient().readFile(mSourceFile);
}
if (mSourceContents == null) {
mSourceContents = "";
}
}
return mSourceContents;
}
/**
* Returns the contents of the source file for this class file, if found. If
* {@code read} is false, do not read the source contents if it has not
* already been read. (This is primarily intended for the lint
* infrastructure; most client code would call {@link #getSourceContents()}
* .)
*
* @param read whether to read the source contents if it has not already
* been initialized
* @return the source contents, which will never be null if {@code read} is
* true, or null if {@code read} is false and the source contents
* hasn't already been read.
*/
@Nullable
public String getSourceContents(boolean read) {
if (read) {
return getSourceContents();
} else {
return mSourceContents;
}
}
/**
* Returns a location for the given source line number in this class file's
* source file, if available.
*
* @param line the line number (1-based, which is what ASM uses)
* @param patternStart optional pattern to search for in the source for
* range start
* @param patternEnd optional pattern to search for in the source for range
* end
* @param hints additional hints about the pattern search (provided
* {@code patternStart} is non null)
* @return a location, never null
*/
@NonNull
public Location getLocationForLine(int line, @Nullable String patternStart,
@Nullable String patternEnd, @Nullable SearchHints hints) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
// ASM line numbers are 1-based, and lint line numbers are 0-based
if (line != -1) {
return Location.create(sourceFile, getSourceContents(), line - 1,
patternStart, patternEnd, hints);
} else {
return Location.create(sourceFile);
}
}
return Location.create(file);
}
/**
* Reports an issue.
* <p>
* Detectors should only call this method if an error applies to the whole class
* scope and there is no specific method or field that applies to the error.
* If so, use
* {@link #report(Issue, MethodNode, AbstractInsnNode, Location, String)} or
* {@link #report(Issue, FieldNode, Location, String)}, such that
* suppress annotations are checked.
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
@Override
public void report(
@NonNull Issue issue,
@NonNull Location location,
@NonNull String message) {
if (mDriver.isSuppressed(issue, mClassNode)) {
return;
}
ClassNode curr = mClassNode;
while (curr != null) {
ClassNode prev = curr;
curr = mDriver.getOuterClassNode(curr);
if (curr != null) {
if (prev.outerMethod != null) {
@SuppressWarnings("rawtypes") // ASM API
List methods = curr.methods;
for (Object m : methods) {
MethodNode method = (MethodNode) m;
if (method.name.equals(prev.outerMethod)
&& method.desc.equals(prev.outerMethodDesc)) {
// Found the outer method for this anonymous class; continue
// reporting on it (which will also work its way up the parent
// class hierarchy)
if (method != null && mDriver.isSuppressed(issue, mClassNode, method,
null)) {
return;
}
break;
}
}
}
if (mDriver.isSuppressed(issue, curr)) {
return;
}
}
}
super.report(issue, location, message);
}
// Unfortunately, ASMs nodes do not extend a common DOM node type with parent
// pointers, so we have to have multiple methods which pass in each type
// of node (class, method, field) to be checked.
/**
* Reports an issue applicable to a given method node.
*
* @param issue the issue to report
* @param method the method scope the error applies to. The lint
* infrastructure will check whether there are suppress
* annotations on this method (or its enclosing class) and if so
* suppress the warning without involving the client.
* @param instruction the instruction within the method the error applies
* to. You cannot place annotations on individual method
* instructions (for example, annotations on local variables are
* allowed, but are not kept in the .class file). However, this
* instruction is needed to handle suppressing errors on field
* initializations; in that case, the errors may be reported in
* the {@code <clinit>} method, but the annotation is found not
* on that method but for the {@link FieldNode}'s.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@NonNull Location location,
@NonNull String message) {
if (method != null && mDriver.isSuppressed(issue, mClassNode, method, instruction)) {
return;
}
report(issue, location, message); // also checks the class node
}
/**
* Reports an issue applicable to a given method node.
*
* @param issue the issue to report
* @param field the scope the error applies to. The lint infrastructure
* will check whether there are suppress annotations on this field (or its enclosing
* class) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@NonNull Location location,
@NonNull String message) {
if (field != null && mDriver.isSuppressed(issue, field)) {
return;
}
report(issue, location, message); // also checks the class node
}
/**
* Report an error.
* Like {@link #report(Issue, MethodNode, AbstractInsnNode, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, method, instruction, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, FieldNode, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, field, location, message);
}
/**
* Finds the line number closest to the given node
*
* @param node the instruction node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull AbstractInsnNode node) {
AbstractInsnNode curr = node;
// First search backwards
while (curr != null) {
if (curr.getType() == AbstractInsnNode.LINE) {
return ((LineNumberNode) curr).line;
}
curr = curr.getPrevious();
}
// Then search forwards
curr = node;
while (curr != null) {
if (curr.getType() == AbstractInsnNode.LINE) {
return ((LineNumberNode) curr).line;
}
curr = curr.getNext();
}
return -1;
}
/**
* Finds the line number closest to the given method declaration
*
* @param node the method node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull MethodNode node) {
if (node.instructions != null && node.instructions.size() > 0) {
return findLineNumber(node.instructions.get(0));
}
return -1;
}
/**
* Finds the line number closest to the given class declaration
*
* @param node the method node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull ClassNode node) {
if (node.methods != null && !node.methods.isEmpty()) {
MethodNode firstMethod = getFirstRealMethod(node);
if (firstMethod != null) {
return findLineNumber(firstMethod);
}
}
return -1;
}
/**
* Returns a location for the given {@link ClassNode}, where class node is
* either the top level class, or an inner class, in the current context.
*
* @param classNode the class in the current context
* @return a location pointing to the class declaration, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull ClassNode classNode) {
// Attempt to find a proper location for this class. This is tricky
// since classes do not have line number entries in the class file; we need
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName;
} else {
pattern = classNode.name;
}
int index = pattern.lastIndexOf('$');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
index = pattern.lastIndexOf('/');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
return getLocationForLine(findLineNumber(classNode), pattern, null,
SearchHints.create(BACKWARD).matchJavaSymbol());
}
@Nullable
private static MethodNode getFirstRealMethod(@NonNull ClassNode classNode) {
// Return the first method in the class for line number purposes. Skip <init>,
// since it's typically not located near the real source of the method.
if (classNode.methods != null) {
@SuppressWarnings("rawtypes") // ASM API
List methods = classNode.methods;
for (Object m : methods) {
MethodNode method = (MethodNode) m;
if (method.name.charAt(0) != '<') {
return method;
}
}
if (!classNode.methods.isEmpty()) {
return (MethodNode) classNode.methods.get(0);
}
}
return null;
}
/**
* Returns a location for the given {@link MethodNode}.
*
* @param methodNode the class in the current context
* @param classNode the class containing the method
* @return a location pointing to the class declaration, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull MethodNode methodNode,
@NonNull ClassNode classNode) {
// Attempt to find a proper location for this class. This is tricky
// since classes do not have line number entries in the class file; we need
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
SearchDirection searchMode;
if (methodNode.name.equals(CONSTRUCTOR_NAME)) {
searchMode = EOL_BACKWARD;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName.substring(classNode.superName.lastIndexOf('/') + 1);
} else {
pattern = classNode.name.substring(classNode.name.lastIndexOf('$') + 1);
}
} else {
searchMode = BACKWARD;
pattern = methodNode.name;
}
return getLocationForLine(findLineNumber(methodNode), pattern, null,
SearchHints.create(searchMode).matchJavaSymbol());
}
/**
* Returns a location for the given {@link AbstractInsnNode}.
*
* @param instruction the instruction to look up the location for
* @return a location pointing to the instruction, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull AbstractInsnNode instruction) {
SearchHints hints = SearchHints.create(FORWARD).matchJavaSymbol();
String pattern = null;
if (instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (call.name.equals(CONSTRUCTOR_NAME)) {
pattern = call.owner;
hints = hints.matchConstructor();
} else {
pattern = call.name;
}
int index = pattern.lastIndexOf('$');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
index = pattern.lastIndexOf('/');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
}
int line = findLineNumber(instruction);
return getLocationForLine(line, pattern, null, hints);
}
private static boolean isAnonymousClass(@NonNull String fqcn) {
int lastIndex = fqcn.lastIndexOf('$');
if (lastIndex != -1 && lastIndex < fqcn.length() - 1) {
if (Character.isDigit(fqcn.charAt(lastIndex + 1))) {
return true;
}
}
return false;
}
/**
* Converts from a VM owner name (such as foo/bar/Foo$Baz) to a
* fully qualified class name (such as foo.bar.Foo.Baz).
*
* @param owner the owner name to convert
* @return the corresponding fully qualified class name
*/
@NonNull
public static String getFqcn(@NonNull String owner) {
return owner.replace('/', '.').replace('$','.');
}
/**
* Computes a user-readable type signature from the given class owner, name
* and description. For example, for owner="foo/bar/Foo$Baz", name="foo",
* description="(I)V", it returns "void foo.bar.Foo.Bar#foo(int)".
*
* @param owner the class name
* @param name the method name
* @param desc the method description
* @return a user-readable string
*/
public static String createSignature(String owner, String name, String desc) {
StringBuilder sb = new StringBuilder(100);
if (desc != null) {
Type returnType = Type.getReturnType(desc);
sb.append(getTypeString(returnType));
sb.append(' ');
}
if (owner != null) {
sb.append(getFqcn(owner));
}
if (name != null) {
sb.append('#');
sb.append(name);
if (desc != null) {
Type[] argumentTypes = Type.getArgumentTypes(desc);
if (argumentTypes != null && argumentTypes.length > 0) {
sb.append('(');
boolean first = true;
for (Type type : argumentTypes) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(getTypeString(type));
}
sb.append(')');
}
}
}
return sb.toString();
}
private static String getTypeString(Type type) {
String s = type.getClassName();
if (s.startsWith("java.lang.")) { //$NON-NLS-1$
s = s.substring("java.lang.".length()); //$NON-NLS-1$
}
return s;
}
/**
* Computes the internal class name of the given fully qualified class name.
* For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar
*
* @param fqcn the fully qualified class name
* @return the internal class name
*/
@NonNull
public static String getInternalName(@NonNull String fqcn) {
if (fqcn.indexOf('.') == -1) {
return fqcn;
}
int index = fqcn.indexOf('<');
if(index != -1) {
fqcn = fqcn.substring(0, index);
}
// If class name contains $, it's not an ambiguous inner class name.
if (fqcn.indexOf('$') != -1) {
return fqcn.replace('.', '/');
}
// Let's assume that components that start with Caps are class names.
StringBuilder sb = new StringBuilder(fqcn.length());
String prev = null;
for (String part : Splitter.on('.').split(fqcn)) {
if (prev != null && !prev.isEmpty()) {
if (Character.isUpperCase(prev.charAt(0))) {
sb.append('$');
} else {
sb.append('/');
}
}
sb.append(part);
prev = part;
}
return sb.toString();
}
}
@@ -1,451 +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.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;
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 java.io.File;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* Context passed to the detectors during an analysis run. It provides
* information about the file being analyzed, it allows shared properties (so
* the detectors can share results), etc.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class Context {
/**
* The file being checked. Note that this may not always be to a concrete
* file. For example, in the {@link Detector#beforeCheckProject(Context)}
* method, the context file is the directory of the project.
*/
public final File file;
/** The driver running through the checks */
protected final LintDriver mDriver;
/** The project containing the file being checked */
@NonNull
private final Project mProject;
/**
* The "main" project. For normal projects, this is the same as {@link #mProject},
* but for library projects, it's the root project that includes (possibly indirectly)
* the various library projects and their library projects.
* <p>
* Note that this is a property on the {@link Context}, not the
* {@link Project}, since a library project can be included from multiple
* different top level projects, so there isn't <b>one</b> main project,
* just one per main project being analyzed with its library projects.
*/
private final Project mMainProject;
/** The current configuration controlling which checks are enabled etc */
private final Configuration mConfiguration;
/** The contents of the file */
private String mContents;
/** Map of properties to share results between detectors */
private Map<String, Object> mProperties;
/** Whether this file contains any suppress markers (null means not yet determined) */
private Boolean mContainsCommentSuppress;
/**
* Construct a new {@link Context}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
*/
public Context(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file) {
this.file = file;
mDriver = driver;
mProject = project;
mMainProject = main;
mConfiguration = project.getConfiguration(driver);
}
/**
* Returns the scope for the lint job
*
* @return the scope, never null
*/
@NonNull
public EnumSet<Scope> getScope() {
return mDriver.getScope();
}
/**
* Returns the configuration for this project.
*
* @return the configuration, never null
*/
@NonNull
public Configuration getConfiguration() {
return mConfiguration;
}
/**
* Returns the project containing the file being checked
*
* @return the project, never null
*/
@NonNull
public Project getProject() {
return mProject;
}
/**
* Returns the main project if this project is a library project, or self
* if this is not a library project. The main project is the root project
* of all library projects, not necessarily the directly including project.
*
* @return the main project, never null
*/
@NonNull
public Project getMainProject() {
return mMainProject != null ? mMainProject : mProject;
}
/**
* Returns the lint client requesting the lint check
*
* @return the client, never null
*/
@NonNull
public LintClient getClient() {
return mDriver.getClient();
}
/**
* Returns the driver running through the lint checks
*
* @return the driver
*/
@NonNull
public LintDriver getDriver() {
return mDriver;
}
/**
* Returns the contents of the file. This may not be the contents of the
* file on disk, since it delegates to the {@link LintClient}, which in turn
* may decide to return the current edited contents of the file open in an
* editor.
*
* @return the contents of the given file, or null if an error occurs.
*/
@Nullable
public String getContents() {
if (mContents == null) {
mContents = mDriver.getClient().readFile(file);
}
return mContents;
}
/**
* Returns the value of the given named property, or null.
*
* @param name the name of the property
* @return the corresponding value, or null
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
@Nullable
public Object getProperty(String name) {
if (mProperties == null) {
return null;
}
return mProperties.get(name);
}
/**
* Sets the value of the given named property.
*
* @param name the name of the property
* @param value the corresponding value
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
public void setProperty(@NonNull String name, @Nullable Object value) {
if (value == null) {
if (mProperties != null) {
mProperties.remove(name);
}
} else {
if (mProperties == null) {
mProperties = new HashMap<String, Object>();
}
mProperties.put(name, value);
}
}
/**
* Gets the SDK info for the current project.
*
* @return the SDK info for the current project, never null
*/
@NonNull
public SdkInfo getSdkInfo() {
return mProject.getSdkInfo();
}
// ---- Convenience wrappers ---- (makes the detector code a bit leaner)
/**
* Returns false if the given issue has been disabled. Convenience wrapper
* around {@link Configuration#getSeverity(Issue)}.
*
* @param issue the issue to check
* @return false if the issue has been disabled
*/
public boolean isEnabled(@NonNull Issue issue) {
return mConfiguration.isEnabled(issue);
}
/**
* Reports an issue. Convenience wrapper around {@link LintClient#report}
*
* @param issue the issue to report
* @param location the location of the issue
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@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.
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
// main project, disable it in the library project too. (In some cases you don't
// control the lint.xml of a library project, and besides, if you're not interested in
// a check for your main project you probably don't care about it in the library either.)
if (configuration != mConfiguration
&& mConfiguration.getSeverity(issue) == Severity.IGNORE) {
return;
}
Severity severity = configuration.getSeverity(issue);
if (severity == Severity.IGNORE) {
return;
}
mDriver.getClient().report(this, issue, severity, location, message, TextFormat.RAW);
}
/**
* Report an error.
* Like {@link #report(Issue, Location, String)} but with
* a now-unused data parameter at the end
*
* @deprecated Use {@link #report(Issue, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, location, message);
}
/**
* Send an exception to the log. Convenience wrapper around {@link LintClient#log}.
*
* @param exception the exception, possibly null
* @param format the error message using {@link String#format} syntax, possibly null
* @param args any arguments for the format string
*/
public void log(
@Nullable Throwable exception,
@Nullable String format,
@Nullable Object... args) {
mDriver.getClient().log(exception, format, args);
}
/**
* Returns the current phase number. The first pass is numbered 1. Only one pass
* will be performed, unless a {@link Detector} calls {@link #requestRepeat}.
*
* @return the current phase, usually 1
*/
public int getPhase() {
return mDriver.getPhase();
}
/**
* Requests another pass through the data for the given detector. This is
* typically done when a detector needs to do more expensive computation,
* but it only wants to do this once it <b>knows</b> that an error is
* present, or once it knows more specifically what to check for.
*
* @param detector the detector that should be included in the next pass.
* Note that the lint runner may refuse to run more than a couple
* of runs.
* @param scope the scope to be revisited. This must be a subset of the
* current scope ({@link #getScope()}, and it is just a performance hint;
* in particular, the detector should be prepared to be called on other
* scopes as well (since they may have been requested by other detectors).
* You can pall null to indicate "all".
*/
public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) {
mDriver.requestRepeat(detector, scope);
}
/** Returns the comment marker used in Studio to suppress statements for language, if any */
@Nullable
protected String getSuppressCommentPrefix() {
// Java and XML files are handled in sub classes (XmlContext, JavaContext)
String path = file.getPath();
if (path.endsWith(DOT_JAVA) || path.endsWith(".kt") || path.endsWith(DOT_GRADLE)) {
return JavaContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(DOT_XML)) {
return XmlContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(".cfg") || path.endsWith(".pro")) {
return "#suppress ";
}
return null;
}
/** Returns whether this file contains any suppress comment markers */
public boolean containsCommentSuppress() {
if (mContainsCommentSuppress == null) {
mContainsCommentSuppress = false;
String prefix = getSuppressCommentPrefix();
if (prefix != null) {
String contents = getContents();
if (contents != null) {
mContainsCommentSuppress = contents.contains(prefix);
}
}
}
return mContainsCommentSuppress;
}
/**
* Returns true if the given issue is suppressed at the given character offset
* in the file's contents
*/
public boolean isSuppressedWithComment(int startOffset, @NonNull Issue issue) {
String prefix = getSuppressCommentPrefix();
if (prefix == null) {
return false;
}
if (startOffset <= 0) {
return false;
}
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
if (startOffset >= contents.length()) {
return false;
}
// Scan backwards to the previous line and see if it contains the marker
int lineStart = contents.lastIndexOf('\n', startOffset) + 1;
if (lineStart <= 1) {
return false;
}
int index = findPrefixOnPreviousLine(contents, lineStart, prefix);
if (index != -1 &&index+prefix.length() < lineStart) {
String line = contents.substring(index + prefix.length(), lineStart);
return line.contains(issue.getId())
|| line.contains(SUPPRESS_ALL) && line.trim().startsWith(SUPPRESS_ALL);
}
return false;
}
private static int findPrefixOnPreviousLine(String contents, int lineStart, String prefix) {
// Search backwards on the previous line until you find the prefix start (also look
// back on previous lines if the previous line(s) contain just whitespace
char first = prefix.charAt(0);
int offset = lineStart - 2; // 0: first char on this line, -1: \n on previous line, -2 last
boolean seenNonWhitespace = false;
for (; offset >= 0; offset--) {
char c = contents.charAt(offset);
if (seenNonWhitespace && c == '\n') {
return -1;
}
if (!seenNonWhitespace && !Character.isWhitespace(c)) {
seenNonWhitespace = true;
}
if (c == first && contents.regionMatches(false, offset, prefix, 0,
prefix.length())) {
return offset;
}
}
return -1;
}
}
@@ -1,68 +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.detector.api;
import com.google.common.annotations.Beta;
/**
* A simple offset-based position *
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class DefaultPosition extends Position {
/** The line number (0-based where the first line is line 0) */
private final int mLine;
/**
* The column number (where the first character on the line is 0), or -1 if
* unknown
*/
private final int mColumn;
/** The character offset */
private final int mOffset;
/**
* Creates a new {@link DefaultPosition}
*
* @param line the 0-based line number, or -1 if unknown
* @param column the 0-based column number, or -1 if unknown
* @param offset the offset, or -1 if unknown
*/
public DefaultPosition(int line, int column, int offset) {
mLine = line;
mColumn = column;
mOffset = offset;
}
@Override
public int getLine() {
return mLine;
}
@Override
public int getOffset() {
return mOffset;
}
@Override
public int getColumn() {
return mColumn;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,189 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.detector.api;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import java.util.EnumSet;
/**
* An {@linkplain Implementation} of an {@link Issue} maps to the {@link Detector}
* class responsible for analyzing the issue, as well as the {@link Scope} required
* by the detector to perform its analysis.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class Implementation {
private final Class<? extends Detector> mClass;
private final EnumSet<Scope> mScope;
private EnumSet<Scope>[] mAnalysisScopes;
@SuppressWarnings("unchecked")
private static final EnumSet<Scope>[] EMPTY = new EnumSet[0];
/**
* Creates a new implementation for analyzing one or more issues
*
* @param detectorClass the class of the detector to find this issue
* @param scope the scope of files required to analyze this issue
*/
@SuppressWarnings("unchecked")
public Implementation(
@NonNull Class<? extends Detector> detectorClass,
@NonNull EnumSet<Scope> scope) {
this(detectorClass, scope, EMPTY);
}
/**
* Creates a new implementation for analyzing one or more issues
*
* @param detectorClass the class of the detector to find this issue
* @param scope the scope of files required to analyze this issue
* @param analysisScopes optional set of extra scopes the detector is capable of working in
*/
public Implementation(
@NonNull Class<? extends Detector> detectorClass,
@NonNull EnumSet<Scope> scope,
@NonNull EnumSet<Scope>... analysisScopes) {
mClass = detectorClass;
mScope = scope;
mAnalysisScopes = analysisScopes;
}
/**
* Returns the class of the detector to use to find this issue
*
* @return the class of the detector to use to find this issue
*/
@NonNull
public Class<? extends Detector> getDetectorClass() {
return mClass;
}
@Override
public String toString() {
return mClass.toString();
}
/**
* Returns the scope required to analyze the code to detect this issue.
* This is determined by the detectors which reports the issue.
*
* @return the required scope
*/
@NonNull
public EnumSet<Scope> getScope() {
return mScope;
}
/**
* Returns the sets of scopes required to analyze this issue, or null if all
* scopes named by {@link #getScope()} are necessary. Note that only
* <b>one</b> match out of this collection is required, not all, and that
* the scope set returned by {@link #getScope()} does not have to be returned
* by this method, but is always implied to be included.
* <p>
* The scopes returned by {@link #getScope()} list all the various
* scopes that are <b>affected</b> by this issue, meaning the detector
* should consider it. Frequently, the detector must analyze all these
* scopes in order to properly decide whether an issue is found. For
* example, the unused resource detector needs to consider both the XML
* resource files and the Java source files in order to decide if a resource
* is unused. If it analyzes just the Java files for example, it might
* incorrectly conclude that a resource is unused because it did not
* discover a resource reference in an XML file.
* <p>
* However, there are other issues where the issue can occur in a variety of
* files, but the detector can consider each in isolation. For example, the
* API checker is affected by both XML files and Java class files (detecting
* both layout constructor references in XML layout files as well as code
* references in .class files). It doesn't have to analyze both; it is
* capable of incrementally analyzing just an XML file, or just a class
* file, without considering the other.
* <p>
* The required scope list provides a list of scope sets that can be used to
* analyze this issue. For each scope set, all the scopes must be matched by
* the incremental analysis, but any one of the scope sets can be analyzed
* in isolation.
* <p>
* The required scope list is not required to include the full scope set
* returned by {@link #getScope()}; that set is always assumed to be
* included.
* <p>
* NOTE: You would normally call {@link #isAdequate(EnumSet)} rather
* than calling this method directly.
*
* @return a list of required scopes, or null.
*/
@NonNull
public EnumSet<Scope>[] getAnalysisScopes() {
return mAnalysisScopes;
}
/**
* Returns true if the given scope is adequate for analyzing this issue.
* This looks through the analysis scopes (see
* {@link #getAnalysisScopes()}) and if the scope passed in fully
* covers at least one of them, or if it covers the scope of the issue
* itself (see {@link #getScope()}, which should be a superset of all the
* analysis scopes) returns true.
* <p>
* The scope set returned by {@link #getScope()} lists all the various
* scopes that are <b>affected</b> by this issue, meaning the detector
* should consider it. Frequently, the detector must analyze all these
* scopes in order to properly decide whether an issue is found. For
* example, the unused resource detector needs to consider both the XML
* resource files and the Java source files in order to decide if a resource
* is unused. If it analyzes just the Java files for example, it might
* incorrectly conclude that a resource is unused because it did not
* discover a resource reference in an XML file.
* <p>
* However, there are other issues where the issue can occur in a variety of
* files, but the detector can consider each in isolation. For example, the
* API checker is affected by both XML files and Java class files (detecting
* both layout constructor references in XML layout files as well as code
* references in .class files). It doesn't have to analyze both; it is
* capable of incrementally analyzing just an XML file, or just a class
* file, without considering the other.
* <p>
* An issue can register additional scope sets that can are adequate
* for analyzing the issue, by supplying it to
* {@link #Implementation(Class, java.util.EnumSet, java.util.EnumSet[])}.
* This method returns true if the given scope matches one or more analysis
* scope, or the overall scope.
*
* @param scope the scope available for analysis
* @return true if this issue can be analyzed with the given available scope
*/
public boolean isAdequate(@NonNull EnumSet<Scope> scope) {
if (scope.containsAll(mScope)) {
return true;
}
if (mAnalysisScopes != null) {
for (EnumSet<Scope> analysisScope : mAnalysisScopes) {
if (scope.containsAll(analysisScope)) {
return true;
}
}
}
return false;
}
}
@@ -1,307 +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.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;
import java.util.Collections;
import java.util.List;
/**
* An issue is a potential bug in an Android application. An issue is discovered
* by a {@link Detector}, and has an associated {@link Severity}.
* <p>
* Issues and detectors are separate classes because a detector can discover
* multiple different issues as it's analyzing code, and we want to be able to
* different severities for different issues, the ability to suppress one but
* not other issues from the same detector, and so on.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public final class Issue implements Comparable<Issue> {
private final String mId;
private final String mBriefDescription;
private final String mExplanation;
private final Category mCategory;
private final int mPriority;
private final Severity mSeverity;
private Object mMoreInfoUrls;
private boolean mEnabledByDefault = true;
private Implementation mImplementation;
// Use factory methods
private Issue(
@NonNull String id,
@NonNull String shortDescription,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
assert !shortDescription.isEmpty();
assert !explanation.isEmpty();
mId = id;
mBriefDescription = shortDescription;
mExplanation = explanation;
mCategory = category;
mPriority = priority;
mSeverity = severity;
mImplementation = implementation;
}
/**
* Creates a new issue. The description strings can use some simple markup;
* see the {@link TextFormat#RAW} documentation
* for details.
*
* @param id the fixed id of the issue
* @param briefDescription short summary (typically 5-6 words or less), typically
* describing the <b>problem</b> rather than the <b>fix</b>
* (e.g. "Missing minSdkVersion")
* @param explanation a full explanation of the issue, with suggestions for
* how to fix it
* @param category the associated category, if any
* @param priority the priority, a number from 1 to 10 with 10 being most
* important/severe
* @param severity the default severity of the issue
* @param implementation the default implementation for this issue
* @return a new {@link Issue}
*/
@NonNull
public static Issue create(
@NonNull String id,
@NonNull String briefDescription,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
return new Issue(id, briefDescription, explanation, category, priority,
severity, implementation);
}
/**
* For compatibility with older custom rules)
*
* @deprecated Use {@link #create(String, String, String, Category, int, Severity, Implementation)} instead
*/
@NonNull
@Deprecated
public static Issue create(
@NonNull String id,
@NonNull String briefDescription,
@SuppressWarnings("UnusedParameters") @NonNull String description,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
return new Issue(id, briefDescription, explanation, category, priority,
severity, implementation);
}
/**
* Returns the unique id of this issue. These should not change over time
* since they are used to persist the names of issues suppressed by the user
* etc. It is typically a single camel-cased word.
*
* @return the associated fixed id, never null and always unique
*/
@NonNull
public String getId() {
return mId;
}
/**
* Briefly (in a couple of words) describes these errors
*
* @return a brief summary of the issue, never null, never empty
*/
@NonNull
public String getBriefDescription(@NonNull TextFormat format) {
return RAW.convertTo(mBriefDescription, format);
}
/**
* Describes the error found by this rule, e.g.
* "Buttons must define contentDescriptions". Preferably the explanation
* should also contain a description of how the problem should be solved.
* Additional info can be provided via {@link #getMoreInfo()}.
*
* @param format the format to write the format as
* @return an explanation of the issue, never null, never empty
*/
@NonNull
public String getExplanation(@NonNull TextFormat format) {
return RAW.convertTo(mExplanation, format);
}
/**
* The primary category of the issue
*
* @return the primary category of the issue, never null
*/
@NonNull
public Category getCategory() {
return mCategory;
}
/**
* Returns a priority, in the range 1-10, with 10 being the most severe and
* 1 the least
*
* @return a priority from 1 to 10
*/
public int getPriority() {
return mPriority;
}
/**
* Returns the default severity of the issues found by this detector (some
* tools may allow the user to specify custom severities for detectors).
* <p>
* Note that even though the normal way for an issue to be disabled is for
* the {@link Configuration} to return {@link Severity#IGNORE}, there is a
* {@link #isEnabledByDefault()} method which can be used to turn off issues
* by default. This is done rather than just having the severity as the only
* attribute on the issue such that an issue can be configured with an
* appropriate severity (such as {@link Severity#ERROR}) even when issues
* are disabled by default for example because they are experimental or not
* yet stable.
*
* @return the severity of the issues found by this detector
*/
@NonNull
public Severity getDefaultSeverity() {
return mSeverity;
}
/**
* Returns a link (a URL string) to more information, or null
*
* @return a link to more information, or null
*/
@NonNull
public List<String> getMoreInfo() {
if (mMoreInfoUrls == null) {
return Collections.emptyList();
} else if (mMoreInfoUrls instanceof String) {
return Collections.singletonList((String) mMoreInfoUrls);
} else {
assert mMoreInfoUrls instanceof List;
//noinspection unchecked
return (List<String>) mMoreInfoUrls;
}
}
/**
* Adds a more info URL string
*
* @param moreInfoUrl url string
* @return this, for constructor chaining
*/
@NonNull
public Issue addMoreInfo(@NonNull String moreInfoUrl) {
// Nearly all issues supply at most a single URL, so don't bother with
// lists wrappers for most of these issues
if (mMoreInfoUrls == null) {
mMoreInfoUrls = moreInfoUrl;
} else if (mMoreInfoUrls instanceof String) {
String existing = (String) mMoreInfoUrls;
List<String> list = new ArrayList<String>(2);
list.add(existing);
list.add(moreInfoUrl);
mMoreInfoUrls = list;
} else {
assert mMoreInfoUrls instanceof List;
//noinspection unchecked
((List<String>) mMoreInfoUrls).add(moreInfoUrl);
}
return this;
}
/**
* Returns whether this issue should be enabled by default, unless the user
* has explicitly disabled it.
*
* @return true if this issue should be enabled by default
*/
public boolean isEnabledByDefault() {
return mEnabledByDefault;
}
/**
* Returns the implementation for the given issue
*
* @return the implementation for this issue
*/
@NonNull
public Implementation getImplementation() {
return mImplementation;
}
/**
* Sets the implementation for the given issue. This is typically done by
* IDEs that can offer a replacement for a given issue which performs better
* or in some other way works better within the IDE.
*
* @param implementation the new implementation to use
*/
public void setImplementation(@NonNull Implementation implementation) {
mImplementation = implementation;
}
/**
* Sorts the detectors alphabetically by id. This is intended to make it
* convenient to store settings for detectors in a fixed order. It is not
* intended as the order to be shown to the user; for that, a tool embedding
* lint might consider the priorities, categories, severities etc of the
* various detectors.
*
* @param other the {@link Issue} to compare this issue to
*/
@Override
public int compareTo(@NonNull Issue other) {
return getId().compareTo(other.getId());
}
/**
* Sets whether this issue is enabled by default.
*
* @param enabledByDefault whether the issue should be enabled by default
* @return this, for constructor chaining
*/
@NonNull
public Issue setEnabledByDefault(boolean enabledByDefault) {
mEnabledByDefault = enabledByDefault;
return this;
}
@Override
public String toString() {
return mId;
}
}
@@ -1,823 +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.detector.api;
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 com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import lombok.ast.*;
import lombok.ast.Position;
import org.jetbrains.uast.*;
import org.jetbrains.uast.psi.UElementWithLocation;
import java.io.File;
import java.util.Iterator;
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;
/**
* A {@link Context} used when checking Java files.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
public class JavaContext extends Context {
static final String SUPPRESS_COMMENT_PREFIX = "//noinspection "; //$NON-NLS-1$
/**
* The parse tree
*
* @deprecated Use {@link #mJavaFile} instead (see {@link JavaPsiScanner})
*/
@Deprecated
private Node mCompilationUnit;
/** The parse tree */
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
* the given scope, in the given project reporting errors to the given
* client.
*
* @param driver the driver running through the checks
* @param project the project to run lint on which contains the given file
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file to be analyzed
* @param parser the parser to use
*/
public JavaContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@NonNull JavaParser parser) {
super(driver, project, main, file);
mParser = parser;
}
@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
*/
@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 {
PsiElement psiElement = 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 Node getCompilationUnit() {
return mCompilationUnit;
}
/**
* Sets the compilation result. Not intended for client usage; the lint infrastructure
* will set this when a context has been processed
*
* @param compilationUnit the parse tree
*/
public void setCompilationUnit(@Nullable Node compilationUnit) {
mCompilationUnit = compilationUnit;
}
/**
* 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, @NonNull Location location,
@NonNull String message) {
if (mDriver.isSuppressed(this, issue, mCompilationUnit)) {
return;
}
super.report(issue, location, message);
}
/**
* Reports an issue applicable to a given AST node. The AST node is used as the
* scope to check for suppress lint annotations.
*
* @param issue the issue to report
* @param scope the AST node scope the error applies to. The lint infrastructure
* will check whether there are suppress annotations on this node (or its enclosing
* nodes) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
public void report(
@NonNull Issue issue,
@Nullable PsiElement scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
public void report(
@NonNull Issue issue,
@Nullable UElement scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/** UDeclaration is a PsiElement, so it's impossible to call report(Issue, UElement, ...)
* without an explicit cast. */
public void reportUast(
@NonNull Issue issue,
@Nullable UElement scope,
@NonNull Location location,
@NonNull String message) {
report(issue, scope, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
/**
* @deprecated Use {@link PsiTreeUtil#getParentOfType(PsiElement, Class[])}
* with PsiMethod.class instead
*/
@Deprecated
@Nullable
public static Node findSurroundingMethod(Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {
return scope;
}
scope = scope.getParent();
}
return null;
}
/**
* @deprecated Use {@link PsiTreeUtil#getParentOfType(PsiElement, Class[])}
* with PsiMethod.class instead
*/
@Deprecated
@Nullable
public static ClassDeclaration findSurroundingClass(@Nullable Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == ClassDeclaration.class) {
return (ClassDeclaration) scope;
}
scope = scope.getParent();
}
return null;
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
/**
* @deprecated Use {@link #isSuppressedWithComment(PsiElement, Issue)} instead
*/
@Deprecated
public boolean isSuppressedWithComment(@NonNull Node scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
Position position = scope.getPosition();
if (position == null) {
return false;
}
int start = position.getStart();
return isSuppressedWithComment(start, issue);
}
public boolean isSuppressedWithComment(@NonNull UElement scope, @NonNull Issue issue) {
PsiElement psi = scope.getPsi();
return psi != null && isSuppressedWithComment(psi, issue);
}
public boolean isSuppressedWithComment(@NonNull PsiElement scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
TextRange textRange = scope.getTextRange();
if (textRange == null) {
return false;
}
int start = textRange.getStartOffset();
return isSuppressedWithComment(start, issue);
}
/**
* @deprecated Location handles aren't needed for AST nodes anymore; just use the
* {@link PsiElement} from the AST
*/
@Deprecated
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
/**
* @deprecated Use PsiElement resolve methods (varies by AST node type, e.g.
* {@link PsiMethodCallExpression#resolveMethod()}
*/
@Deprecated
@Nullable
public ResolvedNode resolve(@NonNull Node node) {
return mParser.resolve(this, node);
}
/**
* @deprecated Use {@link JavaEvaluator#findClass(String)} instead
*/
@Deprecated
@Nullable
public ResolvedClass findClass(@NonNull String fullyQualifiedName) {
return mParser.findClass(this, fullyQualifiedName);
}
/**
* @deprecated Use {@link PsiExpression#getType()} )} instead
*/
@Deprecated
@Nullable
public TypeDescriptor getType(@NonNull Node node) {
return mParser.getType(this, node);
}
/**
* @deprecated Use {@link #getMethodName(PsiElement)} instead
*/
@Deprecated
@Nullable
public static String getMethodName(@NonNull Node call) {
if (call instanceof MethodInvocation) {
return ((MethodInvocation)call).astName().astValue();
} else if (call instanceof ConstructorInvocation) {
return ((ConstructorInvocation)call).astTypeReference().getTypeName();
} else if (call instanceof EnumConstant) {
return ((EnumConstant)call).astName().astValue();
} else {
return null;
}
}
@Nullable
public static String getMethodName(@NonNull PsiElement call) {
if (call instanceof PsiMethodCallExpression) {
return ((PsiMethodCallExpression)call).getMethodExpression().getReferenceName();
} else if (call instanceof PsiNewExpression) {
PsiJavaCodeReferenceElement classReference = ((PsiNewExpression) call).getClassReference();
if (classReference != null) {
return classReference.getReferenceName();
} else {
return null;
}
} else if (call instanceof PsiEnumConstant) {
return ((PsiEnumConstant)call).getName();
} else {
return null;
}
}
@Nullable
public static String getMethodName(@NonNull UElement call) {
if (call instanceof UEnumConstant) {
return ((UEnumConstant)call).getName();
} else if (call instanceof UCallExpression) {
String methodName = ((UCallExpression) call).getMethodName();
if (methodName != null) {
return methodName;
} else {
return UastUtils.getQualifiedName(((UCallExpression) call).getClassReference());
}
} else {
return null;
}
}
/**
* Searches for a name node corresponding to the given node
* @return the name node to use, if applicable
* @deprecated Use {@link #findNameElement(PsiElement)} instead
*/
@Deprecated
@Nullable
public static Node findNameNode(@NonNull Node node) {
if (node instanceof TypeDeclaration) {
// ClassDeclaration, AnnotationDeclaration, EnumDeclaration, InterfaceDeclaration
return ((TypeDeclaration) node).astName();
} else if (node instanceof MethodDeclaration) {
return ((MethodDeclaration)node).astMethodName();
} else if (node instanceof ConstructorDeclaration) {
return ((ConstructorDeclaration)node).astTypeName();
} else if (node instanceof MethodInvocation) {
return ((MethodInvocation)node).astName();
} else if (node instanceof ConstructorInvocation) {
return ((ConstructorInvocation)node).astTypeReference();
} else if (node instanceof EnumConstant) {
return ((EnumConstant)node).astName();
} else if (node instanceof AnnotationElement) {
return ((AnnotationElement)node).astName();
} else if (node instanceof AnnotationMethodDeclaration) {
return ((AnnotationMethodDeclaration)node).astMethodName();
} else if (node instanceof VariableReference) {
return ((VariableReference)node).astIdentifier();
} else if (node instanceof LabelledStatement) {
return ((LabelledStatement)node).astLabel();
}
return null;
}
/**
* Searches for a name node corresponding to the given node
* @return the name node to use, if applicable
*/
@Nullable
public static PsiElement findNameElement(@NonNull PsiElement element) {
if (element instanceof PsiClass) {
if (element instanceof PsiAnonymousClass) {
return ((PsiAnonymousClass)element).getBaseClassReference();
}
return ((PsiClass) element).getNameIdentifier();
} else if (element instanceof PsiMethod) {
return ((PsiMethod) element).getNameIdentifier();
} else if (element instanceof PsiMethodCallExpression) {
return ((PsiMethodCallExpression) element).getMethodExpression().
getReferenceNameElement();
} else if (element instanceof PsiNewExpression) {
return ((PsiNewExpression) element).getClassReference();
} else if (element instanceof PsiField) {
return ((PsiField)element).getNameIdentifier();
} else if (element instanceof PsiAnnotation) {
return ((PsiAnnotation)element).getNameReferenceElement();
} else if (element instanceof PsiReferenceExpression) {
return ((PsiReferenceExpression) element).getReferenceNameElement();
} else if (element instanceof PsiLabeledStatement) {
return ((PsiLabeledStatement)element).getLabelIdentifier();
}
return null;
}
@Deprecated
@NonNull
public static Iterator<Expression> getParameters(@NonNull Node call) {
if (call instanceof MethodInvocation) {
return ((MethodInvocation) call).astArguments().iterator();
} else if (call instanceof ConstructorInvocation) {
return ((ConstructorInvocation) call).astArguments().iterator();
} else if (call instanceof EnumConstant) {
return ((EnumConstant) call).astArguments().iterator();
} else {
return ContainerUtil.emptyIterator();
}
}
@Deprecated
@Nullable
public static Node getParameter(@NonNull Node call, int parameter) {
Iterator<Expression> iterator = getParameters(call);
for (int i = 0; i < parameter - 1; i++) {
if (!iterator.hasNext()) {
return null;
}
iterator.next();
}
return iterator.hasNext() ? iterator.next() : null;
}
/**
* Returns true if the given method invocation node corresponds to a call on a
* {@code android.content.Context}
*
* @param node the method call node
* @return true iff the method call is on a class extending context
* @deprecated use {@link JavaEvaluator#isMemberInSubClassOf(PsiMember, String, boolean)} instead
*/
@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
ResolvedNode resolved = resolve(node);
if (resolved instanceof JavaParser.ResolvedMethod) {
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.isSubclassOf(CLASS_CONTEXT, false)) {
return true;
}
}
return false;
}
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz) {
return getParentOfType(element, clz, true);
}
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz,
boolean strict) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null) {
if (clz.isInstance(element)) {
//noinspection unchecked
return (T) element;
}
element = element.getParent();
}
return null;
}
/**
* Returns the first ancestor node of the given type, stopping at the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param terminators optional node types to terminate the search at
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(@Nullable Node element,
@NonNull Class<T> clz,
boolean strict,
@NonNull Class<? extends Node>... terminators) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null && !clz.isInstance(element)) {
for (Class<?> terminator : terminators) {
if (terminator.isInstance(element)) {
return null;
}
}
element = element.getParent();
}
//noinspection unchecked
return (T) element;
}
/**
* Returns the first sibling of the given node that is of the given class
*
* @param sibling the sibling to search from
* @param clz the type to look for
* @param <T> the type
* @return the first sibling of the given type, or null
* @deprecated Use {@link PsiTreeUtil#getNextSiblingOfType(PsiElement, Class)} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getNextSiblingOfType(@Nullable Node sibling,
@NonNull Class<T> clz) {
if (sibling == null) {
return null;
}
Node parent = sibling.getParent();
if (parent == null) {
return null;
}
Iterator<Node> iterator = parent.getChildren().iterator();
while (iterator.hasNext()) {
if (iterator.next() == sibling) {
break;
}
}
while (iterator.hasNext()) {
Node child = iterator.next();
if (clz.isInstance(child)) {
//noinspection unchecked
return (T) child;
}
}
return null;
}
/**
* Returns the given argument of the given call
*
* @param call the call containing arguments
* @param index the index of the target argument
* @return the argument at the given index
* @throws IllegalArgumentException if index is outside the valid range
*/
@Deprecated
@NonNull
public static Node getArgumentNode(@NonNull MethodInvocation call, int index) {
int i = 0;
for (Expression parameter : call.astArguments()) {
if (i == index) {
return parameter;
}
i++;
}
throw new IllegalArgumentException(Integer.toString(index));
}
}
@@ -1,70 +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.detector.api;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import org.w3c.dom.Element;
/**
* Abstract class specifically intended for layout detectors which provides some
* common utility methods shared by layout detectors.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class LayoutDetector extends ResourceXmlDetector {
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
private static boolean isFillParent(@NonNull Element element, @NonNull String dimension) {
String width = element.getAttributeNS(ANDROID_URI, dimension);
return width.equals(VALUE_MATCH_PARENT) || width.equals(VALUE_FILL_PARENT);
}
protected static boolean isWidthFillParent(@NonNull Element element) {
return isFillParent(element, ATTR_LAYOUT_WIDTH);
}
protected static boolean isHeightFillParent(@NonNull Element element) {
return isFillParent(element, ATTR_LAYOUT_HEIGHT);
}
protected boolean hasPadding(@NonNull Element root) {
return root.hasAttributeNS(ANDROID_URI, ATTR_PADDING)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM);
}
}
@@ -1,814 +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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.tools.klint.client.api.JavaParser;
import com.google.common.annotations.Beta;
import com.intellij.psi.PsiElement;
import java.io.File;
/**
* Location information for a warning
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class Location {
private static final String SUPER_KEYWORD = "super"; //$NON-NLS-1$
private final File mFile;
private final Position mStart;
private final Position mEnd;
private String mMessage;
private Location mSecondary;
private Object mClientData;
/**
* 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)},
* {@link Location#create(File, Position, Position)}, or
* {@link Location#create(File, String, int, int)}.
* <p>
* Constructs a new location range for the given file, from start to end. If
* the length of the range is not known, end may be null.
*
* @param file the associated file (but see the documentation for
* {@link #getFile()} for more information on what the file
* represents)
* @param start the starting position, or null
* @param end the ending position, or null
*/
protected Location(@NonNull File file, @Nullable Position start, @Nullable Position end) {
super();
mFile = file;
mStart = start;
mEnd = end;
}
/**
* Returns the file containing the warning. Note that the file *itself* may
* not yet contain the error. When editing a file in the IDE for example,
* the tool could generate warnings in the background even before the
* document is saved. However, the file is used as a identifying token for
* the document being edited, and the IDE integration can map this back to
* error locations in the editor source code.
*
* @return the file handle for the location
*/
@NonNull
public File getFile() {
return mFile;
}
/**
* The start position of the range
*
* @return the start position of the range, or null
*/
@Nullable
public Position getStart() {
return mStart;
}
/**
* The end position of the range
*
* @return the start position of the range, may be null for an empty range
*/
@Nullable
public Position getEnd() {
return mEnd;
}
/**
* Returns a secondary location associated with this location (if
* applicable), or null.
*
* @return a secondary location or null
*/
@Nullable
public Location getSecondary() {
return mSecondary;
}
/**
* Sets a secondary location for this location.
*
* @param secondary a secondary location associated with this location
*/
public void setSecondary(@Nullable Location secondary) {
mSecondary = secondary;
}
/**
* Sets a custom message for this location. This is typically used for
* secondary locations, to describe the significance of this alternate
* location. For example, for a duplicate id warning, the primary location
* might say "This is a duplicate id", pointing to the second occurrence of
* id declaration, and then the secondary location could point to the
* original declaration with the custom message "Originally defined here".
*
* @param message the message to apply to this location
*/
public void setMessage(@NonNull String message) {
mMessage = message;
}
/**
* Returns the custom message for this location, if any. This is typically
* used for secondary locations, to describe the significance of this
* alternate location. For example, for a duplicate id warning, the primary
* location might say "This is a duplicate id", pointing to the second
* occurrence of id declaration, and then the secondary location could point
* to the original declaration with the custom message
* "Originally defined here".
*
* @return the custom message for this location, or null
*/
@Nullable
public String getMessage() {
return mMessage;
}
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
public Object getClientData() {
return mClientData;
}
@Override
public String toString() {
return "Location [file=" + mFile + ", start=" + mStart + ", end=" + mEnd + ", message="
+ mMessage + ']';
}
/**
* Creates a new location for the given file
*
* @param file the file to create a location for
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file) {
return new Location(file, null /*start*/, null /*end*/);
}
/**
* Creates a new location for the given file and SourcePosition.
*
* @param file the file containing the positions
* @param position the source position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull SourcePosition position) {
if (position.equals(SourcePosition.UNKNOWN)) {
return new Location(file, null /*start*/, null /*end*/);
}
return new Location(file,
new DefaultPosition(
position.getStartLine(),
position.getStartColumn(),
position.getStartOffset()),
new DefaultPosition(
position.getEndLine(),
position.getEndColumn(),
position.getEndOffset()));
}
/**
* Creates a new location for the given file and starting and ending
* positions.
*
* @param file the file containing the positions
* @param start the starting position
* @param end the ending position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull Position start,
@Nullable Position end) {
return new Location(file, start, end);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given offset range.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param startOffset the starting offset
* @param endOffset the ending offset
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@Nullable String contents,
int startOffset,
int endOffset) {
if (startOffset < 0 || endOffset < startOffset) {
throw new IllegalArgumentException("Invalid offsets");
}
if (contents == null) {
return new Location(file,
new DefaultPosition(-1, -1, startOffset),
new DefaultPosition(-1, -1, endOffset));
}
int size = contents.length();
endOffset = Math.min(endOffset, size);
startOffset = Math.min(startOffset, endOffset);
Position start = null;
int line = 0;
int lineOffset = 0;
char prev = 0;
for (int offset = 0; offset <= size; offset++) {
if (offset == startOffset) {
start = new DefaultPosition(line, offset - lineOffset, offset);
}
if (offset == endOffset) {
Position end = new DefaultPosition(line, offset - lineOffset, offset);
return new Location(file, start, end);
}
char c = contents.charAt(offset);
if (c == '\n') {
lineOffset = offset + 1;
if (prev != '\r') {
line++;
}
} else if (c == '\r') {
line++;
lineOffset = offset + 1;
}
prev = c;
}
return create(file);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line) {
return create(file, contents, line, null, null, null);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @param patternStart an optional pattern to search for from the line
* match; if found, adjust the column and offsets to begin at the
* pattern start
* @param patternEnd an optional pattern to search for behind the start
* pattern; if found, adjust the end offset to match the end of
* the pattern
* @param hints optional additional information regarding the pattern search
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line,
@Nullable String patternStart, @Nullable String patternEnd,
@Nullable SearchHints hints) {
int currentLine = 0;
int offset = 0;
while (currentLine < line) {
offset = contents.indexOf('\n', offset);
if (offset == -1) {
return create(file);
}
currentLine++;
offset++;
}
if (line == currentLine) {
if (patternStart != null) {
SearchDirection direction = SearchDirection.NEAREST;
if (hints != null) {
direction = hints.mDirection;
}
int index;
if (direction == SearchDirection.BACKWARD) {
index = findPreviousMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.EOL_BACKWARD) {
int lineEnd = contents.indexOf('\n', offset);
if (lineEnd == -1) {
lineEnd = contents.length();
}
index = findPreviousMatch(contents, lineEnd, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.FORWARD) {
index = findNextMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else {
assert direction == SearchDirection.NEAREST ||
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);
if (before == -1) {
index = after;
line = adjustLine(contents, line, offset, index);
} else if (after == -1) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
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);
}
}
}
if (index != -1) {
int lineStart = contents.lastIndexOf('\n', index);
if (lineStart == -1) {
lineStart = 0;
} else {
lineStart++; // was pointing to the previous line's CR, not line start
}
int column = index - lineStart;
if (patternEnd != null) {
int end = contents.indexOf(patternEnd, offset + patternStart.length());
if (end != -1) {
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, -1, end + patternEnd.length()));
}
} else if (hints != null && (hints.isJavaSymbol() || hints.isWholeWord())) {
if (hints.isConstructor() && contents.startsWith(SUPER_KEYWORD, index)) {
patternStart = SUPER_KEYWORD;
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column + patternStart.length(),
index + patternStart.length()));
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column, index + patternStart.length()));
}
}
Position position = new DefaultPosition(line, -1, offset);
return new Location(file, position, position);
}
return create(file);
}
private static int findPreviousMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
while (true) {
int index = contents.lastIndexOf(pattern, offset);
if (index == -1) {
return -1;
} else {
if (isMatch(contents, index, pattern, hints)) {
return index;
} else {
offset = index - pattern.length();
}
}
}
}
private static int findNextMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
int constructorIndex = -1;
if (hints != null && hints.isConstructor()) {
// Special condition: See if the call is referenced as "super" instead.
assert hints.isWholeWord();
int index = contents.indexOf(SUPER_KEYWORD, offset);
if (index != -1 && isMatch(contents, index, SUPER_KEYWORD, hints)) {
constructorIndex = index;
}
}
while (true) {
int index = contents.indexOf(pattern, offset);
if (index == -1) {
return constructorIndex;
} else {
if (isMatch(contents, index, pattern, hints)) {
if (constructorIndex != -1) {
return Math.min(constructorIndex, index);
}
return index;
} else {
offset = index + pattern.length();
}
}
}
}
private static boolean isMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
if (!contents.startsWith(pattern, offset)) {
return false;
}
if (hints != null) {
char prevChar = offset > 0 ? contents.charAt(offset - 1) : 0;
int lastIndex = offset + pattern.length() - 1;
char nextChar = lastIndex < contents.length() - 1 ? contents.charAt(lastIndex + 1) : 0;
if (hints.isWholeWord() && (Character.isLetter(prevChar)
|| Character.isLetter(nextChar))) {
return false;
}
if (hints.isJavaSymbol()) {
if (Character.isJavaIdentifierPart(prevChar)
|| Character.isJavaIdentifierPart(nextChar)) {
return false;
}
if (prevChar == '"') {
return false;
}
// TODO: Additional validation to see if we're in a comment, string, etc.
// This will require lexing from the beginning of the buffer.
}
if (hints.isConstructor() && SUPER_KEYWORD.equals(pattern)) {
// Only looking for super(), not super.x, so assert that the next
// non-space character is (
int index = lastIndex + 1;
while (index < contents.length() - 1) {
char c = contents.charAt(index);
if (c == '(') {
break;
} else if (!Character.isWhitespace(c)) {
return false;
}
index++;
}
}
}
return true;
}
private static int adjustLine(String doc, int line, int offset, int newOffset) {
if (newOffset == -1) {
return line;
}
if (newOffset < offset) {
return line - countLines(doc, newOffset, offset);
} else {
return line + countLines(doc, offset, newOffset);
}
}
private static int countLines(String doc, int start, int end) {
int lines = 0;
for (int offset = start; offset < end; offset++) {
char c = doc.charAt(offset);
if (c == '\n') {
lines++;
}
}
return lines;
}
/**
* Reverses the secondary location list initiated by the given location
*
* @param location the first location in the list
* @return the first location in the reversed list
*/
public static Location reverse(@NonNull Location location) {
Location next = location.getSecondary();
location.setSecondary(null);
while (next != null) {
Location nextNext = next.getSecondary();
next.setSecondary(location);
location = next;
next = nextNext;
}
return location;
}
/**
* A {@link Handle} is a reference to a location. The point of a location
* handle is to be able to create them cheaply, and then resolve them into
* actual locations later (if needed). This makes it possible to for example
* delay looking up line numbers, for locations that are offset based.
*/
public interface Handle {
/**
* Compute a full location for the given handle
*
* @return create a location for this handle
*/
@NonNull
Location resolve();
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
void setClientData(@Nullable Object clientData);
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
Object getClientData();
}
/** A default {@link Handle} implementation for simple file offsets */
public static class DefaultLocationHandle implements Handle {
private final File mFile;
private final String mContents;
private final int mStartOffset;
private final int mEndOffset;
private Object mClientData;
/**
* Constructs a new {@link DefaultLocationHandle}
*
* @param context the context pointing to the file and its contents
* @param startOffset the start offset within the file
* @param endOffset the end offset within the file
*/
public DefaultLocationHandle(@NonNull Context context, int startOffset, int endOffset) {
mFile = context.file;
mContents = context.getContents();
mStartOffset = startOffset;
mEndOffset = endOffset;
}
@Override
@NonNull
public Location resolve() {
return create(mFile, mContents, mStartOffset, mEndOffset);
}
@Override
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
@Override
@Nullable
public Object getClientData() {
return mClientData;
}
}
public static class ResourceItemHandle implements Handle {
private final ResourceItem mItem;
public ResourceItemHandle(@NonNull ResourceItem item) {
mItem = item;
}
@NonNull
@Override
public Location resolve() {
// TODO: Look up the exact item location more
// closely
ResourceFile source = mItem.getSource();
assert source != null : mItem;
return create(source.getFile());
}
@Override
public void setClientData(@Nullable Object clientData) {
}
@Nullable
@Override
public Object getClientData() {
return null;
}
}
/**
* Whether to look forwards, or backwards, or in both directions, when
* searching for a pattern in the source code to determine the right
* position range for a given symbol.
* <p>
* When dealing with bytecode for example, there are only line number entries
* within method bodies, so when searching for the method declaration, we should only
* search backwards from the first line entry in the method.
*/
public enum SearchDirection {
/** Only search forwards */
FORWARD,
/** Only search backwards */
BACKWARD,
/** Search backwards from the current end of line (normally it's the beginning of
* the current line) */
EOL_BACKWARD,
/**
* Search both forwards and backwards from the given line, and prefer
* the match that is closest
*/
NEAREST,
/**
* Search both forwards and backwards from the end of the given line, and prefer
* the match that is closest
*/
EOL_NEAREST,
}
/**
* Extra information pertaining to finding a symbol in a source buffer,
* used by {@link Location#create(File, String, int, String, String, SearchHints)}
*/
public static class SearchHints {
/**
* the direction to search for the nearest match in (provided
* {@code patternStart} is non null)
*/
@NonNull
private final SearchDirection mDirection;
/** Whether the matched pattern should be a whole word */
private boolean mWholeWord;
/**
* Whether the matched pattern should be a Java symbol (so for example,
* a match inside a comment or string literal should not be used)
*/
private boolean mJavaSymbol;
/**
* Whether the matched pattern corresponds to a constructor; if so, look for
* some other possible source aliases too, such as "super".
*/
private boolean mConstructor;
private SearchHints(@NonNull SearchDirection direction) {
super();
mDirection = direction;
}
/**
* Constructs a new {@link SearchHints} object
*
* @param direction the direction to search in for the pattern
* @return a new @link SearchHints} object
*/
@NonNull
public static SearchHints create(@NonNull SearchDirection direction) {
return new SearchHints(direction);
}
/**
* Indicates that pattern matches should apply to whole words only
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchWholeWord() {
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for whole words only */
public boolean isWholeWord() {
return mWholeWord;
}
/**
* Indicates that pattern matches should apply to Java symbols only
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchJavaSymbol() {
mJavaSymbol = true;
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for Java symbols only */
public boolean isJavaSymbol() {
return mJavaSymbol;
}
/**
* Indicates that pattern matches should apply to constructors. If so, look for
* some other possible source aliases too, such as "super".
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchConstructor() {
mConstructor = true;
mWholeWord = true;
mJavaSymbol = true;
return this;
}
/** @return true if the pattern match should be for a constructor */
public boolean isConstructor() {
return mConstructor;
}
}
}
@@ -1,50 +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.detector.api;
import com.google.common.annotations.Beta;
/**
* Information about a position in a file/document.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class Position {
/**
* Returns the line number (0-based where the first line is line 0)
*
* @return the 0-based line number
*/
public abstract int getLine();
/**
* The character offset
*
* @return the 0-based character offset
*/
public abstract int getOffset();
/**
* Returns the column number (where the first character on the line is 0),
* or -1 if unknown
*
* @return the 0-based column number
*/
public abstract int getColumn();
}
File diff suppressed because it is too large Load Diff
@@ -1,80 +0,0 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.client.api.LintDriver;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* A {@link com.android.tools.lint.detector.api.Context} used when checking resource files
* (both bitmaps and XML files; for XML files a subclass of this context is used:
* {@link com.android.tools.lint.detector.api.XmlContext}.)
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class ResourceContext extends Context {
private final ResourceFolderType mFolderType;
/**
* Construct a new {@link com.android.tools.lint.detector.api.ResourceContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param folderType the {@link com.android.resources.ResourceFolderType} of this file, if any
*/
public ResourceContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable ResourceFolderType folderType) {
super(driver, project, main, file);
mFolderType = folderType;
}
/**
* Returns the resource folder type of this XML file, if any.
*
* @return the resource folder type or null
*/
@Nullable
public ResourceFolderType getResourceFolderType() {
return mFolderType;
}
/**
* Returns the folder version. For example, for the file values-v14/foo.xml,
* it returns 14.
*
* @return the folder version, or -1 if no specific version was specified
*/
public int getFolderVersion() {
return mDriver.getResourceFolderVersion(file);
}
}
@@ -1,690 +0,0 @@
/*
* 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.UReferenceExpression;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
/** Evaluates constant expressions */
public class ResourceEvaluator {
/**
* Marker ResourceType used to signify that an expression is of type {@code @ColorInt},
* which isn't actually a ResourceType but one we want to specifically compare with.
* We're using {@link ResourceType#PUBLIC} because that one won't appear in the R
* class (and ResourceType is an enum we can't just create new constants for.)
*/
public static final ResourceType COLOR_INT_MARKER_TYPE = ResourceType.PUBLIC;
/**
* Marker ResourceType used to signify that an expression is of type {@code @Px},
* which isn't actually a ResourceType but one we want to specifically compare with.
* We're using {@link ResourceType#DECLARE_STYLEABLE} because that one doesn't
* have a corresponding {@code *Res} constant (and ResourceType is an enum we can't
* just create new constants for.)
*/
public static final ResourceType PX_MARKER_TYPE = ResourceType.DECLARE_STYLEABLE;
public static final String COLOR_INT_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "ColorInt"; //$NON-NLS-1$
public static final String PX_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "Px"; //$NON-NLS-1$
public static final String RES_SUFFIX = "Res";
public static final String CLS_TYPED_ARRAY = "android.content.res.TypedArray";
private final JavaContext mContext;
private final JavaEvaluator mEvaluator;
private boolean mAllowDereference = true;
/**
* Creates a new resource evaluator
*
* @param context Java context
*/
public ResourceEvaluator(JavaContext context) {
mContext = context;
mEvaluator = context.getEvaluator();
}
/**
* Whether we allow dereferencing resources when computing constants;
* e.g. if we ask for the resource for {@code x} when the code is
* {@code x = getString(R.string.name)}, if {@code allowDereference} is
* true we'll return R.string.name, otherwise we'll return null.
*
* @return this for constructor chaining
*/
public ResourceEvaluator allowDereference(boolean allow) {
mAllowDereference = allow;
return this;
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource url (type and name)
*/
@Nullable
public static ResourceUrl getResource(
@NonNull JavaContext context,
@NonNull PsiElement element) {
return new ResourceEvaluator(context).getResource(element);
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource url (type and name)
*/
@Nullable
public static ResourceUrl getResource(
@NonNull JavaContext context,
@NonNull UElement element) {
return new ResourceEvaluator(context).getResource(element);
}
/**
* Evaluates the given node and returns the resource types implied by the given element,
* if any.
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource types
*/
@Nullable
public static EnumSet<ResourceType> getResourceTypes(
@NonNull JavaContext context,
@NonNull PsiElement element) {
return new ResourceEvaluator(context).getResourceTypes(element);
}
/**
* Evaluates the given node and returns the resource types implied by the given element,
* if any.
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource types
*/
@Nullable
public static EnumSet<ResourceType> getResourceTypes(
@NonNull JavaContext context,
@NonNull UElement element) {
return new ResourceEvaluator(context).getResourceTypes(element);
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param element the node to compute the constant value for
* @return the corresponding constant value - a String, an Integer, a Float, and so on
*/
@Nullable
public ResourceUrl getResource(@Nullable UElement element) {
if (element == null) {
return null;
}
if (element instanceof UIfExpression) {
UIfExpression expression = (UIfExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResource(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResource(expression.getElseExpression());
}
} else if (element instanceof UParenthesizedExpression) {
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
return getResource(parenthesizedExpression.getExpression());
} else if (mAllowDereference && element instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualifiedExpression = (UQualifiedReferenceExpression) element;
UExpression selector = qualifiedExpression.getSelector();
if ((selector instanceof UCallExpression)) {
UCallExpression call = (UCallExpression) selector;
PsiMethod function = call.resolve();
PsiClass containingClass = UastUtils.getContainingClass(function);
if (function != null && containingClass != null) {
String qualifiedName = containingClass.getQualifiedName();
String name = call.getMethodName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
List<UExpression> args = call.getValueArguments();
if (!args.isEmpty()) {
return getResource(args.get(0));
}
}
}
}
}
if (element instanceof UReferenceExpression) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return url;
}
PsiElement resolved = ((UReferenceExpression) element).resolve();
if (resolved instanceof PsiVariable) {
PsiVariable variable = (PsiVariable) resolved;
UElement lastAssignment =
UastLintUtils.findLastAssignment(
variable, element, mContext);
if (lastAssignment != null) {
return getResource(lastAssignment);
}
return null;
}
}
return null;
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param element the node to compute the constant value for
* @return the corresponding constant value - a String, an Integer, a Float, and so on
*/
@Nullable
public ResourceUrl getResource(@Nullable PsiElement element) {
if (element == null) {
return null;
}
if (element instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResource(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResource(expression.getElseExpression());
}
} else if (element instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
return getResource(parenthesizedExpression.getExpression());
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
PsiReferenceExpression expression = call.getMethodExpression();
PsiMethod method = call.resolveMethod();
if (method != null && method.getContainingClass() != null) {
String qualifiedName = method.getContainingClass().getQualifiedName();
String name = expression.getReferenceName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
PsiExpression[] args = call.getArgumentList().getExpressions();
if (args.length > 0) {
return getResource(args[0]);
}
}
}
} else if (element instanceof PsiReference) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return url;
}
PsiElement resolved = ((PsiReference) element).resolve();
if (resolved instanceof PsiField) {
url = getResourceConstant(resolved);
if (url != null) {
return url;
}
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
return getResource(field.getInitializer());
}
return null;
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
for (PsiElement e : prevStatement.getDeclaredElements()) {
if (variable.equals(e)) {
return getResource(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement) prev)
.getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign
= (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return getResource(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
}
return null;
}
/**
* Evaluates the given node and returns the resource types applicable to the
* node, if any.
*
* @param element the element to compute the types for
* @return the corresponding resource types
*/
@Nullable
public EnumSet<ResourceType> getResourceTypes(@Nullable UElement element) {
if (element == null) {
return null;
}
if (element instanceof UIfExpression) {
UIfExpression expression = (UIfExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResourceTypes(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResourceTypes(expression.getElseExpression());
} else {
EnumSet<ResourceType> left = getResourceTypes(
expression.getThenExpression());
EnumSet<ResourceType> right = getResourceTypes(
expression.getElseExpression());
if (left == null) {
return right;
} else if (right == null) {
return left;
} else {
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
copy.addAll(right);
return copy;
}
}
} else if (element instanceof UParenthesizedExpression) {
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
return getResourceTypes(parenthesizedExpression.getExpression());
} else if ((element instanceof UQualifiedReferenceExpression && mAllowDereference)
|| element instanceof UCallExpression) {
UElement probablyCallExpression = element;
if (element instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualifiedExpression =
(UQualifiedReferenceExpression) element;
probablyCallExpression = qualifiedExpression.getSelector();
}
if ((probablyCallExpression instanceof UCallExpression)) {
UCallExpression call = (UCallExpression) probablyCallExpression;
PsiMethod method = call.resolve();
PsiClass containingClass = UastUtils.getContainingClass(method);
if (method != null && containingClass != null) {
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
if (types != null) {
return types;
}
String qualifiedName = containingClass.getQualifiedName();
String name = call.getMethodName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
List<UExpression> args = call.getValueArguments();
if (!args.isEmpty()) {
types = getResourceTypes(args.get(0));
if (types != null) {
return types;
}
}
}
}
}
}
if (element instanceof UReferenceExpression) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return EnumSet.of(url.type);
}
PsiElement resolved = ((UReferenceExpression) element).resolve();
if (resolved instanceof PsiVariable) {
PsiVariable variable = (PsiVariable) resolved;
UElement lastAssignment =
UastLintUtils.findLastAssignment(variable, element, mContext);
if (lastAssignment != null) {
return getResourceTypes(lastAssignment);
}
return null;
}
}
return null;
}
/**
* Evaluates the given node and returns the resource types applicable to the
* node, if any.
*
* @param element the element to compute the types for
* @return the corresponding resource types
*/
@Nullable
public EnumSet<ResourceType> getResourceTypes(@Nullable PsiElement element) {
if (element == null) {
return null;
}
if (element instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResourceTypes(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResourceTypes(expression.getElseExpression());
} else {
EnumSet<ResourceType> left = getResourceTypes(
expression.getThenExpression());
EnumSet<ResourceType> right = getResourceTypes(
expression.getElseExpression());
if (left == null) {
return right;
} else if (right == null) {
return left;
} else {
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
copy.addAll(right);
return copy;
}
}
} else if (element instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
return getResourceTypes(parenthesizedExpression.getExpression());
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
PsiReferenceExpression expression = call.getMethodExpression();
PsiMethod method = call.resolveMethod();
if (method != null && method.getContainingClass() != null) {
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
if (types != null) {
return types;
}
String qualifiedName = method.getContainingClass().getQualifiedName();
String name = expression.getReferenceName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
PsiExpression[] args = call.getArgumentList().getExpressions();
if (args.length > 0) {
types = getResourceTypes(args[0]);
if (types != null) {
return types;
}
}
}
}
} else if (element instanceof PsiReference) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return EnumSet.of(url.type);
}
PsiElement resolved = ((PsiReference) element).resolve();
if (resolved instanceof PsiField) {
url = getResourceConstant(resolved);
if (url != null) {
return EnumSet.of(url.type);
}
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
return getResourceTypes(field.getInitializer());
}
return null;
} else if (resolved instanceof PsiParameter) {
return getTypesFromAnnotations((PsiParameter)resolved);
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
for (PsiElement e : prevStatement.getDeclaredElements()) {
if (variable.equals(e)) {
return getResourceTypes(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement) prev)
.getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign
= (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return getResourceTypes(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
}
return null;
}
@Nullable
private EnumSet<ResourceType> getTypesFromAnnotations(PsiModifierListOwner owner) {
if (mEvaluator == null) {
return null;
}
for (PsiAnnotation annotation : mEvaluator.getAllAnnotations(owner)) {
String signature = annotation.getQualifiedName();
if (signature == null) {
continue;
}
if (signature.equals(COLOR_INT_ANNOTATION)) {
return EnumSet.of(COLOR_INT_MARKER_TYPE);
}
if (signature.equals(PX_ANNOTATION)) {
return EnumSet.of(PX_MARKER_TYPE);
}
if (signature.endsWith(RES_SUFFIX)
&& signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
String typeString = signature
.substring(SUPPORT_ANNOTATIONS_PREFIX.length(),
signature.length() - RES_SUFFIX.length())
.toLowerCase(Locale.US);
ResourceType type = ResourceType.getEnum(typeString);
if (type != null) {
return EnumSet.of(type);
} else if (typeString.equals("any")) { // @AnyRes
return getAnyRes();
}
}
}
return null;
}
/** Returns a resource URL based on the field reference in the code */
@Nullable
public static ResourceUrl getResourceConstant(@NonNull PsiElement node) {
// R.type.name
if (node instanceof PsiReferenceExpression) {
PsiReferenceExpression expression = (PsiReferenceExpression) node;
if (expression.getQualifier() instanceof PsiReferenceExpression) {
PsiReferenceExpression select = (PsiReferenceExpression) expression.getQualifier();
if (select.getQualifier() instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) select
.getQualifier();
if (R_CLASS.equals(reference.getReferenceName())) {
String typeName = select.getReferenceName();
String name = expression.getReferenceName();
ResourceType type = ResourceType.getEnum(typeName);
if (type != null && name != null) {
boolean isFramework =
reference.getQualifier() instanceof PsiReferenceExpression
&& ANDROID_PKG
.equals(((PsiReferenceExpression) reference.
getQualifier()).getReferenceName());
return ResourceUrl.create(type, name, isFramework, false);
}
}
}
}
} else if (node instanceof PsiField) {
PsiField field = (PsiField) node;
PsiClass typeClass = field.getContainingClass();
if (typeClass != null) {
PsiClass rClass = typeClass.getContainingClass();
if (rClass != null && R_CLASS.equals(rClass.getName())) {
String name = field.getName();
ResourceType type = ResourceType.getEnum(typeClass.getName());
if (type != null && name != null) {
String qualifiedName = rClass.getQualifiedName();
boolean isFramework = qualifiedName != null
&& qualifiedName.startsWith(ANDROID_PKG_PREFIX);
return ResourceUrl.create(type, name, isFramework, false);
}
}
}
}
return null;
}
/** Returns a resource URL based on the field reference in the code */
@Nullable
public static ResourceUrl getResourceConstant(@NonNull UElement node) {
AndroidReference androidReference = toAndroidReferenceViaResolve(node);
if (androidReference == null) {
return null;
}
String name = androidReference.getName();
ResourceType type = androidReference.getType();
boolean isFramework = androidReference.getPackage().equals("android");
return ResourceUrl.create(type, name, isFramework, false);
}
private static EnumSet<ResourceType> getAnyRes() {
EnumSet<ResourceType> types = EnumSet.allOf(ResourceType.class);
types.remove(ResourceEvaluator.COLOR_INT_MARKER_TYPE);
types.remove(ResourceEvaluator.PX_MARKER_TYPE);
return types;
}
}
@@ -1,61 +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.detector.api;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* Specialized detector intended for XML resources. Detectors that apply to XML
* resources should extend this detector instead since it provides special
* iteration hooks that are more efficient.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class ResourceXmlDetector extends Detector implements Detector.XmlScanner {
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return LintUtils.isXmlFile(file);
}
/**
* Returns whether this detector applies to the given folder type. This
* allows the detectors to be pruned from iteration, so for example when we
* are analyzing a string value file we don't need to look up detectors
* related to layout.
*
* @param folderType the folder type to be visited
* @return true if this detector can apply to resources in folders of the
* given type
*/
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
@Override
public void run(@NonNull Context context) {
// The infrastructure should never call this method on an xml detector since
// it will run the various visitors instead
assert false;
}
}
@@ -1,265 +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.detector.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_PROPERTIES;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
import static com.android.SdkConstants.OLD_PROGUARD_FILE;
import static com.android.SdkConstants.RES_FOLDER;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* The scope of a detector is the set of files a detector must consider when
* performing its analysis. This can be used to determine when issues are
* potentially obsolete, whether a detector should re-run on a file save, etc.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public enum Scope {
/**
* The analysis only considers a single XML resource file at a time.
* <p>
* Issues which are only affected by a single resource file can be checked
* for incrementally when a file is edited.
*/
RESOURCE_FILE,
/**
* The analysis only considers a single binary (typically a bitmap) resource file at a time.
* <p>
* Issues which are only affected by a single resource file can be checked
* for incrementally when a file is edited.
*/
BINARY_RESOURCE_FILE,
/**
* The analysis considers the resource folders (which also includes asset folders)
*/
RESOURCE_FOLDER,
/**
* The analysis considers <b>all</b> the resource file. This scope must not
* be used in conjunction with {@link #RESOURCE_FILE}; an issue scope is
* either considering just a single resource file or all the resources, not
* both.
*/
ALL_RESOURCE_FILES,
/**
* The analysis only considers a single Java source file at a time.
* <p>
* Issues which are only affected by a single Java source file can be
* checked for incrementally when a Java source file is edited.
*/
JAVA_FILE,
/**
* The analysis considers <b>all</b> the Java source files together.
* <p>
* This flag is mutually exclusive with {@link #JAVA_FILE}.
*/
ALL_JAVA_FILES,
/**
* The analysis only considers a single Java class file at a time.
* <p>
* Issues which are only affected by a single Java class file can be checked
* for incrementally when a Java source file is edited and then recompiled.
*/
CLASS_FILE,
/**
* The analysis considers <b>all</b> the Java class files together.
* <p>
* This flag is mutually exclusive with {@link #CLASS_FILE}.
*/
ALL_CLASS_FILES,
/** The analysis considers the manifest file */
MANIFEST,
/** The analysis considers the Proguard configuration file */
PROGUARD_FILE,
/**
* The analysis considers classes in the libraries for this project. These
* will be analyzed before the classes themselves. NOTE: This excludes
* provided libraries.
*/
JAVA_LIBRARIES,
/** The analysis considers a Gradle build file */
GRADLE_FILE,
/** The analysis considers Java property files */
PROPERTY_FILE,
/** The analysis considers test sources as well */
TEST_SOURCES,
/**
* Scope for other files. Issues that specify a custom scope will be called unconditionally.
* This will call {@link Detector#run(Context)}} on the detectors unconditionally.
*/
OTHER;
/**
* Returns true if the given scope set corresponds to scanning a single file
* rather than a whole project
*
* @param scopes the scope set to check
* @return true if the scope set references a single file
*/
public static boolean checkSingleFile(@NonNull EnumSet<Scope> scopes) {
int size = scopes.size();
if (size == 2) {
// When single checking a Java source file, we check both its Java source
// and the associated class files
return scopes.contains(JAVA_FILE) && scopes.contains(CLASS_FILE);
} else {
return size == 1 &&
(scopes.contains(JAVA_FILE)
|| scopes.contains(CLASS_FILE)
|| scopes.contains(RESOURCE_FILE)
|| scopes.contains(PROGUARD_FILE)
|| scopes.contains(PROPERTY_FILE)
|| scopes.contains(GRADLE_FILE)
|| scopes.contains(MANIFEST));
}
}
/**
* Returns the intersection of two scope sets
*
* @param scope1 the first set to intersect
* @param scope2 the second set to intersect
* @return the intersection of the two sets
*/
@NonNull
public static EnumSet<Scope> intersect(
@NonNull EnumSet<Scope> scope1,
@NonNull EnumSet<Scope> scope2) {
EnumSet<Scope> scope = EnumSet.copyOf(scope1);
scope.retainAll(scope2);
return scope;
}
/**
* Infers a suitable scope to use from the given projects to be analyzed
* @param projects the projects to find a suitable scope for
* @return the scope to use
*/
@NonNull
public static EnumSet<Scope> infer(@NonNull Collection<Project> projects) {
// Infer the scope
EnumSet<Scope> scope = EnumSet.noneOf(Scope.class);
for (Project project : projects) {
List<File> subset = project.getSubset();
if (subset != null) {
for (File file : subset) {
String name = file.getName();
if (name.equals(ANDROID_MANIFEST_XML)) {
scope.add(MANIFEST);
} else if (name.endsWith(DOT_XML)) {
scope.add(RESOURCE_FILE);
} else if (name.endsWith(".kt")) {
scope.add(JAVA_FILE);
} else if (name.endsWith(DOT_CLASS)) {
scope.add(CLASS_FILE);
} else if (name.endsWith(DOT_GRADLE)) {
scope.add(GRADLE_FILE);
} else if (name.equals(OLD_PROGUARD_FILE)
|| name.equals(FN_PROJECT_PROGUARD_FILE)) {
scope.add(PROGUARD_FILE);
} else if (name.endsWith(DOT_PROPERTIES)) {
scope.add(PROPERTY_FILE);
} else if (name.endsWith(DOT_PNG)) {
scope.add(BINARY_RESOURCE_FILE);
} else if (name.equals(RES_FOLDER)
|| file.getParent().equals(RES_FOLDER)) {
scope.add(ALL_RESOURCE_FILES);
scope.add(RESOURCE_FILE);
scope.add(BINARY_RESOURCE_FILE);
scope.add(RESOURCE_FOLDER);
}
}
} else {
// Specified a full project: just use the full project scope
scope = Scope.ALL;
break;
}
}
return scope;
}
/** All scopes: running lint on a project will check these scopes */
public static final EnumSet<Scope> ALL = EnumSet.allOf(Scope.class);
/** Scope-set used for detectors which are affected by a single resource file */
public static final EnumSet<Scope> RESOURCE_FILE_SCOPE = EnumSet.of(RESOURCE_FILE);
/** Scope-set used for detectors which are affected by a single resource folder */
public static final EnumSet<Scope> RESOURCE_FOLDER_SCOPE = EnumSet.of(RESOURCE_FOLDER);
/** Scope-set used for detectors which scan all resources */
public static final EnumSet<Scope> ALL_RESOURCES_SCOPE = EnumSet.of(ALL_RESOURCE_FILES);
/** Scope-set used for detectors which are affected by a single Java source file */
public static final EnumSet<Scope> JAVA_FILE_SCOPE = EnumSet.of(JAVA_FILE);
/** Scope-set used for detectors which are affected by a single Java class file */
public static final EnumSet<Scope> CLASS_FILE_SCOPE = EnumSet.of(CLASS_FILE);
/** Scope-set used for detectors which are affected by a single Gradle build file */
public static final EnumSet<Scope> GRADLE_SCOPE = EnumSet.of(GRADLE_FILE);
/** Scope-set used for detectors which are affected by the manifest only */
public static final EnumSet<Scope> MANIFEST_SCOPE = EnumSet.of(MANIFEST);
/** Scope-set used for detectors which correspond to some other context */
public static final EnumSet<Scope> OTHER_SCOPE = EnumSet.of(OTHER);
/** Scope-set used for detectors which are affected by a single ProGuard class file */
public static final EnumSet<Scope> PROGUARD_SCOPE = EnumSet.of(PROGUARD_FILE);
/** Scope-set used for detectors which correspond to property files */
public static final EnumSet<Scope> PROPERTY_SCOPE = EnumSet.of(PROPERTY_FILE);
/** Resource XML files and manifest files */
public static final EnumSet<Scope> MANIFEST_AND_RESOURCE_SCOPE =
EnumSet.of(Scope.MANIFEST, Scope.RESOURCE_FILE);
/** Scope-set used for detectors which are affected by single XML and Java source files */
public static final EnumSet<Scope> JAVA_AND_RESOURCE_FILES =
EnumSet.of(RESOURCE_FILE, JAVA_FILE);
/** Scope-set used for analyzing individual class files and all resource files */
public static final EnumSet<Scope> CLASS_AND_ALL_RESOURCE_FILES =
EnumSet.of(ALL_RESOURCE_FILES, CLASS_FILE);
/** Scope-set used for analyzing all class files, including those in libraries */
public static final EnumSet<Scope> ALL_CLASSES_AND_LIBRARIES =
EnumSet.of(Scope.ALL_CLASS_FILES, Scope.JAVA_LIBRARIES);
/** Scope-set used for detectors which are affected by Java libraries */
public static final EnumSet<Scope> JAVA_LIBRARY_SCOPE = EnumSet.of(JAVA_LIBRARIES);
/** Scope-set used for detectors which are affected by a single binary resource file */
public static final EnumSet<Scope> BINARY_RESOURCE_FILE_SCOPE =
EnumSet.of(BINARY_RESOURCE_FILE);
}
@@ -1,103 +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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* Severity of an issue found by lint
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public enum Severity {
/**
* Fatal: Use sparingly because a warning marked as fatal will be
* considered critical and will abort Export APK etc in ADT
*/
@NonNull
FATAL("Fatal"),
/**
* Errors: The issue is known to be a real error that must be addressed.
*/
@NonNull
ERROR("Error"),
/**
* Warning: Probably a problem.
*/
@NonNull
WARNING("Warning"),
/**
* Information only: Might not be a problem, but the check has found
* something interesting to say about the code.
*/
@NonNull
INFORMATIONAL("Information"),
/**
* Ignore: The user doesn't want to see this issue
*/
@NonNull
IGNORE("Ignore");
@NonNull
private final String mDisplay;
Severity(@NonNull String display) {
mDisplay = display;
}
/**
* Returns a description of this severity suitable for display to the user
*
* @return a description of the severity
*/
@NonNull
public String getDescription() {
return mDisplay;
}
/** Returns the name of this severity */
@NonNull
public String getName() {
return name();
}
/**
* Looks up the severity corresponding to a given named severity. The severity
* string should be one returned by {@link #toString()}
*
* @param name the name to look up
* @return the corresponding severity, or null if it is not a valid severity name
*/
@Nullable
public static Severity fromName(@NonNull String name) {
for (Severity severity : values()) {
if (severity.name().equalsIgnoreCase(name)) {
return severity;
}
}
return null;
}
}
@@ -1,58 +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.detector.api;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
/**
* Enum which describes the different computation speeds of various detectors
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public enum Speed {
/** The detector can run very quickly */
FAST("Fast"),
/** The detector runs reasonably fast */
NORMAL("Normal"),
/** The detector might take a long time to run */
SLOW("Slow"),
/** The detector might take a huge amount of time to run */
REALLY_SLOW("Really Slow");
private final String mDisplayName;
Speed(@NonNull String displayName) {
mDisplayName = displayName;
}
/**
* Returns the user-visible description of the speed of the given
* detector
*
* @return the description of the speed to display to the user
*/
@NonNull
public String getDisplayName() {
return mDisplayName;
}
}
@@ -1,405 +0,0 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
/**
* Lint error message, issue explanations and location descriptions
* are described in a {@link #RAW} format which looks similar to text
* but which can contain bold, symbols and links. These issues can
* also be converted to plain text and to HTML markup, using the
* {@link #convertTo(String, TextFormat)} method.
*
* @see Issue#getExplanation(TextFormat)
* @see Issue#getBriefDescription(TextFormat)
*/
public enum TextFormat {
/**
* Raw output format which is similar to text but allows some markup:
* <ul>
* <li>HTTP urls (http://...)
* <li>Sentences immediately surrounded by * will be shown as bold.
* <li>Sentences immediately surrounded by ` will be shown using monospace
* fonts
* </ul>
* Furthermore, newlines are converted to br's when converting newlines.
* Note: It does not insert {@code <html>} tags around the fragment for HTML output.
* <p>
* TODO: Consider switching to the restructured text format -
* http://docutils.sourceforge.net/docs/user/rst/quickstart.html
*/
RAW,
/**
* Plain text output
*/
TEXT,
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags)
*/
HTML,
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags).
* This is like {@link #HTML}, but it does not escape unicode characters with entities.
* <p>
* (This is used for example in the IDE, where some partial HTML support in some
* label widgets support some HTML markup, but not numeric code character entities.)
*/
HTML_WITH_UNICODE;
/**
* Converts the given text to HTML
*
* @param text the text to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toHtml(@NonNull String text) {
return convertTo(text, HTML);
}
/**
* Converts the given text to plain text
*
* @param text the tetx to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toText(@NonNull String text) {
return convertTo(text, TEXT);
}
/**
* Converts the given message to the given format. Note that some
* conversions are lossy; e.g. once converting away from the raw format
* (which contains all the markup) you can't convert back to it.
* Note that you can convert to the format it's already in; that just
* returns the same string.
*
* @param message the message to convert
* @param to the format to convert to
* @return a converted message
*/
public String convertTo(@NonNull String message, @NonNull TextFormat to) {
if (this == to) {
return message;
}
switch (this) {
case RAW: {
switch (to) {
case RAW:
return message;
case TEXT:
case HTML:
case HTML_WITH_UNICODE:
return to.fromRaw(message);
}
}
case TEXT: {
switch (to) {
case TEXT:
case RAW:
return message;
case HTML:
case HTML_WITH_UNICODE:
return XmlUtils.toXmlTextValue(message);
}
}
case HTML: {
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);
}
}
}
}
return message;
}
/** Converts to this output format from the given HTML-format text */
@NonNull
private String fromHtml(@NonNull String html) {
assert this == RAW || this == TEXT : this;
// Drop all tags; replace all entities, insert newlines
// (this won't do wrapping)
StringBuilder sb = new StringBuilder(html.length());
boolean inPre = false;
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '<') {
// Strip comments
if (html.startsWith("<!--", i)) {
int end = html.indexOf("-->", i);
if (end == -1) {
break; // Unclosed comment
} else {
i = end + 2;
}
continue;
}
// Tags: scan forward to the end
int begin;
boolean isEndTag = false;
if (html.startsWith("</", i)) {
begin = i + 2;
isEndTag = true;
} else {
begin = i + 1;
}
i = html.indexOf('>', i);
if (i == -1) {
// Unclosed tag
break;
}
int end = i;
if (html.charAt(i - 1) == '/') {
end--;
isEndTag = true;
}
// TODO: Handle <pre> such that we don't collapse spaces and reformat there!
// (We do need to strip out tags and expand entities)
String tag = html.substring(begin, end).trim();
if (tag.equalsIgnoreCase("br")) {
sb.append('\n');
} else if (tag.equalsIgnoreCase("p") // Most common block tags
|| tag.equalsIgnoreCase("div")
|| tag.equalsIgnoreCase("pre")
|| tag.equalsIgnoreCase("blockquote")
|| tag.equalsIgnoreCase("dl")
|| tag.equalsIgnoreCase("dd")
|| tag.equalsIgnoreCase("dt")
|| tag.equalsIgnoreCase("ol")
|| tag.equalsIgnoreCase("ul")
|| tag.equalsIgnoreCase("li")
|| tag.length() == 2 && tag.startsWith("h")
&& Character.isDigit(tag.charAt(1))) {
// Block tag: ensure new line
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '\n') {
sb.append('\n');
}
if (tag.equals("li") && !isEndTag) {
sb.append("* ");
}
if (tag.equalsIgnoreCase("pre")) {
inPre = !isEndTag;
}
}
} else if (c == '&') {
int end = html.indexOf(';', i);
if (end > i) {
String entity = html.substring(i, end + 1);
String s = XmlUtils.fromXmlAttributeValue(entity);
if (s.startsWith("&")) {
// Not an XML entity; for example, &nbsp;
// Sadly Guava's HtmlEscapes don't handle this either.
if (entity.equalsIgnoreCase("&nbsp;")) {
s = " ";
} else if (entity.startsWith("&#")) {
try {
int value = Integer.parseInt(entity.substring(2));
s = Character.toString((char)value);
} catch (NumberFormatException ignore) {
}
}
}
sb.append(s);
i = end;
} else {
sb.append(c);
}
} else if (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);
}
}
String s = sb.toString();
// Line-wrap
s = SdkUtils.wrap(s, 60, null);
return s;
}
private static final String HTTP_PREFIX = "http://"; //$NON-NLS-1$
/** Converts to this output format from the given raw-format text */
@NonNull
private String fromRaw(@NonNull String text) {
assert this == HTML || this == HTML_WITH_UNICODE || this == TEXT : this;
StringBuilder sb = new StringBuilder(3 * text.length() / 2);
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) {
// Scout ahead for range end
if (!Character.isLetterOrDigit(prev)
&& !Character.isWhitespace(text.charAt(i + 1))) {
// Found * or ` immediately before a letter, and not in the middle of a word
// Find end
int end = text.indexOf(c, i + 1);
if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i, 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, escapeUnicode);
sb.append('<').append('/').append(tag).append('>');
} else {
appendEscapedText(sb, text, html, i + 1, end, escapeUnicode);
}
flushIndex = end + 1;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
} else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't'
&& text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) {
// Find url end
int end = i + HTTP_PREFIX.length();
while (end < n) {
char d = text.charAt(end);
if (Character.isWhitespace(d)) {
break;
}
end++;
}
char last = text.charAt(end - 1);
if (last == '.' || last == ')' || last == '!') {
end--;
}
if (end > i + HTTP_PREFIX.length()) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i, escapeUnicode);
}
String url = text.substring(i, end);
sb.append("<a href=\""); //$NON-NLS-1$
sb.append(url);
sb.append('"').append('>');
sb.append(url);
sb.append("</a>"); //$NON-NLS-1$
flushIndex = end;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
prev = c;
}
if (flushIndex < n) {
appendEscapedText(sb, text, html, flushIndex, n, 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 escapeUnicode) {
if (html) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (c == '<') {
sb.append("&lt;"); //$NON-NLS-1$
} else if (c == '&') {
sb.append("&amp;"); //$NON-NLS-1$
} else if (c == '\n') {
sb.append("<br/>\n");
} else {
if (c > 255 && escapeUnicode) {
sb.append("&#"); //$NON-NLS-1$
sb.append(Integer.toString(c));
sb.append(';');
} else if (c == '\u00a0') {
sb.append("&nbsp;"); //$NON-NLS-1$
} else {
sb.append(c);
}
}
}
} else {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
sb.append(c);
}
}
}
}
@@ -1,417 +0,0 @@
/*
* 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.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import java.util.ListIterator;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.BooleanLiteral;
import lombok.ast.Cast;
import lombok.ast.CharLiteral;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.FloatingPointLiteral;
import lombok.ast.InlineIfExpression;
import lombok.ast.IntegralLiteral;
import lombok.ast.Literal;
import lombok.ast.Node;
import lombok.ast.NullLiteral;
import lombok.ast.Statement;
import lombok.ast.StringLiteral;
import lombok.ast.UnaryExpression;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Evaluates the types of nodes. This goes deeper than
* {@link JavaContext#getType(Node)} in that it analyzes the
* flow and for example figures out that if you ask for the type of {@code var}
* in this code snippet:
* <pre>
* Object o = new StringBuilder();
* Object var = o;
* </pre>
* it will return "java.lang.StringBuilder".
* <p>
* <b>NOTE:</b> This type evaluator does not (yet) compute the correct
* types when involving implicit type conversions, so be careful
* if using this for primitives; e.g. for "int * long" it might return
* the type "int".
*/
public class TypeEvaluator {
private final JavaContext mContext;
/**
* Creates a new constant evaluator
*
* @param context the context to use to resolve field references, if any
*/
public TypeEvaluator(@Nullable JavaContext context) {
mContext = context;
}
/**
* Returns the inferred type of the given node
* @deprecated Use {@link #evaluate(PsiElement)} instead
*/
@Deprecated
@Nullable
public TypeDescriptor evaluate(@NonNull Node node) {
ResolvedNode resolved = null;
if (mContext != null) {
resolved = mContext.resolve(node);
}
if (resolved instanceof ResolvedMethod) {
TypeDescriptor type;
ResolvedMethod method = (ResolvedMethod) resolved;
if (method.isConstructor()) {
ResolvedClass containingClass = method.getContainingClass();
type = containingClass.getType();
} else {
type = method.getReturnType();
}
return type;
}
if (resolved instanceof ResolvedField) {
ResolvedField field = (ResolvedField) resolved;
Node astNode = field.findAstNode();
if (astNode instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration)astNode;
VariableDefinition definition = declaration.astDefinition();
if (definition != null) {
VariableDefinitionEntry first = definition.astVariables().first();
if (first != null) {
Expression initializer = first.astInitializer();
if (initializer != null) {
TypeDescriptor type = evaluate(initializer);
if (type != null) {
return type;
}
}
}
}
}
return field.getType();
}
if (node instanceof VariableReference) {
Statement statement = getParentOfType(node, Statement.class, false);
if (statement != null) {
ListIterator<Node> iterator = statement.getParent().getChildren().listIterator();
while (iterator.hasNext()) {
if (iterator.next() == statement) {
if (iterator.hasPrevious()) { // should always be true
iterator.previous();
}
break;
}
}
String targetName = ((VariableReference) node).astIdentifier().astValue();
while (iterator.hasPrevious()) {
Node previous = iterator.previous();
if (previous instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration) previous;
VariableDefinition definition = declaration.astDefinition();
for (VariableDefinitionEntry entry : definition.astVariables()) {
if (entry.astInitializer() != null && entry.astName().astValue()
.equals(targetName)) {
return evaluate(entry.astInitializer());
}
}
} else if (previous instanceof ExpressionStatement) {
ExpressionStatement expressionStatement = (ExpressionStatement) previous;
Expression expression = expressionStatement.astExpression();
if (expression instanceof BinaryExpression &&
((BinaryExpression) expression).astOperator()
== BinaryOperator.ASSIGN) {
BinaryExpression binaryExpression = (BinaryExpression) expression;
if (targetName.equals(binaryExpression.astLeft().toString())) {
return evaluate(binaryExpression.astRight());
}
}
}
}
}
} else if (node instanceof Cast) {
Cast cast = (Cast) node;
if (mContext != null) {
ResolvedNode typeReference = mContext.resolve(cast.astTypeReference());
if (typeReference instanceof ResolvedClass) {
return ((ResolvedClass) typeReference).getType();
}
}
TypeDescriptor viewType = evaluate(cast.astOperand());
if (viewType != null) {
return viewType;
}
} else if (node instanceof Literal) {
if (node instanceof NullLiteral) {
return null;
} else if (node instanceof BooleanLiteral) {
return new DefaultTypeDescriptor(TYPE_BOOLEAN);
} else if (node instanceof StringLiteral) {
return new DefaultTypeDescriptor(TYPE_STRING);
} else if (node instanceof CharLiteral) {
return new DefaultTypeDescriptor(TYPE_CHAR);
} else if (node instanceof IntegralLiteral) {
IntegralLiteral literal = (IntegralLiteral) node;
// Don't combine to ?: since that will promote astIntValue to a long
if (literal.astMarkedAsLong()) {
return new DefaultTypeDescriptor(TYPE_LONG);
} else {
return new DefaultTypeDescriptor(TYPE_INT);
}
} else if (node instanceof FloatingPointLiteral) {
FloatingPointLiteral literal = (FloatingPointLiteral) node;
// Don't combine to ?: since that will promote astFloatValue to a double
if (literal.astMarkedAsFloat()) {
return new DefaultTypeDescriptor(TYPE_FLOAT);
} else {
return new DefaultTypeDescriptor(TYPE_DOUBLE);
}
}
} else if (node instanceof UnaryExpression) {
return evaluate(((UnaryExpression) node).astOperand());
} else if (node instanceof InlineIfExpression) {
InlineIfExpression expression = (InlineIfExpression) node;
if (expression.astIfTrue() != null) {
return evaluate(expression.astIfTrue());
} else if (expression.astIfFalse() != null) {
return evaluate(expression.astIfFalse());
}
} else if (node instanceof BinaryExpression) {
BinaryExpression expression = (BinaryExpression) node;
BinaryOperator operator = expression.astOperator();
switch (operator) {
case LOGICAL_OR:
case LOGICAL_AND:
case EQUALS:
case NOT_EQUALS:
case GREATER:
case GREATER_OR_EQUAL:
case LESS:
case LESS_OR_EQUAL:
return new DefaultTypeDescriptor(TYPE_BOOLEAN);
}
TypeDescriptor type = evaluate(expression.astLeft());
if (type != null) {
return type;
}
return evaluate(expression.astRight());
}
if (resolved instanceof ResolvedVariable) {
ResolvedVariable variable = (ResolvedVariable) resolved;
return variable.getType();
}
return null;
}
/**
* Returns the inferred type of the given node
*/
@Nullable
public PsiType evaluate(@Nullable PsiElement node) {
if (node == null) {
return null;
}
PsiElement resolved = null;
if (node instanceof PsiReference) {
resolved = ((PsiReference) node).resolve();
}
if (resolved instanceof PsiMethod) {
PsiMethod method = (PsiMethod) resolved;
if (method.isConstructor()) {
PsiClass containingClass = method.getContainingClass();
if (containingClass != null && mContext != null) {
return mContext.getEvaluator().getClassType(containingClass);
}
} else {
return method.getReturnType();
}
}
if (resolved instanceof PsiField) {
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
PsiType type = evaluate(field.getInitializer());
if (type != null) {
return type;
}
}
return field.getType();
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(node, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
for (PsiElement element : ((PsiDeclarationStatement)prev).getDeclaredElements()) {
if (variable.equals(element)) {
return evaluate(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement)prev).getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign = (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return evaluate(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
return variable.getType();
} else if (node instanceof PsiExpression) {
PsiExpression expression = (PsiExpression) node;
return expression.getType();
}
return null;
}
@Nullable
public static PsiType evaluate(@NonNull JavaContext context, @Nullable UElement node) {
if (node == null) {
return null;
}
UElement resolved = node;
if (resolved instanceof UReferenceExpression) {
resolved = UastUtils.tryResolveUDeclaration(resolved, context.getUastContext());
}
if (resolved instanceof UMethod) {
return ((UMethod) resolved).getPsi().getReturnType();
} else if (resolved instanceof UVariable) {
UVariable variable = (UVariable) resolved;
UElement lastAssignment = UastLintUtils.findLastAssignment(variable, node, context);
if (lastAssignment != null) {
return evaluate(context, lastAssignment);
}
return variable.getType();
} else if (resolved instanceof UCallExpression) {
if (UastExpressionUtils.isMethodCall(resolved)) {
PsiMethod resolvedMethod = ((UCallExpression) resolved).resolve();
return resolvedMethod != null ? resolvedMethod.getReturnType() : null;
} else {
return ((UCallExpression) resolved).getExpressionType();
}
} else if (resolved instanceof UExpression) {
return ((UExpression) resolved).getExpressionType();
}
return null;
}
/**
* Evaluates the given node and returns the likely type of the instance. Convenience
* wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
* the result.
*
* @param context the context to use to resolve field references, if any
* @param node the node to compute the type for
* @return the corresponding type descriptor, if found
* @deprecated Use {@link #evaluate(JavaContext, PsiElement)} instead
*/
@Deprecated
@Nullable
public static TypeDescriptor evaluate(@NonNull JavaContext context, @NonNull Node node) {
return new TypeEvaluator(context).evaluate(node);
}
/**
* Evaluates the given node and returns the likely type of the instance. Convenience
* wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
* the result.
*
* @param context the context to use to resolve field references, if any
* @param node the node to compute the type for
* @return the corresponding type descriptor, if found
*/
@Nullable
public static PsiType evaluate(@NonNull JavaContext context, @NonNull PsiElement node) {
return new TypeEvaluator(context).evaluate(node);
}
}
@@ -1,205 +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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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;
import java.io.File;
/**
* A {@link Context} used when checking XML files.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public class XmlContext extends ResourceContext {
static final String SUPPRESS_COMMENT_PREFIX = "<!--suppress "; //$NON-NLS-1$
/** The XML parser */
private final XmlParser mParser;
/** The XML document */
public Document document;
/**
* Construct a new {@link XmlContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param folderType the {@link ResourceFolderType} of this file, if any
*/
public XmlContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable ResourceFolderType folderType,
@NonNull XmlParser parser) {
super(driver, project, main, file, folderType);
mParser = parser;
}
/**
* Returns the location for the given node, which may be an element or an attribute.
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getLocation(@NonNull Node node) {
return mParser.getLocation(this, node);
}
/**
* Returns the location for name-portion of the given element or attribute.
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getNameLocation(@NonNull Node node) {
return mParser.getNameLocation(this, node);
}
/**
* Returns the location for value-portion of the given attribute
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getValueLocation(@NonNull Attr node) {
return mParser.getValueLocation(this, node);
}
/**
* Creates a new location within an XML text node
*
* @param textNode the text node
* @param begin the start offset within the text node (inclusive)
* @param end the end offset within the text node (exclusive)
* @return a new location
*/
@NonNull
public Location getLocation(@NonNull Node textNode, int begin, int end) {
assert textNode.getNodeType() == Node.TEXT_NODE;
return mParser.getLocation(this, textNode, begin, end);
}
@NonNull
public XmlParser getParser() {
return mParser;
}
/**
* Reports an issue applicable to a given DOM node. The DOM node is used as the
* scope to check for suppress lint annotations.
*
* @param issue the issue to report
* @param scope the DOM node scope the error applies to. The lint infrastructure
* will check whether there are suppress directives on this node (or its enclosing
* nodes) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, org.w3c.dom.Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, org.w3c.dom.Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
@Override
public void report(
@NonNull Issue issue,
@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
// not feasible to compute it cheaply)
//mDriver.getClient().log(null, "Warning: Issue " + issue
// + " was reported without a scope node: Can't be suppressed.");
// For now just check the document root itself
if (document != null && mDriver.isSuppressed(this, issue, document)) {
return;
}
super.report(issue, location, message);
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
public boolean isSuppressedWithComment(@NonNull Node node, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
int start = mParser.getNodeStartOffset(this, node);
if (start != -1) {
return isSuppressedWithComment(start, issue);
}
return false;
}
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
}
@@ -1,94 +0,0 @@
/*
* 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.client.api.JavaParser.TYPE_OBJECT;
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.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.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 Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"AddJavascriptInterface", //$NON-NLS-1$
"addJavascriptInterface Called",
"For applications built for API levels below 17, `WebView#addJavascriptInterface` "
+ "presents a security hazard as JavaScript on the target web page has the "
+ "ability to use reflection to access the injected object's public fields and "
+ "thus manipulate the host application in unintended ways.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
AddJavascriptInterfaceDetector.class,
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$
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE);
}
@Override
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.getMainProject().getMinSdk() >= 17) {
return;
}
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";
UElement reportElement = call.getMethodIdentifier();
if (reportElement == null) {
reportElement = call;
}
context.reportUast(ISSUE, reportElement, context.getUastNameLocation(reportElement), message);
}
}
@@ -1,109 +0,0 @@
/*
* 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.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;
/**
* Makes sure that alarms are handled correctly
*/
public class AlarmDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
AlarmDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Alarm set too soon/frequently */
public static final Issue ISSUE = Issue.create(
"ShortAlarm", //$NON-NLS-1$
"Short or Frequent Alarm",
"Frequent alarms are bad for battery life. As of API 22, the `AlarmManager` " +
"will override near-future and high-frequency alarm requests, delaying the alarm " +
"at least 5 seconds into the future and ensuring that the repeat interval is at " +
"least 60 seconds.\n" +
"\n" +
"If you really need to do work sooner than 5 seconds, post a delayed message " +
"or runnable to a Handler.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new {@link AlarmDetector} check */
public AlarmDetector() {
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("setRepeating");
}
@Override
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 JavaContext context,
@NonNull UCallExpression node, int parameter, long min) {
UExpression argument = node.getValueArguments().get(parameter);
long value = getLongValue(context, argument);
if (value < min) {
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, argument, context.getUastLocation(argument), message);
}
}
private static long getLongValue(
@NonNull JavaContext context,
@NonNull UExpression argument) {
Object value = ConstantEvaluator.evaluate(context, argument);
if (value instanceof Number) {
return ((Number)value).longValue();
}
return Long.MAX_VALUE;
}
}
@@ -1,105 +0,0 @@
/*
* 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<String> 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<String> 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);
}
}
}
}
}
@@ -1,221 +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 static com.android.SdkConstants.ATTR_SHOW_AS_ACTION;
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;
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.PsiElement;
import com.intellij.psi.PsiField;
import org.jetbrains.uast.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;
/**
* Check which looks for usage of showAsAction="always" in menus (or
* MenuItem.SHOW_AS_ACTION_ALWAYS in code), which is usually a style guide violation.
* (Use ifRoom instead).
*/
public class AlwaysShowActionDetector extends ResourceXmlDetector implements
Detector.UastScanner {
/** The main issue discovered by this detector */
@SuppressWarnings("unchecked")
public static final Issue ISSUE = Issue.create(
"AlwaysShowAction", //$NON-NLS-1$
"Usage of `showAsAction=always`",
"Using `showAsAction=\"always\"` in menu XML, or `MenuItem.SHOW_AS_ACTION_ALWAYS` in " +
"Java code is usually a deviation from the user interface style guide." +
"Use `ifRoom` or the corresponding `MenuItem.SHOW_AS_ACTION_IF_ROOM` instead.\n" +
"\n" +
"If `always` is used sparingly there are usually no problems and behavior is " +
"roughly equivalent to `ifRoom` but with preference over other `ifRoom` " +
"items. Using it more than twice in the same menu is a bad idea.\n" +
"\n" +
"This check looks for menu XML files that contain more than two `always` " +
"actions, or some `always` actions and no `ifRoom` actions. In Java code, " +
"it looks for projects that contain references to `MenuItem.SHOW_AS_ACTION_ALWAYS` " +
"and no references to `MenuItem.SHOW_AS_ACTION_IF_ROOM`.",
Category.USABILITY,
3,
Severity.WARNING,
new Implementation(
AlwaysShowActionDetector.class,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/design/patterns/actionbar.html"); //$NON-NLS-1$
/** List of showAsAction attributes appearing in the current menu XML file */
private List<Attr> mFileAttributes;
/** If at least one location has been marked ignore in this file, ignore all */
private boolean mIgnoreFile;
/** List of locations of MenuItem.SHOW_AS_ACTION_ALWAYS references in Java code */
private List<Location> mAlwaysFields;
/** True if references to MenuItem.SHOW_AS_ACTION_IF_ROOM were found */
private boolean mHasIfRoomRefs;
/** Constructs a new {@link AlwaysShowActionDetector} */
public AlwaysShowActionDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.MENU;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_SHOW_AS_ACTION);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
mFileAttributes = null;
}
@Override
public void afterCheckFile(@NonNull Context context) {
if (mIgnoreFile) {
mFileAttributes = null;
return;
}
if (mFileAttributes != null) {
assert context instanceof XmlContext; // mFileAttributes is only set in XML files
List<Attr> always = new ArrayList<Attr>();
List<Attr> ifRoom = new ArrayList<Attr>();
for (Attr attribute : mFileAttributes) {
String value = attribute.getValue();
if (value.equals(VALUE_ALWAYS)) {
always.add(attribute);
} else if (value.equals(VALUE_IF_ROOM)) {
ifRoom.add(attribute);
} else if (value.indexOf('|') != -1) {
String[] flags = value.split("\\|"); //$NON-NLS-1$
for (String flag : flags) {
if (flag.equals(VALUE_ALWAYS)) {
always.add(attribute);
break;
} else if (flag.equals(VALUE_IF_ROOM)) {
ifRoom.add(attribute);
break;
}
}
}
}
if (!always.isEmpty() && mFileAttributes.size() > 1) {
// Complain if you're using more than one "always", or if you're
// using "always" and aren't using "ifRoom" at all (and provided you
// have more than a single item)
if (always.size() > 2 || ifRoom.isEmpty()) {
XmlContext xmlContext = (XmlContext) context;
Location location = null;
for (int i = always.size() - 1; i >= 0; i--) {
Location next = location;
location = xmlContext.getLocation(always.get(i));
if (next != null) {
location.setSecondary(next);
}
}
if (location != null) {
context.report(ISSUE, location,
"Prefer \"`ifRoom`\" instead of \"`always`\"");
}
}
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mAlwaysFields != null && !mHasIfRoomRefs) {
for (Location location : mAlwaysFields) {
context.report(ISSUE, location,
"Prefer \"`SHOW_AS_ACTION_IF_ROOM`\" instead of \"`SHOW_AS_ACTION_ALWAYS`\"");
}
}
}
// ---- Implements XmlScanner ----
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (context.getDriver().isSuppressed(context, ISSUE, attribute)) {
mIgnoreFile = true;
return;
}
if (mFileAttributes == null) {
mFileAttributes = new ArrayList<Attr>();
}
mFileAttributes.add(attribute);
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableReferenceNames() {
return Arrays.asList("SHOW_AS_ACTION_IF_ROOM", "SHOW_AS_ACTION_ALWAYS");
}
@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<Location>();
}
mAlwaysFields.add(context.getUastLocation(reference));
} else {
mHasIfRoomRefs = true;
}
}
}
}
@@ -1,399 +0,0 @@
/*
* 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.
* <p> Uses a {@code <meta-data>} 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 <uses> element in `<automotiveApp>` 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" +
"`<intent-filter>`\n" +
" `<action android:name=\"android.media.browse.MediaBrowserService\" />`\n" +
"`</intent-filter>`\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" +
"`<intent-filter>`\n" +
" `<action android:name=\"android.media.action.MEDIA_PLAY_FROM_SEARCH\" />`\n" +
"`</intent-filter>`\n" +
"to your `<activity>` or `<service>`.",
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<String> 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<String> 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};
}
}
@@ -1,865 +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.client.api.ExternalReferenceExpression;
import com.android.tools.klint.client.api.IssueRegistry;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.*;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import org.jetbrains.uast.*;
import org.jetbrains.uast.java.JavaUAnnotation;
import org.jetbrains.uast.java.JavaUTypeCastExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.*;
import static com.android.SdkConstants.*;
import static com.android.tools.klint.checks.PermissionRequirement.getAnnotationBooleanValue;
import static com.android.tools.klint.checks.SupportAnnotationDetector.*;
import static com.android.tools.klint.client.api.JavaParser.*;
import static com.android.tools.klint.detector.api.LintUtils.getAutoBoxedType;
import static com.android.tools.klint.detector.api.ResourceEvaluator.*;
/**
* Checks annotations to make sure they are valid
*/
public class AnnotationDetector extends Detector implements Detector.UastScanner {
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 INSIDE_METHOD = Issue.create(
"LocalSuppress", //$NON-NLS-1$
"@SuppressLint on invalid element",
"The `@SuppressAnnotation` is used to suppress Lint warnings in Java files. However, " +
"while many lint checks analyzes the Java source code, where they can find " +
"annotations on (for example) local variables, some checks are analyzing the " +
"`.class` files. And in class files, annotations only appear on classes, fields " +
"and methods. Annotations placed on local variables disappear. If you attempt " +
"to suppress a lint error for a class-file based lint check, the suppress " +
"annotation not work. You must move the annotation out to the surrounding method.",
Category.CORRECTNESS,
3,
Severity.ERROR,
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<PsiElement> mWarnedFlags;
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
List<Class<? extends UElement>> types = new ArrayList<Class<? extends UElement>>(2);
types.add(UAnnotation.class);
types.add(USwitchExpression.class);
return types;
}
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull JavaContext context) {
return new AnnotationChecker(context);
}
private class AnnotationChecker extends AbstractUastVisitor {
private final JavaContext mContext;
private AnnotationChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitAnnotation(@NonNull UAnnotation annotation) {
String type = annotation.getQualifiedName();
if (type == null || type.startsWith("java.lang.")) {
return false;
}
if (FQCN_SUPPRESS_LINT.equals(type)) {
UElement parent = annotation.getUastParent();
if (parent == null) {
return false;
}
// Only flag local variables and parameters (not classes, fields and methods)
if (!(parent instanceof UDeclarationsExpression
|| parent instanceof ULocalVariable
|| parent instanceof UParameter)) {
return false;
}
List<UNamedExpression> attributes = annotation.getAttributeValues();
if (attributes.size() == 1) {
UNamedExpression attribute = attributes.get(0);
UExpression value = attribute.getExpression();
if (value instanceof ULiteralExpression) {
Object v = ((ULiteralExpression) 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 false;
}
}
}
}
}
}
} 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.getUastParent() instanceof UMethod) {
UMethod method = (UMethod) annotation.getUastParent();
if (!method.isConstructor()
&& PsiType.VOID.equals(method.getReturnType())) {
mContext.report(ANNOTATION_USAGE, annotation.getPsi(),
mContext.getLocation(annotation.getPsi()),
"@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);
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.reportUast(ANNOTATION_USAGE, annotation, mContext.getUastLocation(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.getPsi(), mContext.getLocation(annotation.getPsi()),
"Invalid size range: the `min` attribute must be less than "
+ "the `max` attribute");
} else if (multiple < 1) {
mContext.report(ANNOTATION_USAGE, annotation.getPsi(), mContext.getLocation(annotation.getPsi()),
"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.getPsi(), mContext.getLocation(annotation.getPsi()),
"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.getUastParent() instanceof UMethod) {
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.getPsi(),
mContext.getLocation(annotation.getPsi()),
"For methods, permission annotation should specify one "
+ "of `value`, `anyOf` or `allOf`");
} else if (set > 1) {
mContext.report(ANNOTATION_USAGE, annotation.getPsi(),
mContext.getLocation(annotation.getPsi()),
"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)
PsiElement resolved = annotation.resolve();
if (resolved != null) {
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);
}
}
}
}
}
return false;
}
private void checkTargetType(@NonNull UAnnotation node, @NonNull String type1,
@Nullable String type2, boolean allowCollection) {
UElement parent = node.getUastParent();
PsiType type;
if (parent instanceof UDeclarationsExpression) {
List<UDeclaration> elements = ((UDeclarationsExpression) parent).getDeclarations();
if (!elements.isEmpty()) {
UDeclaration element = elements.get(0);
if (element instanceof ULocalVariable) {
type = ((ULocalVariable) element).getType();
} else {
return;
}
} else {
return;
}
} else if (parent instanceof UMethod) {
UMethod method = (UMethod) parent;
type = method.isConstructor()
? mContext.getEvaluator().getClassType(method.getContainingClass())
: method.getReturnType();
} else if (parent instanceof UVariable) {
// Field or local variable or parameter
type = ((UVariable) 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<Integer>
PsiClassType classType = (PsiClassType)type;
if (classType.getParameters().length == 1) {
PsiClass resolved = classType.resolve();
if (resolved != null &&
InheritanceUtil.isInheritor(resolved, false, "java.util.Collection")) {
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.getUastLocation(node);
mContext.report(ANNOTATION_USAGE, node, location, message);
}
}
@Override
public boolean visitSwitchExpression(USwitchExpression switchExpression) {
UExpression condition = switchExpression.getExpression();
if (condition != null && PsiType.INT.equals(condition.getExpressionType())) {
UAnnotation annotation = findIntDefAnnotation(condition);
if (annotation != null) {
UExpression value =
annotation.findDeclaredAttributeValue(ATTR_VALUE);
if (value == null) {
value = annotation.findDeclaredAttributeValue(null);
}
if (UastExpressionUtils.isArrayInitializer(value)) {
List<UExpression> allowedValues =
((UCallExpression) value).getValueArguments();
switchExpression.accept(new SwitchChecker(switchExpression, allowedValues));
}
}
}
return false;
}
@Nullable
private Integer getConstantValue(@NonNull PsiField intDefConstantRef) {
Object constant = intDefConstantRef.computeConstantValue();
if (constant instanceof Number) {
return ((Number)constant).intValue();
}
return null;
}
/**
* Searches for the corresponding @IntDef annotation definition associated
* with a given node
*/
@Nullable
private UAnnotation findIntDefAnnotation(@NonNull UExpression expression) {
if (expression instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) expression).resolve();
if (resolved instanceof PsiModifierListOwner) {
PsiAnnotation[] annotations = mContext.getEvaluator().getAllAnnotations(
(PsiModifierListOwner)resolved);
PsiAnnotation[] relevantAnnotations =
filterRelevantAnnotations(mContext.getEvaluator(), annotations);
UAnnotation annotation = SupportAnnotationDetector.findIntDef(
JavaUAnnotation.wrap(relevantAnnotations));
if (annotation != null) {
return annotation;
}
}
if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
UExpression lastAssignment = UastLintUtils.findLastAssignment(variable,
expression, mContext);
if(lastAssignment != null) {
return findIntDefAnnotation(lastAssignment);
}
}
} else if (expression instanceof UCallExpression) {
PsiMethod method = ((UCallExpression) expression).resolve();
if (method != null) {
PsiAnnotation[] annotations =
mContext.getEvaluator().getAllAnnotations(method);
PsiAnnotation[] relevantAnnotations =
filterRelevantAnnotations(mContext.getEvaluator(), annotations);
List<UAnnotation> uAnnotations = JavaUAnnotation.wrap(relevantAnnotations);
UAnnotation annotation = SupportAnnotationDetector.findIntDef(uAnnotations);
if (annotation != null) {
return annotation;
}
}
} else if (expression instanceof UIfExpression) {
UIfExpression ifExpression = (UIfExpression) expression;
if (ifExpression.getThenExpression() != null) {
UAnnotation result = findIntDefAnnotation(ifExpression.getThenExpression());
if (result != null) {
return result;
}
}
if (ifExpression.getElseExpression() != null) {
UAnnotation result = findIntDefAnnotation(ifExpression.getElseExpression());
if (result != null) {
return result;
}
}
} else if (expression instanceof JavaUTypeCastExpression) {
return findIntDefAnnotation(((JavaUTypeCastExpression)expression).getOperand());
} else if (expression instanceof UParenthesizedExpression) {
return findIntDefAnnotation(((UParenthesizedExpression) expression).getExpression());
}
return null;
}
private void ensureUniqueValues(@NonNull UAnnotation node) {
UExpression value = node.findAttributeValue(ATTR_VALUE);
if (value == null) {
value = node.findAttributeValue(null);
}
if (!(UastExpressionUtils.isArrayInitializer(value))) {
return;
}
PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue) value;
PsiAnnotationMemberValue[] initializers = array.getInitializers();
Map<Number,Integer> 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);
UElement scope = getAnnotationScope(node);
mContext.reportUast(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 UAnnotation 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.JAVA_FILE)
|| issue == ApiDetector.UNSUPPORTED) {
// This issue doesn't have AST access: annotations are not
// available for local variables or parameters
UElement scope = getAnnotationScope(node);
mContext.report(INSIDE_METHOD, scope, mContext.getUastLocation(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));
return false;
}
return true;
}
private class SwitchChecker extends AbstractUastVisitor {
private final USwitchExpression mSwitchExpression;
private final List<UExpression> mAllowedValues;
private final List<Object> mFields;
private final List<Integer> mSeenValues;
private boolean mReported = false;
private SwitchChecker(USwitchExpression switchExpression,
List<UExpression> allowedValues) {
mSwitchExpression = switchExpression;
mAllowedValues = allowedValues;
mFields = Lists.newArrayListWithCapacity(allowedValues.size());
for (UExpression allowedValue : allowedValues) {
if (allowedValue instanceof ExternalReferenceExpression) {
ExternalReferenceExpression externalRef =
(ExternalReferenceExpression) allowedValue;
PsiElement resolved = UastLintUtils.resolve(externalRef, switchExpression);
if (resolved instanceof PsiField) {
mFields.add(resolved);
}
} else if (allowedValue instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) allowedValue).resolve();
if (resolved != null) {
mFields.add(resolved);
}
} else if (allowedValue instanceof ULiteralExpression) {
mFields.add(allowedValue);
}
}
mSeenValues = Lists.newArrayListWithCapacity(allowedValues.size());
}
@Override
public boolean visitSwitchClauseExpression(USwitchClauseExpression node) {
if (mReported) {
return true;
}
if (mAllowedValues == null) {
return true;
}
List<UExpression> caseValues = node.getCaseValues();
if (caseValues == null) {
return true;
}
for (UExpression caseValue : caseValues) {
if (caseValue instanceof ULiteralExpression) {
// Report warnings if you specify hardcoded constants.
// It's the wrong thing to do.
List<String> list = computeFieldNames(mSwitchExpression,
Arrays.asList(mAllowedValues));
// 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, caseValue,
mContext.getUastLocation(caseValue), message);
// Don't look for other missing typedef constants since you might
// have aliased with value
mReported = true;
} else if (caseValue instanceof UReferenceExpression) { // default case can have null expression
PsiElement resolved = ((UReferenceExpression) caseValue).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 true;
}
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<Object> iterator = mFields.listIterator();
while (iterator.hasNext()) {
Object field = iterator.next();
if (field.equals(resolved)) {
iterator.remove();
found = true;
break;
}
}
if (!found) {
// Look for local alias
UExpression initializer = mContext.getUastContext()
.getInitializerBody(((PsiField) resolved));
if (initializer instanceof UReferenceExpression) {
resolved = ((UReferenceExpression) initializer).resolve();
if (resolved instanceof PsiField) {
iterator = mFields.listIterator();
while (iterator.hasNext()) {
Object field = iterator.next();
if (field.equals(resolved)) {
iterator.remove();
found = true;
break;
}
}
}
}
}
if (found) {
Integer cv = getConstantValue((PsiField) resolved);
if (cv != null) {
mSeenValues.add(cv);
}
} else {
List<String> list = computeFieldNames(
mSwitchExpression, Collections.singletonList(mAllowedValues));
// Keep error message in sync with {@link #getMissingCases}
String message = "Unexpected constant; expected one of: " + Joiner
.on(", ").join(list);
Location location = mContext.getUastNameLocation(caseValue);
mContext.report(SWITCH_TYPE_DEF, caseValue, location, message);
}
}
}
}
return true;
}
@Override
public void afterVisitSwitchExpression(USwitchExpression node) {
reportMissingSwitchCases();
super.afterVisitSwitchExpression(node);
}
private void reportMissingSwitchCases() {
if (mReported) {
return;
}
if (mAllowedValues == null) {
return;
}
// Any missing switch constants? Before we flag them, look to see if any
// of them have the same values: those can be omitted
if (!mFields.isEmpty()) {
ListIterator<Object> iterator = mFields.listIterator();
while (iterator.hasNext()) {
Object next = iterator.next();
if (next instanceof PsiField) {
Integer cv = getConstantValue((PsiField)next);
if (mSeenValues.contains(cv)) {
iterator.remove();
}
}
}
}
if (!mFields.isEmpty()) {
List<String> list = computeFieldNames(mSwitchExpression, mFields);
// 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.getUastLocation(mSwitchExpression.getSwitchIdentifier());
mContext.report(SWITCH_TYPE_DEF, mSwitchExpression, location, message);
}
}
}
}
private static List<String> computeFieldNames(
@NonNull USwitchExpression node, Iterable<?> allowedValues) {
List<String> list = Lists.newArrayList();
for (Object o : allowedValues) {
if (o instanceof ExternalReferenceExpression) {
ExternalReferenceExpression externalRef = (ExternalReferenceExpression) o;
PsiElement resolved = UastLintUtils.resolve(externalRef, node);
if (resolved != null) {
o = resolved;
}
} else 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();
UClass clz = UastUtils.getParentOfType(node, UClass.class, true);
if (clz != null) {
PsiClass containingClass = field.getContainingClass();
if (containingClass != null && !containingClass.equals(clz.getPsi())) {
name = containingClass.getName() + '.' + field.getName();
}
}
list.add('`' + name + '`');
}
}
Collections.sort(list);
return list;
}
/**
* 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 UElement getAnnotationScope(@NonNull UAnnotation node) {
UElement scope = UastUtils.getParentOfType(node, UAnnotation.class, true);
if (scope == null) {
scope = node;
}
return scope;
}
}
@@ -1,109 +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 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;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
* Main entry point for API description.
*
* To create the {@link Api}, use {@link #parseApi(File)}
*
*/
public class Api {
/**
* Parses simplified API file.
* @param apiFile the file to read
* @return a new ApiInfo
*/
public static Api parseApi(File apiFile) {
InputStream inputStream = null;
try {
inputStream = new FileInputStream(apiFile);
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
SAXParser parser = parserFactory.newSAXParser();
ApiParser apiParser = new ApiParser();
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) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// ignore
}
}
}
return null;
}
private final Map<String, ApiClass> mClasses;
private final Map<String, ApiPackage> mPackages;
private Api(
@NonNull Map<String, ApiClass> classes,
@NonNull Map<String, ApiPackage> packages) {
mClasses = new HashMap<String, ApiClass>(classes);
mPackages = new HashMap<String, ApiPackage>(packages);
}
ApiClass getClass(String fqcn) {
return mClasses.get(fqcn);
}
Map<String, ApiClass> getClasses() {
return Collections.unmodifiableMap(mClasses);
}
Map<String, ApiPackage> getPackages() {
return Collections.unmodifiableMap(mPackages);
}
}
@@ -1,513 +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 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;
import com.intellij.openapi.util.text.StringUtil;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Represents a class and its methods/fields.
*
* {@link #getSince()} gives the API level it was introduced.
*
* {@link #getMethod} returns when the method was introduced.
* {@link #getField} returns when the field was introduced.
*/
public class ApiClass implements Comparable<ApiClass> {
private final String mName;
private final int mSince;
private final int mDeprecatedIn;
private final List<Pair<String, Integer>> mSuperClasses = Lists.newArrayList();
private final List<Pair<String, Integer>> mInterfaces = Lists.newArrayList();
private final Map<String, Integer> mFields = new HashMap<String, Integer>();
private final Map<String, Integer> mMethods = new HashMap<String, Integer>();
private final Map<String, Integer> mDeprecatedMembersIn = new HashMap<String, Integer>();
// Persistence data: Used when writing out binary data in ApiLookup
List<String> 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;
}
/**
* Returns the name of the class.
* @return the name of the class
*/
String getName() {
return mName;
}
/**
* Returns when the class was introduced.
* @return the api level the class was introduced.
*/
int getSince() {
return mSince;
}
/**
* 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
*/
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
// super class or interface was added as a super class or interface to this class.
// Look at all the values and take the lowest.
// For instance:
// This class A is introduced in 5 with super class B.
// In 10, the interface C was added.
// Looking for SOME_FIELD we get the following:
// Present in A in API 15
// Present in B in API 11
// Present in C in API 7.
// The answer is 10, which is when C became an interface
int min = Integer.MAX_VALUE;
Integer i = mFields.get(name);
if (i != null) {
min = i;
}
// now look at the super classes
for (Pair<String, Integer> superClassPair : mSuperClasses) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getField(name, info);
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
// now look at the interfaces
for (Pair<String, Integer> superClassPair : mInterfaces) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getField(name, info);
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
return min;
}
/**
* 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<String, Integer> 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<String, Integer> 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) {
// The method can come from this class or from a super class.
// The value can never be lower than this introduction of this class.
// When looking at super classes, it can never be lower than when the super class became
// a super class of this class.
// Look at all the values and take the lowest.
// For instance:
// This class A is introduced in 5 with super class B.
// In 10, the super class changes to C.
// Looking for foo() we get the following:
// Present in A in API 15
// Present in B in API 11
// Present in C in API 7.
// The answer is 10, which is when C became the super class.
int min = Integer.MAX_VALUE;
Integer i = mMethods.get(methodSignature);
if (i != null) {
min = i;
// Constructors aren't inherited
if (methodSignature.startsWith(CONSTRUCTOR_NAME)) {
return i;
}
}
// now look at the super classes
for (Pair<String, Integer> superClassPair : mSuperClasses) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getMethod(methodSignature, info);
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
// now look at the interfaces classes
for (Pair<String, Integer> interfacePair : mInterfaces) {
ApiClass superClass = info.getClass(interfacePair.getFirst());
if (superClass != null) {
i = superClass.getMethod(methodSignature, info);
int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
return min;
}
void addField(String name, int since, int deprecatedIn) {
Integer i = mFields.get(name);
assert i == null;
mFields.put(name, since);
if (deprecatedIn > 0) {
mDeprecatedMembersIn.put(name, deprecatedIn);
}
}
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;",
// and the subclass java/nio/ByteBuffer has the method "array()[B". We want
// the lookup on mMethods to associate the ByteBuffer array method to be
// considered overriding the Buffer method.
int index = name.indexOf(')');
if (index != -1) {
name = name.substring(0, index + 1);
}
Integer i = mMethods.get(name);
assert i == null || i == since : i;
mMethods.put(name, since);
if (deprecatedIn > 0) {
mDeprecatedMembersIn.put(name, deprecatedIn);
}
}
void addSuperClass(String superClass, int since) {
addToArray(mSuperClasses, superClass, since);
}
void addInterface(String interfaceClass, int since) {
addToArray(mInterfaces, interfaceClass, since);
}
static void addToArray(List<Pair<String, Integer>> list, String name, int value) {
// check if we already have that name (at a lower level)
for (Pair<String, Integer> pair : list) {
if (name.equals(pair.getFirst())) {
assert false;
return;
}
}
list.add(Pair.of(name, value));
}
@Nullable
public String getPackage() {
int index = mName.lastIndexOf('/');
if (index != -1) {
return mName.substring(0, index);
}
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;
}
/**
* Returns the set of all methods, including inherited
* ones.
*
* @param info the api to look up super classes from
* @return a set containing all the members fields
*/
Set<String> getAllMethods(Api info) {
Set<String> members = new HashSet<String>(100);
addAllMethods(info, members, true /*includeConstructors*/);
return members;
}
List<Pair<String, Integer>> getInterfaces() {
return mInterfaces;
}
List<Pair<String, Integer>> getSuperClasses() {
return mSuperClasses;
}
private void addAllMethods(Api info, Set<String> set, boolean includeConstructors) {
if (!includeConstructors) {
for (String method : mMethods.keySet()) {
if (!method.startsWith(CONSTRUCTOR_NAME)) {
set.add(method);
}
}
} else {
for (String method : mMethods.keySet()) {
set.add(method);
}
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
if (clz != null) {
clz.addAllMethods(info, set, false);
}
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
if (clz != null) {
clz.addAllMethods(info, set, false);
}
}
}
/**
* Returns the set of all fields, including inherited
* ones.
*
* @param info the api to look up super classes from
* @return a set containing all the fields
*/
Set<String> getAllFields(Api info) {
Set<String> members = new HashSet<String>(100);
addAllFields(info, members);
return members;
}
private void addAllFields(Api info, Set<String> set) {
for (String field : mFields.keySet()) {
set.add(field);
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
if (clz == null) {
throw new NullPointerException(
"Null is not expected in [" + StringUtil.join(mSuperClasses, ", ") + "], " + superClass.getSecond());
}
clz.addAllFields(info, set);
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
if (clz == null) {
throw new NullPointerException(
"Null is not expected in [" + StringUtil.join(mSuperClasses, ", ") + "], " + superClass.getSecond());
}
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
Field android/provider/ContactsContract$CommonDataKinds$Organization#PHONETIC_NAME has api=5 but parent android/provider/ContactsContract$ContactNameColumns provides it as 11
Field android/widget/ListView#CHOICE_MODE_MULTIPLE has api=1 but parent android/widget/AbsListView provides it as 11
Field android/widget/ListView#CHOICE_MODE_NONE has api=1 but parent android/widget/AbsListView provides it as 11
Field android/widget/ListView#CHOICE_MODE_SINGLE has api=1 but parent android/widget/AbsListView provides it as 11
Field android/view/KeyEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9
This is used for example in the ApiDetector to filter out warnings which result
when people follow Eclipse's advice to replace
ListView.CHOICE_MODE_MULTIPLE
references with
AbsListView.CHOICE_MODE_MULTIPLE
since the latter has API=11 and the former has API=1; since the constant is unchanged
between the two, and the literal is copied into the class, using the AbsListView
reference works.
public void checkFields(Api info) {
fieldLoop:
for (String field : mFields.keySet()) {
Integer since = getField(field, info);
if (since == null || since == Integer.MAX_VALUE) {
continue;
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
Integer superSince = clz.getField(field, info);
if (superSince == Integer.MAX_VALUE) {
continue;
}
if (superSince != null && superSince > since) {
String declaredIn = clz.findFieldDeclaration(info, field);
System.out.println("Field " + getName() + "#" + field + " has api="
+ since + " but parent " + declaredIn + " provides it as "
+ superSince);
continue fieldLoop;
}
}
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
Integer superSince = clz.getField(field, info);
if (superSince == Integer.MAX_VALUE) {
continue;
}
if (superSince != null && superSince > since) {
String declaredIn = clz.findFieldDeclaration(info, field);
System.out.println("Field " + getName() + "#" + field + " has api="
+ since + " but parent " + declaredIn + " provides it as "
+ superSince);
continue fieldLoop;
}
}
}
}
}
private String findFieldDeclaration(Api info, String name) {
if (mFields.containsKey(name)) {
return getName();
}
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
String declaredIn = clz.findFieldDeclaration(info, name);
if (declaredIn != null) {
return declaredIn;
}
}
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
String declaredIn = clz.findFieldDeclaration(info, name);
if (declaredIn != null) {
return declaredIn;
}
}
}
return null;
}
*/
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,69 +0,0 @@
/*
* 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<ApiPackage> {
private final String mName;
private final List<ApiClass> 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<ApiClass> 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;
}
}
@@ -1,154 +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 org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.HashMap;
import java.util.Map;
/**
* Parser for the simplified XML API format version 1.
*/
public class ApiParser extends DefaultHandler {
private static final String NODE_API = "api";
private static final String NODE_CLASS = "class";
private static final String NODE_FIELD = "field";
private static final String NODE_METHOD = "method";
private static final String NODE_EXTENDS = "extends";
private static final String NODE_IMPLEMENTS = "implements";
private static final String ATTR_NAME = "name";
private static final String ATTR_SINCE = "since";
private static final String ATTR_DEPRECATED = "deprecated";
private final Map<String, ApiClass> mClasses = new HashMap<String, ApiClass>(1000);
private final Map<String, ApiPackage> mPackages = new HashMap<String, ApiPackage>();
private ApiClass mCurrentClass;
ApiParser() {
}
Map<String, ApiClass> getClasses() {
return mClasses;
}
Map<String, ApiPackage> getPackages() { return mPackages; }
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
if (localName == null || localName.isEmpty()) {
localName = qName;
}
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));
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);
int since = getSince(attributes);
mCurrentClass.addSuperClass(name, since);
} else if (NODE_IMPLEMENTS.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
mCurrentClass.addInterface(name, since);
} else if (NODE_METHOD.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
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, deprecatedIn);
}
} finally {
super.startElement(uri, localName, qName, attributes);
}
}
private ApiClass addClass(String name, int apiLevel, int deprecatedIn) {
// There should not be any duplicates
ApiClass theClass = mClasses.get(name);
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;
}
private int getSince(Attributes attributes) {
int since = mCurrentClass.getSince();
String sinceAttr = attributes.getValue(ATTR_SINCE);
if (sinceAttr != null) {
since = Integer.parseInt(sinceAttr);
}
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;
}
}
@@ -1,174 +0,0 @@
/*
* 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.SdkConstants.APPCOMPAT_LIB_ARTIFACT;
import static com.android.SdkConstants.CLASS_ACTIVITY;
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.TextFormat;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.util.InheritanceUtil;
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;
public class AppCompatCallDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"AppCompatMethod",
"Using Wrong AppCompat Method",
"When using the appcompat library, there are some methods you should be calling " +
"instead of the normal ones; for example, `getSupportActionBar()` instead of " +
"`getActionBar()`. This lint check looks for calls to the wrong method.",
Category.CORRECTNESS, 6, Severity.WARNING,
new Implementation(
AppCompatCallDetector.class,
Scope.JAVA_FILE_SCOPE)).
addMoreInfo("http://developer.android.com/tools/support-library/index.html");
private static final String GET_ACTION_BAR = "getActionBar";
private static final String START_ACTION_MODE = "startActionMode";
private static final String SET_PROGRESS_BAR_VIS = "setProgressBarVisibility";
private static final String SET_PROGRESS_BAR_IN_VIS = "setProgressBarIndeterminateVisibility";
private static final String SET_PROGRESS_BAR_INDETERMINATE = "setProgressBarIndeterminate";
private static final String REQUEST_WINDOW_FEATURE = "requestWindowFeature";
/** If you change number of parameters or order, update {@link #getMessagePart(String, int,TextFormat)} */
private static final String ERROR_MESSAGE_FORMAT = "Should use `%1$s` instead of `%2$s` name";
private boolean mDependsOnAppCompat;
public AppCompatCallDetector() {
}
@Override
public void beforeCheckProject(@NonNull Context context) {
Boolean dependsOnAppCompat = context.getProject().dependsOn(APPCOMPAT_LIB_ARTIFACT);
mDependsOnAppCompat = dependsOnAppCompat != null && dependsOnAppCompat;
}
@Nullable
@Override
public List<String> 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);
}
@Override
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 (GET_ACTION_BAR.equals(name)) {
replace = "getSupportActionBar";
} else if (START_ACTION_MODE.equals(name)) {
replace = "startSupportActionMode";
} else if (SET_PROGRESS_BAR_VIS.equals(name)) {
replace = "setSupportProgressBarVisibility";
} else if (SET_PROGRESS_BAR_IN_VIS.equals(name)) {
replace = "setSupportProgressBarIndeterminateVisibility";
} else if (SET_PROGRESS_BAR_INDETERMINATE.equals(name)) {
replace = "setSupportProgressBarIndeterminate";
} else if (REQUEST_WINDOW_FEATURE.equals(name)) {
replace = "supportRequestWindowFeature";
}
if (replace != null) {
String message = String.format(ERROR_MESSAGE_FORMAT, replace, name);
context.report(ISSUE, node, context.getUastLocation(node), message);
}
}
}
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 && InheritanceUtil.isInheritor(
cls, false, "android.support.v7.app.ActionBarActivity");
}
return false;
}
/**
* Given an error message created by this lint check, return the corresponding old method name
* that it suggests should be deleted. (Intended to support quickfix implementations
* for this lint check.)
*
* @param errorMessage the error message originally produced by this detector
* @param format the format of the error message
* @return the corresponding old method name, or null if not recognized
*/
@Nullable
public static String getOldCall(@NonNull String errorMessage, @NonNull TextFormat format) {
return getMessagePart(errorMessage, 2, format);
}
/**
* Given an error message created by this lint check, return the corresponding new method name
* that it suggests replace the old method name. (Intended to support quickfix implementations
* for this lint check.)
*
* @param errorMessage the error message originally produced by this detector
* @param format the format of the error message
* @return the corresponding new method name, or null if not recognized
*/
@Nullable
public static String getNewCall(@NonNull String errorMessage, @NonNull TextFormat format) {
return getMessagePart(errorMessage, 1, format);
}
@Nullable
private static String getMessagePart(@NonNull String errorMessage, int group,
@NonNull TextFormat format) {
List<String> parameters = LintUtils.getFormattedParameters(
RAW.convertTo(ERROR_MESSAGE_FORMAT, format),
errorMessage);
if (parameters.size() == 2 && group <= 2) {
return parameters.get(group - 1);
}
return null;
}
}
@@ -1,744 +0,0 @@
/*
* 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 com.intellij.psi.util.InheritanceUtil;
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<String> getApplicableElements() {
return Collections.singletonList(NODE_APPLICATION);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element application) {
List<Element> activities = extractChildrenByName(application, NODE_ACTIVITY);
boolean applicationHasActionView = false;
for (Element activity : activities) {
List<Element> 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<String> 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 (!InheritanceUtil.isInheritor(declaration, true, CLASS_ACTIVITY)) {
return;
}
declaration.accept(new MethodVisitor(context, declaration));
}
static class MethodVisitor extends AbstractUastVisitor {
private final JavaContext mContext;
private final UClass mCls;
private final List<UCallExpression> mStartMethods;
private final List<UCallExpression> mEndMethods;
private final List<UCallExpression> mConnectMethods;
private final List<UCallExpression> 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<UExpression> 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<String> 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<UExpression> 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<UExpression> 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<String> getActivitiesToCheck(Context context) {
Set<String> activitiesToCheck = Sets.newHashSet();
List<File> 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<Element> children = LintUtils.getChildren(doc);
for (Element child : children) {
if (child.getNodeName().equals(NODE_MANIFEST)) {
List<Element> apps = extractChildrenByName(child, NODE_APPLICATION);
for (Element app : apps) {
List<Element> acts = extractChildrenByName(app, NODE_ACTIVITY);
for (Element act : acts) {
List<Element> intents = extractChildrenByName(act, NODE_INTENT);
for (Element intent : intents) {
List<Element> 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<Element> 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
// <scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
// 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<Element> 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<Element> 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<ResourceItem> 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<UCallExpression> list) {
for (UCallExpression call : list) {
List<UExpression> 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<UCallExpression> list) {
for (UCallExpression method : list) {
UElement operand2 = method.getReceiver();
if (operand2 != null && operand.asSourceString().equals(operand2.asSourceString())) {
return true;
}
}
return false;
}
private static List<Element> extractChildrenByName(@NonNull Element node,
@NonNull String name) {
List<Element> result = Lists.newArrayList();
List<Element> children = LintUtils.getChildren(node);
for (Element child : children) {
if (child.getNodeName().equals(name)) {
result.add(child);
}
}
return result;
}
}
@@ -1,141 +0,0 @@
/*
* 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<String> 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;
}
}
}
@@ -1,139 +0,0 @@
/*
* 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.*;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import org.jetbrains.uast.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;
import static com.android.SdkConstants.*;
/**
* 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<String> 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<String> 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";
}
}
@@ -1,208 +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 com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.client.api.IssueRegistry;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
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<Issue> sIssues;
static final int INITIAL_CAPACITY = 262;
static {
List<Issue> issues = new ArrayList<Issue>(INITIAL_CAPACITY);
issues.add(AddJavascriptInterfaceDetector.ISSUE);
issues.add(AlarmDetector.ISSUE);
issues.add(AllowAllHostnameVerifierDetector.ISSUE);
issues.add(AlwaysShowActionDetector.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);
issues.add(IconDetector.DUPLICATES_CONFIGURATIONS);
issues.add(IconDetector.DUPLICATES_NAMES);
issues.add(IconDetector.GIF_USAGE);
issues.add(IconDetector.ICON_COLORS);
issues.add(IconDetector.ICON_DENSITIES);
issues.add(IconDetector.ICON_DIP_SIZE);
issues.add(IconDetector.ICON_EXPECTED_SIZE);
issues.add(IconDetector.ICON_EXTENSION);
issues.add(IconDetector.ICON_LAUNCHER_SHAPE);
issues.add(IconDetector.ICON_LOCATION);
issues.add(IconDetector.ICON_MISSING_FOLDER);
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);
issues.add(OverrideConcreteDetector.ISSUE);
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(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);
issues.add(StringFormatDetector.POTENTIAL_PLURAL);
issues.add(SupportAnnotationDetector.CHECK_PERMISSION);
issues.add(SupportAnnotationDetector.CHECK_RESULT);
issues.add(SupportAnnotationDetector.COLOR_USAGE);
issues.add(SupportAnnotationDetector.MISSING_PERMISSION);
issues.add(SupportAnnotationDetector.RANGE);
issues.add(SupportAnnotationDetector.RESOURCE_TYPE);
issues.add(SupportAnnotationDetector.THREAD);
issues.add(SupportAnnotationDetector.TYPE_DEF);
issues.add(ToastDetector.ISSUE);
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);
sIssues = Collections.unmodifiableList(issues);
}
/**
* Constructs a new {@link BuiltinIssueRegistry}
*/
public BuiltinIssueRegistry() {
}
@NonNull
@Override
public List<Issue> getIssues() {
return sIssues;
}
@Override
protected int getIssueCapacity(@NonNull EnumSet<Scope> scope) {
if (scope.equals(Scope.ALL)) {
return getIssues().size();
} else {
int initialSize = 12;
if (scope.contains(Scope.RESOURCE_FILE)) {
initialSize += 80;
} else if (scope.contains(Scope.ALL_RESOURCE_FILES)) {
initialSize += 12;
}
if (scope.contains(Scope.JAVA_FILE)) {
initialSize += 74;
} else if (scope.contains(Scope.CLASS_FILE)) {
initialSize += 15;
} else if (scope.contains(Scope.MANIFEST)) {
initialSize += 38;
} else if (scope.contains(Scope.GRADLE_FILE)) {
initialSize += 5;
}
return initialSize;
}
}
/**
* Reset the registry such that it recomputes its available issues.
* <p>
* NOTE: This is only intended for testing purposes.
*/
@VisibleForTesting
public static void reset() {
IssueRegistry.reset();
}
}
@@ -1,191 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.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.*;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UReferenceExpression;
import org.jetbrains.uast.USuperExpression;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
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;
/**
* Makes sure that methods call super when overriding methods.
*/
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.JAVA_FILE_SCOPE);
/** Missing call to super */
public static final Issue ISSUE = Issue.create(
"MissingSuperCall", //$NON-NLS-1$
"Missing Super Call",
"Some methods, such as `View#onDetachedFromWindow`, require that you also " +
"call the super implementation as part of your method.",
Category.CORRECTNESS,
9,
Severity.ERROR,
IMPLEMENTATION);
/** Constructs a new {@link CallSuperDetector} check */
public CallSuperDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UMethod.class);
}
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull final JavaContext context) {
return new AbstractUastVisitor() {
@Override
public boolean visitMethod(UMethod method) {
checkCallSuper(context, method);
return super.visitMethod(method);
}
};
}
private static void checkCallSuper(@NonNull JavaContext context,
@NonNull UMethod method) {
PsiMethod superMethod = getRequiredSuperMethod(context, method);
if (superMethod != null) {
if (!SuperCallVisitor.callsSuper(method, superMethod)) {
String methodName = method.getName();
String message = "Overriding method should call `super."
+ methodName + "`";
Location location = context.getUastNameLocation(method);
context.reportUast(ISSUE, method, location, message);
}
}
}
/**
* Checks whether the given method overrides a method which requires the super method
* to be invoked, and if so, returns it (otherwise returns null)
*/
@Nullable
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)) {
// No longer annotated on the framework method since it's
// now handled via onDetachedFromWindowInternal, but overriding
// is still dangerous if supporting older versions so flag
// this for now (should make annotation carry metadata like
// compileSdkVersion >= N).
if (!evaluator.isMemberInSubClassOf(method, CLASS_VIEW, false)) {
return null;
}
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 (!evaluator.isMemberInSubClassOf(method,
"android.support.wearable.watchface.WatchFaceService.Engine", false)) {
return null;
}
return directSuper;
}
// Look up annotations metadata
PsiMethod superMethod = directSuper;
while (superMethod != null) {
PsiAnnotation[] annotations = superMethod.getModifierList().getAnnotations();
annotations = filterRelevantAnnotations(context.getEvaluator(), 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;
}
}
superMethod = evaluator.getSuperMethod(superMethod);
}
return null;
}
/** Visits a method and determines whether the method calls its super method */
private static class SuperCallVisitor extends AbstractUastVisitor {
private final PsiMethod mMethod;
private boolean mCallsSuper;
public static boolean callsSuper(@NonNull UMethod method,
@NonNull PsiMethod superMethod) {
SuperCallVisitor visitor = new SuperCallVisitor(superMethod);
method.accept(visitor);
return visitor.mCallsSuper;
}
private SuperCallVisitor(@NonNull PsiMethod method) {
mMethod = method;
}
@Override
public boolean visitSuperExpression(USuperExpression node) {
UElement parent = skipParentheses(node.getUastParent());
if (parent instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) parent).resolve();
if (mMethod.equals(resolved)) {
mCallsSuper = true;
}
}
return super.visitSuperExpression(node);
}
}
}
@@ -1,105 +0,0 @@
/*
* 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.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.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;
/**
* Ensures that Cipher.getInstance is not called with AES as the parameter.
*/
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.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
CipherGetInstanceDetector.class,
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$
private static final Set<String> ALGORITHM_ONLY =
Sets.newHashSet("AES", "DES", "DESede"); //$NON-NLS-1$
private static final String ECB = "ECB"; //$NON-NLS-1$
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(GET_INSTANCE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
if (!context.getEvaluator().isMemberInSubClassOf(method, CIPHER, false)) {
return;
}
List<UExpression> 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 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.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.getUastLocation(node), message);
}
}
}
@@ -1,996 +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;
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.*;
import com.google.common.collect.Lists;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.util.containers.Predicate;
import org.jetbrains.uast.*;
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 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 org.jetbrains.uast.UastUtils.*;
/**
* 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 Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
CleanupDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Problems with missing recycle calls */
public static final Issue RECYCLE_RESOURCE = Issue.create(
"Recycle", //$NON-NLS-1$
"Missing `recycle()` calls",
"Many resources, such as TypedArrays, VelocityTrackers, etc., " +
"should be recycled (with a `recycle()` call) after use. This lint check looks " +
"for missing `recycle()` calls.",
Category.PERFORMANCE,
7,
Severity.WARNING,
IMPLEMENTATION);
/** Problems with missing commit calls. */
public static final Issue COMMIT_FRAGMENT = Issue.create(
"CommitTransaction", //$NON-NLS-1$
"Missing `commit()` calls",
"After creating a `FragmentTransaction`, you typically need to commit it as well",
Category.CORRECTNESS,
7,
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$
private static final String OBTAIN = "obtain"; //$NON-NLS-1$
private static final String SHOW = "show"; //$NON-NLS-1$
private static final String ACQUIRE_CPC = "acquireContentProviderClient"; //$NON-NLS-1$
private static final String OBTAIN_NO_HISTORY = "obtainNoHistory"; //$NON-NLS-1$
private static final String OBTAIN_ATTRIBUTES = "obtainAttributes"; //$NON-NLS-1$
private static final String OBTAIN_TYPED_ARRAY = "obtainTypedArray"; //$NON-NLS-1$
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 USE = "use"; //$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 PARCEL_CLS = "android.os.Parcel"; //$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 =
"android.support.v4.app.DialogFragment"; //$NON-NLS-1$
private static final String FRAGMENT_MANAGER_CLS = "android.app.FragmentManager"; //$NON-NLS-1$
private static final String FRAGMENT_MANAGER_V4_CLS =
"android.support.v4.app.FragmentManager"; //$NON-NLS-1$
private static final String FRAGMENT_TRANSACTION_CLS =
"android.app.FragmentTransaction"; //$NON-NLS-1$
private static final String FRAGMENT_TRANSACTION_V4_CLS =
"android.support.v4.app.FragmentTransaction"; //$NON-NLS-1$
public static final String SURFACE_CLS = "android.view.Surface";
public static final String SURFACE_TEXTURE_CLS = "android.graphics.SurfaceTexture";
public static final String CONTENT_PROVIDER_CLIENT_CLS
= "android.content.ContentProviderClient";
public static final String CONTENT_RESOLVER_CLS = "android.content.ContentResolver";
@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$
private static final String CLOSABLE = "java.io.Closeable"; //$NON-NLS-1$
/** Constructs a new {@link CleanupDetector} */
public CleanupDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
// FragmentManager commit check
BEGIN_TRANSACTION,
// Recycle check
OBTAIN, OBTAIN_NO_HISTORY,
OBTAIN_STYLED_ATTRIBUTES,
OBTAIN_ATTRIBUTES,
OBTAIN_TYPED_ARRAY,
// Release check
ACQUIRE_CPC,
// Cursor close check
QUERY, RAW_QUERY, QUERY_WITH_FACTORY, RAW_QUERY_WITH_FACTORY,
// SharedPreferences check
EDIT
);
}
@Nullable
@Override
public List<String> getApplicableConstructorTypes() {
return Arrays.asList(SURFACE_TEXTURE_CLS, SURFACE_CLS);
}
@Override
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, call, method);
}
}
@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod constructor) {
PsiClass containingClass = constructor.getContainingClass();
if (containingClass != null) {
String type = containingClass.getQualifiedName();
if (type != null) {
checkRecycled(context, node, type, RELEASE);
}
}
}
private static void checkResourceRecycled(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod method) {
String name = method.getName();
// Recycle detector
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return;
}
JavaEvaluator evaluator = context.getEvaluator();
if ((OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) &&
InheritanceUtil.isInheritor(containingClass, false, MOTION_EVENT_CLS)) {
checkRecycled(context, node, MOTION_EVENT_CLS, RECYCLE);
} else if (OBTAIN.equals(name) && InheritanceUtil.isInheritor(containingClass, false, PARCEL_CLS)) {
checkRecycled(context, node, PARCEL_CLS, RECYCLE);
} else if (OBTAIN.equals(name) &&
InheritanceUtil.isInheritor(containingClass, false, VELOCITY_TRACKER_CLS)) {
checkRecycled(context, node, VELOCITY_TRACKER_CLS, RECYCLE);
} else if ((OBTAIN_STYLED_ATTRIBUTES.equals(name)
|| OBTAIN_ATTRIBUTES.equals(name)
|| OBTAIN_TYPED_ARRAY.equals(name)) &&
(InheritanceUtil.isInheritor(containingClass, false, CLASS_CONTEXT) ||
InheritanceUtil.isInheritor(containingClass, false, SdkConstants.CLASS_RESOURCES))) {
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) && InheritanceUtil.isInheritor(containingClass,
false, CONTENT_RESOLVER_CLS)) {
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))
&& (InheritanceUtil.isInheritor(containingClass, false, SQLITE_DATABASE_CLS) ||
InheritanceUtil.isInheritor(containingClass, false, CONTENT_RESOLVER_CLS) ||
InheritanceUtil.isInheritor(containingClass, false, CLASS_CONTENTPROVIDER) ||
InheritanceUtil.isInheritor(containingClass, false, CONTENT_PROVIDER_CLIENT_CLS))) {
// Other potential cursors-returning methods that should be tracked:
// android.app.DownloadManager#query
// android.content.ContentProviderClient#query
// android.content.ContentResolver#query
// android.database.sqlite.SQLiteQueryBuilder#query
// android.provider.Browser#getAllBookmarks
// android.provider.Browser#getAllVisitedUrls
// android.provider.DocumentsProvider#queryChildDocuments
// android.provider.DocumentsProvider#qqueryDocument
// android.provider.DocumentsProvider#queryRecentDocuments
// android.provider.DocumentsProvider#queryRoots
// android.provider.DocumentsProvider#querySearchDocuments
// android.provider.MediaStore$Images$Media#query
// android.widget.FilterQueryProvider#runQuery
checkClosedOrUsed(context, node, CURSOR_CLS);
}
}
private static void reportRecycleResource(JavaContext context, String recycleType, String recycleName, @NonNull UCallExpression node) {
String className = recycleType.substring(recycleType.lastIndexOf('.') + 1);
String message;
if (RECYCLE.equals(recycleName)) {
message = String.format(
"This `%1$s` should be recycled after use with `#recycle()`", className);
} else {
message = String.format(
"This `%1$s` should be freed up after use with `#%2$s()`", className,
recycleName);
}
UElement locationNode = node.getMethodIdentifier();
if (locationNode == null) {
locationNode = node;
}
Location location = context.getUastLocation(locationNode);
context.report(RECYCLE_RESOURCE, node, location, message);
}
private static void checkClosedOrUsed(@NonNull final JavaContext context, @NonNull UCallExpression node,
@NonNull final String recycleType) {
if (isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isCloseMethodCall(call) || isUseMethodCall(call);
}
})) {
return;
}
PsiVariable boundVariable = getVariableElement(node);
if (boundVariable == null) {
reportRecycleResource(context, recycleType, CLOSE, node);
return;
}
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 (isUseMethodCall(call) || isCloseMethodCall(call)) {
UExpression receiver = call.getReceiver();
if (receiver instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) receiver).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
return false;
}
};
method.accept(visitor);
if (visitor.isCleanedUp() || visitor.variableEscapes()) {
return;
}
reportRecycleResource(context, recycleType, CLOSE, node);
}
private static boolean isCloseMethodCall(UCallExpression call) {
return isValidCleanupMethodCall(call, CLOSE, CLOSABLE);
}
private static boolean isUseMethodCall(UCallExpression call) {
return USE.equals(call.getMethodName());
}
private static boolean isValidCleanupMethodCall(@NonNull UCallExpression call, @NonNull String methodName, @NonNull String className) {
if (!methodName.equals(call.getMethodName())) {
return false;
}
PsiMethod method = call.resolve();
if (method == null) {
return false;
}
return InheritanceUtil.isInheritor(method.getContainingClass(), false, className);
}
private static boolean isCleanedUpInChain(UExpression expression, Predicate<UCallExpression> isCleanupCallPredicate) {
List<UExpression> chain = getQualifiedChain(getOutermostQualified(expression));
boolean skip = true;
for (UExpression e : chain) {
if (e == expression) {
skip = false;
continue;
}
if (skip) {
continue;
}
if (e instanceof UCallExpression) {
UCallExpression call = (UCallExpression) e;
if (isCleanupCallPredicate.apply(call)) {
return true;
}
//If function taking lambda
for (UExpression argument : call.getValueArguments()) {
if (argument instanceof ULambdaExpression) {
if (isCleanedUpInLambda((ULambdaExpression) argument, isCleanupCallPredicate)) {
return true;
}
}
}
}
}
return false;
}
private static boolean isCleanedUpInLambda(ULambdaExpression lambda, Predicate<UCallExpression> isCleanupCallPredicate) {
UExpression body = lambda.getBody();
if (body instanceof UBlockExpression) {
List<UExpression> expressions = ((UBlockExpression) body).getExpressions();
for (UExpression expression : expressions) {
//Might be a single cleanup call
if (expression instanceof UCallExpression) {
if (isCleanupCallPredicate.apply((UCallExpression) expression)) {
return true;
}
} else {
//Might be a chain containing the cleanup call
List<UExpression> chain = getQualifiedChain(getOutermostQualified(expression));
if (!chain.isEmpty()) {
for (UExpression e : chain) {
if (e instanceof UCallExpression) {
UCallExpression call = (UCallExpression) e;
if (isCleanupCallPredicate.apply(call)) {
return true;
}
}
}
}
}
}
}
return false;
}
private static void checkRecycled(@NonNull final JavaContext context, @NonNull UCallExpression node,
@NonNull final String recycleType, @NonNull final String recycleName) {
if (isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isValidCleanupMethodCall(call, recycleName, recycleType);
}
})) {
return;
}
PsiVariable boundVariable = getVariableElement(node);
if (boundVariable == null) {
reportRecycleResource(context, recycleType, recycleName, node);
return;
}
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 (isValidCleanupMethodCall(call, recycleName, recycleType)) {
// Yes, called the right recycle() method; now make sure
// we're calling it on the right variable
UExpression operand = call.getReceiver();
if (operand instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) operand).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
return false;
}
};
method.accept(visitor);
if (visitor.isCleanedUp() || visitor.variableEscapes()) {
return;
}
reportRecycleResource(context, recycleType, recycleName, node);
}
private static void checkTransactionCommits(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod calledMethod) {
if (isBeginTransaction(context, calledMethod)) {
if (isCommittedInChainedCalls(context, node)) {
return;
}
PsiVariable boundVariable = getVariableElement(node, true);
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 (isTransactionCommitMethodCall(mContext, call)) {
List<UExpression> 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 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;
}
}
}
}
} else if (isShowFragmentMethodCall(mContext, call)) {
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() == 2) {
UExpression first = arguments.get(0);
PsiElement resolved = UastUtils.tryResolve(first);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
return false;
}
};
method.accept(commitVisitor);
if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) {
return;
}
}
String message = "This transaction should be completed with a `commit()` call";
context.report(COMMIT_FRAGMENT, node, context.getUastNameLocation(node), message);
}
}
private static boolean isCommittedInChainedCalls(@NonNull final 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();
return isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isTransactionCommitMethodCall(context, call) || isShowFragmentMethodCall(context, call);
}
});
}
private static boolean isTransactionCommitMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
return (COMMIT.equals(methodName) || COMMIT_ALLOWING_LOSS.equals(methodName)) &&
isMethodOnFragmentClass(context, call,
FRAGMENT_TRANSACTION_CLS,
FRAGMENT_TRANSACTION_V4_CLS,
true);
}
private static boolean isShowFragmentMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
return SHOW.equals(methodName)
&& isMethodOnFragmentClass(context, call,
DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT, true);
}
private static boolean isMethodOnFragmentClass(
@NonNull JavaContext context,
@NonNull UCallExpression call,
@NonNull String fragmentClass,
@NonNull String v4FragmentClass,
boolean returnForUnresolved) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
return InheritanceUtil.isInheritor(containingClass, false, fragmentClass) ||
InheritanceUtil.isInheritor(containingClass, false, v4FragmentClass);
} 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)) {
if (isEditorCommittedInChainedCalls(context, node)) {
return;
}
PsiVariable boundVariable = getVariableElement(node, true);
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)) {
List<UExpression> 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 PsiMethod
&& operand instanceof UCallExpression
&& isEditorCommittedInChainedCalls(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;
} else if (isEditorCommittedInParentLambda(context, node)) {
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 InheritanceUtil.isInheritor(
containingClass, false, ANDROID_CONTENT_SHARED_PREFERENCES);
}
return false;
}
private static boolean isEditorCommittedInChainedCalls(@NonNull final JavaContext context,
@NonNull UCallExpression node) {
return isCleanedUpInChain(node, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isEditorCommitMethodCall(context, call) || isEditorApplyMethodCall(context, call);
}
});
}
private static boolean isEditorCommittedInParentLambda(@NonNull JavaContext context, @NonNull UCallExpression node) {
UCallExpression call = UastUtils.getParentOfType(node, UCallExpression.class, true);
if (call != null) {
//If function taking lambda
for (UExpression argument : call.getValueArguments()) {
if (argument instanceof ULambdaExpression) {
if (isCleanedUpInLambda((ULambdaExpression) argument, new Predicate<UCallExpression>() {
@Override
public boolean apply(@org.jetbrains.annotations.Nullable UCallExpression call) {
return isEditorCommitMethodCall(context, call) || isEditorApplyMethodCall(context, call);
}
})) {
return true;
}
}
}
}
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 (InheritanceUtil.isInheritor(containingClass, false,
ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR)) {
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 InheritanceUtil.isInheritor(containingClass, false,
ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR);
}
}
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.getUastParent());
while (parent instanceof UReferenceExpression) {
qualifiedNode = parent;
parent = skipParentheses(parent.getUastParent());
}
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).getUastParent());
// 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.getUastParent());
if ((parentParent instanceof UQualifiedReferenceExpression)) {
parent = skipParentheses(parentParent.getUastParent());
} 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 JavaContext context, @NonNull PsiMethod method) {
String methodName = method.getName();
if (BEGIN_TRANSACTION.equals(methodName)) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
if (InheritanceUtil.isInheritor(containingClass, false, FRAGMENT_MANAGER_CLS)
|| InheritanceUtil.isInheritor(containingClass, false, FRAGMENT_MANAGER_V4_CLS)) {
return true;
}
}
return false;
}
/**
* Visitor which checks whether an operation is "finished"; in the case
* of a FragmentTransaction we're looking for a "commit" call; in the
* case of a TypedArray we're looking for a "recycle", call, in the
* case of a database cursor we're looking for a "close" call, etc.
*/
private abstract static class FinishVisitor extends AbstractUastVisitor {
protected final JavaContext mContext;
protected final List<PsiVariable> mVariables;
private final PsiVariable mOriginalVariableNode;
private boolean mContainsCleanup;
private boolean mEscapes;
public FinishVisitor(JavaContext context, @NonNull PsiVariable variableNode) {
mContext = context;
mOriginalVariableNode = variableNode;
mVariables = Lists.newArrayList(variableNode);
}
public boolean isCleanedUp() {
return mContainsCleanup;
}
public boolean variableEscapes() {
return mEscapes;
}
@Override
public boolean visitElement(UElement node) {
return mContainsCleanup || super.visitElement(node);
}
protected abstract boolean isCleanupCall(@NonNull UCallExpression call);
@Override
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 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.getMethodName())) {
PsiMethod method = call.resolve();
if (JavaEvaluator.isMemberInClass(method, MOTION_EVENT_CLS)) {
mEscapes = wasEscaped;
}
}
}
}
}
}
if (isCleanupCall(call)) {
mContainsCleanup = true;
}
}
@Override
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)) {
clearLhs = false;
PsiElement lhs = UastUtils.tryResolve(expression.getLeftOperand());
if (lhs instanceof PsiLocalVariable) {
mVariables.add(((PsiLocalVariable) lhs));
} else if (lhs instanceof PsiField) {
mEscapes = true;
}
}
}
//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
mVariables.remove(lhs);
}
}
return super.visitBinaryExpression(expression);
}
@Override
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 super.visitReturnExpression(node);
}
}
}
@@ -1,199 +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.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.UElement;
import org.jetbrains.uast.UComment;
import org.jetbrains.uast.UFile;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
/**
* Looks for issues in Java comments
*/
public class CommentDetector extends Detector implements Detector.UastScanner {
private static final String STOPSHIP_COMMENT = "STOPSHIP"; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
CommentDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Looks for hidden code */
public static final Issue EASTER_EGG = Issue.create(
"EasterEgg", //$NON-NLS-1$
"Code contains easter egg",
"An \"easter egg\" is code deliberately hidden in the code, both from potential " +
"users and even from other developers. This lint check looks for code which " +
"looks like it may be hidden from sight.",
Category.SECURITY,
6,
Severity.WARNING,
IMPLEMENTATION)
.setEnabledByDefault(false);
/** Looks for special comment markers intended to stop shipping the code */
public static final Issue STOP_SHIP = Issue.create(
"StopShip", //$NON-NLS-1$
"Code contains `STOPSHIP` marker",
"Using the comment `// STOPSHIP` can be used to flag code that is incomplete but " +
"checked in. This comment marker can be used to indicate that the code should not " +
"be shipped until the issue is addressed, and lint will look for these.",
Category.CORRECTNESS,
10,
Severity.WARNING,
IMPLEMENTATION)
.setEnabledByDefault(false);
private static final String ESCAPE_STRING = "\\u002a\\u002f"; //$NON-NLS-1$
/** 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;
/** Constructs a new {@link CommentDetector} check */
public CommentDetector() {
}
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
if (USE_AST) {
return Collections.<Class<? extends UElement>>singletonList(
UFile.class);
} else {
return null;
}
}
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull JavaContext context) {
if (USE_AST) {
return new CommentChecker(context);
} else {
String source = context.getContents();
if (source == null) {
return null;
}
// Process the Java source such that we pass tokens to it
for (int i = 0, n = source.length() - 1; i < n; i++) {
char c = source.charAt(i);
if (c == '\\') {
i += 1;
} else if (c == '/') {
char next = source.charAt(i + 1);
if (next == '/') {
// Line comment
int start = i + 2;
int end = source.indexOf('\n', start);
if (end == -1) {
end = n;
}
checkComment(context, null, source, 0, start, end);
} else if (next == '*') {
// Block comment
int start = i + 2;
int end = source.indexOf("*/", start);
if (end == -1) {
end = n;
}
checkComment(context, null, source, 0, start, end);
}
}
}
return null;
}
}
private static class CommentChecker extends AbstractUastVisitor {
private final JavaContext mContext;
public CommentChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitFile(UFile node) {
for (UComment comment : node.getAllCommentsInFile()) {
String contents = comment.getText();
checkComment(mContext, comment, contents,
comment.getPsi().getTextRange().getStartOffset(), 0, contents.length());
}
return super.visitFile(node);
}
}
private static void checkComment(
@NonNull JavaContext context,
@Nullable UComment node,
@NonNull String source,
int offset,
int start,
int end) {
char prev = 0;
char c;
for (int i = start; i < end - 2; i++, prev = c) {
c = source.charAt(i);
if (prev == '\\') {
if (c == 'u' || c == 'U') {
if (source.regionMatches(true, i - 1, ESCAPE_STRING,
0, ESCAPE_STRING.length())) {
Location location = Location.create(context.file, source,
offset + i - 1, offset + i - 1 + ESCAPE_STRING.length());
context.report(EASTER_EGG, node, location,
"Code might be hidden here; found unicode escape sequence " +
"which is interpreted as comment end, compiled code follows");
}
} else {
i++;
}
} else if (prev == 'S' && c == 'T' &&
source.regionMatches(i - 1, STOPSHIP_COMMENT, 0, STOPSHIP_COMMENT.length())) {
// TODO: Only flag this issue in release mode??
Location location;
if (node != null) {
location = context.getUastLocation(node);
} else {
location = Location.create(context.file, source,
offset + i - 1, offset + i - 1 + STOPSHIP_COMMENT.length());
}
context.report(STOP_SHIP, node, location,
"`STOPSHIP` comment found; points to code which must be fixed prior " +
"to release");
}
}
}
}
@@ -1,537 +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.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 java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
//import org.jetbrains.org.objectweb.asm.util.Printer;
/**
* A {@linkplain ControlFlowGraph} is a graph containing a node for each
* instruction in a method, and an edge for each possible control flow; usually
* just "next" for the instruction following the current instruction, but in the
* case of a branch such as an "if", multiple edges to each successive location,
* or with a "goto", a single edge to the jumped-to instruction.
* <p>
* It also adds edges for abnormal control flow, such as the possibility of a
* method call throwing a runtime exception.
*/
public class ControlFlowGraph {
/** Map from instructions to nodes */
private Map<AbstractInsnNode, Node> mNodeMap;
private MethodNode mMethod;
/**
* Creates a new {@link ControlFlowGraph} and populates it with the flow
* control for the given method. If the optional {@code initial} parameter is
* provided with an existing graph, then the graph is simply populated, not
* created. This allows subclassing of the graph instance, if necessary.
*
* @param initial usually null, but can point to an existing instance of a
* {@link ControlFlowGraph} in which that graph is reused (but
* populated with new edges)
* @param classNode the class containing the method to be analyzed
* @param method the method to be analyzed
* @return a {@link ControlFlowGraph} with nodes for the control flow in the
* given method
* @throws AnalyzerException if the underlying bytecode library is unable to
* analyze the method bytecode
*/
@NonNull
public static ControlFlowGraph create(
@Nullable ControlFlowGraph initial,
@NonNull ClassNode classNode,
@NonNull MethodNode method) throws AnalyzerException {
final ControlFlowGraph graph = initial != null ? initial : new ControlFlowGraph();
final InsnList instructions = method.instructions;
graph.mNodeMap = Maps.newHashMapWithExpectedSize(instructions.size());
graph.mMethod = method;
// Create a flow control graph using ASM5's analyzer. According to the ASM 4 guide
// (download.forge.objectweb.org/asm/asm4-guide.pdf) there are faster ways to construct
// it, but those require a lot more code.
Analyzer analyzer = new Analyzer(new BasicInterpreter()) {
@Override
protected void newControlFlowEdge(int insn, int successor) {
// Update the information as of whether the this object has been
// initialized at the given instruction.
AbstractInsnNode from = instructions.get(insn);
AbstractInsnNode to = instructions.get(successor);
graph.add(from, to);
}
@Override
protected boolean newControlFlowExceptionEdge(int insn, TryCatchBlockNode tcb) {
AbstractInsnNode from = instructions.get(insn);
graph.exception(from, tcb);
return super.newControlFlowExceptionEdge(insn, tcb);
}
@Override
protected boolean newControlFlowExceptionEdge(int insn, int successor) {
AbstractInsnNode from = instructions.get(insn);
AbstractInsnNode to = instructions.get(successor);
graph.exception(from, to);
return super.newControlFlowExceptionEdge(insn, successor);
}
};
analyzer.analyze(classNode.name, method);
return graph;
}
/**
* Checks whether there is a path from the given source node to the given
* destination node
*/
@SuppressWarnings("MethodMayBeStatic")
private boolean isConnected(@NonNull Node from,
@NonNull Node to, @NonNull Set<Node> seen) {
if (from == to) {
return true;
} else if (seen.contains(from)) {
return false;
}
seen.add(from);
List<Node> successors = from.successors;
List<Node> exceptions = from.exceptions;
if (exceptions != null) {
for (Node successor : exceptions) {
if (isConnected(successor, to, seen)) {
return true;
}
}
}
if (successors != null) {
for (Node successor : successors) {
if (isConnected(successor, to, seen)) {
return true;
}
}
}
return false;
}
/**
* Checks whether there is a path from the given source node to the given
* destination node
*/
public boolean isConnected(@NonNull Node from, @NonNull Node to) {
return isConnected(from, to, Sets.<Node>newIdentityHashSet());
}
/**
* Checks whether there is a path from the given instruction to the given
* instruction node
*/
public boolean isConnected(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
return isConnected(getNode(from), getNode(to));
}
/** A {@link Node} is a node in the control flow graph for a method, pointing to
* the instruction and its possible successors */
public static class Node {
/** The instruction */
public final AbstractInsnNode instruction;
/** Any normal successors (e.g. following instruction, or goto or conditional flow) */
public final List<Node> successors = new ArrayList<Node>(2);
/** Any abnormal successors (e.g. the handler to go to following an exception) */
public final List<Node> exceptions = new ArrayList<Node>(1);
/** A tag for use during depth-first-search iteration of the graph etc */
public int visit;
/**
* Constructs a new control graph node
*
* @param instruction the instruction to associate with this node
*/
public Node(@NonNull AbstractInsnNode instruction) {
this.instruction = instruction;
}
void addSuccessor(@NonNull Node node) {
if (!successors.contains(node)) {
successors.add(node);
}
}
void addExceptionPath(@NonNull Node node) {
if (!exceptions.contains(node)) {
exceptions.add(node);
}
}
/**
* Represents this instruction as a string, for debugging purposes
*
* @param includeAdjacent whether it should include a display of
* adjacent nodes as well
* @return a string representation
*/
@NonNull
public String toString(boolean includeAdjacent) {
StringBuilder sb = new StringBuilder(100);
sb.append(getId(instruction));
sb.append(':');
if (instruction instanceof LabelNode) {
//LabelNode l = (LabelNode) instruction;
//sb.append('L' + l.getLabel().getOffset() + ":");
//sb.append('L' + l.getLabel().info + ":");
sb.append("LABEL");
} else if (instruction instanceof LineNumberNode) {
sb.append("LINENUMBER ").append(((LineNumberNode)instruction).line);
} else if (instruction instanceof FrameNode) {
sb.append("FRAME");
} else {
int opcode = instruction.getOpcode();
String opcodeName = getOpcodeName(opcode);
sb.append(opcodeName);
if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
sb.append('(').append(((MethodInsnNode)instruction).name).append(')');
}
}
if (includeAdjacent) {
if (successors != null && !successors.isEmpty()) {
sb.append(" Next:");
for (Node successor : successors) {
sb.append(' ');
sb.append(successor.toString(false));
}
}
if (exceptions != null && !exceptions.isEmpty()) {
sb.append(" Exceptions:");
for (Node exception : exceptions) {
sb.append(' ');
sb.append(exception.toString(false));
}
}
sb.append('\n');
}
return sb.toString();
}
}
/** Adds an exception flow to this graph */
protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
getNode(from).addSuccessor(getNode(to));
}
/** Adds an exception flow to this graph */
protected void exception(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
// For now, these edges appear useless; we also get more specific
// information via the TryCatchBlockNode which we use instead.
//getNode(from).addExceptionPath(getNode(to));
}
/** Adds an exception try block node to this graph */
protected void exception(@NonNull AbstractInsnNode from, @NonNull TryCatchBlockNode tcb) {
// Add tcb's to all instructions in the range
LabelNode start = tcb.start;
LabelNode end = tcb.end; // exclusive
// Add exception edges for all method calls in the range
AbstractInsnNode curr = start;
Node handlerNode = getNode(tcb.handler);
while (curr != end && curr != null) {
// 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
getNode(curr).addSuccessor(handlerNode);
}
getNode(curr).addExceptionPath(handlerNode);
}
curr = curr.getNext();
}
}
/**
* Looks up (and if necessary) creates a graph node for the given instruction
*
* @param instruction the instruction
* @return the control flow graph node corresponding to the given
* instruction
*/
@NonNull
public Node getNode(@NonNull AbstractInsnNode instruction) {
Node node = mNodeMap.get(instruction);
if (node == null) {
node = new Node(instruction);
mNodeMap.put(instruction, node);
}
return node;
}
/**
* Creates a human readable version of the graph
*
* @param start the starting instruction, or null if not known or to use the
* first instruction
* @return a string version of the graph
*/
@NonNull
public String toString(@Nullable Node start) {
StringBuilder sb = new StringBuilder(400);
AbstractInsnNode curr;
if (start != null) {
curr = start.instruction;
} else {
if (mNodeMap.isEmpty()) {
return "<empty>";
} else {
curr = mNodeMap.keySet().iterator().next();
while (curr.getPrevious() != null) {
curr = curr.getPrevious();
}
}
}
while (curr != null) {
Node node = mNodeMap.get(curr);
if (node != null) {
sb.append(node.toString(true));
}
curr = curr.getNext();
}
return sb.toString();
}
@Override
public String toString() {
return toString(null);
}
// ---- For debugging only ----
private static Map<Object, String> sIds = null;
private static int sNextId = 1;
private static String getId(Object object) {
if (sIds == null) {
sIds = Maps.newHashMap();
}
String id = sIds.get(object);
if (id == null) {
id = Integer.toString(sNextId++);
sIds.put(object, id);
}
return id;
}
/**
* Generates dot output of the graph. This can be used with
* graphwiz to visualize the graph. For example, if you
* save the output as graph1.gv you can run
* <pre>
* $ dot -Tps graph1.gv -o graph1.ps
* </pre>
* to generate a postscript file, which you can then view
* with "gv graph1.ps".
*
* (There are also some online web sites where you can
* paste in dot graphs and see the visualization right
* there in the browser.)
*
* @return a dot description of this control flow graph,
* useful for debugging
*/
@SuppressWarnings("unused")
public String toDot(@Nullable Set<Node> highlight) {
StringBuilder sb = new StringBuilder();
sb.append("digraph G {\n");
AbstractInsnNode instruction = mMethod.instructions.getFirst();
// Special start node
sb.append(" start -> ").append(getId(mNodeMap.get(instruction))).append(";\n");
sb.append(" start [shape=plaintext];\n");
while (instruction != null) {
Node node = mNodeMap.get(instruction);
if (node != null) {
if (node.successors != null) {
for (Node to : node.successors) {
sb.append(" ").append(getId(node)).append(" -> ").append(getId(to));
if (node.instruction instanceof JumpInsnNode) {
sb.append(" [label=\"");
if (((JumpInsnNode)node.instruction).label == to.instruction) {
sb.append("yes");
} else {
sb.append("no");
}
sb.append("\"]");
}
sb.append(";\n");
}
}
if (node.exceptions != null) {
for (Node to : node.exceptions) {
sb.append(getId(node)).append(" -> ").append(getId(to));
sb.append(" [label=\"exception\"];\n");
}
}
}
instruction = instruction.getNext();
}
// Labels
sb.append("\n");
for (Node node : mNodeMap.values()) {
instruction = node.instruction;
sb.append(" ").append(getId(node)).append(" ");
sb.append("[label=\"").append(dotDescribe(node)).append("\"");
if (highlight != null && highlight.contains(node)) {
sb.append(",shape=box,style=filled");
} else if (instruction instanceof LineNumberNode ||
instruction instanceof LabelNode ||
instruction instanceof FrameNode) {
sb.append(",shape=oval,style=dotted");
} else {
sb.append(",shape=box");
}
sb.append("];\n");
}
sb.append("}");
return sb.toString();
}
private static String dotDescribe(Node node) {
AbstractInsnNode instruction = node.instruction;
if (instruction instanceof LabelNode) {
return "Label";
} else if (instruction instanceof LineNumberNode) {
LineNumberNode lineNode = (LineNumberNode)instruction;
return "Line " + lineNode.line;
} else if (instruction instanceof FrameNode) {
return "Stack Frame";
} else if (instruction instanceof MethodInsnNode) {
MethodInsnNode method = (MethodInsnNode)instruction;
String cls = method.owner.substring(method.owner.lastIndexOf('/') + 1);
cls = cls.replace('$','.');
return "Call " + cls + "#" + method.name;
} else if (instruction instanceof FieldInsnNode) {
FieldInsnNode field = (FieldInsnNode) instruction;
String cls = field.owner.substring(field.owner.lastIndexOf('/') + 1);
cls = cls.replace('$','.');
return "Field " + cls + "#" + field.name;
} else if (instruction instanceof TypeInsnNode && instruction.getOpcode() == Opcodes.NEW) {
return "New " + ((TypeInsnNode)instruction).desc;
}
StringBuilder sb = new StringBuilder();
String opcodeName = getOpcodeName(instruction.getOpcode());
sb.append(opcodeName);
if (instruction instanceof IntInsnNode) {
IntInsnNode in = (IntInsnNode) instruction;
sb.append(" ").append(Integer.toString(in.operand));
} else if (instruction instanceof LdcInsnNode) {
LdcInsnNode ldc = (LdcInsnNode) instruction;
sb.append(" ");
if (ldc.cst instanceof String) {
sb.append("\\\"");
}
sb.append(ldc.cst);
if (ldc.cst instanceof String) {
sb.append("\\\"");
}
}
return sb.toString();
}
private static String getOpcodeName(int opcode) {
if (sOpcodeNames == null) {
sOpcodeNames = new String[255];
try {
Field[] fields = Opcodes.class.getDeclaredFields();
for (Field field : fields) {
if (field.getType() == int.class) {
String name = field.getName();
if (name.startsWith("ASM") || name.startsWith("V1_") ||
name.startsWith("ACC_") || name.startsWith("T_") ||
name.startsWith("H_") || name.startsWith("F_")) {
continue;
}
int val = field.getInt(null);
if (val >= 0 && val < sOpcodeNames.length) {
sOpcodeNames[val] = field.getName();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (opcode >= 0 && opcode < sOpcodeNames.length) {
String name = sOpcodeNames[opcode];
if (name != null) {
return name;
}
}
return Integer.toString(opcode);
}
private static String[] sOpcodeNames;
}
@@ -1,140 +0,0 @@
/*
* 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.detector.api.*;
import com.intellij.psi.PsiClass;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.*;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
import static com.android.SdkConstants.*;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
/**
* Makes sure that custom views use a declare styleable that matches
* the name of the custom view
*/
public class CustomViewDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
CustomViewDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Mismatched style and class names */
public static final Issue ISSUE = Issue.create(
"CustomViewStyleable", //$NON-NLS-1$
"Mismatched Styleable/Custom View Name",
"The convention for custom views is to use a `declare-styleable` whose name " +
"matches the custom view class name. The IDE relies on this convention such that " +
"for example code completion can be offered for attributes in a custom view " +
"in layout XML resource files.\n" +
"\n" +
"(Similarly, layout parameter classes should use the suffix `_Layout`.)",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION);
private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$
/** Constructs a new {@link CustomViewDetector} check */
public CustomViewDetector() {
}
// ---- Implements UastScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(OBTAIN_STYLED_ATTRIBUTES);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
if (skipParentheses(node.getUastParent()) instanceof UExpression) {
if (!context.getEvaluator().isMemberInSubClassOf(method, CLASS_CONTEXT, false)) {
return;
}
List<UExpression> arguments = node.getValueArguments();
int size = arguments.size();
// Which parameter contains the styleable (attrs) ?
int parameterIndex;
if (size == 1) {
// obtainStyledAttributes(int[] attrs)
parameterIndex = 0;
} else {
// obtainStyledAttributes(int resid, int[] attrs)
// obtainStyledAttributes(AttributeSet set, int[] attrs)
// obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
parameterIndex = 1;
}
UExpression expression = arguments.get(parameterIndex);
if (!UastUtils.startsWithQualified(expression, R_STYLEABLE_PREFIX)) {
return;
}
List<String> path = UastUtils.asQualifiedPath(expression);
if (path == null || path.size() < 3) {
return;
}
String styleableName = path.get(2);
UClass cls = UastUtils.getParentOfType(node, UClass.class, false);
if (cls == null) {
return;
}
String className = cls.getName();
if (InheritanceUtil.isInheritor(cls, false, CLASS_VIEW)) {
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.getUastLocation(expression), message);
}
} else if (InheritanceUtil.isInheritor(cls, false,
CLASS_VIEWGROUP + DOT_LAYOUT_PARAMS)) {
PsiClass outer = PsiTreeUtil.getParentOfType(cls, PsiClass.class, true);
if (outer == null) {
return;
}
String layoutClassName = outer.getName();
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.getUastLocation(expression), message);
}
}
}
}
}
@@ -1,339 +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.*;
import com.google.common.collect.Maps;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.*;
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;
import java.util.Map;
import static com.android.SdkConstants.RESOURCE_CLZ_ID;
/**
* Detector looking for cut &amp; paste issues
*/
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$
"Likely cut & paste mistakes",
"This lint check looks for cases where you have cut & pasted calls to " +
"`findViewById` but have forgotten to update the R.id field. It's possible " +
"that your code is simply (redundantly) looking up the field repeatedly, " +
"but lint cannot distinguish that from a case where you for example want to " +
"initialize fields `prev` and `next` and you cut & pasted `findViewById(R.id.prev)` " +
"and forgot to update the second initialization to `R.id.next`.",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
CutPasteDetector.class,
Scope.JAVA_FILE_SCOPE));
private PsiMethod mLastMethod;
private Map<String, UCallExpression> mIds;
private Map<String, String> mLhs;
private Map<String, String> mCallOperands;
/** Constructs a new {@link CutPasteDetector} check */
public CutPasteDetector() {
}
// ---- Implements UastScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("findViewById"); //$NON-NLS-1$
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod calledMethod) {
String lhs = getLhs(call);
if (lhs == null) {
return;
}
UMethod method = UastUtils.getParentOfType(call, UMethod.class, false);
if (method == null) {
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() : "";
List<UExpression> arguments = call.getValueArguments();
if (arguments.isEmpty()) {
return;
}
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;
}
if (!callOperand.equals(mCallOperands.get(id))) {
return;
}
UCallExpression earlierCall = mIds.get(id);
if (!isReachableFrom(method, earlierCall, call)) {
return;
}
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, call);
mLhs.put(id, lhs);
mCallOperands.put(id, callOperand);
}
}
}
}
}
@Nullable
private static String getLhs(@NonNull UCallExpression call) {
UElement parent = call.getUastParent();
while (parent != null && !(parent instanceof UBlockExpression)) {
if (parent instanceof ULocalVariable) {
return ((ULocalVariable) parent).getName();
} else if (UastExpressionUtils.isAssignment(parent)) {
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();
}
}
parent = parent.getUastParent();
}
return null;
}
static boolean isReachableFrom(
@NonNull UMethod method,
@NonNull UElement from,
@NonNull UElement to) {
ReachabilityVisitor visitor = new ReachabilityVisitor(from, to);
method.accept(visitor);
return visitor.isReachable();
}
private static class ReachabilityVisitor extends AbstractUastVisitor {
private final UElement mFrom;
private final UElement mTarget;
private boolean mIsFromReached;
private boolean mIsTargetReachable;
private boolean mIsFinished;
private UExpression mBreakedExpression;
private UExpression mContinuedExpression;
ReachabilityVisitor(UElement from, UElement target) {
mFrom = from;
mTarget = target;
}
@Override
public boolean visitElement(UElement node) {
if (mIsFinished || mBreakedExpression != null || mContinuedExpression != null) {
return true;
}
if (node.equals(mFrom)) {
mIsFromReached = true;
}
if (node.equals(mTarget)) {
mIsFinished = true;
if (mIsFromReached) {
mIsTargetReachable = true;
}
return true;
}
if (mIsFromReached) {
if (node instanceof UReturnExpression) {
mIsFinished = true;
} else if (node instanceof UBreakExpression) {
mBreakedExpression = getBreakedExpression((UBreakExpression) node);
} else if (node instanceof UContinueExpression) {
UExpression expression = getContinuedExpression((UContinueExpression) node);
if (expression != null && UastUtils.isChildOf(mTarget, expression, false)) {
mIsTargetReachable = true;
mIsFinished = true;
} else {
mContinuedExpression = expression;
}
} else if (UastUtils.isChildOf(mTarget, node, false)) {
mIsTargetReachable = true;
mIsFinished = true;
}
return true;
} else {
if (node instanceof UIfExpression) {
UIfExpression ifExpression = (UIfExpression) node;
ifExpression.getCondition().accept(this);
boolean isFromReached = mIsFromReached;
UExpression thenExpression = ifExpression.getThenExpression();
if (thenExpression != null) {
thenExpression.accept(this);
}
UExpression elseExpression = ifExpression.getElseExpression();
if (elseExpression != null && isFromReached == mIsFromReached) {
elseExpression.accept(this);
}
return true;
} else if (node instanceof ULoopExpression) {
visitLoopExpressionHeader(node);
boolean isFromReached = mIsFromReached;
((ULoopExpression) node).getBody().accept(this);
if (isFromReached != mIsFromReached
&& UastUtils.isChildOf(mTarget, node, false)) {
mIsTargetReachable = true;
mIsFinished = true;
}
return true;
}
}
return false;
}
@Override
public void afterVisitElement(UElement node) {
if (node.equals(mBreakedExpression)) {
mBreakedExpression = null;
} else if (node.equals(mContinuedExpression)) {
mContinuedExpression = null;
}
}
private void visitLoopExpressionHeader(UElement node) {
if (node instanceof UWhileExpression) {
((UWhileExpression) node).getCondition().accept(this);
} else if (node instanceof UDoWhileExpression) {
((UDoWhileExpression) node).getCondition().accept(this);
} else if (node instanceof UForExpression) {
UForExpression forExpression = (UForExpression) node;
if (forExpression.getDeclaration() != null) {
forExpression.getDeclaration().accept(this);
}
if (forExpression.getCondition() != null) {
forExpression.getCondition().accept(this);
}
if (forExpression.getUpdate() != null) {
forExpression.getUpdate().accept(this);
}
} else if (node instanceof UForEachExpression) {
UForEachExpression forEachExpression = (UForEachExpression) node;
forEachExpression.getForIdentifier().accept(this);
forEachExpression.getIteratedValue().accept(this);
}
}
private static UExpression getBreakedExpression(UBreakExpression node) {
UElement parent = node.getUastParent();
String label = node.getLabel();
while (parent != null) {
if (label != null) {
if (parent instanceof ULabeledExpression) {
ULabeledExpression labeledExpression = (ULabeledExpression) parent;
if (labeledExpression.getLabel().equals(label)) {
return labeledExpression.getExpression();
}
}
} else {
if (parent instanceof ULoopExpression || parent instanceof USwitchExpression) {
return (UExpression) parent;
}
}
parent = parent.getUastParent();
}
return null;
}
private static UExpression getContinuedExpression(UContinueExpression node) {
UElement parent = node.getUastParent();
String label = node.getLabel();
while (parent != null) {
if (label != null) {
if (parent instanceof ULabeledExpression) {
ULabeledExpression labeledExpression = (ULabeledExpression) parent;
if (labeledExpression.getLabel().equals(label)) {
return labeledExpression.getExpression();
}
}
} else {
if (parent instanceof ULoopExpression) {
return (UExpression) parent;
}
}
parent = parent.getUastParent();
}
return null;
}
public boolean isReachable() {
return mIsTargetReachable;
}
}
}
@@ -1,113 +0,0 @@
/*
* 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.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.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;
/**
* Checks for errors related to Date Formats
*/
public class DateFormatDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
DateFormatDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Constructing SimpleDateFormat without an explicit locale */
public static final Issue DATE_FORMAT = Issue.create(
"SimpleDateFormat", //$NON-NLS-1$
"Implied locale in date format",
"Almost all callers should use `getDateInstance()`, `getDateTimeInstance()`, or " +
"`getTimeInstance()` to get a ready-made instance of SimpleDateFormat suitable " +
"for the user's locale. The main reason you'd create an instance this class " +
"directly is because you need to format/parse a specific machine-readable format, " +
"in which case you almost certainly want to explicitly ask for US to ensure that " +
"you get ASCII digits (rather than, say, Arabic digits).\n" +
"\n" +
"Therefore, you should either use the form of the SimpleDateFormat constructor " +
"where you pass in an explicit locale, such as Locale.US, or use one of the " +
"get instance methods, or suppress this error if really know what you are doing.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/reference/java/text/SimpleDateFormat.html");//$NON-NLS-1$
public static final String LOCALE_CLS = "java.util.Locale"; //$NON-NLS-1$
public static final String SIMPLE_DATE_FORMAT_CLS = "java.text.SimpleDateFormat"; //$NON-NLS-1$
/** Constructs a new {@link DateFormatDetector} */
public DateFormatDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableConstructorTypes() {
return Collections.singletonList(SIMPLE_DATE_FORMAT_CLS);
}
@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod constructor) {
if (!specifiesLocale(constructor)) {
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, node, location, message);
}
}
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;
}
}
@@ -1,151 +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 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.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;
/**
* Checks that Fragment subclasses can be instantiated via
* {link {@link Class#newInstance()}}: the class is public, static, and has
* a public null constructor.
* <p>
* This helps track down issues like
* http://stackoverflow.com/questions/8058809/fragment-activity-crashes-on-screen-rotate
* (and countless duplicates)
*/
public class FragmentDetector extends Detector implements Detector.UastScanner {
/** Are fragment subclasses instantiatable? */
public static final Issue ISSUE = Issue.create(
"ValidFragment", //$NON-NLS-1$
"Fragment not instantiatable",
"From the Fragment documentation:\n" +
"*Every* fragment must have an empty constructor, so it can be instantiated when " +
"restoring its activity's state. It is strongly recommended that subclasses do not " +
"have other constructors with parameters, since these constructors will not be " +
"called when the fragment is re-instantiated; instead, arguments can be supplied " +
"by the caller with `setArguments(Bundle)` and later retrieved by the Fragment " +
"with `getArguments()`.",
Category.CORRECTNESS,
6,
Severity.FATAL,
new Implementation(
FragmentDetector.class,
Scope.JAVA_FILE_SCOPE)
).addMoreInfo(
"http://developer.android.com/reference/android/app/Fragment.html#Fragment()"); //$NON-NLS-1$
/** Constructs a new {@link FragmentDetector} */
public FragmentDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Arrays.asList(CLASS_FRAGMENT, CLASS_V4_FRAGMENT);
}
@Override
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;
}
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)",
node.getQualifiedName());
context.reportUast(ISSUE, node, context.getUastNameLocation(node), message);
return;
}
if (node.getContainingClass() != null && !evaluator.isStatic(node)) {
String message = String.format(
"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 (PsiMethod constructor : node.getConstructors()) {
hasConstructor = true;
if (constructor.getParameterList().getParametersCount() == 0) {
if (evaluator.isPublic(constructor)) {
hasDefaultConstructor = true;
} else {
Location location = context.getNameLocation(constructor);
context.report(ISSUE, constructor, location,
"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.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";
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`)",
node.getQualifiedName());
context.reportUast(ISSUE, node, context.getNameLocation(node), message);
}
}
}
@@ -1,99 +0,0 @@
/*
* 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.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;
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.",
Category.SECURITY,
8,
Severity.INFORMATIONAL,
new Implementation(
GetSignaturesDetector.class,
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 JavaScanner ----
@Override
@Nullable
public List<String> getApplicableMethodNames() {
return Collections.singletonList(GET_PACKAGE_INFO);
}
@Override
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<UExpression> 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, JavaContext context, UCallExpression node,
UExpression last) {
if ((flagValue & GET_SIGNATURES_FLAG) != 0) {
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.");
}
}
}
@@ -1,149 +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 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.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;
/**
* 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 Detector.UastScanner {
/** Potentially leaking handlers */
public static final Issue ISSUE = Issue.create(
"HandlerLeak", //$NON-NLS-1$
"Handler reference leaks",
"Since this Handler is declared as an inner class, it may prevent the outer " +
"class from being garbage collected. If the Handler is using a Looper or " +
"MessageQueue for a thread other than the main thread, then there is no issue. " +
"If the Handler is using the Looper or MessageQueue of the main thread, you " +
"need to fix your Handler declaration, as follows: Declare the Handler as a " +
"static class; In the outer class, instantiate a WeakReference to the outer " +
"class and pass this object to your Handler when you instantiate the Handler; " +
"Make all references to members of the outer class using the WeakReference object.",
Category.PERFORMANCE,
4,
Severity.WARNING,
new Implementation(
HandlerDetector.class,
Scope.JAVA_FILE_SCOPE));
private static final String LOOPER_CLS = "android.os.Looper";
private static final String HANDLER_CLS = "android.os.Handler";
/** Constructs a new {@link HandlerDetector} */
public HandlerDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(HANDLER_CLS);
}
@Override
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;
}
//// 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;
}
Location location = context.getUastNameLocation(declaration);
String name;
if (isAnonymous) {
name = "anonymous " + ((UAnonymousClass)declaration).getBaseClassReference().getQualifiedName();
} else {
name = declaration.getQualifiedName();
}
//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 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;
}
}
}
return false;
}
}
@@ -1,527 +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.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.*;
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.*;
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.Collection;
import java.util.HashSet;
import java.util.List;
import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT;
import static com.android.tools.klint.client.api.JavaParser.*;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
/**
* 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 Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
JavaPerformanceDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Allocating objects during a paint method */
public static final Issue PAINT_ALLOC = Issue.create(
"DrawAllocation", //$NON-NLS-1$
"Memory allocations within drawing code",
"You should avoid allocating objects during a drawing or layout operation. These " +
"are called frequently, so a smooth UI can be interrupted by garbage collection " +
"pauses caused by the object allocations.\n" +
"\n" +
"The way this is generally handled is to allocate the needed objects up front " +
"and to reuse them for each drawing operation.\n" +
"\n" +
"Some methods allocate memory on your behalf (such as `Bitmap.create`), and these " +
"should be handled in the same way.",
Category.PERFORMANCE,
9,
Severity.WARNING,
IMPLEMENTATION);
/** Using HashMaps where SparseArray would be better */
public static final Issue USE_SPARSE_ARRAY = Issue.create(
"UseSparseArrays", //$NON-NLS-1$
"HashMap can be replaced with SparseArray",
"For maps where the keys are of type integer, it's typically more efficient to " +
"use the Android `SparseArray` API. This check identifies scenarios where you might " +
"want to consider using `SparseArray` instead of `HashMap` for better performance.\n" +
"\n" +
"This is *particularly* useful when the value types are primitives like ints, " +
"where you can use `SparseIntArray` and avoid auto-boxing the values from `int` to " +
"`Integer`.\n" +
"\n" +
"If you need to construct a `HashMap` because you need to call an API outside of " +
"your control which requires a `Map`, you can suppress this warning using for " +
"example the `@SuppressLint` annotation.",
Category.PERFORMANCE,
4,
Severity.WARNING,
IMPLEMENTATION);
/** Using {@code new Integer()} instead of the more efficient {@code Integer.valueOf} */
public static final Issue USE_VALUE_OF = Issue.create(
"UseValueOf", //$NON-NLS-1$
"Should use `valueOf` instead of `new`",
"You should not call the constructor for wrapper classes directly, such as" +
"`new Integer(42)`. Instead, call the `valueOf` factory method, such as " +
"`Integer.valueOf(42)`. This will typically use less memory because common integers " +
"such as 0 and 1 will share a single instance.",
Category.PERFORMANCE,
4,
Severity.WARNING,
IMPLEMENTATION);
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() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
List<Class<? extends UElement>> types = new ArrayList<Class<? extends UElement>>(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 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(JavaContext context) {
mContext = context;
mCheckAllocations = context.isEnabled(PAINT_ALLOC);
mCheckMaps = context.isEnabled(USE_SPARSE_ARRAY);
mCheckValueOf = context.isEnabled(USE_VALUE_OF);
}
@Override
public boolean visitMethod(UMethod node) {
mFlagAllocations = isBlockedAllocationMethod(node);
return super.visitMethod(node);
}
@Override
public boolean visitCallExpression(UCallExpression node) {
if (UastExpressionUtils.isConstructorCall(node)) {
visitConstructorCallExpression(node);
} else if (UastExpressionUtils.isMethodCall(node)) {
visitMethodCallExpression(node);
}
return super.visitCallExpression(node);
}
private void visitConstructorCallExpression(UCallExpression node) {
String typeName = null;
UReferenceExpression classReference = node.getClassReference();
if (mCheckMaps || mCheckValueOf) {
if (classReference != null) {
typeName = UastUtils.getQualifiedName(classReference);
}
}
if (mCheckMaps) {
// 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) {
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).asSourceString();
mContext.report(USE_VALUE_OF, node, mContext.getUastLocation(node), getUseValueOfErrorMessage(
typeName, argument));
}
}
if (mFlagAllocations
&& !(skipParentheses(node.getUastParent()) 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:
PsiMethod method = UastUtils.getParentOfType(node, UMethod.class);
if (method != null && isBlockedAllocationMethod(method)
&& !isLazilyInitialized(node)) {
reportAllocation(node);
}
}
}
private void reportAllocation(UElement node) {
mContext.report(PAINT_ALLOC, node, mContext.getUastLocation(node),
"Avoid object allocations during draw/layout operations (preallocate and " +
"reuse instead)");
}
private void visitMethodCallExpression(UCallExpression node) {
if (!mFlagAllocations) {
return;
}
UExpression receiver = node.getReceiver();
if (receiver == null) {
return;
}
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`");
}
}
}
/**
* Check whether the given invocation is done as a lazy initialization,
* e.g. {@code if (foo == null) foo = new Foo();}.
* <p>
* This tries to also handle the scenario where the check is on some
* <b>other</b> variable - e.g.
* <pre>
* if (foo == null) {
* foo == init1();
* bar = new Bar();
* }
* </pre>
* or
* <pre>
* if (!initialized) {
* initialized = true;
* bar = new Bar();
* }
* </pre>
*/
private static boolean isLazilyInitialized(UElement node) {
UElement curr = node.getUastParent();
while (curr != null) {
if (curr instanceof UMethod) {
return false;
} else if (curr instanceof UIfExpression) {
UIfExpression ifNode = (UIfExpression) curr;
// See if the if block represents a lazy initialization:
// compute all variable names seen in the condition
// (e.g. for "if (foo == null || bar != foo)" the result is "foo,bar"),
// and then compute all variables assigned to in the if body,
// and if there is an overlap, we'll consider the whole if block
// guarded (so lazily initialized and an allocation we won't complain
// about.)
List<String> assignments = new ArrayList<String>();
AssignmentTracker visitor = new AssignmentTracker(assignments);
if (ifNode.getThenExpression() != null) {
ifNode.getThenExpression().accept(visitor);
}
if (!assignments.isEmpty()) {
List<String> references = new ArrayList<String>();
addReferencedVariables(references, ifNode.getCondition());
if (!references.isEmpty()) {
SetView<String> intersection = Sets.intersection(
new HashSet<String>(assignments),
new HashSet<String>(references));
return !intersection.isEmpty();
}
}
return false;
}
curr = curr.getUastParent();
}
return false;
}
/** Adds any variables referenced in the given expression into the given list */
private static void addReferencedVariables(
@NonNull Collection<String> variables,
@Nullable UExpression expression) {
if (expression instanceof UBinaryExpression) {
UBinaryExpression binary = (UBinaryExpression) expression;
addReferencedVariables(variables, binary.getLeftOperand());
addReferencedVariables(variables, binary.getRightOperand());
} else if (expression instanceof UPrefixExpression) {
UPrefixExpression unary = (UPrefixExpression) expression;
addReferencedVariables(variables, unary.getOperand());
} 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 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);
}
}
}
}
/**
* Returns whether the given method declaration represents a method
* where allocating objects is not allowed for performance reasons
*/
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(
@NonNull JavaEvaluator evaluator,
@NonNull PsiMethod node) {
return ON_DRAW.equals(node.getName()) && evaluator.parametersMatch(node, CLASS_CANVAS);
}
/**
* Returns true if this method looks like it's overriding
* android.view.View's
* {@code protected void onLayout(boolean changed, int left, int top,
* int right, int bottom)}
*/
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(
@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(
@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(@NonNull UCallExpression node) {
List<PsiType> types = node.getTypeArguments();
if (types.size() == 2) {
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.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.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.getUastLocation(node),
String.format(
"Use `new SparseArray<%1$s>(...)` instead for better performance",
valueType.substring(valueType.lastIndexOf('.') + 1)));
}
} 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.getUastLocation(node),
message);
}
}
}
private void checkSparseArray(@NonNull UCallExpression node) {
List<PsiType> types = node.getTypeArguments();
if (types.size() == 1) {
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 (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.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);
int index = message.indexOf('.');
if (index != -1 && message.startsWith("Use ")) {
return message.substring(4, index);
}
return null;
}
/** Visitor which records variable names assigned into */
private static class AssignmentTracker extends AbstractUastVisitor {
private final Collection<String> mVariables;
public AssignmentTracker(Collection<String> variables) {
mVariables = variables;
}
@Override
public boolean visitBinaryExpression(UBinaryExpression node) {
if (UastExpressionUtils.isAssignment(node)) {
UExpression left = node.getLeftOperand();
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 USimpleNameReferenceExpression) {
mVariables.add(((USimpleNameReferenceExpression) left).getIdentifier());
}
}
return super.visitBinaryExpression(node);
}
}
}
@@ -1,140 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.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.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;
/**
* Looks for addJavascriptInterface calls on interfaces have been properly annotated
* with {@code @JavaScriptInterface}
*/
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$
"Missing @JavascriptInterface on methods",
"As of API 17, you must annotate methods in objects registered with the " +
"`addJavascriptInterface` method with a `@JavascriptInterface` annotation.",
Category.SECURITY,
8,
Severity.ERROR,
new Implementation(
JavaScriptInterfaceDetector.class,
Scope.JAVA_FILE_SCOPE))
.addMoreInfo(
"http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)"); //$NON-NLS-1$
private static final String ADD_JAVASCRIPT_INTERFACE = "addJavascriptInterface"; //$NON-NLS-1$
private static final String JAVASCRIPT_INTERFACE_CLS = "android.webkit.JavascriptInterface"; //$NON-NLS-1$
private static final String WEB_VIEW_CLS = "android.webkit.WebView"; //$NON-NLS-1$
/** Constructs a new {@link JavaScriptInterfaceDetector} check */
public JavaScriptInterfaceDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
if (context.getMainProject().getTargetSdk() < 17) {
return;
}
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() != 2) {
return;
}
JavaEvaluator evaluator = context.getEvaluator();
if (!JavaEvaluator.isMemberInClass(method, WEB_VIEW_CLS)) {
return;
}
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;
}
if (isJavaScriptAnnotated(cls)) {
return;
}
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, call, location, message);
}
}
private static boolean isJavaScriptAnnotated(PsiClass clz) {
while (clz != null) {
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;
}
}
clz = clz.getSuperClass();
}
return false;
}
}
@@ -1,442 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.klint.checks;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.tools.klint.detector.api.LintUtils.stripIdPrefix;
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.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.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.visitor.UastVisitor;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Checks for consistency in layouts across different resource folders
*/
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
* used by that id in this file */
private final Map<String, List<Pair<File, Map<String, String>>>> mMap =
Maps.newHashMapWithExpectedSize(64);
/** Ids referenced from .java files. Only ids referenced from code are considered
* vital to be consistent among the layout variations (others could just have ids
* assigned to them in the layout either automatically by the layout editor or there
* in order to support RelativeLayout constraints etc, but not be problematic
* in findViewById calls.)
*/
private final Set<String> mRelevantIds = Sets.newLinkedHashSetWithExpectedSize(64);
/** Map from layout to id name to a list of locations */
private Map<String, Map<String, List<Location>>> mLocations;
/** Map from layout to id name to the error message to display for each */
private Map<String, Map<String, String>> mErrorMessages;
/** Inconsistent widget types */
public static final Issue INCONSISTENT_IDS = Issue.create(
"InconsistentLayout", //$NON-NLS-1$
"Inconsistent Layouts",
"This check ensures that a layout resource which is defined in multiple "
+ "resource folders, specifies the same set of widgets.\n"
+ "\n"
+ "This finds cases where you have accidentally forgotten to add "
+ "a widget to all variations of the layout, which could result "
+ "in a runtime crash for some resource configurations when a "
+ "`findViewById()` fails.\n"
+ "\n"
+ "There *are* cases where this is intentional. For example, you "
+ "may have a dedicated large tablet layout which adds some extra "
+ "widgets that are not present in the phone version of the layout. "
+ "As long as the code accessing the layout resource is careful to "
+ "handle this properly, it is valid. In that case, you can suppress "
+ "this lint check for the given extra or missing views, or the whole "
+ "layout",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
LayoutConsistencyDetector.class,
Scope.JAVA_AND_RESOURCE_FILES));
/** Constructs a consistency check */
public LayoutConsistencyDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root != null) {
if (context.getPhase() == 1) {
// Map from ids to types
Map<String,String> fileMap = Maps.newHashMapWithExpectedSize(10);
addIds(root, fileMap);
getFileMapList(context).add(Pair.of(context.file, fileMap));
} else {
String name = LintUtils.getLayoutName(context.file);
Map<String, List<Location>> map = mLocations.get(name);
if (map != null) {
lookupLocations(context, root, map);
}
}
}
}
@NonNull
private List<Pair<File, Map<String, String>>> getFileMapList(
@NonNull XmlContext context) {
String name = LintUtils.getLayoutName(context.file);
List<Pair<File, Map<String, String>>> list = mMap.get(name);
if (list == null) {
list = Lists.newArrayListWithCapacity(4);
mMap.put(name, list);
}
return list;
}
@Nullable
private static String getId(@NonNull Element element) {
String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
if (id != null && !id.isEmpty() && !id.startsWith(ANDROID_PREFIX)) {
return stripIdPrefix(id);
}
return null;
}
private static void addIds(Element element, Map<String,String> map) {
String id = getId(element);
if (id != null) {
String s = stripIdPrefix(id);
map.put(s, element.getTagName());
}
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
addIds((Element) child, map);
}
}
}
private static void lookupLocations(
@NonNull XmlContext context,
@NonNull Element element,
@NonNull Map<String, List<Location>> map) {
String id = getId(element);
if (id != null) {
if (map.containsKey(id)) {
if (context.getDriver().isSuppressed(context, INCONSISTENT_IDS, element)) {
map.remove(id);
return;
}
List<Location> locations = map.get(id);
if (locations == null) {
locations = Lists.newArrayList();
map.put(id, locations);
}
Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_ID);
assert attr != null;
Location location = context.getLocation(attr);
String folder = context.file.getParentFile().getName();
location.setMessage(String.format("Occurrence in %1$s", folder));
locations.add(location);
}
}
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
lookupLocations(context, (Element) child, map);
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
LintDriver driver = context.getDriver();
if (driver.getPhase() == 1) {
// First phase: gather all the ids and look for consistency issues.
// If any are found, request location computation in phase 2 by
// writing the ids needed for each layout in the {@link #mLocations} map.
for (Map.Entry<String,List<Pair<File,Map<String,String>>>> entry : mMap.entrySet()) {
String layout = entry.getKey();
List<Pair<File, Map<String, String>>> files = entry.getValue();
if (files.size() < 2) {
// No consistency problems for files that don't have resource variations
continue;
}
checkConsistentIds(layout, files);
}
if (mLocations != null) {
driver.requestRepeat(this, Scope.ALL_RESOURCES_SCOPE);
}
} else {
// Collect results and print
if (!mLocations.isEmpty()) {
reportErrors(context);
}
}
}
@NonNull
private Set<String> stripIrrelevantIds(@NonNull Set<String> ids) {
if (!mRelevantIds.isEmpty()) {
Set<String> stripped = new HashSet<String>(ids);
stripped.retainAll(mRelevantIds);
return stripped;
}
return Collections.emptySet();
}
private void checkConsistentIds(
@NonNull String layout,
@NonNull List<Pair<File, Map<String, String>>> files) {
int layoutCount = files.size();
assert layoutCount >= 2;
Map<File, Set<String>> idMap = getIdMap(files, layoutCount);
Set<String> inconsistent = getInconsistentIds(idMap);
if (inconsistent.isEmpty()) {
return;
}
if (mLocations == null) {
mLocations = Maps.newHashMap();
}
if (mErrorMessages == null) {
mErrorMessages = Maps.newHashMap();
}
// Map from each id, to a list of layout folders it is present in
int idCount = inconsistent.size();
Map<String, List<String>> presence = Maps.newHashMapWithExpectedSize(idCount);
Set<String> allLayouts = Sets.newHashSetWithExpectedSize(layoutCount);
for (Map.Entry<File, Set<String>> entry : idMap.entrySet()) {
File file = entry.getKey();
String folder = file.getParentFile().getName();
allLayouts.add(folder);
Set<String> ids = entry.getValue();
for (String id : ids) {
List<String> list = presence.get(id);
if (list == null) {
list = Lists.newArrayListWithExpectedSize(layoutCount);
presence.put(id, list);
}
list.add(folder);
}
}
// Compute lookup maps which will be used in phase 2 to initialize actual
// locations for the id references
Map<String, List<Location>> map = Maps.newHashMapWithExpectedSize(idCount);
mLocations.put(layout, map);
Map<String, String> messages = Maps.newHashMapWithExpectedSize(idCount);
mErrorMessages.put(layout, messages);
for (String id : inconsistent) {
map.put(id, null); // The locations will be filled in during the second phase
// Determine presence description for this id
String message;
List<String> layouts = presence.get(id);
Collections.sort(layouts);
Set<String> missingSet = new HashSet<String>(allLayouts);
missingSet.removeAll(layouts);
List<String> missing = new ArrayList<String>(missingSet);
Collections.sort(missing);
if (layouts.size() < layoutCount / 2) {
message = String.format(
"The id \"%1$s\" in layout \"%2$s\" is only present in the following "
+ "layout configurations: %3$s (missing from %4$s)",
id, layout,
LintUtils.formatList(layouts, Integer.MAX_VALUE),
LintUtils.formatList(missing, Integer.MAX_VALUE));
} else {
message = String.format(
"The id \"%1$s\" in layout \"%2$s\" is missing from the following layout "
+ "configurations: %3$s (present in %4$s)",
id, layout, LintUtils.formatList(missing, Integer.MAX_VALUE),
LintUtils.formatList(layouts, Integer.MAX_VALUE));
}
messages.put(id, message);
}
}
private static Set<String> getInconsistentIds(Map<File, Set<String>> idMap) {
Set<String> union = getAllIds(idMap);
Set<String> inconsistent = new HashSet<String>();
for (Map.Entry<File, Set<String>> entry : idMap.entrySet()) {
Set<String> ids = entry.getValue();
if (ids.size() < union.size()) {
Set<String> missing = new HashSet<String>(union);
missing.removeAll(ids);
inconsistent.addAll(missing);
}
}
return inconsistent;
}
private static Set<String> getAllIds(Map<File, Set<String>> idMap) {
Iterator<Set<String>> iterator = idMap.values().iterator();
assert iterator.hasNext();
Set<String> union = new HashSet<String>(iterator.next());
while (iterator.hasNext()) {
union.addAll(iterator.next());
}
return union;
}
private Map<File, Set<String>> getIdMap(List<Pair<File, Map<String, String>>> files,
int layoutCount) {
Map<File, Set<String>> idMap = new HashMap<File, Set<String>>(layoutCount);
for (Pair<File, Map<String, String>> pair : files) {
File file = pair.getFirst();
Map<String, String> typeMap = pair.getSecond();
Set<String> ids = typeMap.keySet();
idMap.put(file, stripIrrelevantIds(ids));
}
return idMap;
}
private void reportErrors(Context context) {
List<String> layouts = new ArrayList<String>(mLocations.keySet());
Collections.sort(layouts);
for (String layout : layouts) {
Map<String, List<Location>> locationMap = mLocations.get(layout);
Map<String, String> messageMap = mErrorMessages.get(layout);
assert locationMap != null;
assert messageMap != null;
List<String> ids = new ArrayList<String>(locationMap.keySet());
Collections.sort(ids);
for (String id : ids) {
String message = messageMap.get(id);
List<Location> locations = locationMap.get(id);
if (locations != null) {
Location location = chainLocations(locations);
context.report(INCONSISTENT_IDS, location, message);
}
}
}
}
@NonNull
private static Location chainLocations(@NonNull List<Location> locations) {
assert !locations.isEmpty();
// Sort locations by the file parent folders
if (locations.size() > 1) {
Collections.sort(locations, new Comparator<Location>() {
@Override
public int compare(Location location1, Location location2) {
File file1 = location1.getFile();
File file2 = location2.getFile();
String folder1 = file1.getParentFile().getName();
String folder2 = file2.getParentFile().getName();
return folder1.compareTo(folder2);
}
});
// Chain locations together
Iterator<Location> iterator = locations.iterator();
assert iterator.hasNext();
Location prev = iterator.next();
while (iterator.hasNext()) {
Location next = iterator.next();
prev.setSecondary(next);
prev = next;
}
}
return locations.get(0);
}
// ---- Implements UastScanner ----
@Override
public boolean appliesToResourceRefs() {
return true;
}
@Override
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);
}
}
}
@@ -1,256 +0,0 @@
/*
* 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.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.tools.klint.checks.ViewHolderDetector.INFLATE;
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;
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.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
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;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collections;
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 Detector.UastScanner {
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
LayoutInflationDetector.class,
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(
"InflateParams", //$NON-NLS-1$
"Layout Inflation without a Parent",
"When inflating a layout, avoid passing in null as the parent view, since " +
"otherwise any layout parameters on the root of the inflated layout will be ignored.",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("http://www.doubleencore.com/2013/05/layout-inflation-as-intended");
private static final String ERROR_MESSAGE =
"Avoid passing `null` as the view root (needed to resolve "
+ "layout parameters on the inflated layout's root element)";
/** Constructs a new {@link LayoutInflationDetector} check */
public LayoutInflationDetector() {
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPendingErrors != null) {
for (Pair<String,Location> pair : mPendingErrors) {
String inflatedLayout = pair.getFirst();
if (mLayoutsWithRootLayoutParams == null ||
!mLayoutsWithRootLayoutParams.contains(inflatedLayout)) {
// No root layout parameters on the inflated layout: no need to complain
continue;
}
Location location = pair.getSecond();
context.report(ISSUE, location, ERROR_MESSAGE);
}
}
}
// ---- Implements XmlScanner ----
private Set<String> mLayoutsWithRootLayoutParams;
private List<Pair<String,Location>> mPendingErrors;
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
if (root != null) {
NamedNodeMap attributes = root.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
if (attribute.getLocalName() != null
&& attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
if (mLayoutsWithRootLayoutParams == null) {
mLayoutsWithRootLayoutParams = Sets.newHashSetWithExpectedSize(20);
}
mLayoutsWithRootLayoutParams.add(LintUtils.getBaseName(context.file.getName()));
break;
}
}
}
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(INFLATE);
}
@Override
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<UExpression> arguments = call.getValueArguments();
if (arguments.size() < 2) {
return;
}
UExpression second = arguments.get(1);
if (!UastLiteralUtils.isNullLiteral(second)) {
return;
}
UExpression first = arguments.get(0);
AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(first);
if (androidReference == null) {
return;
}
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);
}
}
private static boolean hasLayoutParams(@NonNull JavaContext context, String name) {
LintClient client = context.getClient();
if (!client.supportsProjectResources()) {
return true; // not certain
}
Project project = context.getProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return true; // not certain
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.LAYOUT, name);
if (items == null || items.isEmpty()) {
return false;
}
for (ResourceItem item : items) {
ResourceFile source = item.getSource();
if (source == null) {
return true; // not certain
}
File file = source.getFile();
if (file.exists()) {
try {
String s = context.getClient().readFile(file);
if (hasLayoutParams(new StringReader(s))) {
return true;
}
} catch (Exception e) {
context.log(e, "Could not read/parse inflated layout");
return true; // not certain
}
}
}
return false;
}
@VisibleForTesting
static boolean hasLayoutParams(@NonNull Reader reader)
throws XmlPullParserException, IOException {
KXmlParser parser = new KXmlParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(reader);
while (true) {
int event = parser.next();
if (event == XmlPullParser.START_TAG) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
String prefix = parser.getAttributePrefix(i);
if (prefix != null && !prefix.isEmpty() &&
ANDROID_URI.equals(parser.getNamespace(prefix))) {
return true;
}
}
}
return false;
} else if (event == XmlPullParser.END_DOCUMENT) {
return false;
}
}
}
}
@@ -1,183 +0,0 @@
/*
* 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.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.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 com.intellij.psi.util.InheritanceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
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<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Collections.<Class<? extends PsiElement>>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)) {
String message = "Do not place Android context classes in static fields; "
+ "this is a memory leak (and also breaks Instant Run)";
report(field, 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)) {
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, message);
break;
}
}
}
}
}
private void report(@NonNull UElement element, @NonNull String message) {
mContext.report(ISSUE, element, mContext.getUastLocation(element), message);
}
}
private static boolean isLeakCandidate(@NonNull PsiClass cls) {
return InheritanceUtil.isInheritor(cls, false, CLASS_CONTEXT)
|| InheritanceUtil.isInheritor(cls, false, CLASS_VIEW)
|| InheritanceUtil.isInheritor(cls, false, CLASS_FRAGMENT);
}
}
@@ -1,166 +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 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<String> 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<UExpression> 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);
}
}
}
@@ -1,354 +0,0 @@
/*
* 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.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.*;
import com.intellij.psi.*;
import org.jetbrains.uast.*;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.*;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
/**
* Detector for finding inefficiencies and errors in logging calls.
*/
public class LogDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LogDetector.class, Scope.JAVA_FILE_SCOPE);
/** Log call missing surrounding if */
public static final Issue CONDITIONAL = Issue.create(
"LogConditional", //$NON-NLS-1$
"Unconditional Logging Calls",
"The BuildConfig class (available in Tools 17) provides a constant, \"DEBUG\", " +
"which indicates whether the code is being built in release mode or in debug " +
"mode. In release mode, you typically want to strip out all the logging calls. " +
"Since the compiler will automatically remove all code which is inside a " +
"\"if (false)\" check, surrounding your logging calls with a check for " +
"BuildConfig.DEBUG is a good idea.\n" +
"\n" +
"If you *really* intend for the logging to be present in release mode, you can " +
"suppress this warning with a @SuppressLint annotation for the intentional " +
"logging calls.",
Category.PERFORMANCE,
5,
Severity.WARNING,
IMPLEMENTATION).setEnabledByDefault(false);
/** Mismatched tags between isLogging and log calls within it */
public static final Issue WRONG_TAG = Issue.create(
"LogTagMismatch", //$NON-NLS-1$
"Mismatched Log Tags",
"When guarding a `Log.v(tag, ...)` call with `Log.isLoggable(tag)`, the " +
"tag passed to both calls should be the same. Similarly, the level passed " +
"in to `Log.isLoggable` should typically match the type of `Log` call, e.g. " +
"if checking level `Log.DEBUG`, the corresponding `Log` call should be `Log.d`, " +
"not `Log.i`.",
Category.CORRECTNESS,
5,
Severity.ERROR,
IMPLEMENTATION);
/** Log tag is too long */
public static final Issue LONG_TAG = Issue.create(
"LongLogTag", //$NON-NLS-1$
"Too Long Log Tags",
"Log tags are only allowed to be at most 23 tag characters long.",
Category.CORRECTNESS,
5,
Severity.ERROR,
IMPLEMENTATION);
@SuppressWarnings("SpellCheckingInspection")
private static final String IS_LOGGABLE = "isLoggable"; //$NON-NLS-1$
public static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$
private static final String PRINTLN = "println"; //$NON-NLS-1$
private static final Map<String, String> TAG_PAIRS;
static {
Map<String, String> pairs = new HashMap<String, String>();
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<String> 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);
}
@Override
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;
}
String name = method.getName();
boolean withinConditional = IS_LOGGABLE.equals(name) ||
checkWithinConditional(context, node.getUastParent(), 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.isEnabled(CONDITIONAL)) {
String message = String.format("The log call Log.%1$s(...) should be " +
"conditional: surround with `if (Log.isLoggable(...))` or " +
"`if (BuildConfig.DEBUG) { ... }`",
name);
context.report(CONDITIONAL, node, context.getUastLocation(node), message);
}
// Check tag length
if (context.isEnabled(LONG_TAG)) {
int tagArgumentIndex = PRINTLN.equals(name) ? 1 : 0;
PsiParameterList parameterList = method.getParameterList();
List<UExpression> 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.getUastLocation(node), message);
}
}
}
}
/** Returns true if the given logging call performs "work" to compute the message */
private static boolean performsWork(@NonNull UCallExpression node) {
String referenceName = node.getMethodName();
if (referenceName == null) {
return false;
}
int messageArgumentIndex = PRINTLN.equals(referenceName) ? 2 : 1;
List<UExpression> arguments = node.getValueArguments();
if (arguments.size() > messageArgumentIndex) {
UExpression argument = arguments.get(messageArgumentIndex);
if (argument == null) {
return false;
}
if (argument instanceof ULiteralExpression) {
return false;
}
if (argument instanceof UBinaryExpression) {
String string = UastUtils.evaluateString(argument);
//noinspection VariableNotUsedInsideIf
if (string != null) { // does it resolve to a constant?
return false;
}
} 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
return true;
}
return false;
}
private static boolean checkWithinConditional(
@NonNull JavaContext context,
@Nullable UElement curr,
@NonNull UCallExpression logCall) {
while (curr != null) {
if (curr instanceof UIfExpression) {
UExpression condition = ((UIfExpression) curr).getCondition();
if (condition instanceof UQualifiedReferenceExpression) {
condition = getLastInQualifiedChain((UQualifiedReferenceExpression) condition);
}
if (condition instanceof UCallExpression) {
UCallExpression call = (UCallExpression) condition;
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.getUastParent();
}
return false;
}
/** Checks that the tag passed to Log.s and Log.isLoggable match */
private static void checkTagConsistent(JavaContext context, UCallExpression logCall,
UCallExpression isLoggableCall) {
List<UExpression> isLoggableArguments = isLoggableCall.getValueArguments();
List<UExpression> logArguments = logCall.getValueArguments();
if (isLoggableArguments.isEmpty() || logArguments.isEmpty()) {
return;
}
UExpression isLoggableTag = isLoggableArguments.get(0);
UExpression logTag = logArguments.get(0);
String logCallName = logCall.getMethodName();
if (logCallName == null) {
return;
}
boolean isPrintln = PRINTLN.equals(logCallName);
if (isPrintln && logArguments.size() > 1) {
logTag = logArguments.get(1);
}
if (logTag != null) {
if (!areLiteralsEqual(isLoggableTag, logTag) &&
!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`",
logCallName,
isLoggableDescription,
logCallDescription);
context.report(WRONG_TAG, isLoggableCall, location, message);
}
}
}
// 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 || isLoggableArguments.size() < 2) { // e.g. println
return;
}
UExpression isLoggableLevel = isLoggableArguments.get(1);
if (isLoggableLevel == null) {
return;
}
PsiNamedElement resolved = UastUtils.tryResolveNamed(isLoggableLevel);
if (resolved == null) {
return;
}
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 = 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, isLoggableCall, location, message);
}
}
@NonNull
private static UExpression getLastInQualifiedChain(@NonNull UQualifiedReferenceExpression node) {
UExpression last = node.getSelector();
while (last instanceof UQualifiedReferenceExpression) {
last = ((UQualifiedReferenceExpression) last).getSelector();
}
return last;
}
private static boolean areLiteralsEqual(UExpression first, UExpression second) {
if (!(first instanceof ULiteralExpression)) {
return false;
}
if (!(second instanceof ULiteralExpression)) {
return false;
}
Object firstValue = ((ULiteralExpression) first).getValue();
Object secondValue = ((ULiteralExpression) second).getValue();
if (firstValue == null) {
return secondValue == null;
}
return firstValue.equals(secondValue);
}
}
@@ -1,94 +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 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<String> 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);
}
}
}
@@ -1,176 +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 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.*;
import com.android.tools.klint.detector.api.Location.Handle;
import com.android.utils.Pair;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.*;
import static com.android.SdkConstants.*;
/**
* Checks whether a root FrameLayout can be replaced with a {@code <merge>} tag.
*/
public class MergeRootFrameLayoutDetector extends LayoutDetector implements Detector.UastScanner {
/**
* Set of layouts that we want to enable the warning for. We only warn for
* {@code <FrameLayout>}'s that are the root of a layout included from
* another layout, or directly referenced via a {@code setContentView} call.
*/
private Set<String> mWhitelistedLayouts;
/**
* Set of pending [layout, location] pairs where the given layout is a
* FrameLayout that perhaps should be replaced by a {@code <merge>} tag (if
* the layout is included or set as the content view. This must be processed
* after the whole project has been scanned since the set of includes etc
* can be encountered after the included layout.
*/
private List<Pair<String, Location.Handle>> mPending;
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"MergeRootFrame", //$NON-NLS-1$
"FrameLayout can be replaced with `<merge>` tag",
"If a `<FrameLayout>` is the root of a layout and does not provide background " +
"or padding etc, it can often be replaced with a `<merge>` tag which is slightly " +
"more efficient. Note that this depends on context, so make sure you understand " +
"how the `<merge>` tag works before proceeding.",
Category.PERFORMANCE,
4,
Severity.WARNING,
new Implementation(
MergeRootFrameLayoutDetector.class,
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$
/** Constructs a new {@link MergeRootFrameLayoutDetector} */
public MergeRootFrameLayoutDetector() {
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPending != null && mWhitelistedLayouts != null) {
// Process all the root FrameLayouts that are eligible, and generate
// suggestions for <merge> replacements for any layouts that are included
// from other layouts
for (Pair<String, Handle> pair : mPending) {
String layout = pair.getFirst();
if (mWhitelistedLayouts.contains(layout)) {
Handle handle = pair.getSecond();
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ISSUE, (Node) clientData)) {
return;
}
}
Location location = handle.resolve();
context.report(ISSUE, location,
"This `<FrameLayout>` can be replaced with a `<merge>` tag");
}
}
}
}
// Implements XmlScanner
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(VIEW_INCLUDE, FRAME_LAYOUT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
if (tag.equals(VIEW_INCLUDE)) {
String layout = element.getAttribute(ATTR_LAYOUT); // NOTE: Not in android: namespace
if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts
layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
whiteListLayout(layout);
}
} else {
assert tag.equals(FRAME_LAYOUT);
if (LintUtils.isRootElement(element) &&
((isWidthFillParent(element) && isHeightFillParent(element)) ||
!element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY))
&& !element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_FOREGROUND)
&& !hasPadding(element)) {
String layout = LintUtils.getLayoutName(context.file);
Handle handle = context.createLocationHandle(element);
handle.setClientData(element);
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mPending == null) {
mPending = new ArrayList<Pair<String,Handle>>();
}
mPending.add(Pair.of(layout, handle));
}
}
}
private void whiteListLayout(String layout) {
if (mWhitelistedLayouts == null) {
mWhitelistedLayouts = new HashSet<String>();
}
mWhitelistedLayouts.add(layout);
}
// Implements JavaScanner
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("setContentView"); //$NON-NLS-1$
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
List<UExpression> expressions = call.getValueArguments();
if (expressions.size() == 1) {
AndroidReference androidReference =
UastLintUtils.toAndroidReferenceViaResolve(expressions.get(0));
if (androidReference != null && androidReference.getType() == ResourceType.LAYOUT) {
whiteListLayout(androidReference.getName());
}
}
}
}
@@ -1,100 +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.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.ULiteralExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.ArrayList;
import java.util.List;
/** Detector looking for text messages sent to an unlocalized phone number. */
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$
"SMS phone number missing country code",
"SMS destination numbers must start with a country code or the application code " +
"must ensure that the SMS is only sent when the user is in the same country as " +
"the receiver.",
Category.CORRECTNESS,
5,
Severity.WARNING,
new Implementation(
NonInternationalizedSmsDetector.class,
Scope.JAVA_FILE_SCOPE));
/** Constructs a new {@link NonInternationalizedSmsDetector} check */
public NonInternationalizedSmsDetector() {
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
List<String> methodNames = new ArrayList<String>(2);
methodNames.add("sendTextMessage"); //$NON-NLS-1$
methodNames.add("sendMultipartTextMessage"); //$NON-NLS-1$
return methodNames;
}
@Override
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<UExpression> 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.");
}
}
@@ -1,554 +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_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.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.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.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.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 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 Detector.UastScanner {
private static final String SET_THEME = "setTheme"; //$NON-NLS-1$
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"Overdraw", //$NON-NLS-1$
"Overdraw: Painting regions more than once",
"If you set a background drawable on a root view, then you should use a " +
"custom theme where the theme background is null. Otherwise, the theme background " +
"will be painted first, only to have your custom background completely cover it; " +
"this is called \"overdraw\".\n" +
"\n" +
"NOTE: This detector relies on figuring out which layouts are associated with " +
"which activities based on scanning the Java code, and it's currently doing that " +
"using an inexact pattern matching algorithm. Therefore, it can incorrectly " +
"conclude which activity the layout is associated with and then wrongly complain " +
"that a background-theme is hidden.\n" +
"\n" +
"If you want your custom background on multiple pages, then you should consider " +
"making a custom theme with your custom background and just using that theme " +
"instead of a root element background.\n" +
"\n" +
"Of course it's possible that your custom drawable is translucent and you want " +
"it to be mixed with the background. However, you will get better performance " +
"if you pre-mix the background with your drawable and use that resulting image or " +
"color as a custom theme background instead.\n",
Category.PERFORMANCE,
3,
Severity.WARNING,
new Implementation(
OverdrawDetector.class,
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<String, String> mActivityToTheme;
/** The default theme declared in the manifest, or null */
private String mManifestTheme;
/** Mapping from layout name (not including {@code @layout/} prefix) to activity FQN */
private Map<String, List<String>> mLayoutToActivity;
/** List of theme names registered in the project which have blank backgrounds */
private List<String> mBlankThemes;
/** List of drawable resources that are not flagged for overdraw (XML drawables
* except for {@code <bitmap>} drawables without tiling) */
private List<String> mValidDrawables;
/**
* List of pairs of (location, background drawable) corresponding to root elements
* in layouts that define a given background drawable. These should be checked to
* see if they are painting on top of a non-transparent theme.
*/
private List<Pair<Location, String>> mRootAttributes;
/** Constructs a new {@link OverdrawDetector} */
public OverdrawDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
// Look in layouts for drawable resources
return super.appliesTo(folderType)
// and in resource files for theme definitions
|| folderType == ResourceFolderType.VALUES
// and in drawable files for bitmap tiling modes
|| folderType == ResourceFolderType.DRAWABLE;
}
/** 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$
if (name.contains("NoFrame") //$NON-NLS-1$
|| name.contains("Theme_Wallpaper") //$NON-NLS-1$
|| name.contains("Theme_Holo_Wallpaper") //$NON-NLS-1$
|| name.contains("Theme_Translucent") //$NON-NLS-1$
|| name.contains("Theme_Dialog_NoFrame") //$NON-NLS-1$
|| name.contains("Theme_Holo_Dialog_Alert") //$NON-NLS-1$
|| name.contains("Theme_Holo_Light_Dialog_Alert") //$NON-NLS-1$
|| name.contains("Theme_Dialog_Alert") //$NON-NLS-1$
|| name.contains("Theme_Panel") //$NON-NLS-1$
|| name.contains("Theme_Light_Panel") //$NON-NLS-1$
|| name.contains("Theme_Holo_Panel") //$NON-NLS-1$
|| name.contains("Theme_Holo_Light_Panel")) { //$NON-NLS-1$
return true;
}
}
return mBlankThemes != null && mBlankThemes.contains(name);
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mRootAttributes != null) {
for (Pair<Location, String> pair : mRootAttributes) {
Location location = pair.getFirst();
Object clientData = location.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ISSUE, (Node) clientData)) {
return;
}
}
String layoutName = location.getFile().getName();
if (endsWith(layoutName, DOT_XML)) {
layoutName = layoutName.substring(0, layoutName.length() - DOT_XML.length());
}
String theme = getTheme(context, layoutName);
if (theme == null || !isBlankTheme(theme)) {
String drawable = pair.getSecond();
String message = String.format(
"Possible overdraw: Root element paints background `%1$s` with " +
"a theme that also paints a background (inferred theme is `%2$s`)",
drawable, theme);
// TODO: Compute applicable scope node
context.report(ISSUE, location, message);
}
}
}
}
/** Return the theme to be used for the given layout */
private String getTheme(Context context, String layoutName) {
if (mActivityToTheme != null && mLayoutToActivity != null) {
List<String> activities = mLayoutToActivity.get(layoutName);
if (activities != null) {
for (String activity : activities) {
String theme = mActivityToTheme.get(activity);
if (theme != null) {
return theme;
}
}
}
}
if (mManifestTheme != null) {
return mManifestTheme;
}
Project project = context.getMainProject();
int apiLevel = project.getTargetSdk();
if (apiLevel == -1) {
apiLevel = project.getMinSdk();
}
if (apiLevel >= 11) {
return "@android:style/Theme.Holo"; //$NON-NLS-1$
} else {
return "@android:style/Theme"; //$NON-NLS-1$
}
}
// ---- Implements XmlScanner ----
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
// Ignore tools:background and any other custom attribute that isn't actually the
// android View background attribute
if (!ANDROID_URI.equals(attribute.getNamespaceURI())) {
return;
}
// Only consider the root element's background
Element documentElement = attribute.getOwnerDocument().getDocumentElement();
if (documentElement == attribute.getOwnerElement()) {
// If the drawable is a non-repeated pattern then the overdraw might be
// intentional since the image isn't covering the whole screen
String background = attribute.getValue();
if (mValidDrawables != null && mValidDrawables.contains(background)) {
return;
}
if (background.equals(TRANSPARENT_COLOR) || background.equals(NULL_RESOURCE)) {
return;
}
if (background.startsWith("@android:drawable/")) { //$NON-NLS-1$
// We haven't had a chance to study the builtin drawables the way we
// check the project local ones in scanBitmap() and beforeCheckFile(),
// but many of these are not bitmaps, so ignore these
return;
}
String name = context.file.getName();
if (name.contains("list_") || name.contains("_item")) { //$NON-NLS-1$ //$NON-NLS-2$
// Canonical list_item layout name: don't warn about these, it's
// pretty common to want to paint custom list item backgrounds
return;
}
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
Location location = context.getLocation(attribute);
location.setClientData(attribute);
if (mRootAttributes == null) {
mRootAttributes = new ArrayList<Pair<Location,String>>();
}
mRootAttributes.add(Pair.of(location, attribute.getValue()));
String activity = documentElement.getAttributeNS(TOOLS_URI, ATTR_CONTEXT);
if (activity != null && !activity.isEmpty()) {
if (activity.startsWith(".")) { //$NON-NLS-1$
activity = context.getProject().getPackage() + activity;
}
registerLayoutActivity(LintUtils.getLayoutName(context.file), activity);
}
}
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(
// Layouts: Look for background attributes on root elements for possible overdraw
ATTR_BACKGROUND
);
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
// Manifest: Look at theme registrations
TAG_ACTIVITY,
TAG_APPLICATION,
// Resource files: Look at theme definitions
TAG_STYLE,
// Bitmaps
TAG_BITMAP
);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
if (endsWith(context.file.getName(), DOT_XML)) {
// Drawable XML files should not be considered for overdraw, except for <bitmap>'s.
// The bitmap elements are handled in the scanBitmap() method; it will clear
// out anything added by this method.
File parent = context.file.getParentFile();
ResourceFolderType type = ResourceFolderType.getFolderType(parent.getName());
if (type == ResourceFolderType.DRAWABLE) {
if (mValidDrawables == null) {
mValidDrawables = new ArrayList<String>();
}
String resource = getDrawableResource(context.file);
mValidDrawables.add(resource);
}
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
if (tag.equals(TAG_STYLE)) {
scanTheme(element);
} else if (tag.equals(TAG_ACTIVITY)) {
scanActivity(context, element);
} else if (tag.equals(TAG_APPLICATION)) {
if (element.hasAttributeNS(ANDROID_URI, ATTR_THEME)) {
mManifestTheme = element.getAttributeNS(ANDROID_URI, ATTR_THEME);
}
} else if (tag.equals(TAG_BITMAP)) {
scanBitmap(context, element);
}
}
private static String getDrawableResource(File drawableFile) {
String resource = drawableFile.getName();
if (endsWith(resource, DOT_XML)) {
resource = resource.substring(0, resource.length() - DOT_XML.length());
}
return DRAWABLE_PREFIX + resource;
}
private void scanBitmap(Context context, Element element) {
String tileMode = element.getAttributeNS(ANDROID_URI, ATTR_TILE_MODE);
if (!(tileMode.equals(VALUE_DISABLED) || tileMode.isEmpty())) {
if (mValidDrawables != null) {
String resource = getDrawableResource(context.file);
mValidDrawables.remove(resource);
}
}
}
private void scanActivity(Context context, Element element) {
String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (name.indexOf('$') != -1) {
name = name.replace('$', '.');
}
if (name.startsWith(".")) { //$NON-NLS-1$
String pkg = context.getProject().getPackage();
if (pkg != null && !pkg.isEmpty()) {
name = pkg + name;
}
}
String theme = element.getAttributeNS(ANDROID_URI, ATTR_THEME);
if (theme != null && !theme.isEmpty()) {
if (mActivityToTheme == null) {
mActivityToTheme = new HashMap<String, String>();
}
mActivityToTheme.put(name, getResourceFieldName(theme));
}
}
private void scanTheme(Element element) {
// Look for theme definitions, and record themes that provide a null background.
String styleName = element.getAttribute(ATTR_NAME);
String parent = element.getAttribute(ATTR_PARENT);
if (parent == null) {
// Eclipse DOM workaround
parent = "";
}
if (parent.isEmpty()) {
int index = styleName.lastIndexOf('.');
if (index != -1) {
parent = styleName.substring(0, index);
}
}
parent = parent.replace('.', '_');
String resource = STYLE_RESOURCE_PREFIX + getResourceFieldName(styleName);
NodeList items = element.getChildNodes();
for (int i = 0, n = items.getLength(); i < n; i++) {
if (items.item(i).getNodeType() == Node.ELEMENT_NODE) {
Element item = (Element) items.item(i);
String name = item.getAttribute(ATTR_NAME);
if (name.equals("android:windowBackground")) { //$NON-NLS-1$
NodeList textNodes = item.getChildNodes();
for (int j = 0, m = textNodes.getLength(); j < m; j++) {
Node textNode = textNodes.item(j);
if (textNode.getNodeType() == Node.TEXT_NODE) {
String text = textNode.getNodeValue();
String trim = text.trim();
if (!trim.isEmpty()) {
if (trim.equals(NULL_RESOURCE)
|| trim.equals(TRANSPARENT_COLOR)
|| mValidDrawables != null
&& mValidDrawables.contains(trim)) {
if (mBlankThemes == null) {
mBlankThemes = new ArrayList<String>();
}
mBlankThemes.add(resource);
}
}
}
}
return;
}
}
}
if (isBlankTheme(parent)) {
if (mBlankThemes == null) {
mBlankThemes = new ArrayList<String>();
}
mBlankThemes.add(resource);
}
}
private void registerLayoutActivity(String layout, String classFqn) {
if (mLayoutToActivity == null) {
mLayoutToActivity = new HashMap<String, List<String>>();
}
List<String> list = mLayoutToActivity.get(layout);
if (list == null) {
list = new ArrayList<String>();
mLayoutToActivity.put(layout, list);
}
list.add(classFqn);
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList("android.app.Activity");
}
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>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));
}
}
private class OverdrawVisitor extends AbstractUastVisitor {
private final String mName;
private final PsiClass mCls;
public OverdrawVisitor(String name, PsiClass cls) {
mName = name;
mCls = cls;
}
@Override
public boolean visitClass(UClass node) {
// Don't go into inner classes
if (mCls.equals(node.getPsi())) {
return true;
}
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);
}
return super.visitSimpleNameReferenceExpression(node);
}
@Override
public boolean visitCallExpression(UCallExpression node) {
if (SET_THEME.equals(node.getMethodName()) && node.getValueArgumentCount() == 1) {
// Look at argument
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<String, String>();
}
mActivityToTheme.put(mName, STYLE_RESOURCE_PREFIX + style);
}
}
return super.visitCallExpression(node);
}
}
}
@@ -1,159 +0,0 @@
/*
* 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.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;
/**
* 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 Detector.UastScanner {
/** Are previously-abstract methods all overridden? */
public static final Issue ISSUE = Issue.create(
"OverrideAbstract", //$NON-NLS-1$
"Not overriding abstract methods on older platforms",
"To improve the usability of some APIs, some methods that used to be `abstract` have " +
"been made concrete by adding default implementations. This means that when compiling " +
"with new versions of the SDK, your code does not have to override these methods.\n" +
"\n" +
"However, if your code is also targeting older versions of the platform where these " +
"methods were still `abstract`, the code will crash. You must override all methods " +
"that used to be abstract in any versions targeted by your application's " +
"`minSdkVersion`.",
Category.CORRECTNESS,
6,
Severity.FATAL,
new Implementation(
OverrideConcreteDetector.class,
Scope.JAVA_FILE_SCOPE)
);
// This check is currently hardcoded for the specific case of the
// NotificationListenerService change in API 21. We should consider
// attempting to infer this information automatically from changes in
// the API current.txt file and making this detector more database driven,
// like the API detector.
private static final String NOTIFICATION_LISTENER_SERVICE_FQN
= "android.service.notification.NotificationListenerService";
public static final String STATUS_BAR_NOTIFICATION_FQN
= "android.service.notification.StatusBarNotification";
private static final String ON_NOTIFICATION_POSTED = "onNotificationPosted";
private static final String ON_NOTIFICATION_REMOVED = "onNotificationRemoved";
private static final int CONCRETE_IN = 21;
/** Constructs a new {@link OverrideConcreteDetector} */
public OverrideConcreteDetector() {
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(NOTIFICATION_LISTENER_SERVICE_FQN);
}
@Override
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isAbstract(declaration)) {
return;
}
int minSdk = Math.max(context.getProject().getMinSdk(), getTargetApi(declaration));
if (minSdk >= CONCRETE_IN) {
return;
}
String[] methodNames = {ON_NOTIFICATION_POSTED, ON_NOTIFICATION_REMOVED};
for (String methodName : methodNames) {
boolean found = false;
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
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 (evaluator.isAbstract(method)) {
continue;
}
// Make sure it has the exact right signature
if (method.getParameterList().getParametersCount() != 1) {
continue; // Wrong signature
}
if (!evaluator.parameterHasType(method, 0, STATUS_BAR_NOTIFICATION_FQN)) {
continue;
}
found = true;
break;
}
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.reportUast(ISSUE, declaration, context.getUastNameLocation(declaration), message);
break;
}
}
}
private static int getTargetApi(@NonNull PsiClass node) {
while (node != null) {
int targetApi = ApiDetector.getTargetApi(node.getModifierList());
if (targetApi != -1) {
return targetApi;
}
node = PsiTreeUtil.getParentOfType(node, PsiClass.class, true);
}
return -1;
}
}
@@ -1,106 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.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.intellij.psi.PsiClass;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.util.InheritanceUtil;
import kotlinx.android.parcel.Parcelize;
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 Detector.UastScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"ParcelCreator", //$NON-NLS-1$
"Missing Parcelable `CREATOR` field",
"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.\"",
Category.CORRECTNESS,
3,
Severity.ERROR,
new Implementation(
ParcelDetector.class,
Scope.JAVA_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/reference/android/os/Parcelable.html");
/** Constructs a new {@link ParcelDetector} check */
public ParcelDetector() {
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(CLASS_PARCELABLE);
}
@Override
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
if (declaration instanceof UAnonymousClass) {
// Anonymous classes aren't parcelable
return;
}
// 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 (InheritanceUtil.isInheritor(declaration, false, "android.text.ParcelableSpan")) {
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");
}
}
}
@@ -1,224 +0,0 @@
/*
* 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 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.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiVariable;
import org.jetbrains.uast.*;
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<UExpression> 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) {
UField field = (UField) mContext.getUastContext().convertElementWithParent(resolved, UField.class);
if (field == null) {
return null;
}
if (mOperation == Operation.ACTION) {
UAnnotation annotation = field.findAnnotation(PERMISSION_ANNOTATION);
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;
UAnnotation annotation = field.findAnnotation(fqn);
if (annotation != null) {
List<UNamedExpression> attributes = annotation.getAttributeValues();
UNamedExpression o = attributes.size() == 1 ? attributes.get(0) : null;
if (o != null && o.getExpression() instanceof UAnnotation) {
annotation = (UAnnotation) o.getExpression();
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 UAnnotation annotation) {
PermissionRequirement requirement = PermissionRequirement.create(annotation);
PsiClass containingClass = field.getContainingClass();
String name = containingClass != null
? containingClass.getName() + "." + field.getName()
: field.getName();
assert name != null;
return new Result(mOperation, requirement, name);
}
}
@@ -1,149 +0,0 @@
/*
* 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.annotations.VisibleForTesting;
import com.android.sdklib.AndroidVersion;
import java.util.Collections;
import java.util.Set;
/**
* A {@linkplain PermissionHolder} knows which permissions are held/granted and can look up
* individual permissions and respond to queries about whether they are held or not.
*/
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 &ge; 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 final Set<String> mGrantedPermissions;
private final Set<String> mRevocablePermissions;
private final AndroidVersion mMinSdkVersion;
private final AndroidVersion mTargetSdkVersion;
public SetPermissionLookup(
@NonNull Set<String> grantedPermissions,
@NonNull Set<String> revocablePermissions,
@NonNull AndroidVersion minSdkVersion,
@NonNull AndroidVersion targetSdkVersion) {
mGrantedPermissions = grantedPermissions;
mRevocablePermissions = revocablePermissions;
mMinSdkVersion = minSdkVersion;
mTargetSdkVersion = targetSdkVersion;
}
@VisibleForTesting
public SetPermissionLookup(@NonNull Set<String> grantedPermissions,
@NonNull Set<String> revocablePermissions) {
this(grantedPermissions, revocablePermissions, AndroidVersion.DEFAULT,
AndroidVersion.DEFAULT);
}
@VisibleForTesting
public SetPermissionLookup(@NonNull Set<String> grantedPermissions) {
this(grantedPermissions, Collections.<String>emptySet());
}
@Override
public boolean hasPermission(@NonNull String permission) {
return mGrantedPermissions.contains(permission);
}
@Override
public boolean isRevocable(@NonNull String permission) {
return mRevocablePermissions.contains(permission);
}
@NonNull
@Override
public AndroidVersion getMinSdkVersion() {
return mMinSdkVersion;
}
@NonNull
@Override
public AndroidVersion getTargetSdkVersion() {
return mTargetSdkVersion;
}
/**
* Creates a {@linkplain PermissionHolder} which combines the permissions
* held by the given holder, with the permissions implied by the given
* {@link PermissionRequirement}
*/
@NonNull
public static PermissionHolder join(@NonNull PermissionHolder lookup,
@NonNull PermissionRequirement requirement) {
SetPermissionLookup empty = new SetPermissionLookup(Collections.<String>emptySet(),
Collections.<String>emptySet(), lookup.getMinSdkVersion(),
lookup.getTargetSdkVersion());
return join(lookup, requirement.getMissingPermissions(empty));
}
/**
* Creates a {@linkplain PermissionHolder} which combines the permissions
* held by the given holder, along with a set of additional permission names
*/
@NonNull
public static PermissionHolder join(@NonNull final PermissionHolder lookup,
@Nullable final Set<String> permissions) {
if (permissions != null && !permissions.isEmpty()) {
return new PermissionHolder() {
@Override
public boolean hasPermission(@NonNull String permission) {
return lookup.hasPermission(permission)
|| permissions.contains(permission);
}
@Override
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;
}
}
}
@@ -1,655 +0,0 @@
/*
* 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.annotations.VisibleForTesting;
import com.android.sdklib.AndroidVersion;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.uast.UAnnotation;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static com.android.SdkConstants.ATTR_VALUE;
import static com.android.tools.klint.checks.SupportAnnotationDetector.*;
/**
* A permission requirement is a boolean expression of permission names that a
* caller must satisfy for a given Android API.
*/
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$
protected final UAnnotation annotation;
private int firstApi;
private int lastApi;
@SuppressWarnings("ConstantConditions")
public static final PermissionRequirement NONE = new PermissionRequirement(null) {
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
return true;
}
@Override
public boolean appliesTo(@NonNull PermissionHolder available) {
return false;
}
@Override
public boolean isConditional() {
return false;
}
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
return false;
}
@Override
public String toString() {
return "None";
}
@Override
protected void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set<String> result) {
}
@Override
protected void addRevocablePermissions(@NonNull Set<String> result,
@NonNull PermissionHolder revocable) {
}
@Nullable
@Override
public IElementType getOperator() {
return null;
}
@NonNull
@Override
public Iterable<PermissionRequirement> getChildren() {
return Collections.emptyList();
}
};
private PermissionRequirement(@NonNull UAnnotation annotation) {
this.annotation = annotation;
}
@NonNull
public static PermissionRequirement create(@NonNull UAnnotation annotation) {
String value = getAnnotationStringValue(annotation, ATTR_VALUE);
if (value != null && !value.isEmpty()) {
return new Single(annotation, value);
}
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]);
}
}
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 UAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
UExpression 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 UAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
UExpression 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 UAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
UExpression 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 UAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
UExpression 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 UAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
UExpression attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
if (attributeValue == null) {
return null;
}
if (UastExpressionUtils.isArrayInitializer(attributeValue)) {
List<UExpression> initializers =
((UCallExpression) attributeValue).getValueArguments();
List<String> result = Lists.newArrayListWithCapacity(initializers.size());
ConstantEvaluator constantEvaluator = new ConstantEvaluator(null);
for (UExpression 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
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<String> 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
* example, consider
* {@code android.app.backup.BackupManager.dataChanged(java.lang.String)} .
* Here the {@code android.permission.BACKUP} is required but only if the
* argument is not your own package.
* <p>
* This is used to handle permissions differently between the "missing" and
* "unused" checks. When checking for missing permissions, we err on the
* side of caution: if you are missing a permission, but the permission is
* conditional, you may not need it so we may not want to complain. However,
* when looking for unused permissions, we don't want to flag the
* conditional permissions as unused since they may be required.
*
* @return true if this requirement is conditional
*/
public boolean isConditional() {
Boolean o = getAnnotationBooleanValue(annotation, ATTR_CONDITIONAL);
if (o != null) {
return o;
}
return false;
}
/**
* Returns whether this requirement is for a single permission (rather than
* a boolean expression such as one permission or another.)
*
* @return true if this requirement is just a simple permission name
*/
public boolean isSingle() {
return true;
}
/**
* Whether the permission requirement is satisfied given the set of granted permissions
*
* @param available the available permissions
* @return true if all permissions specified by this requirement are available
*/
public abstract boolean isSatisfied(@NonNull PermissionHolder available);
/** Describes the missing permissions (e.g. "P1, P2 and P3") */
public String describeMissingPermissions(@NonNull PermissionHolder available) {
return "";
}
/** Returns the missing permissions (e.g. {"P1", "P2", "P3"} */
public Set<String> getMissingPermissions(@NonNull PermissionHolder available) {
Set<String> result = Sets.newHashSet();
addMissingPermissions(available, result);
return result;
}
protected abstract void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set<String> result);
/** Returns the permissions in the requirement that are revocable */
public Set<String> getRevocablePermissions(@NonNull PermissionHolder revocable) {
Set<String> result = Sets.newHashSet();
addRevocablePermissions(result, revocable);
return result;
}
protected abstract void addRevocablePermissions(@NonNull Set<String> result,
@NonNull PermissionHolder revocable);
/**
* Returns whether this permission is revocable
*
* @param revocable the set of revocable permissions
* @return true if a user can revoke the permission
*/
public abstract boolean isRevocable(@NonNull PermissionHolder revocable);
/**
* For permission requirements that combine children, the operator to combine them with; null
* for leaf nodes
*/
@Nullable
public abstract IElementType getOperator();
/**
* Returns nested requirements, combined via {@link #getOperator()}
*/
@NonNull
public abstract Iterable<PermissionRequirement> getChildren();
/** Require a single permission */
private static class Single extends PermissionRequirement {
public final String name;
public Single(@NonNull UAnnotation annotation, @NonNull String name) {
super(annotation);
this.name = name;
}
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
return revocable.isRevocable(name) || isRevocableSystemPermission(name);
}
@Nullable
@Override
public IElementType getOperator() {
return null;
}
@NonNull
@Override
public Iterable<PermissionRequirement> getChildren() {
return Collections.emptyList();
}
@Override
public boolean isSingle() {
return true;
}
@Override
public String toString() {
return name;
}
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
return available.hasPermission(name) || !appliesTo(available);
}
@Override
public String describeMissingPermissions(@NonNull PermissionHolder available) {
return isSatisfied(available) ? "" : name;
}
@Override
protected void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set<String> missing) {
if (!isSatisfied(available)) {
missing.add(name);
}
}
@Override
protected void addRevocablePermissions(@NonNull Set<String> result,
@NonNull PermissionHolder revocable) {
if (isRevocable(revocable)) {
result.add(name);
}
}
}
protected static void appendOperator(StringBuilder sb, IElementType operator) {
sb.append(' ');
if (operator == JavaTokenType.ANDAND) {
sb.append("and");
} else if (operator == JavaTokenType.OROR) {
sb.append("or");
} else {
assert operator == JavaTokenType.XOR : operator;
sb.append("xor");
}
sb.append(' ');
}
/**
* Require a series of permissions, all with the same operator.
*/
private static class Many extends PermissionRequirement {
public final IElementType operator;
public final List<PermissionRequirement> permissions;
public Many(
@NonNull UAnnotation annotation,
IElementType operator,
String[] names) {
super(annotation);
assert operator == JavaTokenType.OROR
|| operator == JavaTokenType.ANDAND : operator;
assert names.length >= 2;
this.operator = operator;
this.permissions = Lists.newArrayListWithExpectedSize(names.length);
for (String name : names) {
permissions.add(new Single(annotation, name));
}
}
@Override
public boolean isSingle() {
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(permissions.get(0));
for (int i = 1; i < permissions.size(); i++) {
appendOperator(sb, operator);
sb.append(permissions.get(i));
}
return sb.toString();
}
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
if (operator == JavaTokenType.ANDAND) {
for (PermissionRequirement requirement : permissions) {
if (!requirement.isSatisfied(available) && requirement.appliesTo(available)) {
return false;
}
}
return true;
} else {
assert operator == JavaTokenType.OROR : operator;
for (PermissionRequirement requirement : permissions) {
if (requirement.isSatisfied(available) || !requirement.appliesTo(available)) {
return true;
}
}
return false;
}
}
@Override
public String describeMissingPermissions(@NonNull PermissionHolder available) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (PermissionRequirement requirement : permissions) {
if (!requirement.isSatisfied(available)) {
if (first) {
first = false;
} else {
appendOperator(sb, operator);
}
sb.append(requirement.describeMissingPermissions(available));
}
}
return sb.toString();
}
@Override
protected void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set<String> missing) {
for (PermissionRequirement requirement : permissions) {
if (!requirement.isSatisfied(available)) {
requirement.addMissingPermissions(available, missing);
}
}
}
@Override
protected void addRevocablePermissions(@NonNull Set<String> result,
@NonNull PermissionHolder revocable) {
for (PermissionRequirement requirement : permissions) {
requirement.addRevocablePermissions(result, revocable);
}
}
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
// TODO: Pass in the available set of permissions here, and if
// 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.
for (PermissionRequirement requirement : permissions) {
if (requirement.isRevocable(revocable)) {
return true;
}
}
return false;
}
@Nullable
@Override
public IElementType getOperator() {
return operator;
}
@NonNull
@Override
public Iterable<PermissionRequirement> getChildren() {
return permissions;
}
}
/**
* Returns true if the given permission name is a revocable permission for
* targetSdkVersion &ge; 23
*
* @param name permission name
* @return true if this is a revocable permission
*/
public static boolean isRevocableSystemPermission(@NonNull String name) {
return Arrays.binarySearch(REVOCABLE_PERMISSION_NAMES, name) >= 0;
}
@VisibleForTesting
static final String[] REVOCABLE_PERMISSION_NAMES = new String[] {
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.BODY_SENSORS",
"android.permission.CALL_PHONE",
"android.permission.CAMERA",
"android.permission.PROCESS_OUTGOING_CALLS",
"android.permission.READ_CALENDAR",
"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",
"android.permission.READ_SOCIAL_STREAM",
"android.permission.RECEIVE_MMS",
"android.permission.RECEIVE_SMS",
"android.permission.RECEIVE_WAP_PUSH",
"android.permission.RECORD_AUDIO",
"android.permission.SEND_SMS",
"android.permission.USE_FINGERPRINT",
"android.permission.USE_SIP",
"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",
};
}

Some files were not shown because too many files have changed in this diff Show More