package butterknife.lint; 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.LintUtils; import com.android.tools.lint.detector.api.Scope; import com.android.tools.lint.detector.api.Severity; import com.google.common.collect.ImmutableSet; import com.intellij.psi.JavaElementVisitor; import com.intellij.psi.JavaRecursiveElementVisitor; import com.intellij.psi.PsiAnnotation; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiExpression; import com.intellij.psi.PsiReferenceExpression; import java.util.Collections; import java.util.List; import java.util.Set; /** * Custom lint rule to make sure that generated R2 is not referenced outside annotations. */ public class InvalidR2UsageDetector extends Detector implements Detector.JavaPsiScanner { private static final String LINT_ERROR_BODY = "R2 should only be used inside annotations"; private static final String LINT_ERROR_TITLE = "Invalid usage of R2"; private static final String ISSUE_ID = "InvalidR2Usage"; private static final Set<String> SUPPORTED_TYPES = ImmutableSet.of("array", "attr", "bool", "color", "dimen", "drawable", "id", "integer", "string"); static final Issue ISSUE = Issue.create(ISSUE_ID, LINT_ERROR_TITLE, LINT_ERROR_BODY, Category.CORRECTNESS, 6, Severity.ERROR, new Implementation(InvalidR2UsageDetector.class, Scope.JAVA_FILE_SCOPE)); private static final String R2 = "R2"; @Override public List<Class<? extends PsiElement>> getApplicablePsiTypes() { return Collections.<Class<? extends PsiElement>>singletonList(PsiClass.class); } @Override public JavaElementVisitor createPsiVisitor(final JavaContext context) { return new JavaElementVisitor() { @Override public void visitClass(PsiClass node) { node.accept(new R2UsageVisitor(context)); } }; } private static class R2UsageVisitor extends JavaRecursiveElementVisitor { private final JavaContext context; R2UsageVisitor(JavaContext context) { this.context = context; } @Override public void visitAnnotation(PsiAnnotation annotation) { // skip annotations } @Override public void visitReferenceExpression(PsiReferenceExpression expression) { detectR2(context, expression); super.visitReferenceExpression(expression); } private static void detectR2(JavaContext context, PsiElement node) { PsiClass[] classes = context.getJavaFile().getClasses(); if (classes.length > 0 && classes[0].getName() != null) { String qualifiedName = classes[0].getName(); if (qualifiedName.contains("_ViewBinder") || qualifiedName.contains("_ViewBinding") || qualifiedName.equals(R2)) { // skip generated files and R2 return; } } boolean isR2 = isR2Expression(node); if (isR2 && !context.isSuppressedWithComment(node, ISSUE)) { context.report(ISSUE, node, context.getLocation(node), LINT_ERROR_BODY); } } private static boolean isR2Expression(PsiElement node) { if (node.getParent() == null) { return false; } String text = node.getText(); PsiElement parent = LintUtils.skipParentheses(node.getParent()); return (text.equals(R2) || text.contains(".R2")) && parent instanceof PsiExpression && endsWithAny(parent.getText(), SUPPORTED_TYPES); } private static boolean endsWithAny(String text, Set<String> possibleValues) { String[] tokens = text.split("\\."); return tokens.length > 1 && possibleValues.contains(tokens[tokens.length - 1]); } } }