-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
255 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
195
library-lint/src/main/java/io/noties/debug/lint/DebugLintIssue.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
library-lint/src/main/java/io/noties/debug/lint/DebugLintRegistry.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
include ':sample', ':library' | ||
include ':sample', ':library', ':library-lint' |