package io.noties.debug.lint; import com.android.annotations.NonNull; import com.android.tools.lint.client.api.UElementHandler; 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.PsiClass; import com.intellij.psi.PsiClassType; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.uast.UCallExpression; import org.jetbrains.uast.UElement; import org.jetbrains.uast.UExpression; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DebugLintIssue extends Detector implements Detector.UastScanner { public static final Issue ISSUE = Issue.create( "DebugStringFormat", "String#format arguments mismatch", "Checks if Debug is called with correct pattern and arguments", Category.PERFORMANCE, 10, Severity.WARNING, new Implementation(DebugLintIssue.class, Scope.JAVA_FILE_SCOPE)); private static final Pattern STRING_FORMAT_PATTERN = Pattern.compile("%(\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"); private static final Set<String> METHODS = new HashSet<>(Arrays.asList("v", "d", "i", "w", "e", "wtf")); @Nullable @Override public List<Class<? extends UElement>> getApplicableUastTypes() { //noinspection unchecked return (List) Collections.singletonList(UCallExpression.class); } @Nullable @Override public UElementHandler createUastHandler(@NotNull final JavaContext context) { return new UElementHandler() { @Override public void visitCallExpression(@NotNull UCallExpression node) { final PsiMethod psiMethod = node.resolve(); if (psiMethod != null && context.getEvaluator().isMemberInClass(psiMethod, "io.noties.debug.Debug")) { final String name = node.getMethodName(); if (name != null && METHODS.contains(name)) { process(context, node); } } } }; } private static void process(@NonNull JavaContext context, @NonNull UCallExpression expression) { // to be able to mutate (we remove first Throwable if present) final List<UExpression> arguments = new ArrayList<>(expression.getValueArguments()); if (arguments.isEmpty()) { // if there are no arguments -> no check return; } // remove throwable (comes first0 if (isSubclassOf(context, arguments.get(0), Throwable.class)) { arguments.remove(0); } // still check for empty arguments (method can be called with just a throwable) // if first argument is not a string, then also nothing to do here if (arguments.isEmpty() || !isSubclassOf(context, arguments.get(0), String.class)) { return; } // now, first arg is string, check if it matches the pattern final String pattern = (String) arguments.get(0).evaluate(); if (pattern == null || pattern.length() == 0) { // if no pattern is available -> return return; } final Matcher matcher = STRING_FORMAT_PATTERN.matcher(pattern); // we must _find_, not _matches_ if (matcher.find()) { // okay, first argument is string // evaluate other arguments (actually create them) // remove pattern arguments.remove(0); // what else can we do -> count actual placeholders and arguments // (if mismatch... no, we can have positioned) final Object[] mock = mockArguments(arguments); try { //noinspection ResultOfMethodCallIgnored String.format(pattern, mock); } catch (Throwable t) { context.report( ISSUE, expression, context.getLocation(expression), t.getMessage()); } } } @Nullable private static Object[] mockArguments(@NonNull List<UExpression> list) { if (list.isEmpty()) { return null; } final List<Object> objects = new ArrayList<>(list.size()); for (UExpression expression : list) { final Object eval = expression.evaluate(); if (eval != null) { objects.add(eval); } else { // we must really _mock_ it // check for primitives -> and create them, else just `new Object()` final Object o; final PsiType psiType = expression.getExpressionType(); if (PsiType.BOOLEAN.equals(psiType)) { o = false; } else if (PsiType.BYTE.equals(psiType)) { o = (byte) 0; } else if (PsiType.CHAR.equals(psiType)) { o = 'a'; } else if (PsiType.DOUBLE.equals(psiType)) { o = 0.0D; } else if (PsiType.FLOAT.equals(psiType)) { o = 0.0F; } else if (PsiType.INT.equals(psiType)) { o = 0; } else if (PsiType.LONG.equals(psiType)) { o = 0L; } else if (PsiType.SHORT.equals(psiType)) { o = (short) 0; } else if (PsiType.NULL.equals(psiType)) { o = null; } else { o = new Object(); } objects.add(o); } } return objects.toArray(); } private static boolean isSubclassOf( @NonNull JavaContext context, @NonNull UExpression expression, @NonNull Class<?> cls) { final PsiType expressionType = expression.getExpressionType(); if (!(expressionType instanceof PsiClassType)) { return false; } final PsiClassType classType = (PsiClassType) expressionType; final PsiClass resolvedClass = classType.resolve(); return context.getEvaluator().extendsClass(resolvedClass, cls.getName(), false); } }