Remove 173 bunch files
This commit is contained in:
-77
@@ -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 {
|
||||
}
|
||||
-47
@@ -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 {
|
||||
}
|
||||
-50
@@ -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;
|
||||
}
|
||||
-30
@@ -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();
|
||||
}
|
||||
-29
@@ -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 {
|
||||
}
|
||||
-65
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-81
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -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() {
|
||||
}
|
||||
}
|
||||
-608
@@ -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);
|
||||
}
|
||||
}
|
||||
-25
@@ -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();
|
||||
}
|
||||
}
|
||||
-236
@@ -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
-1716
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
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;
|
||||
}
|
||||
}
|
||||
-207
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-245
@@ -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.
|
||||
}
|
||||
-1088
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
-1924
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
-68
@@ -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
-189
@@ -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));
|
||||
}
|
||||
}
|
||||
-70
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
-80
@@ -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);
|
||||
}
|
||||
}
|
||||
-690
@@ -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;
|
||||
}
|
||||
}
|
||||
-61
@@ -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,
|
||||
// Sadly Guava's HtmlEscapes don't handle this either.
|
||||
if (entity.equalsIgnoreCase(" ")) {
|
||||
s = " ";
|
||||
} else if (entity.startsWith("&#")) {
|
||||
try {
|
||||
int value = Integer.parseInt(entity.substring(2));
|
||||
s = Character.toString((char)value);
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append(s);
|
||||
i = end;
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
} else if (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("<"); //$NON-NLS-1$
|
||||
} else if (c == '&') {
|
||||
sb.append("&"); //$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(" "); //$NON-NLS-1$
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = start; i < end; i++) {
|
||||
char c = text.charAt(i);
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-417
@@ -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);
|
||||
}
|
||||
}
|
||||
-94
@@ -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;
|
||||
}
|
||||
}
|
||||
-105
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-221
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-399
@@ -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};
|
||||
}
|
||||
}
|
||||
-865
@@ -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;
|
||||
}
|
||||
}
|
||||
-174
@@ -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;
|
||||
}
|
||||
}
|
||||
-744
@@ -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;
|
||||
}
|
||||
}
|
||||
-141
@@ -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";
|
||||
}
|
||||
}
|
||||
-208
@@ -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();
|
||||
}
|
||||
}
|
||||
-191
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-105
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-537
@@ -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;
|
||||
}
|
||||
|
||||
-140
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-339
@@ -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 & 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-113
@@ -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;
|
||||
}
|
||||
}
|
||||
-151
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-99
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
-527
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-140
@@ -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;
|
||||
}
|
||||
}
|
||||
-442
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-256
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-176
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-100
@@ -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.");
|
||||
}
|
||||
}
|
||||
-554
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-159
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
-224
@@ -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);
|
||||
}
|
||||
}
|
||||
-149
@@ -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 ≥ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-655
@@ -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 ≥ 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
Reference in New Issue
Block a user