diff --git a/lintchecks/src/main/java/org/signal/lint/AlertDialogBuilderDetector.java b/lintchecks/src/main/java/org/signal/lint/AlertDialogBuilderDetector.java deleted file mode 100644 index ceefa5b2bb..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/AlertDialogBuilderDetector.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.client.api.JavaEvaluator; -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.LintFix; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.intellij.psi.PsiMethod; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UExpression; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -@SuppressWarnings("UnstableApiUsage") -public final class AlertDialogBuilderDetector extends Detector implements Detector.UastScanner { - - static final Issue ALERT_DIALOG_BUILDER_USAGE = Issue.create("AlertDialogBuilderUsage", - "Creating dialog with AlertDialog.Builder instead of MaterialAlertDialogBuilder", - "Signal utilizes MaterialAlertDialogBuilder for more consistent and pleasant AlertDialogs.", - Category.MESSAGES, - 5, - Severity.WARNING, - new Implementation(AlertDialogBuilderDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - public @Nullable List getApplicableConstructorTypes() { - return Arrays.asList("android.app.AlertDialog.Builder", "androidx.appcompat.app.AlertDialog.Builder"); - } - - @Override - public void visitConstructor(JavaContext context, @NotNull UCallExpression call, @NotNull PsiMethod method) { - JavaEvaluator evaluator = context.getEvaluator(); - - if (evaluator.isMemberInClass(method, "android.app.AlertDialog.Builder")) { - LintFix fix = quickFixIssueAlertDialogBuilder(call); - context.report(ALERT_DIALOG_BUILDER_USAGE, - call, - context.getLocation(call), - "Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder", - fix); - } - - if (evaluator.isMemberInClass(method, "androidx.appcompat.app.AlertDialog.Builder")) { - LintFix fix = quickFixIssueAlertDialogBuilder(call); - context.report(ALERT_DIALOG_BUILDER_USAGE, - call, - context.getLocation(call), - "Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder", - fix); - } - } - - private LintFix quickFixIssueAlertDialogBuilder(@NotNull UCallExpression alertBuilderCall) { - List arguments = alertBuilderCall.getValueArguments(); - UExpression context = arguments.get(0); - - String fixSource = "new com.google.android.material.dialog.MaterialAlertDialogBuilder"; - - switch (arguments.size()) { - case 1: - fixSource += String.format("(%s)", context); - break; - case 2: - UExpression themeOverride = arguments.get(1); - fixSource += String.format("(%s, %s)", context, themeOverride); - break; - - default: - throw new IllegalStateException("MaterialAlertDialogBuilder overloads should have 1 or 2 arguments"); - } - - String builderCallSource = alertBuilderCall.asSourceString(); - LintFix.GroupBuilder fixGrouper = fix().group(); - fixGrouper.add(fix().replace().text(builderCallSource).shortenNames().reformat(true).with(fixSource).build()); - return fixGrouper.build(); - } -} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/AlertDialogBuilderDetector.kt b/lintchecks/src/main/java/org/signal/lint/AlertDialogBuilderDetector.kt new file mode 100644 index 0000000000..2887bba3e4 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/AlertDialogBuilderDetector.kt @@ -0,0 +1,84 @@ +package org.signal.lint + +import com.android.tools.lint.detector.api.Category.Companion.MESSAGES +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.LintFix +import com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.WARNING +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +class AlertDialogBuilderDetector : Detector(), Detector.UastScanner { + override fun getApplicableConstructorTypes(): List { + return listOf("android.app.AlertDialog.Builder", "androidx.appcompat.app.AlertDialog.Builder") + } + + override fun visitConstructor(context: JavaContext, node: UCallExpression, constructor: PsiMethod) { + val evaluator = context.evaluator + + if (evaluator.isMemberInClass(constructor, "android.app.AlertDialog.Builder")) { + context.report( + issue = ALERT_DIALOG_BUILDER_USAGE, + scope = node, + location = context.getLocation(node), + message = "Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder", + quickfixData = quickFixIssueAlertDialogBuilder(node) + ) + } + + if (evaluator.isMemberInClass(constructor, "androidx.appcompat.app.AlertDialog.Builder")) { + context.report( + issue = ALERT_DIALOG_BUILDER_USAGE, + scope = node, + location = context.getLocation(node), + message = "Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder", + quickfixData = quickFixIssueAlertDialogBuilder(node) + ) + } + } + + private fun quickFixIssueAlertDialogBuilder(alertBuilderCall: UCallExpression): LintFix { + val arguments = alertBuilderCall.valueArguments + val context = arguments[0] + + var fixSource = "new com.google.android.material.dialog.MaterialAlertDialogBuilder" + + when (arguments.size) { + 1 -> fixSource += String.format("(%s)", context) + 2 -> { + val themeOverride = arguments[1] + fixSource += String.format("(%s, %s)", context, themeOverride) + } + + else -> throw IllegalStateException("MaterialAlertDialogBuilder overloads should have 1 or 2 arguments") + } + + return fix() + .group() + .add( + fix() + .replace() + .text(alertBuilderCall.asSourceString()) + .shortenNames() + .reformat(true) + .with(fixSource) + .build() + ) + .build() + } + + companion object { + val ALERT_DIALOG_BUILDER_USAGE: Issue = Issue.create( + id = "AlertDialogBuilderUsage", + briefDescription = "Creating dialog with AlertDialog.Builder instead of MaterialAlertDialogBuilder", + explanation = "Signal utilizes MaterialAlertDialogBuilder for more consistent and pleasant AlertDialogs.", + category = MESSAGES, + priority = 5, + severity = WARNING, + implementation = Implementation(AlertDialogBuilderDetector::class.java, JAVA_FILE_SCOPE) + ) + } +} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/BlockingGetDetector.java b/lintchecks/src/main/java/org/signal/lint/BlockingGetDetector.java deleted file mode 100644 index beff4a11e8..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/BlockingGetDetector.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.client.api.JavaEvaluator; -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.PsiMethod; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UCallExpression; - -import java.util.List; - -/** - * Detects usages of Rx observable stream's blockingGet method. This is considered harmful, as - * blockingGet will take any error it emits and throw it as a runtime error. The alternative options - * are to: - * - * 1. Provide a synchronous method instead of relying on an observable method. - * 2. Pass the observable to the caller to allow them to wait on it via a flatMap or other operator. - * 3. Utilize safeBlockingGet, which will bubble up the interrupted exception. - * - * Note that (1) is the most preferred route here. - */ -@SuppressWarnings("UnstableApiUsage") -public final class BlockingGetDetector extends Detector implements Detector.UastScanner { - - static final Issue UNSAFE_BLOCKING_GET = Issue.create("UnsafeBlockingGet", - "BlockingGet is considered unsafe and should be avoided.", - "Prefer exposing the Observable instead. If you need to block, use RxExtensions.safeBlockingGet", - Category.MESSAGES, - 5, - Severity.WARNING, - new Implementation(BlockingGetDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - public List getApplicableMethodNames() { - return List.of("blockingGet"); - } - - @Override - public void visitMethodCall(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) { - JavaEvaluator evaluator = context.getEvaluator(); - - if (evaluator.isMemberInClass(method, "io.reactivex.rxjava3.core.Single")) { - context.report(UNSAFE_BLOCKING_GET, - context.getLocation(node), - "Using 'Single#blockingGet' instead of 'RxExtensions.safeBlockingGet'", - null); - } - - if (evaluator.isMemberInClass(method, "io.reactivex.rxjava3.core.Observable")) { - context.report(UNSAFE_BLOCKING_GET, - context.getLocation(node), - "Using 'Observable#blockingGet' instead of 'RxExtensions.safeBlockingGet'", - null); - } - - if (evaluator.isMemberInClass(method, "io.reactivex.rxjava3.core.Flowable")) { - context.report(UNSAFE_BLOCKING_GET, - context.getLocation(node), - "Using 'Flowable#blockingGet' instead of 'RxExtensions.safeBlockingGet'", - null); - } - } -} diff --git a/lintchecks/src/main/java/org/signal/lint/BlockingGetDetector.kt b/lintchecks/src/main/java/org/signal/lint/BlockingGetDetector.kt new file mode 100644 index 0000000000..415ff3a808 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/BlockingGetDetector.kt @@ -0,0 +1,71 @@ +package org.signal.lint + +import com.android.tools.lint.detector.api.Category.Companion.MESSAGES +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.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.WARNING +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +/** + * Detects usages of Rx observable stream's blockingGet method. This is considered harmful, as + * blockingGet will take any error it emits and throw it as a runtime error. The alternative options + * are to: + * + * 1. Provide a synchronous method instead of relying on an observable method. + * 2. Pass the observable to the caller to allow them to wait on it via a flatMap or other operator. + * 3. Utilize safeBlockingGet, which will bubble up the interrupted exception. + * + * Note that (1) is the most preferred route here. + */ +class BlockingGetDetector : Detector(), Detector.UastScanner { + override fun getApplicableMethodNames(): List { + return listOf("blockingGet") + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + + if (evaluator.isMemberInClass(method, "io.reactivex.rxjava3.core.Single")) { + context.report( + issue = UNSAFE_BLOCKING_GET, + location = context.getLocation(node), + message = "Using 'Single#blockingGet' instead of 'RxExtensions.safeBlockingGet'", + quickfixData = null + ) + } + + if (evaluator.isMemberInClass(method, "io.reactivex.rxjava3.core.Observable")) { + context.report( + issue = UNSAFE_BLOCKING_GET, + location = context.getLocation(node), + message = "Using 'Observable#blockingGet' instead of 'RxExtensions.safeBlockingGet'", + quickfixData = null + ) + } + + if (evaluator.isMemberInClass(method, "io.reactivex.rxjava3.core.Flowable")) { + context.report( + issue = UNSAFE_BLOCKING_GET, + location = context.getLocation(node), + message = "Using 'Flowable#blockingGet' instead of 'RxExtensions.safeBlockingGet'", + quickfixData = null + ) + } + } + + companion object { + val UNSAFE_BLOCKING_GET: Issue = Issue.create( + id = "UnsafeBlockingGet", + briefDescription = "BlockingGet is considered unsafe and should be avoided.", + explanation = "Prefer exposing the Observable instead. If you need to block, use RxExtensions.safeBlockingGet", + category = MESSAGES, + priority = 5, + severity = WARNING, + implementation = Implementation(BlockingGetDetector::class.java, JAVA_FILE_SCOPE) + ) + } +} diff --git a/lintchecks/src/main/java/org/signal/lint/CardViewDetector.java b/lintchecks/src/main/java/org/signal/lint/CardViewDetector.java deleted file mode 100644 index 84ad79754f..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/CardViewDetector.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.client.api.JavaEvaluator; -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.LintFix; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.intellij.psi.PsiMethod; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UExpression; - -import java.util.Collections; -import java.util.List; - -@SuppressWarnings("UnstableApiUsage") -public final class CardViewDetector extends Detector implements Detector.UastScanner { - - static final Issue CARD_VIEW_USAGE = Issue.create("CardViewUsage", - "Utilizing CardView instead of MaterialCardView subclass", - "Signal utilizes MaterialCardView for more consistent and pleasant CardViews.", - Category.MESSAGES, - 5, - Severity.WARNING, - new Implementation(CardViewDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - public @Nullable List getApplicableConstructorTypes() { - return Collections.singletonList("androidx.cardview.widget.CardView"); - } - - @Override - public void visitConstructor(JavaContext context, @NotNull UCallExpression call, @NotNull PsiMethod method) { - JavaEvaluator evaluator = context.getEvaluator(); - - if (evaluator.isMemberInClass(method, "androidx.cardview.widget.CardView")) { - LintFix fix = quickFixIssueAlertDialogBuilder(call); - context.report(CARD_VIEW_USAGE, - call, - context.getLocation(call), - "Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView", - fix); - } - } - - private LintFix quickFixIssueAlertDialogBuilder(@NotNull UCallExpression alertBuilderCall) { - List arguments = alertBuilderCall.getValueArguments(); - UExpression context = arguments.get(0); - - String fixSource = "new com.google.android.material.card.MaterialCardView"; - - //Context context, AttributeSet attrs, int defStyleAttr - switch (arguments.size()) { - case 1: - fixSource += String.format("(%s)", context); - break; - case 2: - UExpression attrs = arguments.get(1); - fixSource += String.format("(%s, %s)", context, attrs); - break; - case 3: - UExpression attributes = arguments.get(1); - UExpression defStyleAttr = arguments.get(2); - fixSource += String.format("(%s, %s, %s)", context, attributes, defStyleAttr); - break; - - default: - throw new IllegalStateException("MaterialAlertDialogBuilder overloads should have 1 or 2 arguments"); - } - - String builderCallSource = alertBuilderCall.asSourceString(); - LintFix.GroupBuilder fixGrouper = fix().group(); - fixGrouper.add(fix().replace().text(builderCallSource).shortenNames().reformat(true).with(fixSource).build()); - return fixGrouper.build(); - } -} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/CardViewDetector.kt b/lintchecks/src/main/java/org/signal/lint/CardViewDetector.kt new file mode 100644 index 0000000000..0c05efb157 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/CardViewDetector.kt @@ -0,0 +1,81 @@ +package org.signal.lint + +import com.android.tools.lint.detector.api.Category.Companion.MESSAGES +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.LintFix +import com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.WARNING +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +class CardViewDetector : Detector(), Detector.UastScanner { + override fun getApplicableConstructorTypes(): List { + return listOf("androidx.cardview.widget.CardView") + } + + override fun visitConstructor(context: JavaContext, node: UCallExpression, constructor: PsiMethod) { + val evaluator = context.evaluator + + if (evaluator.isMemberInClass(constructor, "androidx.cardview.widget.CardView")) { + context.report( + issue = CARD_VIEW_USAGE, + scope = node, + location = context.getLocation(node), + message = "Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView", + quickfixData = quickFixIssueAlertDialogBuilder(node) + ) + } + } + + private fun quickFixIssueAlertDialogBuilder(alertBuilderCall: UCallExpression): LintFix { + val arguments = alertBuilderCall.valueArguments + val context = arguments[0] + + var fixSource = "new com.google.android.material.card.MaterialCardView" + + //Context context, AttributeSet attrs, int defStyleAttr + when (arguments.size) { + 1 -> fixSource += String.format("(%s)", context) + 2 -> { + val attrs = arguments[1] + fixSource += String.format("(%s, %s)", context, attrs) + } + + 3 -> { + val attributes = arguments[1] + val defStyleAttr = arguments[2] + fixSource += String.format("(%s, %s, %s)", context, attributes, defStyleAttr) + } + + else -> throw IllegalStateException("MaterialAlertDialogBuilder overloads should have 1 or 2 arguments") + } + + return fix() + .group() + .add( + fix() + .replace() + .text(alertBuilderCall.asSourceString()) + .shortenNames() + .reformat(true) + .with(fixSource) + .build() + ) + .build() + } + + companion object { + val CARD_VIEW_USAGE: Issue = Issue.create( + id = "CardViewUsage", + briefDescription = "Utilizing CardView instead of MaterialCardView subclass", + explanation = "Signal utilizes MaterialCardView for more consistent and pleasant CardViews.", + category = MESSAGES, + priority = 5, + severity = WARNING, + implementation = Implementation(CardViewDetector::class.java, JAVA_FILE_SCOPE) + ) + } +} diff --git a/lintchecks/src/main/java/org/signal/lint/RecipientIdDatabaseDetector.java b/lintchecks/src/main/java/org/signal/lint/RecipientIdDatabaseDetector.java deleted file mode 100644 index 12029af7cf..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/RecipientIdDatabaseDetector.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.signal.lint; - -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.android.tools.lint.detector.api.SourceCodeScanner; -import com.intellij.psi.PsiField; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UElement; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@SuppressWarnings("UnstableApiUsage") -public final class RecipientIdDatabaseDetector extends Detector implements SourceCodeScanner { - - static final Issue RECIPIENT_ID_DATABASE_REFERENCE_ISSUE = Issue.create("RecipientIdDatabaseReferenceUsage", - "Referencing a RecipientId in a database without implementing RecipientIdDatabaseReference.", - "If you reference a RecipientId in a column, you need to be able to handle the remapping of one RecipientId to another, which RecipientIdDatabaseReference enforces.", - Category.MESSAGES, - 5, - Severity.ERROR, - new Implementation(RecipientIdDatabaseDetector.class, Scope.JAVA_FILE_SCOPE)); - - private static final Set EXEMPTED_CLASSES = new HashSet<>() {{ - add("org.thoughtcrime.securesms.database.RecipientDatabase"); - }}; - - - @Override - public List> getApplicableUastTypes() { - return Collections.singletonList(UClass.class); - } - - @Override - public UElementHandler createUastHandler(@NotNull JavaContext context) { - return new UElementHandler() { - @Override - public void visitClass(@NotNull UClass node) { - if (node.getQualifiedName() == null) { - return; - } - - if (node.getExtendsList() == null) { - return; - } - - if (EXEMPTED_CLASSES.contains(node.getQualifiedName())) { - return; - } - - boolean doesNotExtendDatabase = Arrays.stream(node.getExtendsList().getReferencedTypes()).noneMatch(t -> "Database".equals(t.getClassName())); - if (doesNotExtendDatabase) { - return; - } - - boolean implementsReference = Arrays.stream(node.getInterfaces()).anyMatch(i -> "org.thoughtcrime.securesms.database.RecipientIdDatabaseReference".equals(i.getQualifiedName())); - if (implementsReference) { - return; - } - - List recipientFields = Arrays.stream(node.getAllFields()) - .filter(f -> f.getType().equalsToText("java.lang.String")) - .filter(f -> f.getName().toLowerCase().contains("recipient")) - .collect(Collectors.toList()); - - for (PsiField field : recipientFields) { - context.report(RECIPIENT_ID_DATABASE_REFERENCE_ISSUE, - field, - context.getLocation(field), - "If you reference a RecipientId in your table, you must implement the RecipientIdDatabaseReference interface.", - null); - } - } - }; - } -} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/RecipientIdDatabaseDetector.kt b/lintchecks/src/main/java/org/signal/lint/RecipientIdDatabaseDetector.kt new file mode 100644 index 0000000000..61a7010653 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/RecipientIdDatabaseDetector.kt @@ -0,0 +1,75 @@ +package org.signal.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category.Companion.MESSAGES +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.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.ERROR +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UClass +import java.util.Locale + +class RecipientIdDatabaseDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List> { + return listOf(UClass::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitClass(node: UClass) { + if (node.qualifiedName == null) { + return + } + + if (node.extendsList == null) { + return + } + + if (EXEMPTED_CLASSES.contains(node.qualifiedName)) { + return + } + + val doesNotExtendDatabase = node.extendsList?.referencedTypes.orEmpty().none { it.className == "Database" } + if (doesNotExtendDatabase) { + return + } + + val implementsReference = node.interfaces.any { it.qualifiedName == "org.thoughtcrime.securesms.database.RecipientIdDatabaseReference" } + if (implementsReference) { + return + } + + val recipientFields = node.allFields + .filter { it.type.equalsToText("java.lang.String") } + .filter { it.name.lowercase(Locale.getDefault()).contains("recipient") } + + for (field in recipientFields) { + context.report( + issue = RECIPIENT_ID_DATABASE_REFERENCE_ISSUE, + scope = field, + location = context.getLocation(field), + message = "If you reference a RecipientId in your table, you must implement the RecipientIdDatabaseReference interface.", + quickfixData = null + ) + } + } + } + } + + companion object { + val RECIPIENT_ID_DATABASE_REFERENCE_ISSUE: Issue = Issue.create( + id = "RecipientIdDatabaseReferenceUsage", + briefDescription = "Referencing a RecipientId in a database without implementing RecipientIdDatabaseReference.", + explanation = "If you reference a RecipientId in a column, you need to be able to handle the remapping of one RecipientId to another, which RecipientIdDatabaseReference enforces.", + category = MESSAGES, + priority = 5, + severity = ERROR, + implementation = Implementation(RecipientIdDatabaseDetector::class.java, JAVA_FILE_SCOPE) + ) + + private val EXEMPTED_CLASSES = setOf("org.thoughtcrime.securesms.database.RecipientDatabase") + } +} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/Registry.java b/lintchecks/src/main/java/org/signal/lint/Registry.java deleted file mode 100644 index 9fe5314732..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/Registry.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.client.api.Vendor; -import com.android.tools.lint.detector.api.ApiKt; -import com.android.tools.lint.detector.api.Issue; - -import org.jetbrains.annotations.Nullable; - -import java.util.Arrays; -import java.util.List; - -@SuppressWarnings("UnstableApiUsage") -public final class Registry extends IssueRegistry { - - @Override - public Vendor getVendor() { - return new Vendor("Signal", "Signal", "Signal", "Signal"); - } - - @Override - public List getIssues() { - return Arrays.asList(SignalLogDetector.LOG_NOT_SIGNAL, - SignalLogDetector.LOG_NOT_APP, - SignalLogDetector.INLINE_TAG, - VersionCodeDetector.VERSION_CODE_USAGE, - AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE, - BlockingGetDetector.UNSAFE_BLOCKING_GET, - RecipientIdDatabaseDetector.RECIPIENT_ID_DATABASE_REFERENCE_ISSUE, - ThreadIdDatabaseDetector.THREAD_ID_DATABASE_REFERENCE_ISSUE, - StartForegroundServiceDetector.START_FOREGROUND_SERVICE_ISSUE, - CardViewDetector.CARD_VIEW_USAGE); - } - - @Override - public int getApi() { - return ApiKt.CURRENT_API; - } -} diff --git a/lintchecks/src/main/java/org/signal/lint/Registry.kt b/lintchecks/src/main/java/org/signal/lint/Registry.kt new file mode 100644 index 0000000000..1d7e34fda4 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/Registry.kt @@ -0,0 +1,29 @@ +package org.signal.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API + +class Registry : IssueRegistry() { + override val vendor = Vendor( + vendorName = "Signal", + identifier = "Signal", + feedbackUrl = "Signal", + contact = "Signal" + ) + + override val issues = listOf( + SignalLogDetector.LOG_NOT_SIGNAL, + SignalLogDetector.LOG_NOT_APP, + SignalLogDetector.INLINE_TAG, + VersionCodeDetector.VERSION_CODE_USAGE, + AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE, + BlockingGetDetector.UNSAFE_BLOCKING_GET, + RecipientIdDatabaseDetector.RECIPIENT_ID_DATABASE_REFERENCE_ISSUE, + ThreadIdDatabaseDetector.THREAD_ID_DATABASE_REFERENCE_ISSUE, + StartForegroundServiceDetector.START_FOREGROUND_SERVICE_ISSUE, + CardViewDetector.CARD_VIEW_USAGE + ) + + override val api = CURRENT_API +} diff --git a/lintchecks/src/main/java/org/signal/lint/SignalLogDetector.java b/lintchecks/src/main/java/org/signal/lint/SignalLogDetector.java deleted file mode 100644 index 1d0bbd433b..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/SignalLogDetector.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.client.api.JavaEvaluator; -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.LintFix; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.intellij.psi.PsiMethod; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UExpression; -import org.jetbrains.uast.java.JavaUSimpleNameReferenceExpression; -import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression; -import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression; - -import java.util.Arrays; -import java.util.List; - -@SuppressWarnings("UnstableApiUsage") -public final class SignalLogDetector extends Detector implements Detector.UastScanner { - - static final Issue LOG_NOT_SIGNAL = Issue.create("LogNotSignal", - "Logging call to Android Log instead of Signal's Logger", - "Signal has its own logger which must be used.", - Category.MESSAGES, - 5, - Severity.ERROR, - new Implementation(SignalLogDetector.class, Scope.JAVA_FILE_SCOPE)); - - static final Issue LOG_NOT_APP = Issue.create("LogNotAppSignal", - "Logging call to Signal Service Log instead of App level Logger", - "Signal app layer has its own logger which must be used.", - Category.MESSAGES, - 5, - Severity.ERROR, - new Implementation(SignalLogDetector.class, Scope.JAVA_FILE_SCOPE)); - - static final Issue INLINE_TAG = Issue.create("LogTagInlined", - "Use of an inline string in a TAG", - "Often a sign of left in temporary log statements, always use a tag constant.", - Category.MESSAGES, - 5, - Severity.ERROR, - new Implementation(SignalLogDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - public List getApplicableMethodNames() { - return Arrays.asList("v", "d", "i", "w", "e", "wtf"); - } - - @Override - public void visitMethodCall(JavaContext context, @NotNull UCallExpression call, @NotNull PsiMethod method) { - JavaEvaluator evaluator = context.getEvaluator(); - - if (evaluator.isMemberInClass(method, "android.util.Log")) { - LintFix fix = quickFixIssueLog(call); - context.report(LOG_NOT_SIGNAL, call, context.getLocation(call), "Using 'android.util.Log' instead of a Signal Logger", fix); - } - - if (evaluator.isMemberInClass(method, "org.signal.glide.Log")) { - LintFix fix = quickFixIssueLog(call); - context.report(LOG_NOT_SIGNAL, call, context.getLocation(call), "Using 'org.signal.glide.Log' instead of a Signal Logger", fix); - } - - if (evaluator.isMemberInClass(method, "org.signal.libsignal.protocol.logging.Log")) { - LintFix fix = quickFixIssueLog(call); - context.report(LOG_NOT_APP, call, context.getLocation(call), "Using Signal server logger instead of app level Logger", fix); - } - - if (evaluator.isMemberInClass(method, "org.signal.core.util.logging.Log")) { - List arguments = call.getValueArguments(); - UExpression tag = arguments.get(0); - if (!(tag instanceof JavaUSimpleNameReferenceExpression || tag instanceof KotlinUSimpleReferenceExpression || tag instanceof KotlinUQualifiedReferenceExpression)) { - context.report(INLINE_TAG, call, context.getLocation(call), "Not using a tag constant"); - } - } - } - - private LintFix quickFixIssueLog(@NotNull UCallExpression logCall) { - List arguments = logCall.getValueArguments(); - String methodName = logCall.getMethodName(); - UExpression tag = arguments.get(0); - - String fixSource = "org.signal.core.util.logging.Log."; - - switch (arguments.size()) { - case 2: - UExpression msgOrThrowable = arguments.get(1); - fixSource += String.format("%s(%s, %s)", methodName, tag, msgOrThrowable.asSourceString()); - break; - - case 3: - UExpression msg = arguments.get(1); - UExpression throwable = arguments.get(2); - fixSource += String.format("%s(%s, %s, %s)", methodName, tag, msg.asSourceString(), throwable.asSourceString()); - break; - - default: - throw new IllegalStateException("Log overloads should have 2 or 3 arguments"); - } - - String logCallSource = logCall.asSourceString(); - LintFix.GroupBuilder fixGrouper = fix().group(); - fixGrouper.add(fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource).build()); - return fixGrouper.build(); - } - - -} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/SignalLogDetector.kt b/lintchecks/src/main/java/org/signal/lint/SignalLogDetector.kt new file mode 100644 index 0000000000..4cf68ac88b --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/SignalLogDetector.kt @@ -0,0 +1,145 @@ +package org.signal.lint + +import com.android.tools.lint.detector.api.Category.Companion.MESSAGES +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.LintFix +import com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.ERROR +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.java.JavaUSimpleNameReferenceExpression +import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression +import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression + +class SignalLogDetector : Detector(), Detector.UastScanner { + override fun getApplicableMethodNames(): List { + return listOf("v", "d", "i", "w", "e", "wtf") + } + + @Suppress("UnstableApiUsage") + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + + if (evaluator.isMemberInClass(method, "android.util.Log")) { + context.report( + issue = LOG_NOT_SIGNAL, + scope = node, + location = context.getLocation(node), + message = "Using 'android.util.Log' instead of a Signal Logger", + quickfixData = quickFixIssueLog(node) + ) + } + + if (evaluator.isMemberInClass(method, "org.signal.glide.Log")) { + context.report( + issue = LOG_NOT_SIGNAL, + scope = node, + location = context.getLocation(node), + message = "Using 'org.signal.glide.Log' instead of a Signal Logger", + quickfixData = quickFixIssueLog(node) + ) + } + + if (evaluator.isMemberInClass(method, "org.signal.libsignal.protocol.logging.Log")) { + context.report( + issue = LOG_NOT_APP, + scope = node, + location = context.getLocation(node), + message = "Using Signal server logger instead of app level Logger", + quickfixData = quickFixIssueLog(node) + ) + } + + if (evaluator.isMemberInClass(method, "org.signal.core.util.logging.Log")) { + val arguments = node.valueArguments + val tag = arguments[0] + + val invalidTagType = setOf( + JavaUSimpleNameReferenceExpression::class, + KotlinUSimpleReferenceExpression::class, + KotlinUQualifiedReferenceExpression::class + ).none { it.isInstance(tag) } + + if (invalidTagType) { + context.report( + issue = INLINE_TAG, + scope = node, + location = context.getLocation(node), + message = "Not using a tag constant" + ) + } + } + } + + private fun quickFixIssueLog(logCall: UCallExpression): LintFix { + val arguments = logCall.valueArguments + val methodName = logCall.methodName + val tag = arguments[0] + + var fixSource = "org.signal.core.util.logging.Log." + + when (arguments.size) { + 2 -> { + val msgOrThrowable = arguments[1] + fixSource += String.format("%s(%s, %s)", methodName, tag, msgOrThrowable.asSourceString()) + } + + 3 -> { + val msg = arguments[1] + val throwable = arguments[2] + fixSource += String.format("%s(%s, %s, %s)", methodName, tag, msg.asSourceString(), throwable.asSourceString()) + } + + else -> throw IllegalStateException("Log overloads should have 2 or 3 arguments") + } + + return fix() + .group() + .add( + fix() + .replace() + .text(logCall.asSourceString()) + .shortenNames() + .reformat(true) + .with(fixSource) + .build() + ) + .build() + } + + + companion object { + val LOG_NOT_SIGNAL: Issue = Issue.create( + id = "LogNotSignal", + briefDescription = "Logging call to Android Log instead of Signal's Logger", + explanation = "Signal has its own logger which must be used.", + category = MESSAGES, + priority = 5, + severity = ERROR, + implementation = Implementation(SignalLogDetector::class.java, JAVA_FILE_SCOPE) + ) + + val LOG_NOT_APP: Issue = Issue.create( + id = "LogNotAppSignal", + briefDescription = "Logging call to Signal Service Log instead of App level Logger", + explanation = "Signal app layer has its own logger which must be used.", + category = MESSAGES, + priority = 5, + severity = ERROR, + implementation = Implementation(SignalLogDetector::class.java, JAVA_FILE_SCOPE) + ) + + val INLINE_TAG: Issue = Issue.create( + id = "LogTagInlined", + briefDescription = "Use of an inline string in a TAG", + explanation = "Often a sign of left in temporary log statements, always use a tag constant.", + category = MESSAGES, + priority = 5, + severity = ERROR, + implementation = Implementation(SignalLogDetector::class.java, JAVA_FILE_SCOPE) + ) + } +} diff --git a/lintchecks/src/main/java/org/signal/lint/StartForegroundServiceDetector.java b/lintchecks/src/main/java/org/signal/lint/StartForegroundServiceDetector.java deleted file mode 100644 index 7c021e91af..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/StartForegroundServiceDetector.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.client.api.JavaEvaluator; -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.LintFix; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.intellij.psi.PsiMethod; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UExpression; - -import java.util.Arrays; -import java.util.List; - -@SuppressWarnings("UnstableApiUsage") -public final class StartForegroundServiceDetector extends Detector implements Detector.UastScanner { - - static final Issue START_FOREGROUND_SERVICE_ISSUE = Issue.create("StartForegroundServiceUsage", - "Starting a foreground service using ContextCompat.startForegroundService instead of ForegroundServiceUtil", - "Starting a foreground service may fail, and we should prefer our utils to make sure they're started correctly", - Category.MESSAGES, - 5, - Severity.ERROR, - new Implementation(StartForegroundServiceDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - public List getApplicableMethodNames() { - return Arrays.asList("startForegroundService"); - } - - @Override - public void visitMethodCall(@NotNull JavaContext context, @NotNull UCallExpression call, @NotNull PsiMethod method) { - JavaEvaluator evaluator = context.getEvaluator(); - - if (context.getUastFile() != null && context.getUastFile().getClasses().stream().anyMatch(it -> "ForegroundServiceUtil".equals(it.getName()))) { - return; - } - - if (evaluator.isMemberInClass(method, "androidx.core.content.ContextCompat")) { - context.report(START_FOREGROUND_SERVICE_ISSUE, call, context.getLocation(call), "Using 'ContextCompat.startForegroundService' instead of a ForegroundServiceUtil"); - } else if (evaluator.isMemberInClass(method, "android.content.Context")) { - context.report(START_FOREGROUND_SERVICE_ISSUE, call, context.getLocation(call), "Using 'Context.startForegroundService' instead of a ForegroundServiceUtil"); - } - } -} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/StartForegroundServiceDetector.kt b/lintchecks/src/main/java/org/signal/lint/StartForegroundServiceDetector.kt new file mode 100644 index 0000000000..1a023f6a98 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/StartForegroundServiceDetector.kt @@ -0,0 +1,55 @@ +package org.signal.lint + +import com.android.tools.lint.detector.api.Category.Companion.MESSAGES +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.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.ERROR +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +class StartForegroundServiceDetector : Detector(), Detector.UastScanner { + override fun getApplicableMethodNames(): List { + return listOf("startForegroundService") + } + + override fun visitMethodCall(context: JavaContext, call: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + + val classes = context.uastFile?.classes.orEmpty() + val isForegroundServiceUtil = classes.any { it.name == "ForegroundServiceUtil" } + if (isForegroundServiceUtil) { + return + } + + if (evaluator.isMemberInClass(method, "androidx.core.content.ContextCompat")) { + context.report( + issue = START_FOREGROUND_SERVICE_ISSUE, + scope = call, + location = context.getLocation(call), + message = "Using 'ContextCompat.startForegroundService' instead of a ForegroundServiceUtil" + ) + } else if (evaluator.isMemberInClass(method, "android.content.Context")) { + context.report( + issue = START_FOREGROUND_SERVICE_ISSUE, + scope = call, + location = context.getLocation(call), + message = "Using 'Context.startForegroundService' instead of a ForegroundServiceUtil" + ) + } + } + + companion object { + val START_FOREGROUND_SERVICE_ISSUE: Issue = Issue.create( + id = "StartForegroundServiceUsage", + briefDescription = "Starting a foreground service using ContextCompat.startForegroundService instead of ForegroundServiceUtil", + explanation = "Starting a foreground service may fail, and we should prefer our utils to make sure they're started correctly", + category = MESSAGES, + priority = 5, + severity = ERROR, + implementation = Implementation(StartForegroundServiceDetector::class.java, JAVA_FILE_SCOPE) + ) + } +} diff --git a/lintchecks/src/main/java/org/signal/lint/ThreadIdDatabaseDetector.java b/lintchecks/src/main/java/org/signal/lint/ThreadIdDatabaseDetector.java deleted file mode 100644 index aa7ee645c5..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/ThreadIdDatabaseDetector.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.signal.lint; - -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.android.tools.lint.detector.api.SourceCodeScanner; -import com.intellij.psi.PsiField; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UElement; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@SuppressWarnings("UnstableApiUsage") -public final class ThreadIdDatabaseDetector extends Detector implements SourceCodeScanner { - - static final Issue THREAD_ID_DATABASE_REFERENCE_ISSUE = Issue.create("ThreadIdDatabaseReferenceUsage", - "Referencing a thread ID in a database without implementing ThreadIdDatabaseReference.", - "If you reference a thread ID in a column, you need to be able to handle the remapping of one thread ID to another, which ThreadIdDatabaseReference enforces.", - Category.MESSAGES, - 5, - Severity.ERROR, - new Implementation(ThreadIdDatabaseDetector.class, Scope.JAVA_FILE_SCOPE)); - - private static final Set EXEMPTED_CLASSES = new HashSet<>() {{ - add("org.thoughtcrime.securesms.database.ThreadDatabase"); - }}; - - - @Override - public List> getApplicableUastTypes() { - return Collections.singletonList(UClass.class); - } - - @Override - public UElementHandler createUastHandler(@NotNull JavaContext context) { - return new UElementHandler() { - @Override - public void visitClass(@NotNull UClass node) { - if (node.getQualifiedName() == null) { - return; - } - - if (node.getExtendsList() == null) { - return; - } - - if (EXEMPTED_CLASSES.contains(node.getQualifiedName())) { - return; - } - - boolean doesNotExtendDatabase = Arrays.stream(node.getExtendsList().getReferencedTypes()).noneMatch(t -> "Database".equals(t.getClassName())); - if (doesNotExtendDatabase) { - return; - } - - boolean implementsReference = Arrays.stream(node.getInterfaces()).anyMatch(i -> "org.thoughtcrime.securesms.database.ThreadIdDatabaseReference".equals(i.getQualifiedName())); - if (implementsReference) { - return; - } - - List recipientFields = Arrays.stream(node.getAllFields()) - .filter(f -> f.getType().equalsToText("java.lang.String")) - .filter(f -> f.getName().toLowerCase().contains("thread")) - .collect(Collectors.toList()); - - for (PsiField field : recipientFields) { - context.report(THREAD_ID_DATABASE_REFERENCE_ISSUE, - field, - context.getLocation(field), - "If you reference a thread ID in your table, you must implement the ThreadIdDatabaseReference interface.", - null); - } - } - }; - } -} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/ThreadIdDatabaseDetector.kt b/lintchecks/src/main/java/org/signal/lint/ThreadIdDatabaseDetector.kt new file mode 100644 index 0000000000..5a5b770853 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/ThreadIdDatabaseDetector.kt @@ -0,0 +1,78 @@ +package org.signal.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category.Companion.MESSAGES +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.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.ERROR +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UClass +import java.util.Locale + +class ThreadIdDatabaseDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List> { + return listOf(UClass::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitClass(node: UClass) { + if (node.qualifiedName == null) { + return + } + + if (node.extendsList == null) { + return + } + + if (EXEMPTED_CLASSES.contains(node.qualifiedName)) { + return + } + + val referencedTypes = node.extendsList?.referencedTypes.orEmpty() + val doesNotExtendDatabase = referencedTypes.none { classType -> "Database" == classType.className } + if (doesNotExtendDatabase) { + return + } + + val implementsReference = node.interfaces.any { nodeInterface -> + "org.thoughtcrime.securesms.database.ThreadIdDatabaseReference" == nodeInterface.qualifiedName + } + if (implementsReference) { + return + } + + val recipientFields = node.allFields + .filter { field -> field.type.equalsToText("java.lang.String") } + .filter { field -> field.name.lowercase(Locale.getDefault()).contains("thread") } + + for (field in recipientFields) { + context.report( + issue = THREAD_ID_DATABASE_REFERENCE_ISSUE, + scope = field, + location = context.getLocation(field), + message = "If you reference a thread ID in your table, you must implement the ThreadIdDatabaseReference interface.", + quickfixData = null + ) + } + } + } + } + + companion object { + val THREAD_ID_DATABASE_REFERENCE_ISSUE: Issue = Issue.create( + id = "ThreadIdDatabaseReferenceUsage", + briefDescription = "Referencing a thread ID in a database without implementing ThreadIdDatabaseReference.", + explanation = "If you reference a thread ID in a column, you need to be able to handle the remapping of one thread ID to another, which ThreadIdDatabaseReference enforces.", + category = MESSAGES, + priority = 5, + severity = ERROR, + implementation = Implementation(ThreadIdDatabaseDetector::class.java, JAVA_FILE_SCOPE) + ) + + private val EXEMPTED_CLASSES = setOf("org.thoughtcrime.securesms.database.ThreadDatabase") + } +} diff --git a/lintchecks/src/main/java/org/signal/lint/VersionCodeDetector.java b/lintchecks/src/main/java/org/signal/lint/VersionCodeDetector.java deleted file mode 100644 index a06bec4e6b..0000000000 --- a/lintchecks/src/main/java/org/signal/lint/VersionCodeDetector.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.signal.lint; - -import com.android.annotations.NonNull; -import com.android.tools.lint.client.api.JavaEvaluator; -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.LintFix; -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.PsiElement; -import com.intellij.psi.PsiType; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UExpression; - -import java.util.Collections; -import java.util.List; - -@SuppressWarnings("UnstableApiUsage") -public final class VersionCodeDetector extends Detector implements Detector.UastScanner { - - static final Issue VERSION_CODE_USAGE = Issue.create("VersionCodeUsage", - "Using 'VERSION_CODES' reference instead of the numeric value", - "Signal style is to use the numeric value.", - Category.CORRECTNESS, - 5, - Severity.WARNING, - new Implementation(VersionCodeDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - public List> getApplicableUastTypes() { - return Collections.singletonList(UExpression.class); - } - - @Override - public UElementHandler createUastHandler(@NonNull JavaContext context) { - return new ExpressionChecker(context); - } - - private class ExpressionChecker extends UElementHandler { - private final JavaContext context; - private final JavaEvaluator evaluator; - private final PsiClass versionCodeClass; - - public ExpressionChecker(JavaContext context) { - this.context = context; - this.evaluator = context.getEvaluator(); - this.versionCodeClass = evaluator.findClass("android.os.Build.VERSION_CODES"); - } - - @Override - public void visitExpression(@NotNull UExpression node) { - if (versionCodeClass != null && node.getExpressionType() == PsiType.INT) { - PsiElement javaPsi = node.getJavaPsi(); - - if (javaPsi != null) { - PsiElement resolved = evaluator.resolve(javaPsi); - - if (resolved != null && resolved.getParent().equals(versionCodeClass)) { - Object evaluated = node.evaluate(); - - if (evaluated != null) { - context.report(VERSION_CODE_USAGE, node, context.getLocation(node), "Using 'VERSION_CODES' reference instead of the numeric value " + evaluated, quickFixIssueInlineValue(node, evaluated.toString())); - } else { - context.report(VERSION_CODE_USAGE, node, context.getLocation(node), "Using 'VERSION_CODES' reference instead of the numeric value", null); - } - } - } - } - } - } - - private LintFix quickFixIssueInlineValue(@NotNull UExpression node, @NotNull String fixSource) { - String expressionSource = node.asSourceString(); - LintFix.GroupBuilder fixGrouper = fix().group(); - - fixGrouper.add(fix().replace() - .text(expressionSource) - .reformat(true) - .with(fixSource) - .build()); - - return fixGrouper.build(); - } -} \ No newline at end of file diff --git a/lintchecks/src/main/java/org/signal/lint/VersionCodeDetector.kt b/lintchecks/src/main/java/org/signal/lint/VersionCodeDetector.kt new file mode 100644 index 0000000000..24c6d8adb8 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/VersionCodeDetector.kt @@ -0,0 +1,86 @@ +package org.signal.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS +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.LintFix +import com.android.tools.lint.detector.api.Scope.Companion.JAVA_FILE_SCOPE +import com.android.tools.lint.detector.api.Severity.WARNING +import com.intellij.psi.PsiTypes +import org.jetbrains.uast.UExpression + +class VersionCodeDetector : Detector(), Detector.UastScanner { + override fun getApplicableUastTypes(): List> { + return listOf(UExpression::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return ExpressionChecker(context) + } + + private inner class ExpressionChecker(private val context: JavaContext) : UElementHandler() { + private val evaluator = context.evaluator + private val versionCodeClass = evaluator.findClass("android.os.Build.VERSION_CODES") + + override fun visitExpression(node: UExpression) { + if (versionCodeClass != null && node.getExpressionType() === PsiTypes.intType()) { + val javaPsi = node.javaPsi + + if (javaPsi != null) { + val resolved = evaluator.resolve(javaPsi) + + if (resolved != null && resolved.parent == versionCodeClass) { + val evaluated = node.evaluate() + + if (evaluated != null) { + context.report( + issue = VERSION_CODE_USAGE, + scope = node, + location = context.getLocation(node), + message = "Using 'VERSION_CODES' reference instead of the numeric value $evaluated", + quickfixData = quickFixIssueInlineValue(node, evaluated.toString()) + ) + } else { + context.report( + issue = VERSION_CODE_USAGE, + scope = node, + location = context.getLocation(node), + message = "Using 'VERSION_CODES' reference instead of the numeric value", + quickfixData = null + ) + } + } + } + } + } + } + + private fun quickFixIssueInlineValue(node: UExpression, fixSource: String): LintFix { + return fix() + .group() + .add( + fix() + .replace() + .text(node.asSourceString()) + .reformat(true) + .with(fixSource) + .build() + ) + .build() + } + + companion object { + val VERSION_CODE_USAGE: Issue = Issue.create( + id = "VersionCodeUsage", + briefDescription = "Using 'VERSION_CODES' reference instead of the numeric value", + explanation = "Signal style is to use the numeric value.", + category = CORRECTNESS, + priority = 5, + severity = WARNING, + implementation = Implementation(VersionCodeDetector::class.java, JAVA_FILE_SCOPE) + ) + } +} \ No newline at end of file diff --git a/lintchecks/src/test/java/org/signal/lint/AlertDialogBuilderDetectorTest.java b/lintchecks/src/test/java/org/signal/lint/AlertDialogBuilderDetectorTest.java deleted file mode 100644 index dc0e01015f..0000000000 --- a/lintchecks/src/test/java/org/signal/lint/AlertDialogBuilderDetectorTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.Scanner; - -import static com.android.tools.lint.checks.infrastructure.TestFiles.java; -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("UnstableApiUsage") -public final class AlertDialogBuilderDetectorTest { - - private static final TestFile appCompatAlertDialogStub = java(readResourceAsString("AppCompatAlertDialogStub.java")); - - @Test - public void androidAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_1_arg() { - lint() - .files( - java("package foo;\n" + - "import android.app.AlertDialog;\n" + - "public class Example {\n" + - " public void buildDialog() {\n" + - " new AlertDialog.Builder(context).show();\n" + - " }\n" + - "}") - ) - .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage]\n" + - " new AlertDialog.Builder(context).show();\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context):\n" + - "@@ -5 +5\n" + - "- new AlertDialog.Builder(context).show();\n" + - "+ new com.google.android.material.dialog.MaterialAlertDialogBuilder(context).show();"); - } - - @Test - public void androidAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_2_arg() { - lint() - .files( - java("package foo;\n" + - "import android.app.AlertDialog;\n" + - "public class Example {\n" + - " public void buildDialog() {\n" + - " new AlertDialog.Builder(context, themeOverride).show();\n" + - " }\n" + - "}") - ) - .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage]\n" + - " new AlertDialog.Builder(context, themeOverride).show();\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride):\n" + - "@@ -5 +5\n" + - "- new AlertDialog.Builder(context, themeOverride).show();\n" + - "+ new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride).show();"); - } - - @Test - public void androidAlertDialogBuilderUsed_withAssignment_LogAlertDialogBuilderUsage_1_arg() { - lint() - .files( - java("package foo;\n" + - "import android.app.AlertDialog;\n" + - "public class Example {\n" + - " public void buildDialog() {\n" + - " AlertDialog.Builder builder = new AlertDialog.Builder(context)\n" + - " .show();\n" + - " }\n" + - "}") - ) - .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage]\n" + - " AlertDialog.Builder builder = new AlertDialog.Builder(context)\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context):\n" + - "@@ -5 +5\n" + - "- AlertDialog.Builder builder = new AlertDialog.Builder(context)\n" + - "+ AlertDialog.Builder builder = new com.google.android.material.dialog.MaterialAlertDialogBuilder(context)"); - } - - @Test - public void appcompatAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_1_arg() { - lint() - .files(appCompatAlertDialogStub, - java("package foo;\n" + - "import androidx.appcompat.app.AlertDialog;\n" + - "public class Example {\n" + - " public void buildDialog() {\n" + - " new AlertDialog.Builder(context).show();\n" + - " }\n" + - "}") - ) - .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage]\n" + - " new AlertDialog.Builder(context).show();\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context):\n" + - "@@ -5 +5\n" + - "- new AlertDialog.Builder(context).show();\n" + - "+ new com.google.android.material.dialog.MaterialAlertDialogBuilder(context).show();"); - } - - @Test - public void appcompatAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_2_arg() { - lint() - .files(appCompatAlertDialogStub, - java("package foo;\n" + - "import androidx.appcompat.app.AlertDialog;\n" + - "public class Example {\n" + - " public void buildDialog() {\n" + - " new AlertDialog.Builder(context, themeOverride).show();\n" + - " }\n" + - "}") - ) - .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage]\n" + - " new AlertDialog.Builder(context, themeOverride).show();\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride):\n" + - "@@ -5 +5\n" + - "- new AlertDialog.Builder(context, themeOverride).show();\n" + - "+ new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride).show();"); - } - - @Test - public void appcompatAlertDialogBuilderUsed_withAssignment_LogAlertDialogBuilderUsage_1_arg() { - lint() - .files(appCompatAlertDialogStub, - java("package foo;\n" + - "import androidx.appcompat.app.AlertDialog;\n" + - "public class Example {\n" + - " public void buildDialog() {\n" + - " AlertDialog.Builder builder = new AlertDialog.Builder(context)\n" + - " .show();\n" + - " }\n" + - "}") - ) - .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage]\n" + - " AlertDialog.Builder builder = new AlertDialog.Builder(context)\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context):\n" + - "@@ -5 +5\n" + - "- AlertDialog.Builder builder = new AlertDialog.Builder(context)\n" + - "+ AlertDialog.Builder builder = new com.google.android.material.dialog.MaterialAlertDialogBuilder(context)"); - } - - private static String readResourceAsString(String resourceName) { - InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName); - assertNotNull(inputStream); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - assertTrue(scanner.hasNext()); - return scanner.next(); - } -} diff --git a/lintchecks/src/test/java/org/signal/lint/AlertDialogBuilderDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/AlertDialogBuilderDetectorTest.kt new file mode 100644 index 0000000000..0e8afce793 --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/AlertDialogBuilderDetectorTest.kt @@ -0,0 +1,244 @@ +package org.signal.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Scanner + +class AlertDialogBuilderDetectorTest { + @Test + fun androidAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_1_arg() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.app.AlertDialog; + public class Example { + public void buildDialog() { + new AlertDialog.Builder(context).show(); + } + } + """.trimIndent() + ) + ) + .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage] + new AlertDialog.Builder(context).show(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context): + @@ -5 +5 + - new AlertDialog.Builder(context).show(); + + new com.google.android.material.dialog.MaterialAlertDialogBuilder(context).show(); + """.trimIndent() + ) + } + + @Test + fun androidAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_2_arg() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.app.AlertDialog; + public class Example { + public void buildDialog() { + new AlertDialog.Builder(context, themeOverride).show(); + } + } + """.trimIndent() + ) + ) + .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage] + new AlertDialog.Builder(context, themeOverride).show(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride): + @@ -5 +5 + - new AlertDialog.Builder(context, themeOverride).show(); + + new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride).show(); + """.trimIndent() + ) + } + + @Test + fun androidAlertDialogBuilderUsed_withAssignment_LogAlertDialogBuilderUsage_1_arg() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.app.AlertDialog; + public class Example { + public void buildDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .show(); + } + } + """.trimIndent() + ) + ) + .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'android.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage] + AlertDialog.Builder builder = new AlertDialog.Builder(context) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context): + @@ -5 +5 + - AlertDialog.Builder builder = new AlertDialog.Builder(context) + + AlertDialog.Builder builder = new com.google.android.material.dialog.MaterialAlertDialogBuilder(context) + """.trimIndent() + ) + } + + @Test + fun appcompatAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_1_arg() { + TestLintTask.lint() + .files( + appCompatAlertDialogStub, + java( + """ + package foo; + import androidx.appcompat.app.AlertDialog; + public class Example { + public void buildDialog() { + new AlertDialog.Builder(context).show(); + } + } + """.trimIndent() + ) + ) + .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage] + new AlertDialog.Builder(context).show(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context): + @@ -5 +5 + - new AlertDialog.Builder(context).show(); + + new com.google.android.material.dialog.MaterialAlertDialogBuilder(context).show(); + """.trimIndent() + ) + } + + @Test + fun appcompatAlertDialogBuilderUsed_LogAlertDialogBuilderUsage_2_arg() { + TestLintTask.lint() + .files( + appCompatAlertDialogStub, + java( + """ + package foo; + import androidx.appcompat.app.AlertDialog; + public class Example { + public void buildDialog() { + new AlertDialog.Builder(context, themeOverride).show(); + } + } + """.trimIndent() + ) + ) + .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage] + new AlertDialog.Builder(context, themeOverride).show(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride): + @@ -5 +5 + - new AlertDialog.Builder(context, themeOverride).show(); + + new com.google.android.material.dialog.MaterialAlertDialogBuilder(context, themeOverride).show(); + """.trimIndent() + ) + } + + @Test + fun appcompatAlertDialogBuilderUsed_withAssignment_LogAlertDialogBuilderUsage_1_arg() { + TestLintTask.lint() + .files( + appCompatAlertDialogStub, + java( + """ + package foo; + import androidx.appcompat.app.AlertDialog; + public class Example { + public void buildDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .show(); + } + } + """.trimIndent() + ) + ) + .issues(AlertDialogBuilderDetector.ALERT_DIALOG_BUILDER_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'androidx.appcompat.app.AlertDialog.Builder' instead of com.google.android.material.dialog.MaterialAlertDialogBuilder [AlertDialogBuilderUsage] + AlertDialog.Builder builder = new AlertDialog.Builder(context) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.dialog.MaterialAlertDialogBuilder(context): + @@ -5 +5 + - AlertDialog.Builder builder = new AlertDialog.Builder(context) + + AlertDialog.Builder builder = new com.google.android.material.dialog.MaterialAlertDialogBuilder(context) + """.trimIndent() + ) + } + + companion object { + private val appCompatAlertDialogStub = kotlin(readResourceAsString("AppCompatAlertDialogStub.kt")) + + private fun readResourceAsString(@Suppress("SameParameterValue") resourceName: String): String { + val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName) + assertNotNull(inputStream) + val scanner = Scanner(inputStream!!).useDelimiter("\\A") + assertTrue(scanner.hasNext()) + return scanner.next() + } + } +} diff --git a/lintchecks/src/test/java/org/signal/lint/CardViewDetectorTest.java b/lintchecks/src/test/java/org/signal/lint/CardViewDetectorTest.java deleted file mode 100644 index 725f904eb2..0000000000 --- a/lintchecks/src/test/java/org/signal/lint/CardViewDetectorTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.Scanner; - -import static com.android.tools.lint.checks.infrastructure.TestFiles.java; -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("UnstableApiUsage") -public final class CardViewDetectorTest { - - private static final TestFile cardViewStub = java(readResourceAsString("CardViewStub.java")); - - @Test - public void cardViewUsed_LogCardViewUsage_1_arg() { - lint() - .files(cardViewStub, - java("package foo;\n" + - "import androidx.cardview.widget.CardView;\n" + - "public class Example {\n" + - " public void buildCardView() {\n" + - " new CardView(context);\n" + - " }\n" + - "}") - ) - .issues(CardViewDetector.CARD_VIEW_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView [CardViewUsage]\n" + - " new CardView(context);\n" + - " ~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.card.MaterialCardView(context):\n" + - "@@ -5 +5\n" + - "- new CardView(context);\n" + - "+ new com.google.android.material.card.MaterialCardView(context);"); - } - - @Test - public void cardViewUsed_LogCardViewUsage_2_arg() { - lint() - .files(cardViewStub, - java("package foo;\n" + - "import androidx.cardview.widget.CardView;\n" + - "public class Example {\n" + - " public void buildCardView() {\n" + - " new CardView(context, attrs);\n" + - " }\n" + - "}") - ) - .issues(CardViewDetector.CARD_VIEW_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView [CardViewUsage]\n" + - " new CardView(context, attrs);\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.card.MaterialCardView(context, attrs):\n" + - "@@ -5 +5\n" + - "- new CardView(context, attrs);\n" + - "+ new com.google.android.material.card.MaterialCardView(context, attrs);"); - } - - @Test - public void cardViewUsed_withAssignment_LogCardViewUsage_1_arg() { - lint() - .files(cardViewStub, - java("package foo;\n" + - "import androidx.cardview.widget.CardView;\n" + - "public class Example {\n" + - " public void buildCardView() {\n" + - " CardView cardView = new CardView(context)\n" + - " ;\n" + - " }\n" + - "}") - ) - .issues(CardViewDetector.CARD_VIEW_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView [CardViewUsage]\n" + - " CardView cardView = new CardView(context)\n" + - " ~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.card.MaterialCardView(context):\n" + - "@@ -5 +5\n" + - "- CardView cardView = new CardView(context)\n" + - "+ CardView cardView = new com.google.android.material.card.MaterialCardView(context)"); - } - - private static String readResourceAsString(String resourceName) { - InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName); - assertNotNull(inputStream); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - assertTrue(scanner.hasNext()); - return scanner.next(); - } -} diff --git a/lintchecks/src/test/java/org/signal/lint/CardViewDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/CardViewDetectorTest.kt new file mode 100644 index 0000000000..1f2a6c0938 --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/CardViewDetectorTest.kt @@ -0,0 +1,135 @@ +package org.signal.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Scanner + +class CardViewDetectorTest { + @Test + fun cardViewUsed_LogCardViewUsage_1_arg() { + TestLintTask.lint() + .files( + cardViewStub, + java( + """ + package foo; + import androidx.cardview.widget.CardView; + public class Example { + public void buildCardView() { + new CardView(context); + } + } + """.trimIndent() + ) + ) + .issues(CardViewDetector.CARD_VIEW_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView [CardViewUsage] + new CardView(context); + ~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.card.MaterialCardView(context): + @@ -5 +5 + - new CardView(context); + + new com.google.android.material.card.MaterialCardView(context); + """.trimIndent() + ) + } + + @Test + fun cardViewUsed_LogCardViewUsage_2_arg() { + TestLintTask.lint() + .files( + cardViewStub, + java( + """ + package foo; + import androidx.cardview.widget.CardView; + public class Example { + public void buildCardView() { + new CardView(context, attrs); + } + } + """.trimIndent() + ) + ) + .issues(CardViewDetector.CARD_VIEW_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView [CardViewUsage] + new CardView(context, attrs); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.card.MaterialCardView(context, attrs): + @@ -5 +5 + - new CardView(context, attrs); + + new com.google.android.material.card.MaterialCardView(context, attrs); + """.trimIndent() + ) + } + + @Test + fun cardViewUsed_withAssignment_LogCardViewUsage_1_arg() { + TestLintTask.lint() + .files( + cardViewStub, + java( + """ + package foo; + import androidx.cardview.widget.CardView; + public class Example { + public void buildCardView() { + CardView cardView = new CardView(context) + ; + } + } + """.trimIndent() + ) + ) + .issues(CardViewDetector.CARD_VIEW_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'androidx.cardview.widget.CardView' instead of com.google.android.material.card.MaterialCardView [CardViewUsage] + CardView cardView = new CardView(context) + ~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with new com.google.android.material.card.MaterialCardView(context): + @@ -5 +5 + - CardView cardView = new CardView(context) + + CardView cardView = new com.google.android.material.card.MaterialCardView(context) + """.trimIndent() + ) + } + + companion object { + private val cardViewStub = kotlin(readResourceAsString("CardViewStub.kt")) + + private fun readResourceAsString(@Suppress("SameParameterValue") resourceName: String): String { + val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName) + assertNotNull(inputStream) + val scanner = Scanner(inputStream!!).useDelimiter("\\A") + assertTrue(scanner.hasNext()) + return scanner.next() + } + } +} diff --git a/lintchecks/src/test/java/org/signal/lint/LogDetectorTest.java b/lintchecks/src/test/java/org/signal/lint/LogDetectorTest.java deleted file mode 100644 index 63a977a88e..0000000000 --- a/lintchecks/src/test/java/org/signal/lint/LogDetectorTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.Scanner; - -import static com.android.tools.lint.checks.infrastructure.TestFiles.java; -import static com.android.tools.lint.checks.infrastructure.TestFiles.kotlin; -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("UnstableApiUsage") -public final class LogDetectorTest { - - private static final TestFile serviceLogStub = java(readResourceAsString("ServiceLogStub.java")); - private static final TestFile appLogStub = java(readResourceAsString("AppLogStub.java")); - private static final TestFile glideLogStub = java(readResourceAsString("GlideLogStub.java")); - - @Test - public void androidLogUsed_LogNotSignal_2_args() { - lint() - .files( - java("package foo;\n" + - "import android.util.Log;\n" + - "public class Example {\n" + - " public void log() {\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.LOG_NOT_SIGNAL) - .run() - .expect("src/foo/Example.java:5: Error: Using 'android.util.Log' instead of a Signal Logger [LogNotSignal]\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " ~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.d(\"TAG\", \"msg\"):\n" + - "@@ -5 +5\n" + - "- Log.d(\"TAG\", \"msg\");\n" + - "+ org.signal.core.util.logging.Log.d(\"TAG\", \"msg\");"); - } - - @Test - public void androidLogUsed_LogNotSignal_3_args() { - lint() - .files( - java("package foo;\n" + - "import android.util.Log;\n" + - "public class Example {\n" + - " public void log() {\n" + - " Log.w(\"TAG\", \"msg\", new Exception());\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.LOG_NOT_SIGNAL) - .run() - .expect("src/foo/Example.java:5: Error: Using 'android.util.Log' instead of a Signal Logger [LogNotSignal]\n" + - " Log.w(\"TAG\", \"msg\", new Exception());\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.w(\"TAG\", \"msg\", new Exception()):\n" + - "@@ -5 +5\n" + - "- Log.w(\"TAG\", \"msg\", new Exception());\n" + - "+ org.signal.core.util.logging.Log.w(\"TAG\", \"msg\", new Exception());"); - } - - @Test - public void signalServiceLogUsed_LogNotApp_2_args() { - lint() - .files(serviceLogStub, - java("package foo;\n" + - "import org.signal.libsignal.protocol.logging.Log;\n" + - "public class Example {\n" + - " public void log() {\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.LOG_NOT_APP) - .run() - .expect("src/foo/Example.java:5: Error: Using Signal server logger instead of app level Logger [LogNotAppSignal]\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " ~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.d(\"TAG\", \"msg\"):\n" + - "@@ -5 +5\n" + - "- Log.d(\"TAG\", \"msg\");\n" + - "+ org.signal.core.util.logging.Log.d(\"TAG\", \"msg\");"); - } - - @Test - public void signalServiceLogUsed_LogNotApp_3_args() { - lint() - .files(serviceLogStub, - java("package foo;\n" + - "import org.signal.libsignal.protocol.logging.Log;\n" + - "public class Example {\n" + - " public void log() {\n" + - " Log.w(\"TAG\", \"msg\", new Exception());\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.LOG_NOT_APP) - .run() - .expect("src/foo/Example.java:5: Error: Using Signal server logger instead of app level Logger [LogNotAppSignal]\n" + - " Log.w(\"TAG\", \"msg\", new Exception());\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.w(\"TAG\", \"msg\", new Exception()):\n" + - "@@ -5 +5\n" + - "- Log.w(\"TAG\", \"msg\", new Exception());\n" + - "+ org.signal.core.util.logging.Log.w(\"TAG\", \"msg\", new Exception());"); - } - - @Test - public void log_uses_tag_constant() { - lint() - .files(appLogStub, - java("package foo;\n" + - "import org.signal.core.util.logging.Log;\n" + - "public class Example {\n" + - " private static final String TAG = Log.tag(Example.class);\n" + - " public void log() {\n" + - " Log.d(TAG, \"msg\");\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.INLINE_TAG) - .run() - .expectClean(); - } - - @Test - public void log_uses_tag_constant_kotlin() { - lint() - .files(appLogStub, - kotlin("package foo\n" + - "import org.signal.core.util.logging.Log\n" + - "class Example {\n" + - " const val TAG: String = Log.tag(Example::class.java)\n" + - " fun log() {\n" + - " Log.d(TAG, \"msg\")\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.INLINE_TAG) - .run() - .expectClean(); - } - - @Test - public void log_uses_tag_companion_kotlin() { - lint() - .files(appLogStub, - kotlin("package foo\n" + - "import org.signal.core.util.logging.Log\n" + - "class Example {\n" + - " companion object { val TAG: String = Log.tag(Example::class.java) }\n" + - " fun log() {\n" + - " Log.d(TAG, \"msg\")\n" + - " }\n" + - "}\n"+ - "fun logOutsie() {\n" + - " Log.d(Example.TAG, \"msg\")\n" + - "}\n") - ) - .issues(SignalLogDetector.INLINE_TAG) - .run() - .expectClean(); - } - - @Test - public void log_uses_inline_tag() { - lint() - .files(appLogStub, - java("package foo;\n" + - "import org.signal.core.util.logging.Log;\n" + - "public class Example {\n" + - " public void log() {\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.INLINE_TAG) - .run() - .expect("src/foo/Example.java:5: Error: Not using a tag constant [LogTagInlined]\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " ~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings") - .expectFixDiffs(""); - } - - @Test - public void log_uses_inline_tag_kotlin() { - lint() - .files(appLogStub, - kotlin("package foo\n" + - "import org.signal.core.util.logging.Log\n" + - "class Example {\n" + - " fun log() {\n" + - " Log.d(\"TAG\", \"msg\")\n" + - " }\n" + - "}")) - .issues(SignalLogDetector.INLINE_TAG) - .run() - .expect("src/foo/Example.kt:5: Error: Not using a tag constant [LogTagInlined]\n" + - " Log.d(\"TAG\", \"msg\")\n" + - " ~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings") - .expectFixDiffs(""); - } - - @Test - public void glideLogUsed_LogNotSignal_2_args() { - lint() - .files(glideLogStub, - java("package foo;\n" + - "import org.signal.glide.Log;\n" + - "public class Example {\n" + - " public void log() {\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " }\n" + - "}") - ) - .issues(SignalLogDetector.LOG_NOT_SIGNAL) - .run() - .expect("src/foo/Example.java:5: Error: Using 'org.signal.glide.Log' instead of a Signal Logger [LogNotSignal]\n" + - " Log.d(\"TAG\", \"msg\");\n" + - " ~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.d(\"TAG\", \"msg\"):\n" + - "@@ -5 +5\n" + - "- Log.d(\"TAG\", \"msg\");\n" + - "+ org.signal.core.util.logging.Log.d(\"TAG\", \"msg\");"); - } - - private static String readResourceAsString(String resourceName) { - InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName); - assertNotNull(inputStream); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - assertTrue(scanner.hasNext()); - return scanner.next(); - } -} diff --git a/lintchecks/src/test/java/org/signal/lint/RecipientIdDatabaseDetectorTest.java b/lintchecks/src/test/java/org/signal/lint/RecipientIdDatabaseDetectorTest.java deleted file mode 100644 index b53587b57c..0000000000 --- a/lintchecks/src/test/java/org/signal/lint/RecipientIdDatabaseDetectorTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.Scanner; - -import static com.android.tools.lint.checks.infrastructure.TestFiles.java; -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("UnstableApiUsage") -public final class RecipientIdDatabaseDetectorTest { - - private static final TestFile recipientReferenceStub = java(readResourceAsString("RecipientIdDatabaseReferenceStub.java")); - - @Test - public void recipientIdDatabase_databaseHasRecipientFieldButDoesNotImplementInterface_showError() { - lint() - .files( - java("package foo;\n" + - "public class Example extends Database {\n" + - " private static final String RECIPIENT_ID = \"recipient_id\";\n" + - "}") - ) - .issues(RecipientIdDatabaseDetector.RECIPIENT_ID_DATABASE_REFERENCE_ISSUE) - .run() - .expect("src/foo/Example.java:3: Error: If you reference a RecipientId in your table, you must implement the RecipientIdDatabaseReference interface. [RecipientIdDatabaseReferenceUsage]\n" + - " private static final String RECIPIENT_ID = \"recipient_id\";\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings"); - } - - @Test - public void recipientIdDatabase_databaseHasRecipientFieldAndImplementsInterface_noError() { - lint() - .files( - recipientReferenceStub, - java("package foo;\n" + - "import org.thoughtcrime.securesms.database.RecipientIdDatabaseReference;\n" + - "public class Example extends Database implements RecipientIdDatabaseReference {\n" + - " private static final String RECIPIENT_ID = \"recipient_id\";\n" + - " @Override\n" + - " public void remapRecipient(RecipientId fromId, RecipientId toId) {}\n" + - "}") - ) - .issues(RecipientIdDatabaseDetector.RECIPIENT_ID_DATABASE_REFERENCE_ISSUE) - .run() - .expectClean(); - } - - private static String readResourceAsString(String resourceName) { - InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName); - assertNotNull(inputStream); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - assertTrue(scanner.hasNext()); - return scanner.next(); - } -} diff --git a/lintchecks/src/test/java/org/signal/lint/RecipientIdDatabaseDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/RecipientIdDatabaseDetectorTest.kt new file mode 100644 index 0000000000..13a1dfdab4 --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/RecipientIdDatabaseDetectorTest.kt @@ -0,0 +1,70 @@ +package org.signal.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Scanner + +class RecipientIdDatabaseDetectorTest { + @Test + fun recipientIdDatabase_databaseHasRecipientFieldButDoesNotImplementInterface_showError() { + TestLintTask.lint() + .files( + java( + """ + package foo; + public class Example extends Database { + private static final String RECIPIENT_ID = "recipient_id"; + } + """.trimIndent() + ) + ) + .issues(RecipientIdDatabaseDetector.RECIPIENT_ID_DATABASE_REFERENCE_ISSUE) + .run() + .expect( + """ + src/foo/Example.java:3: Error: If you reference a RecipientId in your table, you must implement the RecipientIdDatabaseReference interface. [RecipientIdDatabaseReferenceUsage] + private static final String RECIPIENT_ID = "recipient_id"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun recipientIdDatabase_databaseHasRecipientFieldAndImplementsInterface_noError() { + TestLintTask.lint() + .files( + recipientReferenceStub, + java( + """ + package foo; + import org.thoughtcrime.securesms.database.RecipientIdDatabaseReference; + public class Example extends Database implements RecipientIdDatabaseReference { + private static final String RECIPIENT_ID = "recipient_id"; + @Override + public void remapRecipient(RecipientId fromId, RecipientId toId) {} + } + """.trimIndent() + ) + ) + .issues(RecipientIdDatabaseDetector.RECIPIENT_ID_DATABASE_REFERENCE_ISSUE) + .run() + .expectClean() + } + + companion object { + private val recipientReferenceStub = kotlin(readResourceAsString("RecipientIdDatabaseReferenceStub.kt")) + + private fun readResourceAsString(@Suppress("SameParameterValue") resourceName: String): String { + val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName) + assertNotNull(inputStream) + val scanner = Scanner(inputStream!!).useDelimiter("\\A") + assertTrue(scanner.hasNext()) + return scanner.next() + } + } +} diff --git a/lintchecks/src/test/java/org/signal/lint/SignalLogDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/SignalLogDetectorTest.kt new file mode 100644 index 0000000000..2cd44dca1a --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/SignalLogDetectorTest.kt @@ -0,0 +1,343 @@ +package org.signal.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestMode +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Scanner + +class SignalLogDetectorTest { + @Test + fun androidLogUsed_LogNotSignal_2_args() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.util.Log; + public class Example { + public void log() { + Log.d("TAG", "msg"); + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.LOG_NOT_SIGNAL) + .run() + .expect( + """ + src/foo/Example.java:5: Error: Using 'android.util.Log' instead of a Signal Logger [LogNotSignal] + Log.d("TAG", "msg"); + ~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.d("TAG", "msg"): + @@ -5 +5 + - Log.d("TAG", "msg"); + + org.signal.core.util.logging.Log.d("TAG", "msg"); + """.trimIndent() + ) + } + + @Test + fun androidLogUsed_LogNotSignal_3_args() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.util.Log; + public class Example { + public void log() { + Log.w("TAG", "msg", new Exception()); + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.LOG_NOT_SIGNAL) + .run() + .expect( + """ + src/foo/Example.java:5: Error: Using 'android.util.Log' instead of a Signal Logger [LogNotSignal] + Log.w("TAG", "msg", new Exception()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.w("TAG", "msg", new Exception()): + @@ -5 +5 + - Log.w("TAG", "msg", new Exception()); + + org.signal.core.util.logging.Log.w("TAG", "msg", new Exception()); + """.trimIndent() + ) + } + + @Test + fun signalServiceLogUsed_LogNotApp_2_args() { + TestLintTask.lint() + .files( + serviceLogStub, + java( + """ + package foo; + import org.signal.libsignal.protocol.logging.Log; + public class Example { + public void log() { + Log.d("TAG", "msg"); + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.LOG_NOT_APP) + .run() + .expect( + """ + src/foo/Example.java:5: Error: Using Signal server logger instead of app level Logger [LogNotAppSignal] + Log.d("TAG", "msg"); + ~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.d("TAG", "msg"): + @@ -5 +5 + - Log.d("TAG", "msg"); + + org.signal.core.util.logging.Log.d("TAG", "msg"); + """.trimIndent() + ) + } + + @Test + fun signalServiceLogUsed_LogNotApp_3_args() { + TestLintTask.lint() + .files( + serviceLogStub, + java( + """ + package foo; + import org.signal.libsignal.protocol.logging.Log; + public class Example { + public void log() { + Log.w("TAG", "msg", new Exception()); + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.LOG_NOT_APP) + .run() + .expect( + """ + src/foo/Example.java:5: Error: Using Signal server logger instead of app level Logger [LogNotAppSignal] + Log.w("TAG", "msg", new Exception()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.w("TAG", "msg", new Exception()): + @@ -5 +5 + - Log.w("TAG", "msg", new Exception()); + + org.signal.core.util.logging.Log.w("TAG", "msg", new Exception()); + """.trimIndent() + ) + } + + @Test + fun log_uses_tag_constant() { + TestLintTask.lint() + .files( + appLogStub, + java( + """ + package foo; + import org.signal.core.util.logging.Log; + public class Example { + private static final String TAG = Log.tag(Example.class); + public void log() { + Log.d(TAG, "msg"); + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.INLINE_TAG) + .run() + .expectClean() + } + + @Test + fun log_uses_tag_constant_kotlin() { + TestLintTask.lint() + .files( + appLogStub, + kotlin( + """ + package foo + import org.signal.core.util.logging.Log + class Example { + const val TAG: String = Log.tag(Example::class.java) + fun log() { + Log.d(TAG, "msg") + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.INLINE_TAG) + .skipTestModes(TestMode.REORDER_ARGUMENTS) + .run() + .expectClean() + } + + @Test + fun log_uses_tag_companion_kotlin() { + TestLintTask.lint() + .files( + appLogStub, + kotlin( + """ + package foo + import org.signal.core.util.logging.Log + class Example { + companion object { val TAG: String = Log.tag(Example::class.java) } + fun log() { + Log.d(TAG, "msg") + } + } + fun logOutsie() { + Log.d(Example.TAG, "msg") + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.INLINE_TAG) + .skipTestModes(TestMode.REORDER_ARGUMENTS) + .run() + .expectClean() + } + + @Test + fun log_uses_inline_tag() { + TestLintTask.lint() + .files( + appLogStub, + java( + """ + package foo; + import org.signal.core.util.logging.Log; + public class Example { + public void log() { + Log.d("TAG", "msg"); + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.INLINE_TAG) + .run() + .expect( + """ + src/foo/Example.java:5: Error: Not using a tag constant [LogTagInlined] + Log.d("TAG", "msg"); + ~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + .expectFixDiffs("") + } + + @Test + fun log_uses_inline_tag_kotlin() { + TestLintTask.lint() + .files( + appLogStub, + kotlin( + """ + package foo + import org.signal.core.util.logging.Log + class Example { + fun log() { + Log.d("TAG", "msg") + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.INLINE_TAG) + .run() + .expect( + """ + src/foo/Example.kt:5: Error: Not using a tag constant [LogTagInlined] + Log.d("TAG", "msg") + ~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + .expectFixDiffs("") + } + + @Test + fun glideLogUsed_LogNotSignal_2_args() { + TestLintTask.lint() + .files( + glideLogStub, + java( + """ + package foo; + import org.signal.glide.Log; + public class Example { + public void log() { + Log.d("TAG", "msg"); + } + } + """.trimIndent() + ) + ) + .issues(SignalLogDetector.LOG_NOT_SIGNAL) + .run() + .expect( + """ + src/foo/Example.java:5: Error: Using 'org.signal.glide.Log' instead of a Signal Logger [LogNotSignal] + Log.d("TAG", "msg"); + ~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with org.signal.core.util.logging.Log.d("TAG", "msg"): + @@ -5 +5 + - Log.d("TAG", "msg"); + + org.signal.core.util.logging.Log.d("TAG", "msg"); + """.trimIndent() + ) + } + + companion object { + private val serviceLogStub = kotlin(readResourceAsString("ServiceLogStub.kt")) + private val appLogStub = kotlin(readResourceAsString("AppLogStub.kt")) + private val glideLogStub = kotlin(readResourceAsString("GlideLogStub.kt")) + + private fun readResourceAsString(resourceName: String): String { + val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName) + assertNotNull(inputStream) + val scanner = Scanner(inputStream!!).useDelimiter("\\A") + assertTrue(scanner.hasNext()) + return scanner.next() + } + } +} diff --git a/lintchecks/src/test/java/org/signal/lint/StartForegroundServiceDetectorTest.java b/lintchecks/src/test/java/org/signal/lint/StartForegroundServiceDetectorTest.java deleted file mode 100644 index 23c0d23a72..0000000000 --- a/lintchecks/src/test/java/org/signal/lint/StartForegroundServiceDetectorTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.Scanner; - -import static com.android.tools.lint.checks.infrastructure.TestFiles.java; -import static com.android.tools.lint.checks.infrastructure.TestFiles.kotlin; -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("UnstableApiUsage") -public final class StartForegroundServiceDetectorTest { - - private static final TestFile contextCompatStub = java(readResourceAsString("ContextCompatStub.java")); - private static final TestFile contextStub = java(readResourceAsString("ContextStub.java")); - - @Test - public void contextCompatUsed() { - lint() - .files( - contextCompatStub, - java("package foo;\n" + - "import androidx.core.content.ContextCompat;\n" + - "public class Example {\n" + - " public void start() {\n" + - " ContextCompat.startForegroundService(context, new Intent());\n" + - " }\n" + - "}") - ) - .allowMissingSdk() - .issues(StartForegroundServiceDetector.START_FOREGROUND_SERVICE_ISSUE) - .run() - .expect("src/foo/Example.java:5: Error: Using 'ContextCompat.startForegroundService' instead of a ForegroundServiceUtil [StartForegroundServiceUsage]\n" + - " ContextCompat.startForegroundService(context, new Intent());\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings"); - } - - @Test - public void contextUsed() { - lint() - .files( - contextStub, - java("package foo;\n" + - "import android.content.Context;\n" + - "public class Example {\n" + - " Context context;\n" + - " public void start() {\n" + - " context.startForegroundService(new Intent());\n" + - " }\n" + - "}") - ) - .allowMissingSdk() - .issues(StartForegroundServiceDetector.START_FOREGROUND_SERVICE_ISSUE) - .run() - .expect("src/foo/Example.java:6: Error: Using 'Context.startForegroundService' instead of a ForegroundServiceUtil [StartForegroundServiceUsage]\n" + - " context.startForegroundService(new Intent());\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings"); - } - - - private static String readResourceAsString(String resourceName) { - InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName); - assertNotNull(inputStream); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - assertTrue(scanner.hasNext()); - return scanner.next(); - } -} diff --git a/lintchecks/src/test/java/org/signal/lint/StartForegroundServiceDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/StartForegroundServiceDetectorTest.kt new file mode 100644 index 0000000000..f5f082a243 --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/StartForegroundServiceDetectorTest.kt @@ -0,0 +1,86 @@ +package org.signal.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Scanner + +class StartForegroundServiceDetectorTest { + @Test + fun contextCompatUsed() { + TestLintTask.lint() + .files( + contextCompatStub, + java( + """ + package foo; + import androidx.core.content.ContextCompat; + public class Example { + public void start() { + ContextCompat.startForegroundService(context, new Intent()); + } + } + """.trimIndent() + ) + ) + .allowMissingSdk() + .issues(StartForegroundServiceDetector.START_FOREGROUND_SERVICE_ISSUE) + .run() + .expect( + """ + src/foo/Example.java:5: Error: Using 'ContextCompat.startForegroundService' instead of a ForegroundServiceUtil [StartForegroundServiceUsage] + ContextCompat.startForegroundService(context, new Intent()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun contextUsed() { + TestLintTask.lint() + .files( + contextStub, + java( + """ + package foo; + import android.content.Context; + public class Example { + Context context; + public void start() { + context.startForegroundService(new Intent()); + } + } + """.trimIndent() + ) + ) + .allowMissingSdk() + .issues(StartForegroundServiceDetector.START_FOREGROUND_SERVICE_ISSUE) + .run() + .expect( + """ + src/foo/Example.java:6: Error: Using 'Context.startForegroundService' instead of a ForegroundServiceUtil [StartForegroundServiceUsage] + context.startForegroundService(new Intent()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + + companion object { + private val contextCompatStub = kotlin(readResourceAsString("ContextCompatStub.kt")) + private val contextStub = kotlin(readResourceAsString("ContextStub.kt")) + + private fun readResourceAsString(resourceName: String): String { + val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName) + assertNotNull(inputStream) + val scanner = Scanner(inputStream!!).useDelimiter("\\A") + assertTrue(scanner.hasNext()) + return scanner.next() + } + } +} diff --git a/lintchecks/src/test/java/org/signal/lint/ThreadIdDatabaseDetectorTest.java b/lintchecks/src/test/java/org/signal/lint/ThreadIdDatabaseDetectorTest.java deleted file mode 100644 index ce76e866bb..0000000000 --- a/lintchecks/src/test/java/org/signal/lint/ThreadIdDatabaseDetectorTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.Scanner; - -import static com.android.tools.lint.checks.infrastructure.TestFiles.java; -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("UnstableApiUsage") -public final class ThreadIdDatabaseDetectorTest { - - private static final TestFile threadReferenceStub = java(readResourceAsString("ThreadIdDatabaseReferenceStub.java")); - - @Test - public void threadIdDatabase_databaseHasThreadFieldButDoesNotImplementInterface_showError() { - lint() - .files( - java("package foo;\n" + - "public class Example extends Database {\n" + - " private static final String THREAD_ID = \"thread_id\";\n" + - "}") - ) - .issues(ThreadIdDatabaseDetector.THREAD_ID_DATABASE_REFERENCE_ISSUE) - .run() - .expect("src/foo/Example.java:3: Error: If you reference a thread ID in your table, you must implement the ThreadIdDatabaseReference interface. [ThreadIdDatabaseReferenceUsage]\n" + - " private static final String THREAD_ID = \"thread_id\";\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings"); - } - - @Test - public void threadIdDatabase_databaseHasThreadFieldAndImplementsInterface_noError() { - lint() - .files( - threadReferenceStub, - java("package foo;\n" + - "import org.thoughtcrime.securesms.database.ThreadIdDatabaseReference;\n" + - "public class Example extends Database implements ThreadIdDatabaseReference {\n" + - " private static final String THREAD_ID = \"thread_id\";\n" + - " @Override\n" + - " public void remapThread(long fromId, long toId) {}\n" + - "}") - ) - .issues(ThreadIdDatabaseDetector.THREAD_ID_DATABASE_REFERENCE_ISSUE) - .run() - .expectClean(); - } - - private static String readResourceAsString(String resourceName) { - InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName); - assertNotNull(inputStream); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - assertTrue(scanner.hasNext()); - return scanner.next(); - } -} diff --git a/lintchecks/src/test/java/org/signal/lint/ThreadIdDatabaseDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/ThreadIdDatabaseDetectorTest.kt new file mode 100644 index 0000000000..43e7915b02 --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/ThreadIdDatabaseDetectorTest.kt @@ -0,0 +1,70 @@ +package org.signal.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Scanner + +class ThreadIdDatabaseDetectorTest { + @Test + fun threadIdDatabase_databaseHasThreadFieldButDoesNotImplementInterface_showError() { + TestLintTask.lint() + .files( + java( + """ + package foo; + public class Example extends Database { + private static final String THREAD_ID = "thread_id"; + } + """.trimIndent() + ) + ) + .issues(ThreadIdDatabaseDetector.THREAD_ID_DATABASE_REFERENCE_ISSUE) + .run() + .expect( + """ + src/foo/Example.java:3: Error: If you reference a thread ID in your table, you must implement the ThreadIdDatabaseReference interface. [ThreadIdDatabaseReferenceUsage] + private static final String THREAD_ID = "thread_id"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.trimIndent() + ) + } + + @Test + fun threadIdDatabase_databaseHasThreadFieldAndImplementsInterface_noError() { + TestLintTask.lint() + .files( + threadReferenceStub, + java( + """ + package foo; + import org.thoughtcrime.securesms.database.ThreadIdDatabaseReference; + public class Example extends Database implements ThreadIdDatabaseReference { + private static final String THREAD_ID = "thread_id"; + @Override + public void remapThread(long fromId, long toId) {} + } + """.trimIndent() + ) + ) + .issues(ThreadIdDatabaseDetector.THREAD_ID_DATABASE_REFERENCE_ISSUE) + .run() + .expectClean() + } + + companion object { + private val threadReferenceStub = kotlin(readResourceAsString("ThreadIdDatabaseReferenceStub.kt")) + + private fun readResourceAsString(@Suppress("SameParameterValue") resourceName: String): String { + val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName) + assertNotNull(inputStream) + val scanner = Scanner(inputStream!!).useDelimiter("\\A") + assertTrue(scanner.hasNext()) + return scanner.next() + } + } +} diff --git a/lintchecks/src/test/java/org/signal/lint/VersionCodeDetectorTest.java b/lintchecks/src/test/java/org/signal/lint/VersionCodeDetectorTest.java deleted file mode 100644 index 153cb8652c..0000000000 --- a/lintchecks/src/test/java/org/signal/lint/VersionCodeDetectorTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.signal.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; - -import org.junit.Test; - -import java.io.InputStream; -import java.util.Scanner; - -import static com.android.tools.lint.checks.infrastructure.TestFiles.java; -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("UnstableApiUsage") -public final class VersionCodeDetectorTest { - - private static final TestFile requiresApiStub = java(readResourceAsString("RequiresApiStub.java")); - - @Test - public void version_code_constant_referenced_in_code() { - lint() - .files( - java("package foo;\n" + - "import android.os.Build;\n" + - "public class Example {\n" + - " public void versionCodeMention() {\n" + - " if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n" + - " }\n" + - " }\n" + - "}") - ) - .issues(VersionCodeDetector.VERSION_CODE_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'VERSION_CODES' reference instead of the numeric value 21 [VersionCodeUsage]\n" + - " if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with 21:\n" + - "@@ -5 +5\n" + - "- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n" + - "+ if (Build.VERSION.SDK_INT >= 21) {"); - } - - @Test - public void numeric_value_referenced_in_code() { - lint() - .files( - java("package foo;\n" + - "import android.os.Build;\n" + - "public class Example {\n" + - " public void versionCodeMention() {\n" + - " if (Build.VERSION.SDK_INT >= 22) {\n" + - " }\n" + - " }\n" + - "}") - ) - .issues(VersionCodeDetector.VERSION_CODE_USAGE) - .run() - .expectClean(); - } - - @Test - public void non_version_code_constant_referenced_in_code() { - lint() - .files( - java("package foo;\n" + - "import android.os.Build;\n" + - "public class Example {\n" + - " private final static int LOLLIPOP = 21;\n" + - " public void versionCodeMention() {\n" + - " if (Build.VERSION.SDK_INT >= LOLLIPOP) {\n" + - " }\n" + - " }\n" + - "}") - ) - .issues(VersionCodeDetector.VERSION_CODE_USAGE) - .run() - .expectClean(); - } - - @Test - public void version_code_constant_referenced_in_TargetApi_attribute_and_inner_class_import() { - lint() - .files( - java("package foo;\n" + - "import android.os.Build.VERSION_CODES;\n" + - "import android.annotation.TargetApi;\n" + - "public class Example {\n" + - " @TargetApi(VERSION_CODES.N)\n" + - " public void versionCodeMention() {\n" + - " }\n" + - "}") - ) - .issues(VersionCodeDetector.VERSION_CODE_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'VERSION_CODES' reference instead of the numeric value 24 [VersionCodeUsage]\n" + - " @TargetApi(VERSION_CODES.N)\n" + - " ~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with 24:\n" + - "@@ -5 +5\n" + - "- @TargetApi(VERSION_CODES.N)\n" + - "+ @TargetApi(24)"); - } - - @Test - public void version_code_constant_referenced_in_RequiresApi_attribute_with_named_parameter() { - lint() - .files( - requiresApiStub, - java("package foo;\n" + - "import android.os.Build;\n" + - "import android.annotation.RequiresApi;\n" + - "public class Example {\n" + - " @RequiresApi(app = Build.VERSION_CODES.M)\n" + - " public void versionCodeMention() {\n" + - " }\n" + - "}") - ) - .issues(VersionCodeDetector.VERSION_CODE_USAGE) - .run() - .expect("src/foo/Example.java:5: Warning: Using 'VERSION_CODES' reference instead of the numeric value 23 [VersionCodeUsage]\n" + - " @RequiresApi(app = Build.VERSION_CODES.M)\n" + - " ~~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings") - .expectFixDiffs("Fix for src/foo/Example.java line 5: Replace with 23:\n" + - "@@ -5 +5\n" + - "- @RequiresApi(app = Build.VERSION_CODES.M)\n" + - "+ @RequiresApi(app = 23)"); - } - - private static String readResourceAsString(String resourceName) { - InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName); - assertNotNull(inputStream); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - assertTrue(scanner.hasNext()); - return scanner.next(); - } -} diff --git a/lintchecks/src/test/java/org/signal/lint/VersionCodeDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/VersionCodeDetectorTest.kt new file mode 100644 index 0000000000..83f2ac5c96 --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/VersionCodeDetectorTest.kt @@ -0,0 +1,183 @@ +package org.signal.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Scanner + +class VersionCodeDetectorTest { + @Test + fun version_code_constant_referenced_in_code() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.os.Build; + public class Example { + public void versionCodeMention() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + continue; + } + } + } + """.trimIndent() + ) + ) + .issues(VersionCodeDetector.VERSION_CODE_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'VERSION_CODES' reference instead of the numeric value 21 [VersionCodeUsage] + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with 21: + @@ -5 +5 + - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + + if (Build.VERSION.SDK_INT >= 21) { + """.trimIndent() + ) + } + + @Test + fun numeric_value_referenced_in_code() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.os.Build; + public class Example { + public void versionCodeMention() { + if (Build.VERSION.SDK_INT >= 22) { + continue; + } + } + } + """.trimIndent() + ) + ) + .issues(VersionCodeDetector.VERSION_CODE_USAGE) + .run() + .expectClean() + } + + @Test + fun non_version_code_constant_referenced_in_code() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.os.Build; + public class Example { + private final static int LOLLIPOP = 21; + public void versionCodeMention() { + if (Build.VERSION.SDK_INT >= LOLLIPOP) { + continue; + } + } + } + """.trimIndent() + ) + ) + .issues(VersionCodeDetector.VERSION_CODE_USAGE) + .run() + .expectClean() + } + + @Test + fun version_code_constant_referenced_in_TargetApi_attribute_and_inner_class_import() { + TestLintTask.lint() + .files( + java( + """ + package foo; + import android.os.Build.VERSION_CODES; + import android.annotation.TargetApi; + public class Example { + @TargetApi(VERSION_CODES.N) + public void versionCodeMention() { + } + } + """.trimIndent() + ) + ) + .issues(VersionCodeDetector.VERSION_CODE_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'VERSION_CODES' reference instead of the numeric value 24 [VersionCodeUsage] + @TargetApi(VERSION_CODES.N) + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with 24: + @@ -5 +5 + - @TargetApi(VERSION_CODES.N) + + @TargetApi(24) + """.trimIndent() + ) + } + + @Test + fun version_code_constant_referenced_in_RequiresApi_attribute_with_named_parameter() { + TestLintTask.lint() + .files( + requiresApiStub, + java( + """ + package foo; + import android.os.Build; + import android.annotation.RequiresApi; + public class Example { + @RequiresApi(app = Build.VERSION_CODES.M) + public void versionCodeMention() { + } + } + """.trimIndent() + ) + ) + .issues(VersionCodeDetector.VERSION_CODE_USAGE) + .run() + .expect( + """ + src/foo/Example.java:5: Warning: Using 'VERSION_CODES' reference instead of the numeric value 23 [VersionCodeUsage] + @RequiresApi(app = Build.VERSION_CODES.M) + ~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent() + ) + .expectFixDiffs( + """ + Fix for src/foo/Example.java line 5: Replace with 23: + @@ -5 +5 + - @RequiresApi(app = Build.VERSION_CODES.M) + + @RequiresApi(app = 23) + """.trimIndent() + ) + } + + companion object { + private val requiresApiStub = kotlin(readResourceAsString("RequiresApiStub.kt")) + + private fun readResourceAsString(@Suppress("SameParameterValue") resourceName: String): String { + val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName) + assertNotNull(inputStream) + val scanner = Scanner(inputStream!!).useDelimiter("\\A") + assertTrue(scanner.hasNext()) + return scanner.next() + } + } +} diff --git a/lintchecks/src/test/resources/AppCompatAlertDialogStub.java b/lintchecks/src/test/resources/AppCompatAlertDialogStub.java deleted file mode 100644 index 2df340fe0b..0000000000 --- a/lintchecks/src/test/resources/AppCompatAlertDialogStub.java +++ /dev/null @@ -1,13 +0,0 @@ -package androidx.appcompat.app; - -public class AlertDialog { - - public static class Builder { - - public Builder(Context context) { - } - - public Builder(Context context, int themeOverrideId) { - } - } -} diff --git a/lintchecks/src/test/resources/AppCompatAlertDialogStub.kt b/lintchecks/src/test/resources/AppCompatAlertDialogStub.kt new file mode 100644 index 0000000000..1b8072b7ad --- /dev/null +++ b/lintchecks/src/test/resources/AppCompatAlertDialogStub.kt @@ -0,0 +1,8 @@ +package androidx.appcompat.app + +class AlertDialog { + class Builder { + constructor(context: Context?) + constructor(context: Context?, themeOverrideId: Int) + } +} diff --git a/lintchecks/src/test/resources/AppLogStub.java b/lintchecks/src/test/resources/AppLogStub.java deleted file mode 100644 index fa3f71847c..0000000000 --- a/lintchecks/src/test/resources/AppLogStub.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.signal.core.util.logging; - -public class Log { - - public static String tag(Class clazz) { - return ""; - } - - public static void v(String tag, String msg) { - } - - public static void v(String tag, String msg, Throwable tr) { - } - - public static void d(String tag, String msg) { - } - - public static void d(String tag, String msg, Throwable tr) { - } - - public static void i(String tag, String msg) { - } - - public static void i(String tag, String msg, Throwable tr) { - } - - public static void w(String tag, String msg) { - } - - public static void w(String tag, String msg, Throwable tr) { - } - - public static void w(String tag, Throwable tr) { - } - - public static void e(String tag, String msg) { - } - - public static void e(String tag, String msg, Throwable tr) { - } -} diff --git a/lintchecks/src/test/resources/AppLogStub.kt b/lintchecks/src/test/resources/AppLogStub.kt new file mode 100644 index 0000000000..50bdc8aceb --- /dev/null +++ b/lintchecks/src/test/resources/AppLogStub.kt @@ -0,0 +1,40 @@ +package org.signal.core.util.logging + +object Log { + fun tag(clazz: Class<*>?): String { + return "" + } + + fun v(tag: String?, msg: String?) { + } + + fun v(tag: String?, msg: String?, tr: Throwable?) { + } + + fun d(tag: String?, msg: String?) { + } + + fun d(tag: String?, msg: String?, tr: Throwable?) { + } + + fun i(tag: String?, msg: String?) { + } + + fun i(tag: String?, msg: String?, tr: Throwable?) { + } + + fun w(tag: String?, msg: String?) { + } + + fun w(tag: String?, msg: String?, tr: Throwable?) { + } + + fun w(tag: String?, tr: Throwable?) { + } + + fun e(tag: String?, msg: String?) { + } + + fun e(tag: String?, msg: String?, tr: Throwable?) { + } +} diff --git a/lintchecks/src/test/resources/CardViewStub.java b/lintchecks/src/test/resources/CardViewStub.java deleted file mode 100644 index 1afd20e2f5..0000000000 --- a/lintchecks/src/test/resources/CardViewStub.java +++ /dev/null @@ -1,16 +0,0 @@ -package androidx.cardview.widget; - -public class CardView { - - public CardView(Context context) { - - } - - public CardView(Context context, AttributeSet attrs) { - - } - - public CardView(Context context, AttributeSet attrs, int defStyleAttr) { - - } -} diff --git a/lintchecks/src/test/resources/CardViewStub.kt b/lintchecks/src/test/resources/CardViewStub.kt new file mode 100644 index 0000000000..a5971bbbb0 --- /dev/null +++ b/lintchecks/src/test/resources/CardViewStub.kt @@ -0,0 +1,9 @@ +package androidx.cardview.widget + +class CardView { + constructor(context: Context?) + + constructor(context: Context?, attrs: AttributeSet?) + + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) +} diff --git a/lintchecks/src/test/resources/ContextCompatStub.java b/lintchecks/src/test/resources/ContextCompatStub.java deleted file mode 100644 index be01430126..0000000000 --- a/lintchecks/src/test/resources/ContextCompatStub.java +++ /dev/null @@ -1,6 +0,0 @@ -package androidx.core.content; - -public class ContextCompat { - public static void startForegroundService(Context context, Intent intent) { - } -} diff --git a/lintchecks/src/test/resources/ContextCompatStub.kt b/lintchecks/src/test/resources/ContextCompatStub.kt new file mode 100644 index 0000000000..a385b62dcb --- /dev/null +++ b/lintchecks/src/test/resources/ContextCompatStub.kt @@ -0,0 +1,6 @@ +package androidx.core.content + +object ContextCompat { + fun startForegroundService(context: Context?, intent: Intent?) { + } +} diff --git a/lintchecks/src/test/resources/ContextStub.java b/lintchecks/src/test/resources/ContextStub.java deleted file mode 100644 index 10fc09ed34..0000000000 --- a/lintchecks/src/test/resources/ContextStub.java +++ /dev/null @@ -1,6 +0,0 @@ -package android.content; - -public class Context { - public void startForegroundService(Intent intent) { - } -} diff --git a/lintchecks/src/test/resources/ContextStub.kt b/lintchecks/src/test/resources/ContextStub.kt new file mode 100644 index 0000000000..df1c02a355 --- /dev/null +++ b/lintchecks/src/test/resources/ContextStub.kt @@ -0,0 +1,6 @@ +package android.content + +class Context { + fun startForegroundService(intent: Intent?) { + } +} diff --git a/lintchecks/src/test/resources/GlideLogStub.java b/lintchecks/src/test/resources/GlideLogStub.java deleted file mode 100644 index f7da3f0344..0000000000 --- a/lintchecks/src/test/resources/GlideLogStub.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.signal.glide; - -public class Log { - - public static String tag(Class clazz) { - return ""; - } - - public static void v(String tag, String msg) { - } - - public static void v(String tag, String msg, Throwable tr) { - } - - public static void d(String tag, String msg) { - } - - public static void d(String tag, String msg, Throwable tr) { - } - - public static void i(String tag, String msg) { - } - - public static void i(String tag, String msg, Throwable tr) { - } - - public static void w(String tag, String msg) { - } - - public static void w(String tag, String msg, Throwable tr) { - } - - public static void w(String tag, Throwable tr) { - } - - public static void e(String tag, String msg) { - } - - public static void e(String tag, String msg, Throwable tr) { - } -} diff --git a/lintchecks/src/test/resources/GlideLogStub.kt b/lintchecks/src/test/resources/GlideLogStub.kt new file mode 100644 index 0000000000..ce3f0ff181 --- /dev/null +++ b/lintchecks/src/test/resources/GlideLogStub.kt @@ -0,0 +1,40 @@ +package org.signal.glide + +object Log { + fun tag(clazz: Class<*>?): String { + return "" + } + + fun v(tag: String?, msg: String?) { + } + + fun v(tag: String?, msg: String?, tr: Throwable?) { + } + + fun d(tag: String?, msg: String?) { + } + + fun d(tag: String?, msg: String?, tr: Throwable?) { + } + + fun i(tag: String?, msg: String?) { + } + + fun i(tag: String?, msg: String?, tr: Throwable?) { + } + + fun w(tag: String?, msg: String?) { + } + + fun w(tag: String?, msg: String?, tr: Throwable?) { + } + + fun w(tag: String?, tr: Throwable?) { + } + + fun e(tag: String?, msg: String?) { + } + + fun e(tag: String?, msg: String?, tr: Throwable?) { + } +} diff --git a/lintchecks/src/test/resources/RecipientIdDatabaseReferenceStub.java b/lintchecks/src/test/resources/RecipientIdDatabaseReferenceStub.java deleted file mode 100644 index 2f59cf0b57..0000000000 --- a/lintchecks/src/test/resources/RecipientIdDatabaseReferenceStub.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.thoughtcrime.securesms.database; - -interface RecipientIdDatabaseReference { - void remapRecipient(RecipientId fromId, RecipientId toId); -} diff --git a/lintchecks/src/test/resources/RecipientIdDatabaseReferenceStub.kt b/lintchecks/src/test/resources/RecipientIdDatabaseReferenceStub.kt new file mode 100644 index 0000000000..bfd2165b26 --- /dev/null +++ b/lintchecks/src/test/resources/RecipientIdDatabaseReferenceStub.kt @@ -0,0 +1,5 @@ +package org.thoughtcrime.securesms.database + +internal interface RecipientIdDatabaseReference { + fun remapRecipient(fromId: RecipientId?, toId: RecipientId?) +} diff --git a/lintchecks/src/test/resources/RequiresApiStub.java b/lintchecks/src/test/resources/RequiresApiStub.java deleted file mode 100644 index cca39cf0b0..0000000000 --- a/lintchecks/src/test/resources/RequiresApiStub.java +++ /dev/null @@ -1,4 +0,0 @@ -package android.annotation; - -public @interface RequiresApi { -} diff --git a/lintchecks/src/test/resources/RequiresApiStub.kt b/lintchecks/src/test/resources/RequiresApiStub.kt new file mode 100644 index 0000000000..38318d3fa1 --- /dev/null +++ b/lintchecks/src/test/resources/RequiresApiStub.kt @@ -0,0 +1,3 @@ +package android.annotation + +annotation class RequiresApi diff --git a/lintchecks/src/test/resources/ServiceLogStub.java b/lintchecks/src/test/resources/ServiceLogStub.java deleted file mode 100644 index 8573695643..0000000000 --- a/lintchecks/src/test/resources/ServiceLogStub.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.signal.libsignal.protocol.logging; - -public class Log { - - public static void v(String tag, String msg) { - } - - public static void v(String tag, String msg, Throwable tr) { - } - - public static void d(String tag, String msg) { - } - - public static void d(String tag, String msg, Throwable tr) { - } - - public static void i(String tag, String msg) { - } - - public static void i(String tag, String msg, Throwable tr) { - } - - public static void w(String tag, String msg) { - } - - public static void w(String tag, String msg, Throwable tr) { - } - - public static void w(String tag, Throwable tr) { - } - - public static void e(String tag, String msg) { - } - - public static void e(String tag, String msg, Throwable tr) { - } -} diff --git a/lintchecks/src/test/resources/ServiceLogStub.kt b/lintchecks/src/test/resources/ServiceLogStub.kt new file mode 100644 index 0000000000..8b6af2a87a --- /dev/null +++ b/lintchecks/src/test/resources/ServiceLogStub.kt @@ -0,0 +1,36 @@ +package org.signal.libsignal.protocol.logging + +object Log { + fun v(tag: String?, msg: String?) { + } + + fun v(tag: String?, msg: String?, tr: Throwable?) { + } + + fun d(tag: String?, msg: String?) { + } + + fun d(tag: String?, msg: String?, tr: Throwable?) { + } + + fun i(tag: String?, msg: String?) { + } + + fun i(tag: String?, msg: String?, tr: Throwable?) { + } + + fun w(tag: String?, msg: String?) { + } + + fun w(tag: String?, msg: String?, tr: Throwable?) { + } + + fun w(tag: String?, tr: Throwable?) { + } + + fun e(tag: String?, msg: String?) { + } + + fun e(tag: String?, msg: String?, tr: Throwable?) { + } +} diff --git a/lintchecks/src/test/resources/ThreadIdDatabaseReferenceStub.java b/lintchecks/src/test/resources/ThreadIdDatabaseReferenceStub.java deleted file mode 100644 index e83b523541..0000000000 --- a/lintchecks/src/test/resources/ThreadIdDatabaseReferenceStub.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.thoughtcrime.securesms.database; - -interface ThreadIdDatabaseReference { - void remapThread(long fromId, long toId); -} diff --git a/lintchecks/src/test/resources/ThreadIdDatabaseReferenceStub.kt b/lintchecks/src/test/resources/ThreadIdDatabaseReferenceStub.kt new file mode 100644 index 0000000000..0a5e55f527 --- /dev/null +++ b/lintchecks/src/test/resources/ThreadIdDatabaseReferenceStub.kt @@ -0,0 +1,5 @@ +package org.thoughtcrime.securesms.database + +internal interface ThreadIdDatabaseReference { + fun remapThread(fromId: Long, toId: Long) +}