Skip to content

Commit

Permalink
Merge pull request #227 from eyedol/226-update-custom-rules-to-new-api
Browse files Browse the repository at this point in the history
Migrate from lombok API to Psi APIs
  • Loading branch information
hotchemi committed Dec 7, 2016
2 parents b90696c + 96df703 commit 263bf45
Show file tree
Hide file tree
Showing 10 changed files with 518 additions and 145 deletions.
1 change: 1 addition & 0 deletions build.gradle
Expand Up @@ -14,5 +14,6 @@ buildscript {
allprojects {
repositories {
jcenter()
mavenCentral()
}
}
4 changes: 2 additions & 2 deletions gradle.properties
Expand Up @@ -11,7 +11,7 @@ WEBSITE = https://github.com/hotchemi/PermissionsDispatcher
LICENCES = ['Apache-2.0']

# Plugin versions
GRADLE_PLUGIN_VERSION=2.2.0
GRADLE_PLUGIN_VERSION=2.2.2
KOTLIN_VERSION=1.0.4
APT_PLUGIN_VERSION=1.8
BINTRAY_PLUGIN_VERSION=0.3.4
Expand All @@ -25,7 +25,7 @@ MOCKITO_VERSION=1.10.19
POWERMOCK_VERSION=1.6.4
COMPILE_TESTING_VERSION=0.6
GOOGLE_ANDROID_VERSION=4.1.1.4
LINT_VERSION=24.5.0
LINT_VERSION=25.2.0

# Android configuration
COMPILE_SDK_VERSION=android-23
Expand Down
@@ -1,55 +1,81 @@
package permissions.dispatcher;


import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lombok.ast.Annotation;
import lombok.ast.AstVisitor;
import lombok.ast.ClassDeclaration;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodInvocation;

public class CallNeedsPermissionDetector extends Detector implements Detector.JavaPsiScanner {

public class CallNeedsPermissionDetector extends Detector implements Detector.JavaScanner {
public static final Issue ISSUE = Issue.create("CallNeedsPermission",
"Call the corresponding \"withCheck\" method of the generated PermissionsDispatcher class instead",
"Directly invoking a method annotated with @NeedsPermission may lead to misleading behaviour on devices running Marshmallow and up. Therefore, it is advised to use the generated PermissionsDispatcher class instead, which provides a \"withCheck\" method that safely handles runtime permissions.",
Category.CORRECTNESS,
7,
Severity.ERROR,
new Implementation(CallNeedsPermissionDetector.class, EnumSet.of(Scope.ALL_JAVA_FILES)));
new Implementation(CallNeedsPermissionDetector.class,
EnumSet.of(Scope.ALL_JAVA_FILES)));

static List<String> generatedClassNames = new ArrayList<String>();

static List<String> methods = Collections.emptyList();

@Override
public AstVisitor createJavaVisitor(JavaContext context) {
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Collections.<Class<? extends PsiElement>>singletonList(PsiClass.class);
}

public CallNeedsPermissionDetector() {
// No-op
}

@Override
public JavaElementVisitor createPsiVisitor(final JavaContext context) {
if (context.getPhase() == 1) {
// find out which class has RuntimePermissions
return new AnnotationChecker(context);
} else if (context.getPhase() == 2) {
// find out which class call method with NeedPermission
// exclude class with above with name XxxPermissionsDispatcher in the same package.
return new MethodCallChecker(context);
}
return null;
}

private static class AnnotationChecker extends ForwardingAstVisitor {
@Override
public List<String> getApplicableMethodNames() {
return methods;
}

@Override
public void visitMethod(JavaContext context, JavaElementVisitor visitor,
PsiMethodCallExpression node, PsiMethod method) {
if (methods.contains(method.getName())) {
context.report(ISSUE, node, context.getLocation(node),
"Trying to access permission-protected method directly");
}
}

private static class AnnotationChecker extends JavaElementVisitor {

private final JavaContext context;
private Set<String> matchingAnnotationTypeNames;

private final Set<String> matchingAnnotationTypeNames;

private AnnotationChecker(JavaContext context) {
this.context = context;
Expand All @@ -60,53 +86,43 @@ private AnnotationChecker(JavaContext context) {
}

@Override
public boolean visitAnnotation(Annotation node) {
public void visitReferenceExpression(PsiReferenceExpression expression) {
skipGeneratedFiles(context);
super.visitReferenceExpression(expression);
}

@Override
public void visitAnnotation(PsiAnnotation annotation) {
if (!context.isEnabled(ISSUE)) {
return super.visitAnnotation(node);
super.visitAnnotation(annotation);
return;
}

String type = node.astAnnotationTypeReference().getTypeName();
String type = annotation.getQualifiedName();
if (!matchingAnnotationTypeNames.contains(type)) {
return super.visitAnnotation(node);
super.visitAnnotation(annotation);
return;
}

JavaParser.ResolvedNode resolvedNode = context.resolve(node.getParent());
if (resolvedNode instanceof JavaParser.ResolvedClass) {
generatedClassNames.add(resolvedNode.getName() + "PermissionsDispatcher");
PsiClass[] classes = context.getJavaFile().getClasses();
if (classes.length > 0 && classes[0].getName() != null) {
generatedClassNames.add(classes[0].getName() + "PermissionsDispatcher");
// let's check method call!
context.requestRepeat(new CallNeedsPermissionDetector(), EnumSet.of(Scope.ALL_JAVA_FILES));
context.requestRepeat(new CallNeedsPermissionDetector(),
EnumSet.of(Scope.ALL_JAVA_FILES));
}
return super.visitAnnotation(node);
}
}

private class MethodCallChecker extends ForwardingAstVisitor {
JavaContext javaContext;

public MethodCallChecker(JavaContext context) {
javaContext = context;
}

@Override
public boolean visitClassDeclaration(ClassDeclaration node) {
// Ignore a class that is generated by PermissionsDispatcher
return generatedClassNames.contains(javaContext.resolve(node).getName());
super.visitAnnotation(annotation);
}

@Override
public boolean visitMethodInvocation(MethodInvocation node) {
JavaParser.ResolvedNode resolved = javaContext.resolve(node);
if (!(resolved instanceof JavaParser.ResolvedMethod)) {
return super.visitMethodInvocation(node);
}
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved;
JavaParser.ResolvedAnnotation annotation = method.getAnnotation("permissions.dispatcher.NeedsPermission");
if (annotation == null) {
return super.visitMethodInvocation(node);
private static void skipGeneratedFiles(JavaContext context) {
PsiClass[] classes = context.getJavaFile().getClasses();
if (classes.length > 0 && classes[0].getName() != null) {
String qualifiedName = classes[0].getName();
if (qualifiedName.contains("PermissionsDispatcher")) {
// skip generated files
return;
}
}

javaContext.report(ISSUE, javaContext.getLocation(node), "Trying to access permission-protected method directly");
return super.visitMethodInvocation(node);
}
}
}
Expand Up @@ -7,109 +7,121 @@
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;

import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiStatement;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lombok.ast.Annotation;
import lombok.ast.AstVisitor;
import lombok.ast.ClassDeclaration;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.VariableReference;
public class CallOnRequestPermissionsResultDetector extends Detector
implements Detector.JavaPsiScanner {

public class CallOnRequestPermissionsResultDetector extends Detector implements Detector.ClassScanner {
public static final Issue ISSUE = Issue.create("NeedOnRequestPermissionsResult",
"Call the \"onRequestPermissionsResult\" method of the generated PermissionsDispatcher class in the respective method of your Activity or Fragment",
"You are required to inform the generated PermissionsDispatcher class about the results of a permission request. In your class annotated with @RuntimePermissions, override the \"onRequestPermissionsResult\" method and call through to the generated PermissionsDispatcher method with the same name.",
Category.CORRECTNESS,
5,
Severity.ERROR,
new Implementation(CallOnRequestPermissionsResultDetector.class, EnumSet.of(Scope.CLASS_FILE)));
new Implementation(CallOnRequestPermissionsResultDetector.class,
EnumSet.of(Scope.JAVA_FILE)));

static final Set<String> RUNTIME_PERMISSIONS_NAME = new HashSet<String>() {{
add("RuntimePermissions");
add("permissions.dispatcher.RuntimePermissions");
}};

public CallOnRequestPermissionsResultDetector() {
// No-op
}

@Override
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Arrays.asList(PsiAnnotation.class, PsiClass.class);
}

@Override
public AstVisitor createJavaVisitor(JavaContext context) {
return new OnRequestPermissionsResultChecker(context);
public JavaElementVisitor createPsiVisitor(final JavaContext context) {
return new JavaElementVisitor() {
@Override
public void visitClass(PsiClass node) {
node.accept(new OnRequestPermissionsResultChecker(context, node));
}
};
}

private static class OnRequestPermissionsResultChecker extends ForwardingAstVisitor {
private static class OnRequestPermissionsResultChecker extends JavaRecursiveElementVisitor {

private final JavaContext context;

private boolean hasRuntimePermissionAnnotation;
private boolean hasOnRequestPermissionResultCall;
private String generatedClassName;
private ClassDeclaration classDeclaration;

private OnRequestPermissionsResultChecker(JavaContext context) {
this.context = context;
}
private String generatedClassName;

@Override
public boolean visitClassDeclaration(ClassDeclaration node) {
if (!context.isEnabled(ISSUE)) {
// stop executing lint for this class
return true;
}
private PsiClass psiClass;

classDeclaration = node;
generatedClassName = node.astName() + "PermissionsDispatcher";
return super.visitClassDeclaration(node);
private OnRequestPermissionsResultChecker(JavaContext context, PsiClass psiClass) {
this.context = context;
this.psiClass = psiClass;
}

@Override
public boolean visitAnnotation(Annotation node) {
String type = node.astAnnotationTypeReference().getTypeName();
public void visitAnnotation(PsiAnnotation annotation) {
String type = annotation.getQualifiedName();
if (!RUNTIME_PERMISSIONS_NAME.contains(type)) {
return super.visitAnnotation(node);
super.visitAnnotation(annotation);
return;
}

hasRuntimePermissionAnnotation = true;
return super.visitAnnotation(node);
super.visitAnnotation(annotation);
}

@Override
public boolean visitMethodDeclaration(MethodDeclaration node) {
if (hasRuntimePermissionAnnotation && "public void onRequestPermissionsResult(int, java.lang.String[], int[])".equals(context.resolve(node).getSignature().trim())) {
return super.visitMethodDeclaration(node);
} else {
// ignore this node
return true;
public void visitMethod(PsiMethod method) {
if (!hasRuntimePermissionAnnotation) {
super.visitMethod(method);
return;
}
}

@Override
public boolean visitMethodInvocation(MethodInvocation node) {
if (!hasRuntimePermissionAnnotation || hasOnRequestPermissionResultCall) {
return super.visitMethodInvocation(node);
if (!"onRequestPermissionsResult".equals(method.getName())) {
super.visitMethod(method);
return;
}

if (!"onRequestPermissionsResult".equals(node.astName().astValue())) {
return super.visitMethodInvocation(node);
if (hasRuntimePermissionAnnotation && !checkMethodCall(method, psiClass)) {
PsiCodeBlock codeBlock = method.getBody();
context.report(ISSUE, context.getLocation(method),
codeBlock.getText()
+ "Generated onRequestPermissionsResult method not called");
}

if (node.astOperand() instanceof VariableReference) {
VariableReference ref = (VariableReference) node.astOperand();
if (generatedClassName.equals(ref.astIdentifier().astValue())) {
hasOnRequestPermissionResultCall = true;
}
}
super.visitMethod(method);

return super.visitMethodInvocation(node);
}
}

@Override
public void afterVisitClassDeclaration(ClassDeclaration node) {
if (hasRuntimePermissionAnnotation && !hasOnRequestPermissionResultCall) {
context.report(ISSUE, context.getLocation(classDeclaration), "Generated onRequestPermissionsResult method not called");
private static boolean checkMethodCall(PsiMethod method, PsiClass psiClass) {
// FIXME: I'm sure there is a better way of checking if the on onRequestPermissionsResult
// method from the generated class is being called.
PsiCodeBlock codeBlock = method.getBody();
PsiStatement[] statements = codeBlock.getStatements();
for (int i = 0; i < statements.length; i++) {
if (statements[i].getText()
.startsWith(psiClass.getName()
+ "PermissionsDispatcher.onRequestPermissionsResult")) {
return true;
}
super.afterVisitClassDeclaration(node);
}
return false;
}

}

0 comments on commit 263bf45

Please sign in to comment.