Source added

This commit is contained in:
Fr4nz D13trich 2025-11-20 09:26:33 +01:00
parent b2864b500e
commit ba28ca859e
8352 changed files with 1487182 additions and 1 deletions

View file

@ -0,0 +1,35 @@
val signalJavaVersion: JavaVersion by rootProject.extra
val signalKotlinJvmTarget: String by rootProject.extra
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
}
java {
sourceCompatibility = signalJavaVersion
targetCompatibility = signalJavaVersion
}
kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(signalKotlinJvmTarget)
}
}
dependencies {
compileOnly(lintLibs.lint.api)
compileOnly(lintLibs.lint.checks)
testImplementation(lintLibs.lint.tests)
testImplementation(lintLibs.lint.api)
testImplementation(testLibs.junit.junit)
}
tasks.jar {
manifest {
attributes(
"Lint-Registry-v2" to "org.signal.lint.Registry"
)
}
}

View file

@ -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<String> {
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)
)
}
}

View file

@ -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<String> {
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)
)
}
}

View file

@ -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<String> {
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)
)
}
}

View file

@ -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<Class<UClass>> {
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")
}
}

View file

@ -0,0 +1,31 @@
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,
SystemOutPrintLnDetector.SYSTEM_OUT_PRINTLN_USAGE,
SystemOutPrintLnDetector.KOTLIN_IO_PRINTLN_USAGE
)
override val api = CURRENT_API
}

View file

@ -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<String> {
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)
)
}
}

View file

@ -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<String> {
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)
)
}
}

View file

@ -0,0 +1,100 @@
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.UExpression
import org.jetbrains.uast.UQualifiedReferenceExpression
/**
* Lint detector that flags usage of System.out.println and kotlin.io.println methods.
*/
class SystemOutPrintLnDetector : Detector(), Detector.UastScanner {
override fun getApplicableMethodNames(): List<String> {
return listOf("println", "print")
}
@Suppress("UnstableApiUsage")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
val evaluator = context.evaluator
if (evaluator.isMemberInClass(method, "java.io.PrintStream")) {
if (isSystemOutCall(node.receiver)) {
context.report(
issue = SYSTEM_OUT_PRINTLN_USAGE,
scope = node,
location = context.getLocation(node),
message = "Using 'System.out.${method.name}' instead of Signal Logger",
quickfixData = createQuickFix(node)
)
}
}
// Check for kotlin.io.println (top-level function)
if (method.name == "println" && evaluator.isMemberInClass(method, "kotlin.io.ConsoleKt")) {
context.report(
issue = KOTLIN_IO_PRINTLN_USAGE,
scope = node,
location = context.getLocation(node),
message = "Using 'kotlin.io.println' instead of Signal Logger.",
quickfixData = createQuickFix(node)
)
}
}
private fun isSystemOutCall(receiver: UExpression?): Boolean {
return receiver is UQualifiedReferenceExpression &&
receiver.selector.asRenderString() == "out" &&
receiver.receiver.asRenderString().endsWith("System")
}
private fun createQuickFix(node: UCallExpression): LintFix {
val arguments = node.valueArguments
val message = if (arguments.isNotEmpty()) arguments[0].asSourceString() else "\"\""
val fixSource = "org.signal.core.util.logging.Log.d(TAG, $message)"
return fix()
.group()
.add(
fix()
.replace()
.text(node.sourcePsi?.text)
.shortenNames()
.reformat(true)
.with(fixSource)
.build()
)
.build()
}
companion object {
val SYSTEM_OUT_PRINTLN_USAGE: Issue = Issue.create(
id = "SystemOutPrintLnUsage",
briefDescription = "Usage of System.out.println/print",
explanation = "System.out.println/print should not be used in production code. Use Signal Logger instead.",
category = MESSAGES,
priority = 5,
severity = ERROR,
implementation = Implementation(SystemOutPrintLnDetector::class.java, JAVA_FILE_SCOPE)
)
val KOTLIN_IO_PRINTLN_USAGE: Issue = Issue.create(
id = "KotlinIOPrintLnUsage",
briefDescription = "Usage of kotlin.io.println",
explanation = "kotlin.io.println should not be used in production code. Use proper logging instead.",
category = MESSAGES,
priority = 5,
severity = ERROR,
implementation = Implementation(SystemOutPrintLnDetector::class.java, JAVA_FILE_SCOPE)
)
}
}

View file

@ -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<Class<UClass>> {
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")
}
}

View file

