diff --git a/api/api.base b/api/api.base index 2d22e989a9..aca6d5fbdf 100644 --- a/api/api.base +++ b/api/api.base @@ -141,10 +141,23 @@ package com.google.devtools.ksp.processing { public interface KSPLogger { method public void error(@NonNull String message, @Nullable com.google.devtools.ksp.symbol.KSNode symbol); + method public default void error(@NonNull String message, @Nullable com.google.devtools.ksp.symbol.KSNode symbol, @NonNull com.google.devtools.ksp.processing.KSPSuggestedFix fix); method public void exception(@NonNull Throwable e); method public void info(@NonNull String message, @Nullable com.google.devtools.ksp.symbol.KSNode symbol); method public void logging(@NonNull String message, @Nullable com.google.devtools.ksp.symbol.KSNode symbol); method public void warn(@NonNull String message, @Nullable com.google.devtools.ksp.symbol.KSNode symbol); + method public default void warn(@NonNull String message, @Nullable com.google.devtools.ksp.symbol.KSNode symbol, @NonNull com.google.devtools.ksp.processing.KSPSuggestedFix fix); + } + + public final class KSPSuggestedFix { + ctor public KSPSuggestedFix(@NonNull String replacementText, @Nullable String description); + method @NonNull public String component1(); + method @Nullable public String component2(); + method @NonNull public com.google.devtools.ksp.processing.KSPSuggestedFix copy(@NonNull String replacementText, @Nullable String description); + method @InaccessibleFromKotlin @Nullable public String getDescription(); + method @InaccessibleFromKotlin @NonNull public String getReplacementText(); + property @Nullable public String description; + property @NonNull public String replacementText; } public interface NativePlatformInfo extends com.google.devtools.ksp.processing.PlatformInfo { diff --git a/api/src/main/kotlin/com/google/devtools/ksp/processing/KSPLogger.kt b/api/src/main/kotlin/com/google/devtools/ksp/processing/KSPLogger.kt index 3ca30a67fd..56a217b6b0 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/processing/KSPLogger.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/processing/KSPLogger.kt @@ -25,5 +25,22 @@ interface KSPLogger { fun warn(message: String, symbol: KSNode? = null) fun error(message: String, symbol: KSNode? = null) + fun warn(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + warn(message, symbol) + } + + fun error(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + error(message, symbol) + } + fun exception(e: Throwable) } + +/** + * Represents a suggested code fix provided by a processor. + * Tools like IntelliJ/Android Studio can use this to provide Quick Fixes. + */ +data class KSPSuggestedFix( + val replacementText: String, + val description: String? = null +) diff --git a/common-deps/build.gradle.kts b/common-deps/build.gradle.kts index dc863f447e..53a0d9c143 100644 --- a/common-deps/build.gradle.kts +++ b/common-deps/build.gradle.kts @@ -16,6 +16,7 @@ plugins { dependencies { compileOnly(project(":api")) + testImplementation(project(":api")) testImplementation("junit:junit:$junitVersion") ksp(project(":cmdline-parser-gen")) diff --git a/common-deps/src/main/kotlin/com/google/devtools/ksp/KspGradleLogger.kt b/common-deps/src/main/kotlin/com/google/devtools/ksp/KspGradleLogger.kt index ef0d2f94e3..a53e89275e 100644 --- a/common-deps/src/main/kotlin/com/google/devtools/ksp/KspGradleLogger.kt +++ b/common-deps/src/main/kotlin/com/google/devtools/ksp/KspGradleLogger.kt @@ -29,6 +29,12 @@ class KspGradleLogger(val loglevel: Int) : KSPLogger { is NonExistLocation, null -> message } + private fun decorateFix(message: String, symbol: KSNode?, fix: KSPSuggestedFix): String { + val baseMessage = decorateMessage(message, symbol) + val fixDesc = if (fix.description != null) " (${fix.description})" else "" + return "$baseMessage -> Suggested Fix$fixDesc: [${fix.replacementText}]" + } + override fun logging(message: String, symbol: KSNode?) { if (loglevel <= LOGGING_LEVEL_LOGGING) messager.println("v: [ksp] ${decorateMessage(message, symbol)}") @@ -49,6 +55,16 @@ class KspGradleLogger(val loglevel: Int) : KSPLogger { messager.println("e: [ksp] ${decorateMessage(message, symbol)}") } + override fun warn(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + if (loglevel <= LOGGING_LEVEL_WARN) + messager.println("w: [ksp] ${decorateFix(message, symbol, fix)}") + } + + override fun error(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + if (loglevel <= LOGGING_LEVEL_ERROR) + messager.println("e: [ksp] ${decorateFix(message, symbol, fix)}") + } + override fun exception(e: Throwable) { if (loglevel <= LOGGING_LEVEL_ERROR) messager.println("e: [ksp] $e") diff --git a/common-deps/src/test/kotlin/com/google/devtools/ksp/processing/KspGradleLoggerTest.kt b/common-deps/src/test/kotlin/com/google/devtools/ksp/processing/KspGradleLoggerTest.kt new file mode 100644 index 0000000000..dcdb918069 --- /dev/null +++ b/common-deps/src/test/kotlin/com/google/devtools/ksp/processing/KspGradleLoggerTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.ksp.processing + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +class KspGradleLoggerTest { + + @Test + fun testWarnWithFix() { + val out = ByteArrayOutputStream() + val originalOut = System.out + System.setOut(PrintStream(out)) + try { + val logger = KspGradleLogger(KspGradleLogger.LOGGING_LEVEL_WARN) + val fix = KSPSuggestedFix("NewAnnotation", "Replace with NewAnnotation") + logger.warn("Deprecated annotation", null, fix) + + val output = out.toString().trim() + assertEquals( + "w: [ksp] Deprecated annotation -> Suggested Fix (Replace with NewAnnotation): [NewAnnotation]", + output + ) + } finally { + System.setOut(originalOut) + } + } + + @Test + fun testErrorWithFix() { + val out = ByteArrayOutputStream() + val originalOut = System.out + System.setOut(PrintStream(out)) + try { + val logger = KspGradleLogger(KspGradleLogger.LOGGING_LEVEL_ERROR) + val fix = KSPSuggestedFix("replacement") + logger.error("Something wrong", null, fix) + + val output = out.toString().trim() + assertEquals( + "e: [ksp] Something wrong -> Suggested Fix: [replacement]", + output + ) + } finally { + System.setOut(originalOut) + } + } + + @Test + fun testWarnWithoutFix() { + val out = ByteArrayOutputStream() + val originalOut = System.out + System.setOut(PrintStream(out)) + try { + val logger = KspGradleLogger(KspGradleLogger.LOGGING_LEVEL_WARN) + logger.warn("Simple warning", null) + + val output = out.toString().trim() + assertEquals("w: [ksp] Simple warning", output) + } finally { + System.setOut(originalOut) + } + } + + @Test + fun testErrorWithoutFix() { + val out = ByteArrayOutputStream() + val originalOut = System.out + System.setOut(PrintStream(out)) + try { + val logger = KspGradleLogger(KspGradleLogger.LOGGING_LEVEL_ERROR) + logger.error("Simple error", null) + + val output = out.toString().trim() + assertEquals("e: [ksp] Simple error", output) + } finally { + System.setOut(originalOut) + } + } +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/CommandLineKSPLogger.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/CommandLineKSPLogger.kt index dc046cebe4..051d9a2571 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/CommandLineKSPLogger.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/CommandLineKSPLogger.kt @@ -18,6 +18,7 @@ package com.google.devtools.ksp.impl import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.KSPSuggestedFix import com.google.devtools.ksp.symbol.KSNode class CommandLineKSPLogger : KSPLogger { @@ -39,6 +40,16 @@ class CommandLineKSPLogger : KSPLogger { messager.println(message) } + override fun warn(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + val fixDesc = if (fix.description != null) " (${fix.description})" else "" + messager.println("Warning: $message -> Suggested Fix$fixDesc: [${fix.replacementText}]") + } + + override fun error(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + val fixDesc = if (fix.description != null) " (${fix.description})" else "" + messager.println("Error: $message -> Suggested Fix$fixDesc: [${fix.replacementText}]") + } + override fun exception(e: Throwable) { messager.println(e.message) } diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt index 25e974e1e8..d99238a047 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt @@ -467,11 +467,22 @@ class KotlinSymbolProcessing( logger.error(message, symbol) } + override fun error(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + hasError = true + logger.error(message, symbol, fix) + } + override fun warn(message: String, symbol: KSNode?) { if (kspConfig.allWarningsAsErrors) hasError = true logger.warn(message, symbol) } + + override fun warn(message: String, symbol: KSNode?, fix: KSPSuggestedFix) { + if (kspConfig.allWarningsAsErrors) + hasError = true + logger.warn(message, symbol, fix) + } } val projectDisposable: Disposable = Disposer.newDisposable("StandaloneAnalysisAPISession.project") diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/impl/CommandLineKSPLoggerTest.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/impl/CommandLineKSPLoggerTest.kt new file mode 100644 index 0000000000..56217c4e63 --- /dev/null +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/impl/CommandLineKSPLoggerTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.ksp.impl + +import com.google.devtools.ksp.processing.KSPSuggestedFix +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +@Execution(ExecutionMode.SAME_THREAD) +class CommandLineKSPLoggerTest { + + @Test + fun testWarnWithFix() { + val err = ByteArrayOutputStream() + val originalErr = System.err + System.setErr(PrintStream(err)) + try { + val logger = CommandLineKSPLogger() + val fix = KSPSuggestedFix("NewAnnotation", "Replace with NewAnnotation") + logger.warn("Deprecated annotation", null, fix) + + val output = err.toString().trim() + assertEquals( + "Warning: Deprecated annotation -> Suggested Fix (Replace with NewAnnotation): [NewAnnotation]", + output + ) + } finally { + System.setErr(originalErr) + } + } + + @Test + fun testErrorWithFix() { + val err = ByteArrayOutputStream() + val originalErr = System.err + System.setErr(PrintStream(err)) + try { + val logger = CommandLineKSPLogger() + val fix = KSPSuggestedFix("replacement") + logger.error("Something wrong", null, fix) + + val output = err.toString().trim() + assertEquals( + "Error: Something wrong -> Suggested Fix: [replacement]", + output + ) + } finally { + System.setErr(originalErr) + } + } + + @Test + fun testWarnWithoutFix() { + val err = ByteArrayOutputStream() + val originalErr = System.err + System.setErr(PrintStream(err)) + try { + val logger = CommandLineKSPLogger() + logger.warn("Simple warning", null) + + val output = err.toString().trim() + assertEquals("Simple warning", output) + } finally { + System.setErr(originalErr) + } + } + + @Test + fun testErrorWithoutFix() { + val err = ByteArrayOutputStream() + val originalErr = System.err + System.setErr(PrintStream(err)) + try { + val logger = CommandLineKSPLogger() + logger.error("Simple error", null) + + val output = err.toString().trim() + assertEquals("Simple error", output) + } finally { + System.setErr(originalErr) + } + } +} diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt index abc4cff665..80af26ead8 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt @@ -865,6 +865,12 @@ abstract class KSPUnitTestSuite( runTest("$AA_PATH/exitCode.kt") } + @TestMetadata("suggestedFix.kt") + @Test + fun testSuggestedFix() { + runTest("$AA_PATH/suggestedFix.kt") + } + @TestMetadata("packageProviderForGenerated.kt") @Test fun testPackageProviderForGenerated() { diff --git a/kotlin-analysis-api/testData/suggestedFix.kt b/kotlin-analysis-api/testData/suggestedFix.kt new file mode 100644 index 0000000000..506baa7ec7 --- /dev/null +++ b/kotlin-analysis-api/testData/suggestedFix.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TEST PROCESSOR: SuggestedFixProcessor +// EXPECTED: +// KSP FAILED WITH EXIT CODE: PROCESSING_ERROR +// warn:NewClass:Replace with NewClass +// error:AlternativeClass:null +// END + +// FILE: DeprecatedClass.kt +class DeprecatedClass diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/SuggestedFixProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/SuggestedFixProcessor.kt new file mode 100644 index 0000000000..75345de44a --- /dev/null +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/SuggestedFixProcessor.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.ksp.processor + +import com.google.devtools.ksp.processing.KSPSuggestedFix +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration + +class SuggestedFixProcessor : AbstractTestProcessor() { + val results = mutableListOf() + + override fun toResult(): List { + return results.toList() + } + + override fun process(resolver: Resolver): List { + resolver.getNewFiles().forEach { file -> + file.declarations.forEach { decl -> + if (decl is KSClassDeclaration && decl.simpleName.asString() == "DeprecatedClass") { + val fixWithDesc = KSPSuggestedFix("NewClass", "Replace with NewClass") + env.logger.warn("Class is deprecated", decl, fixWithDesc) + results.add("warn:${fixWithDesc.replacementText}:${fixWithDesc.description}") + + val fixWithoutDesc = KSPSuggestedFix("AlternativeClass") + env.logger.error("Use AlternativeClass instead", decl, fixWithoutDesc) + results.add("error:${fixWithoutDesc.replacementText}:${fixWithoutDesc.description}") + } + } + } + return emptyList() + } + + lateinit var env: SymbolProcessorEnvironment + + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + env = environment + return this + } +}