
com.android.tools.lint.checks.FirebaseAnalyticsDetector Maven / Gradle / Ivy
The newest version!
/*
* 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.lint.checks;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ConstantEvaluator;
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.Location;
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.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiReturnStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiTreeUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class FirebaseAnalyticsDetector extends Detector implements Detector.JavaPsiScanner {
private static final int EVENT_NAME_MAX_LENGTH = 32;
private static final int EVENT_PARAM_NAME_MAX_LENGTH = 24;
private static final Implementation IMPLEMENTATION = new Implementation(
FirebaseAnalyticsDetector.class,
Scope.JAVA_FILE_SCOPE);
public static final Issue INVALID_NAME = Issue.create(
"InvalidAnalyticsName",
"Invalid Analytics Name",
"Event names and parameters must follow the naming conventions defined in the" +
"`FirebaseAnalytics#logEvent()` documentation.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPLEMENTATION)
.addMoreInfo(
"http://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics#logEvent(java.lang.String,%20android.os.Bundle)");
/**
* Constructs a new {@link FirebaseAnalyticsDetector}
*/
public FirebaseAnalyticsDetector() {
}
// This list is taken from:
// https://developers.google.com/android/reference/com/google/firebase/analytics/FirebaseAnalytics.Event
private static boolean isReservedEventName(@NonNull String name) {
switch (name) {
case "app_clear_data":
case "app_uninstall":
case "app_update":
case "error":
case "first_open":
case "in_app_purchase":
case "notification_dismiss":
case "notification_foreground":
case "notification_open":
case "notification_receive":
case "os_update":
case "session_start":
case "user_engagement":
return true;
default:
return false;
}
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
@NonNull PsiMethodCallExpression call, @NonNull PsiMethod method) {
String firebaseAnalytics = "com.google.firebase.analytics.FirebaseAnalytics";
if (!context.getEvaluator().isMemberInClass(method, firebaseAnalytics)) {
return;
}
PsiExpression[] expressions = call.getArgumentList().getExpressions();
if (expressions.length < 2) {
return;
}
PsiElement firstArgumentExpression = expressions[0];
String value = ConstantEvaluator.evaluateString(context, firstArgumentExpression, false);
if (value == null) {
return;
}
String error = getErrorForEventName(value);
if (error != null) {
context.report(INVALID_NAME, call, context.getLocation(call), error);
}
PsiExpression secondParameter = expressions[1];
List bundleModifications = getBundleModifications(context,
secondParameter);
if (bundleModifications != null && !bundleModifications.isEmpty()) {
validateEventParameters(context, bundleModifications, call);
}
}
private static void validateEventParameters(JavaContext context,
List parameters,
PsiElement call) {
for (BundleModification bundleModification : parameters) {
String error = getErrorForEventParameterName(bundleModification.mName);
if (error != null) {
Location location = context.getLocation(call);
location.withSecondary(context.getLocation(bundleModification.mLocation), error);
context.report(INVALID_NAME, call, location,
"Bundle with invalid Analytics event parameters passed to logEvent.");
}
}
}
@Nullable
private static List getBundleModifications(JavaContext context,
PsiExpression secondParameter) {
PsiType type = secondParameter.getType();
if (type != null && !type.getCanonicalText().equals(SdkConstants.CLASS_BUNDLE)) {
return null;
}
if (secondParameter instanceof PsiNewExpression) {
return Collections.emptyList();
}
List modifications = null;
if (secondParameter instanceof PsiReferenceExpression) {
PsiReferenceExpression bundleReference = (PsiReferenceExpression) secondParameter;
modifications = BundleModificationFinder.find(context, bundleReference);
}
return modifications;
}
/**
* Given a reference to an instance of Bundle, find the putString method calls that modify the
* bundle.
*
* This will recursively search across files within the project.
*/
private static class BundleModificationFinder extends JavaRecursiveElementVisitor {
private final PsiReferenceExpression mBundleReference;
private final JavaContext mContext;
private final List mParameters = new ArrayList<>();
private BundleModificationFinder(JavaContext context,
PsiReferenceExpression bundleReference) {
mContext = context;
mBundleReference = bundleReference;
}
@Override
public void visitDeclarationStatement(PsiDeclarationStatement statement) {
for (PsiElement element : statement.getDeclaredElements()) {
if (!(element instanceof PsiLocalVariable)) {
continue;
}
PsiLocalVariable local = (PsiLocalVariable) element;
String name = local.getName();
if (name == null || !name.equals(mBundleReference.getText())) {
continue;
}
if (!(local.getInitializer() instanceof PsiMethodCallExpression)) {
continue;
}
PsiMethodCallExpression call = (PsiMethodCallExpression) local.getInitializer();
PsiReferenceExpression returnReference = ReturnReferenceExpressionFinder
.find(call.resolveMethod());
if (returnReference != null) {
addParams(find(mContext, returnReference));
}
}
}
@Override
public void visitMethodCallExpression(PsiMethodCallExpression expression) {
String method = expression.getMethodExpression().getCanonicalText();
if (!method.endsWith(".putString") && !method.endsWith(".putLong") && !method
.endsWith(".putDouble")) {
return;
}
PsiElement token = expression.getMethodExpression().getQualifier();
if (token == null || !mBundleReference.textMatches(token)) {
return;
}
PsiExpression[] expressions = expression.getArgumentList().getExpressions();
String evaluatedName = ConstantEvaluator.evaluateString(mContext,
expressions[0], false);
if (evaluatedName != null) {
addParam(evaluatedName, expressions[1].getText(), expression);
}
}
private void addParam(String key, String value, PsiMethodCallExpression location) {
mParameters.add(new BundleModification(key, value, location));
}
private void addParams(Collection bundleModifications) {
mParameters.addAll(bundleModifications);
}
@NonNull
static List find(JavaContext context,
PsiReferenceExpression bundleReference) {
BundleModificationFinder scanner = new BundleModificationFinder(context,
bundleReference);
PsiMethod enclosingMethod = PsiTreeUtil
.getParentOfType(bundleReference, PsiMethod.class);
if (enclosingMethod == null) {
return Collections.emptyList();
}
enclosingMethod.accept(scanner);
return scanner.mParameters;
}
}
/**
* Given a method, find the last `return` expression that returns a reference.
*/
@SuppressWarnings("UnsafeReturnStatementVisitor")
private static class ReturnReferenceExpressionFinder extends JavaRecursiveElementVisitor {
private PsiReferenceExpression mReturnReference = null;
@Override
public void visitReturnStatement(PsiReturnStatement statement) {
PsiExpression returnExpression = statement.getReturnValue();
if (returnExpression instanceof PsiReferenceExpression) {
mReturnReference = (PsiReferenceExpression) returnExpression;
}
}
@Nullable
static PsiReferenceExpression find(PsiMethod method) {
ReturnReferenceExpressionFinder finder = new ReturnReferenceExpressionFinder();
method.accept(finder);
return finder.mReturnReference;
}
}
private static class BundleModification {
public final String mName;
@SuppressWarnings("unused")
public final String mValue;
public final PsiMethodCallExpression mLocation;
public BundleModification(String name, String value,
PsiMethodCallExpression location) {
mName = name;
mValue = value;
mLocation = location;
}
}
@Nullable
@Override
public List getApplicableMethodNames() {
return Collections.singletonList("logEvent");
}
@Nullable
private static String getErrorForEventName(String eventName) {
if (eventName.length() > EVENT_NAME_MAX_LENGTH) {
String message = "Analytics event name must be less than %1$d characters (found %2$d)";
return String.format(message, EVENT_NAME_MAX_LENGTH, eventName.length());
}
if (eventName.isEmpty()) {
return "Analytics event name cannot be empty";
}
if (!Character.isAlphabetic(eventName.charAt(0))) {
String message
= "Analytics event name must start with an alphabetic character (found %1$s)";
return String.format(message, eventName);
}
String message = "Analytics event name must only consist of letters, numbers and " +
"underscores (found %1$s)";
for (int i = 0; i < eventName.length(); i++) {
char character = eventName.charAt(i);
if (!Character.isLetterOrDigit(character) && character != '_') {
return String.format(message, eventName);
}
}
if (eventName.startsWith("firebase_")) {
return "Analytics event name should not start with `firebase_`";
}
if (isReservedEventName(eventName)) {
return String.format("`%1$s` is a reserved Analytics event name and cannot be used",
eventName);
}
return null;
}
@Nullable
private static String getErrorForEventParameterName(String eventParameterName) {
if (eventParameterName.length() > EVENT_PARAM_NAME_MAX_LENGTH) {
String message =
"Analytics event parameter name must be %1$d characters or less (found %2$d)";
return String.format(message, EVENT_PARAM_NAME_MAX_LENGTH, eventParameterName.length());
}
if (eventParameterName.isEmpty()) {
return "Analytics event parameter name cannot be empty";
}
if (!Character.isAlphabetic(eventParameterName.charAt(0))) {
String message = "Analytics event parameter name must start with an alphabetic " +
"character (found %1$s)";
return String.format(message, eventParameterName);
}
String message = "Analytics event name must only consist of letters, numbers and " +
"underscores (found %1$s)";
for (int i = 0; i < eventParameterName.length(); i++) {
char character = eventParameterName.charAt(i);
if (!Character.isLetterOrDigit(character) && character != '_') {
return String.format(message, eventParameterName);
}
}
if (eventParameterName.startsWith("firebase_")) {
return "Analytics event parameter name cannot be start with `firebase_`";
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy