Skip to content

Commit

Permalink
Add simple lint check
Browse files Browse the repository at this point in the history
  • Loading branch information
noties committed Jul 2, 2019
1 parent d4bc061 commit 9ae1645
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 12 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ org.gradle.configureondemand=true

android.enableBuildCache=true

VERSION_NAME=5.0.0
VERSION_NAME=5.1.0

GROUP=io.noties
POM_DESCRIPTION=Library for easy Android logging
Expand Down
24 changes: 24 additions & 0 deletions library-lint/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apply plugin: 'java-library'

jar {
manifest {
attributes('Lint-Registry-v2': 'io.noties.debug.lint.DebugLintRegistry')
}
}

//configurations {
// lintChecks
//}

dependencies {

final def lint_version = '26.4.1'

compileOnly "com.android.tools.lint:lint-api:$lint_version"
compileOnly "com.android.tools.lint:lint-checks:$lint_version"

// lintChecks files(jar)
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"
195 changes: 195 additions & 0 deletions library-lint/src/main/java/io/noties/debug/lint/DebugLintIssue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.noties.debug.lint;

import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.ApiKt;
import com.android.tools.lint.detector.api.Issue;

import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.List;

public class DebugLintRegistry extends IssueRegistry {

@NotNull
@Override
public List<Issue> getIssues() {
return Collections.singletonList(DebugLintIssue.ISSUE);
}

@Override
public int getApi() {
return ApiKt.CURRENT_API;
}
}
17 changes: 7 additions & 10 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,21 @@ android {
}
}

task jar(type: Jar) {
from 'build/intermediates/classes/release'
exclude '**/BuildConfig.class'
exclude '**/R.class'
}

artifacts {
archives jar
}

dependencies {

api deps['annotations']

lintPublish project(':library-lint')

testImplementation deps.test.junit
}

if (project.hasProperty('release')) {
apply from: config['push-aar-gradle']
}

// okay, we are at this state again when BuildConfig is generated no matter what...
//afterEvaluate {
// tasks.named('generateDebugBuildConfig').configure { it.enabled = false }
// tasks.named('generateReleaseBuildConfig').configure { it.enabled = false }
//}
3 changes: 3 additions & 0 deletions sample/src/main/java/io/noties/debug/sample/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ protected void onCreate(Bundle savedInstanceState) {
Debug.e(true, null, Integer.MAX_VALUE, Long.MIN_VALUE);
Debug.wtf("No, really, WTF?!");

// lint in action
Debug.i("%d", false);

Debug.i("array: %s", new int[]{1, 2, 3, 4, 5});

// Trace current method calls chain
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
include ':sample', ':library'
include ':sample', ':library', ':library-lint'

0 comments on commit 9ae1645

Please sign in to comment.