From c901639ce87ba08f7c7130a0f8a4464b1003380a Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 19 Sep 2025 09:26:05 -0300 Subject: [PATCH] Add lint detection for System.out.println add kotlin.io.println usage. --- .../ConversationListFilterPullView.kt | 1 - .../src/main/java/org/signal/lint/Registry.kt | 4 +- .../signal/lint/SystemOutPrintLnDetector.kt | 100 ++++++++ .../lint/SystemOutPrintLnDetectorTest.kt | 232 ++++++++++++++++++ lintchecks/src/test/resources/KotlinIOStub.kt | 18 ++ 5 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 lintchecks/src/main/java/org/signal/lint/SystemOutPrintLnDetector.kt create mode 100644 lintchecks/src/test/java/org/signal/lint/SystemOutPrintLnDetectorTest.kt create mode 100644 lintchecks/src/test/resources/KotlinIOStub.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt index 4c18f4f11c..422568bdc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt @@ -196,7 +196,6 @@ class ConversationListFilterPullView @JvmOverloads constructor( } fun openImmediate() { - println("openImmediate from $state") if (state == FilterPullState.CLOSED) { setState(FilterPullState.OPEN_APEX, source) setState(FilterPullState.OPENING, source) diff --git a/lintchecks/src/main/java/org/signal/lint/Registry.kt b/lintchecks/src/main/java/org/signal/lint/Registry.kt index 1d7e34fda4..4afd3cf47f 100644 --- a/lintchecks/src/main/java/org/signal/lint/Registry.kt +++ b/lintchecks/src/main/java/org/signal/lint/Registry.kt @@ -22,7 +22,9 @@ class Registry : IssueRegistry() { RecipientIdDatabaseDetector.RECIPIENT_ID_DATABASE_REFERENCE_ISSUE, ThreadIdDatabaseDetector.THREAD_ID_DATABASE_REFERENCE_ISSUE, StartForegroundServiceDetector.START_FOREGROUND_SERVICE_ISSUE, - CardViewDetector.CARD_VIEW_USAGE + CardViewDetector.CARD_VIEW_USAGE, + SystemOutPrintLnDetector.SYSTEM_OUT_PRINTLN_USAGE, + SystemOutPrintLnDetector.KOTLIN_IO_PRINTLN_USAGE ) override val api = CURRENT_API diff --git a/lintchecks/src/main/java/org/signal/lint/SystemOutPrintLnDetector.kt b/lintchecks/src/main/java/org/signal/lint/SystemOutPrintLnDetector.kt new file mode 100644 index 0000000000..992cc889c4 --- /dev/null +++ b/lintchecks/src/main/java/org/signal/lint/SystemOutPrintLnDetector.kt @@ -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 { + 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) + ) + } +} \ No newline at end of file diff --git a/lintchecks/src/test/java/org/signal/lint/SystemOutPrintLnDetectorTest.kt b/lintchecks/src/test/java/org/signal/lint/SystemOutPrintLnDetectorTest.kt new file mode 100644 index 0000000000..f163a2e3d8 --- /dev/null +++ b/lintchecks/src/test/java/org/signal/lint/SystemOutPrintLnDetectorTest.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/lintchecks/src/test/resources/KotlinIOStub.kt b/lintchecks/src/test/resources/KotlinIOStub.kt new file mode 100644 index 0000000000..82c7f83588 --- /dev/null +++ b/lintchecks/src/test/resources/KotlinIOStub.kt @@ -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 +} \ No newline at end of file