@ -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<Class<UExpression>> {
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)
)
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -0,0 +1,232 @@
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 SystemOutPrintLnDetectorTest {
@Test
fun systemOutPrintlnUsed_Java() {
TestLintTask.lint()
.allowMissingSdk()
.files(
java(
"""
package foo;
public class Example {
public void log() {
System.out.println("Hello World");
}
}
""".trimIndent()
)
)
.issues(SystemOutPrintLnDetector.SYSTEM_OUT_PRINTLN_USAGE)
.run()
.expect(
"""
src/foo/Example.java:4: Error: Using 'System.out.println' instead of proper logging [SystemOutPrintLnUsage]
System.out.println("Hello World");
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
.expectFixDiffs(
"""
Fix for src/foo/Example.java line 4: Replace with org.signal.core.util.logging.Log.d(TAG, "Hello World"):
@@ -4 +4
- System.out.println("Hello World");
+ org.signal.core.util.logging.Log.d(TAG, "Hello World");
""".trimIndent()
)
}
@Test
fun systemOutPrintUsed_Java() {
TestLintTask.lint()
.allowMissingSdk()
.files(
java(
"""
package foo;
public class Example {
public void log() {
System.out.print("Hello");
}
}
""".trimIndent()
)
)
.issues(SystemOutPrintLnDetector.SYSTEM_OUT_PRINTLN_USAGE)
.run()
.expect(
"""
src/foo/Example.java:4: Error: Using 'System.out.print' instead of proper logging [SystemOutPrintLnUsage]
System.out.print("Hello");
~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
.expectFixDiffs(
"""
Fix for src/foo/Example.java line 4: Replace with org.signal.core.util.logging.Log.d(TAG, "Hello"):
@@ -4 +4
- System.out.print("Hello");
+ org.signal.core.util.logging.Log.d(TAG, "Hello");
""".trimIndent()
)
}
@Test
fun kotlinIOPrintlnUsed_Kotlin() {
TestLintTask.lint()
.allowMissingSdk()
.files(
kotlinIOStub,
kotlin(
"""
package foo
import kotlin.io.println
class Example {
fun log() {
println("Hello World")
}
}
""".trimIndent()
)
)
.issues(SystemOutPrintLnDetector.KOTLIN_IO_PRINTLN_USAGE)
.run()
.expect(
"""
src/foo/Example.kt:5: Error: Using 'kotlin.io.println' instead of proper logging [KotlinIOPrintLnUsage]
println("Hello World")
~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
.expectFixDiffs(
"""
Fix for src/foo/Example.kt line 5: Replace with org.signal.core.util.logging.Log.d(TAG, "Hello World"):
@@ -5 +5
- println("Hello World")
+ org.signal.core.util.logging.Log.d(TAG, "Hello World")
""".trimIndent()
)
}
@Test
fun kotlinIOPrintlnUsed_TopLevel_Kotlin() {
TestLintTask.lint()
.allowMissingSdk()
.files(
kotlinIOStub,
kotlin(
"""
package foo
fun example() {
println("Hello World")
}
""".trimIndent()
)
)
.issues(SystemOutPrintLnDetector.KOTLIN_IO_PRINTLN_USAGE)
.run()
.expect(
"""
src/foo/test.kt:3: Error: Using 'kotlin.io.println' instead of proper logging [KotlinIOPrintLnUsage]
println("Hello World")
~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
.expectFixDiffs(
"""
Fix for src/foo/test.kt line 3: Replace with org.signal.core.util.logging.Log.d(TAG, "Hello World"):
@@ -3 +3
- println("Hello World")
+ org.signal.core.util.logging.Log.d(TAG, "Hello World")
""".trimIndent()
)
}
@Test
fun systemOutPrintlnWithNoArgs_Java() {
TestLintTask.lint()
.allowMissingSdk()
.files(
java(
"""
package foo;
public class Example {
public void log() {
System.out.println();
}
}
""".trimIndent()
)
)
.issues(SystemOutPrintLnDetector.SYSTEM_OUT_PRINTLN_USAGE)
.run()
.expect(
"""
src/foo/Example.java:4: Error: Using 'System.out.println' instead of proper logging [SystemOutPrintLnUsage]
System.out.println();
~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
.expectFixDiffs(
"""
Fix for src/foo/Example.java line 4: Replace with org.signal.core.util.logging.Log.d(TAG, ""):
@@ -4 +4
- System.out.println();
+ org.signal.core.util.logging.Log.d(TAG, "");
""".trimIndent()
)
}
@Test
fun regularPrintStreamMethodsNotFlagged() {
TestLintTask.lint()
.allowMissingSdk()
.files(
java(
"""
package foo;
import java.io.PrintStream;
import java.io.ByteArrayOutputStream;
public class Example {
public void log() {
PrintStream ps = new PrintStream(new ByteArrayOutputStream());
ps.println("This should not be flagged");
}
}
""".trimIndent()
)
)
.issues(
SystemOutPrintLnDetector.SYSTEM_OUT_PRINTLN_USAGE,
SystemOutPrintLnDetector.KOTLIN_IO_PRINTLN_USAGE
)
.run()
.expectClean()
}
companion object {
private val kotlinIOStub = kotlin(readResourceAsString("KotlinIOStub.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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -0,0 +1,8 @@
package androidx.appcompat.app
class AlertDialog {
class Builder {
constructor(context: Context?)
constructor(context: Context?, themeOverrideId: Int)
}
}

View file

@ -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?) {
}
}

View file

@ -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)
}

View file

@ -0,0 +1,6 @@
package androidx.core.content
object ContextCompat {
fun startForegroundService(context: Context?, intent: Intent?) {
}
}

View file

@ -0,0 +1,6 @@
package android.content
class Context {
fun startForegroundService(intent: Intent?) {
}
}

View file

@ -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?) {
}
}

View file

@ -0,0 +1,18 @@
@file:JvmName("ConsoleKt")
package kotlin.io
/**
* Stub for kotlin.io.println function for testing purposes
*/
fun println(message: Any?) {
// Stub implementation
}
fun println() {
// Stub implementation
}
fun print(message: Any?) {
// Stub implementation
}

View file

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.database
internal interface RecipientIdDatabaseReference {
fun remapRecipient(fromId: RecipientId?, toId: RecipientId?)
}

View file

@ -0,0 +1,3 @@
package android.annotation
annotation class RequiresApi

View file

@ -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?) {
}
}

View file

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.database
internal interface ThreadIdDatabaseReference {
fun remapThread(fromId: Long, toId: Long)
}