From a46699810c42c92985600ef80458cd1f962087f4 Mon Sep 17 00:00:00 2001 From: Drew Carlson Date: Tue, 23 Apr 2024 11:37:24 -0700 Subject: [PATCH] Support Unredacted properties and supertype based redaction (#210) * Support Unredacted properties and supertype based redaction * Update compiler plugin tests * Review feedback * Review feedback 2 * Update FIR declaration checker * Add tests for sealed/abstract redaction * remove wildcard import --- README.md | 39 +++++++++++ .../redacted-compiler-plugin-annotations.api | 3 + .../redacted/annotations/Redacted.kt | 3 +- .../redacted/annotations/Unredacted.kt | 36 ++++++++++ .../gradle/RedactedGradlePluginExtension.kt | 4 ++ .../gradle/RedactedGradleSubplugin.kt | 2 + .../compiler/RedactedCommandLineProcessor.kt | 21 +++++- .../compiler/RedactedIrGenerationExtension.kt | 9 ++- .../redacted/compiler/RedactedIrVisitor.kt | 67 +++++++++++++++---- .../redacted/compiler/RedactedPlugin.kt | 10 ++- .../fir/FirRedactedExtensionRegistrar.kt | 13 +++- .../redacted/compiler/fir/KtErrorsRedacted.kt | 2 + .../redacted/compiler/RedactedPluginTest.kt | 11 ++- .../zacsweers/redacted/sample/SmokeTest.kt | 60 +++++++++++++++++ 14 files changed, 261 insertions(+), 19 deletions(-) create mode 100644 redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Unredacted.kt diff --git a/README.md b/README.md index aa526037..4c5e5bc0 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ redacted { // classes by '.', e.g. "kotlin/Map.Entry" redactedAnnotation = "dev/zacsweers/redacted/annotations/Redacted" // Default + // Define a custom unredacted annotation. + unredactedAnnotation = "dev/zacsweers/redacted/annotations/Unredacted" // Default + // Define whether or not this is enabled. Useful if you want to gate this behind a dynamic // build configuration. enabled = true // Default @@ -85,6 +88,42 @@ multiplatform and supports all common JVM, JS, and native targets. but usage in newer versions of kotlinc are not guaranteed to be stable. - IDE support is not currently possible. See [#8](https://github.com/ZacSweers/redacted-compiler-plugin/issues/8). + +## Advanced Usage + +In situations where it is desirable to redact everything and opt-out on certain properties, +two options are provided: + +**Class redaction** + +For one-off classes that may contain a large number of fields that should be redacted, you can augment the `@Redacted` +class behavior: + +```kotlin +@Redacted +data class User(@Unredacted val name: String, val phoneNumber: String, val ssn: String) +``` + +``` +User(name=Bob, phoneNumber=██, ssn=██) +``` + +**Supertype redaction** + +For situations where you need to enforce that an API only accepts redacted inputs, you can apply `@Redacted` to a +parent interface. + +```kotlin +@Redacted +interface RedactedObject + +data class User(@Unredacted val name: String, val phoneNumber: String, val ssn: String) : RedactedObject +``` + +``` +User(name=Bob, phoneNumber=██, ssn=██) +``` + License ------- diff --git a/redacted-compiler-plugin-annotations/api/redacted-compiler-plugin-annotations.api b/redacted-compiler-plugin-annotations/api/redacted-compiler-plugin-annotations.api index 6620bcf3..329793fd 100644 --- a/redacted-compiler-plugin-annotations/api/redacted-compiler-plugin-annotations.api +++ b/redacted-compiler-plugin-annotations/api/redacted-compiler-plugin-annotations.api @@ -1,3 +1,6 @@ public abstract interface annotation class dev/zacsweers/redacted/annotations/Redacted : java/lang/annotation/Annotation { } +public abstract interface annotation class dev/zacsweers/redacted/annotations/Unredacted : java/lang/annotation/Annotation { +} + diff --git a/redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Redacted.kt b/redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Redacted.kt index 48ed68aa..3457b6e1 100644 --- a/redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Redacted.kt +++ b/redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Redacted.kt @@ -21,7 +21,8 @@ import kotlin.annotation.AnnotationTarget.PROPERTY /** * An annotation to indicate that a particular property or class should be redacted in `toString()` - * implementations. + * implementations. When applied to an interface, subtypes will behave as if the class is annotated + * with `@Redacted`. * * For properties, each individual property will be redacted. Example: `User(name=Bob, * phoneNumber=██)` diff --git a/redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Unredacted.kt b/redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Unredacted.kt new file mode 100644 index 00000000..a8d93c33 --- /dev/null +++ b/redacted-compiler-plugin-annotations/src/commonMain/kotlin/dev/zacsweers/redacted/annotations/Unredacted.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Zac Sweers + * + * 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 + * + * https://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 dev.zacsweers.redacted.annotations + +import kotlin.annotation.AnnotationRetention.BINARY +import kotlin.annotation.AnnotationTarget.PROPERTY + +/** + * An annotation to indicate that a particular property should NOT be redacted in `toString()` + * implementations. + * + * This annotation can be applied to a property in any class that applies `@Redacted` or implements + * an interface that applies `@Redacted`. + * + * Example: + * ``` + * @Redacted + * data class User(@Unredacted val name: String, val phoneNumber: String) + * + * println(user) // User(name = "Bob", phoneNumber = "██") + * ``` + */ +@Retention(BINARY) @Target(PROPERTY) public annotation class Unredacted diff --git a/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradlePluginExtension.kt b/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradlePluginExtension.kt index acd59fe7..f0ec2eda 100644 --- a/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradlePluginExtension.kt +++ b/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradlePluginExtension.kt @@ -20,6 +20,7 @@ import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property internal const val DEFAULT_ANNOTATION = "dev/zacsweers/redacted/annotations/Redacted" +internal const val DEFAULT_UNREDACTED_ANNOTATION = "dev/zacsweers/redacted/annotations/Unredacted" public abstract class RedactedPluginExtension @Inject constructor(objects: ObjectFactory) { /** @@ -32,6 +33,9 @@ public abstract class RedactedPluginExtension @Inject constructor(objects: Objec public val redactedAnnotation: Property = objects.property(String::class.java).convention(DEFAULT_ANNOTATION) + public val unredactedAnnotation: Property = + objects.property(String::class.java).convention(DEFAULT_UNREDACTED_ANNOTATION) + public val enabled: Property = objects.property(Boolean::class.javaObjectType).convention(true) diff --git a/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradleSubplugin.kt b/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradleSubplugin.kt index 78c863cd..526c8fc9 100644 --- a/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradleSubplugin.kt +++ b/redacted-compiler-plugin-gradle/src/main/kotlin/dev/zacsweers/redacted/gradle/RedactedGradleSubplugin.kt @@ -45,6 +45,7 @@ public class RedactedGradleSubplugin : KotlinCompilerPluginSupportPlugin { val project = kotlinCompilation.target.project val extension = project.extensions.getByType(RedactedPluginExtension::class.java) val annotation = extension.redactedAnnotation + val unredactedAnnotation = extension.unredactedAnnotation // Default annotation is used, so add it as a dependency // Note only multiplatform, jvm/android, and js are supported. Anyone else is on their own. @@ -62,6 +63,7 @@ public class RedactedGradleSubplugin : KotlinCompilerPluginSupportPlugin { SubpluginOption(key = "enabled", value = enabled.toString()), SubpluginOption(key = "replacementString", value = extension.replacementString.get()), SubpluginOption(key = "redactedAnnotation", value = annotation.get()), + SubpluginOption(key = "unredactedAnnotation", value = unredactedAnnotation.get()), ) } } diff --git a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedCommandLineProcessor.kt b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedCommandLineProcessor.kt index 7624b148..6037eed4 100644 --- a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedCommandLineProcessor.kt +++ b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedCommandLineProcessor.kt @@ -31,6 +31,10 @@ internal val KEY_REDACTED_ANNOTATION = CompilerConfigurationKey( "The redacted marker annotation (i.e. com/example/Redacted) to look for when redacting" ) +internal val KEY_UNREDACTED_ANNOTATION = + CompilerConfigurationKey( + "The unredacted marker annotation (i.e. com/example/Unredacted) to look for when redacting" + ) @OptIn(ExperimentalCompilerApi::class) @AutoService(CommandLineProcessor::class) @@ -63,12 +67,26 @@ public class RedactedCommandLineProcessor : CommandLineProcessor { required = true, allowMultipleOccurrences = false, ) + + val OPTION_UNREDACTED_ANNOTATION = + CliOption( + optionName = "unredactedAnnotation", + valueDescription = "String", + description = KEY_UNREDACTED_ANNOTATION.toString(), + required = true, + allowMultipleOccurrences = false, + ) } override val pluginId: String = "dev.zacsweers.redacted.compiler" override val pluginOptions: Collection = - listOf(OPTION_ENABLED, OPTION_REPLACEMENT_STRING, OPTION_REDACTED_ANNOTATION) + listOf( + OPTION_ENABLED, + OPTION_REPLACEMENT_STRING, + OPTION_REDACTED_ANNOTATION, + OPTION_UNREDACTED_ANNOTATION, + ) override fun processOption( option: AbstractCliOption, @@ -79,6 +97,7 @@ public class RedactedCommandLineProcessor : CommandLineProcessor { "enabled" -> configuration.put(KEY_ENABLED, value.toBoolean()) "replacementString" -> configuration.put(KEY_REPLACEMENT_STRING, value) "redactedAnnotation" -> configuration.put(KEY_REDACTED_ANNOTATION, value) + "unredactedAnnotation" -> configuration.put(KEY_UNREDACTED_ANNOTATION, value) else -> error("Unknown plugin option: ${option.optionName}") } } diff --git a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrGenerationExtension.kt b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrGenerationExtension.kt index 210cddf2..c1e21e53 100644 --- a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrGenerationExtension.kt +++ b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrGenerationExtension.kt @@ -25,11 +25,18 @@ internal class RedactedIrGenerationExtension( private val messageCollector: MessageCollector, private val replacementString: String, private val redactedAnnotationName: FqName, + private val unredactedAnnotationName: FqName, ) : IrGenerationExtension { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { val redactedTransformer = - RedactedIrVisitor(pluginContext, redactedAnnotationName, replacementString, messageCollector) + RedactedIrVisitor( + pluginContext, + redactedAnnotationName, + unredactedAnnotationName, + replacementString, + messageCollector, + ) moduleFragment.transform(redactedTransformer, null) } } diff --git a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrVisitor.kt b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrVisitor.kt index 0a122221..9a23cb6d 100644 --- a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrVisitor.kt +++ b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedIrVisitor.kt @@ -15,6 +15,7 @@ */ package dev.zacsweers.redacted.compiler +import kotlin.LazyThreadSafetyMode.NONE import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder @@ -24,6 +25,7 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.backend.js.utils.isInstantiableEnum import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder import org.jetbrains.kotlin.ir.builders.irBlockBody import org.jetbrains.kotlin.ir.builders.irCall @@ -44,7 +46,11 @@ import org.jetbrains.kotlin.ir.types.isArray import org.jetbrains.kotlin.ir.types.isString import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET import org.jetbrains.kotlin.ir.util.file +import org.jetbrains.kotlin.ir.util.getAllSuperclasses import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.isEnumClass +import org.jetbrains.kotlin.ir.util.isEnumEntry +import org.jetbrains.kotlin.ir.util.isFinalClass import org.jetbrains.kotlin.ir.util.isObject import org.jetbrains.kotlin.ir.util.isPrimitiveArray import org.jetbrains.kotlin.ir.util.primaryConstructor @@ -57,6 +63,7 @@ internal const val LOG_PREFIX = "*** REDACTED (IR):" internal class RedactedIrVisitor( private val pluginContext: IrPluginContext, private val redactedAnnotation: FqName, + private val unredactedAnnotation: FqName, private val replacementString: String, private val messageCollector: MessageCollector, ) : IrElementTransformerVoidWithContext() { @@ -64,6 +71,7 @@ internal class RedactedIrVisitor( private class Property( val ir: IrProperty, val isRedacted: Boolean, + val isUnredacted: Boolean, val parameter: IrValueParameter, ) @@ -79,23 +87,43 @@ internal class RedactedIrVisitor( val properties = mutableListOf() val classIsRedacted = declarationParent.hasAnnotation(redactedAnnotation) + val supertypeIsRedacted by + lazy(NONE) { + declarationParent.getAllSuperclasses().any { it.hasAnnotation(redactedAnnotation) } + } var anyRedacted = false + var anyUnredacted = false for (prop in declarationParent.properties) { val parameter = constructorParameters[prop.name.asString()] ?: continue - val isRedacted = prop.isRedacted() + val isRedacted = prop.isRedacted + val isUnredacted = prop.isUnredacted if (isRedacted) { anyRedacted = true } - properties += Property(prop, isRedacted, parameter) + if (isUnredacted) { + anyUnredacted = true + } + properties += Property(prop, isRedacted, isUnredacted, parameter) } - if (classIsRedacted || anyRedacted) { + + if (classIsRedacted || supertypeIsRedacted || anyRedacted) { if (declaration.origin == IrDeclarationOrigin.DEFINED) { declaration.reportError( "@Redacted is only supported on data or value classes that do *not* have a custom toString() function. Please remove the function or remove the @Redacted annotations." ) return super.visitFunctionNew(declaration) } - if (!declarationParent.isData && !declarationParent.isValue) { + if ( + declarationParent.isInstantiableEnum || + declarationParent.isEnumClass || + declarationParent.isEnumEntry + ) { + declarationParent.reportError("@Redacted does not support enum classes or entries!") + return super.visitFunctionNew(declaration) + } + if ( + declarationParent.isFinalClass && !declarationParent.isData && !declarationParent.isValue + ) { declarationParent.reportError("@Redacted is only supported on data or value classes!") return super.visitFunctionNew(declaration) } @@ -109,13 +137,19 @@ internal class RedactedIrVisitor( declarationParent.reportError("@Redacted is useless on object classes.") return super.visitFunctionNew(declaration) } - if (!(classIsRedacted xor anyRedacted)) { + if (anyUnredacted && (!classIsRedacted && !supertypeIsRedacted)) { + declarationParent.reportError( + "@Unredacted should only be applied to properties in a class or a supertype is marked @Redacted." + ) + return super.visitFunctionNew(declaration) + } + if (!(classIsRedacted xor anyRedacted xor supertypeIsRedacted)) { declarationParent.reportError( "@Redacted should only be applied to the class or its properties, not both." ) return super.visitFunctionNew(declaration) } - declaration.convertToGeneratedToString(properties, classIsRedacted) + declaration.convertToGeneratedToString(properties, classIsRedacted, supertypeIsRedacted) } } @@ -132,6 +166,7 @@ internal class RedactedIrVisitor( private fun IrFunction.convertToGeneratedToString( properties: List, classIsRedacted: Boolean, + supertypeIsRedacted: Boolean, ) { val parent = parent as IrClass @@ -144,6 +179,7 @@ internal class RedactedIrVisitor( irFunction = this@convertToGeneratedToString, irProperties = properties, classIsRedacted = classIsRedacted, + supertypeIsRedacted = supertypeIsRedacted, ) } @@ -157,9 +193,11 @@ internal class RedactedIrVisitor( } } - private fun IrProperty.isRedacted(): Boolean { - return hasAnnotation(redactedAnnotation) - } + private val IrProperty.isRedacted: Boolean + get() = hasAnnotation(redactedAnnotation) + + private val IrProperty.isUnredacted: Boolean + get() = hasAnnotation(unredactedAnnotation) /** * The actual body of the toString method. Copied from @@ -171,10 +209,12 @@ internal class RedactedIrVisitor( irFunction: IrFunction, irProperties: List, classIsRedacted: Boolean, + supertypeIsRedacted: Boolean, ) { val irConcat = irConcat() irConcat.addArgument(irString(irClass.name.asString() + "(")) - if (classIsRedacted) { + val hasUnredactedProperties by lazy(NONE) { irProperties.any { it.isUnredacted } } + if (classIsRedacted && !hasUnredactedProperties) { irConcat.addArgument(irString(replacementString)) } else { var first = true @@ -182,8 +222,11 @@ internal class RedactedIrVisitor( if (!first) irConcat.addArgument(irString(", ")) irConcat.addArgument(irString(property.ir.name.asString() + "=")) - - if (property.isRedacted) { + val redactProperty = + property.isRedacted || + (classIsRedacted && !property.isUnredacted) || + (supertypeIsRedacted && !property.isUnredacted) + if (redactProperty) { irConcat.addArgument(irString(replacementString)) } else { val irPropertyValue = irGetField(receiver(irFunction), property.ir.backingField!!) diff --git a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedPlugin.kt b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedPlugin.kt index 1d6891ac..580146d2 100644 --- a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedPlugin.kt +++ b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/RedactedPlugin.kt @@ -40,11 +40,19 @@ public class RedactedComponentRegistrar : CompilerPluginRegistrar() { configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) val replacementString = checkNotNull(configuration[KEY_REPLACEMENT_STRING]) val redactedAnnotation = checkNotNull(configuration[KEY_REDACTED_ANNOTATION]) + val unredactedAnnotation = checkNotNull(configuration[KEY_UNREDACTED_ANNOTATION]) val redactedAnnotationClassId = ClassId.fromString(redactedAnnotation) val fqRedactedAnnotation = redactedAnnotationClassId.asSingleFqName() + val unredactedAnnotationClassId = ClassId.fromString(unredactedAnnotation) + val fqUnredactedAnnotation = unredactedAnnotationClassId.asSingleFqName() IrGenerationExtension.registerExtension( - RedactedIrGenerationExtension(messageCollector, replacementString, fqRedactedAnnotation) + RedactedIrGenerationExtension( + messageCollector, + replacementString, + fqRedactedAnnotation, + fqUnredactedAnnotation, + ) ) FirExtensionRegistrarAdapter.registerExtension( diff --git a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/FirRedactedExtensionRegistrar.kt b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/FirRedactedExtensionRegistrar.kt index da2212d6..3ff66f58 100644 --- a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/FirRedactedExtensionRegistrar.kt +++ b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/FirRedactedExtensionRegistrar.kt @@ -29,6 +29,9 @@ import org.jetbrains.kotlin.fir.declarations.FirDeclaration import org.jetbrains.kotlin.fir.declarations.FirFunction import org.jetbrains.kotlin.fir.declarations.FirProperty import org.jetbrains.kotlin.fir.declarations.FirRegularClass +import org.jetbrains.kotlin.fir.declarations.utils.isEnumClass +import org.jetbrains.kotlin.fir.declarations.utils.isFinal +import org.jetbrains.kotlin.fir.declarations.utils.isFromEnumClass import org.jetbrains.kotlin.fir.declarations.utils.isOverride import org.jetbrains.kotlin.fir.expressions.FirAnnotation import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar @@ -92,13 +95,19 @@ internal object FirRedactedDeclarationChecker : FirRegularClassChecker() { } } - if (declaration.classKind != ClassKind.CLASS) { + if (declaration.classKind != ClassKind.CLASS && declaration.classKind != ClassKind.INTERFACE) { report(KtErrorsRedacted.REDACTED_ON_NON_CLASS_ERROR) return } + if (declaration.isFromEnumClass || declaration.isEnumClass) { + report(KtErrorsRedacted.REDACTED_ON_ENUM_CLASS_ERROR) + return + } + if ( - !declaration.hasModifier(KtTokens.DATA_KEYWORD) && + declaration.isFinal && + !declaration.hasModifier(KtTokens.DATA_KEYWORD) && !declaration.hasModifier(KtTokens.VALUE_KEYWORD) ) { report(KtErrorsRedacted.REDACTED_ON_NON_DATA_OR_VALUE_CLASS_ERROR) diff --git a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/KtErrorsRedacted.kt b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/KtErrorsRedacted.kt index e45fc024..7f1491e2 100644 --- a/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/KtErrorsRedacted.kt +++ b/redacted-compiler-plugin/src/main/kotlin/dev/zacsweers/redacted/compiler/fir/KtErrorsRedacted.kt @@ -26,6 +26,8 @@ internal object KtErrorsRedacted { error0(SourceElementPositioningStrategies.NAME_IDENTIFIER) val REDACTED_ON_NON_CLASS_ERROR by error0(SourceElementPositioningStrategies.NAME_IDENTIFIER) + val REDACTED_ON_ENUM_CLASS_ERROR by + error0(SourceElementPositioningStrategies.NAME_IDENTIFIER) val REDACTED_ON_NON_DATA_OR_VALUE_CLASS_ERROR by error0(SourceElementPositioningStrategies.NAME_IDENTIFIER) val REDACTED_ON_VALUE_CLASS_PROPERTY_ERROR by diff --git a/redacted-compiler-plugin/src/test/kotlin/dev/zacsweers/redacted/compiler/RedactedPluginTest.kt b/redacted-compiler-plugin/src/test/kotlin/dev/zacsweers/redacted/compiler/RedactedPluginTest.kt index 6541ab61..d4dce64d 100644 --- a/redacted-compiler-plugin/src/test/kotlin/dev/zacsweers/redacted/compiler/RedactedPluginTest.kt +++ b/redacted-compiler-plugin/src/test/kotlin/dev/zacsweers/redacted/compiler/RedactedPluginTest.kt @@ -25,6 +25,7 @@ import com.tschuchort.compiletesting.SourceFile.Companion.kotlin import dev.zacsweers.redacted.compiler.RedactedCommandLineProcessor.Companion.OPTION_ENABLED import dev.zacsweers.redacted.compiler.RedactedCommandLineProcessor.Companion.OPTION_REDACTED_ANNOTATION import dev.zacsweers.redacted.compiler.RedactedCommandLineProcessor.Companion.OPTION_REPLACEMENT_STRING +import dev.zacsweers.redacted.compiler.RedactedCommandLineProcessor.Companion.OPTION_UNREDACTED_ANNOTATION import org.jetbrains.kotlin.compiler.plugin.CliOption import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi @@ -58,6 +59,10 @@ class RedactedPluginTest(private val useK2: Boolean) { @Retention(BINARY) @Target(PROPERTY, CLASS) annotation class Redacted + + @Retention(BINARY) + @Target(PROPERTY) + annotation class Unredacted """, ) @@ -111,7 +116,7 @@ class RedactedPluginTest(private val useK2: Boolean) { assertThat(result.messages).contains("NonClass.kt:") // TODO K2 doesn't support custom error messages yet if (!useK2) { - assertThat(result.messages).contains("@Redacted is only supported on data or value classes!") + assertThat(result.messages).contains("@Redacted does not support enum classes or entries!") } } @@ -481,6 +486,10 @@ class RedactedPluginTest(private val useK2: Boolean) { OPTION_REDACTED_ANNOTATION, "dev/zacsweers/redacted/compiler/test/Redacted", ), + processor.option( + OPTION_UNREDACTED_ANNOTATION, + "dev/zacsweers/redacted/compiler/test/Unredacted", + ), ) inheritClassPath = true sources = sourceFiles.asList() + redacted diff --git a/sample/src/jvmTest/kotlin/dev/zacsweers/redacted/sample/SmokeTest.kt b/sample/src/jvmTest/kotlin/dev/zacsweers/redacted/sample/SmokeTest.kt index 24b435c1..9298e26d 100644 --- a/sample/src/jvmTest/kotlin/dev/zacsweers/redacted/sample/SmokeTest.kt +++ b/sample/src/jvmTest/kotlin/dev/zacsweers/redacted/sample/SmokeTest.kt @@ -17,9 +17,69 @@ package dev.zacsweers.redacted.sample import com.google.common.truth.Truth.assertThat import dev.zacsweers.redacted.annotations.Redacted +import dev.zacsweers.redacted.annotations.Unredacted import org.junit.Test class SmokeTest { + + @Test + fun abstractExample() { + val secretChild = AbstractBase.SecretChild("private") + assertThat(secretChild.toString()).isEqualTo("SecretChild(redact=██)") + } + + @Test + fun unredactedAbstractExample() { + val notSoSecretChild = AbstractBase.NotSoSecretChild("public") + assertThat(notSoSecretChild.toString()).isEqualTo("NotSoSecretChild(unredacted=public)") + } + + @Redacted + abstract class AbstractBase { + + data class SecretChild(val redact: String) : AbstractBase() + + data class NotSoSecretChild(@Unredacted val unredacted: String) : AbstractBase() + } + + @Test + fun sealedExample() { + val secretChild = SecretParent.SecretChild("private") + assertThat(secretChild.toString()).isEqualTo("SecretChild(redact=██)") + } + + @Test + fun unredactedSealedExample() { + val notSoSecretChild = SecretParent.NotSoSecretChild("public") + assertThat(notSoSecretChild.toString()).isEqualTo("NotSoSecretChild(unredacted=public)") + } + + @Redacted + sealed class SecretParent { + + data class SecretChild(val redact: String) : SecretParent() + + data class NotSoSecretChild(@Unredacted val unredacted: String) : SecretParent() + } + + @Test + fun supertypeRedactedExample() { + val data = SuperRedacted("Bob", "2815551234") + assertThat(data.toString()).isEqualTo("SuperRedacted(name=Bob, phoneNumber=██)") + } + + @Test + fun unredactedPropertyOnRedactedClassExample() { + val data = RedactedClass("Bob", "2815551234") + assertThat(data.toString()).isEqualTo("RedactedClass(name=Bob, phoneNumber=██)") + } + + @Redacted interface Base + + data class SuperRedacted(@Unredacted val name: String, val phoneNumber: String) : Base + + @Redacted data class RedactedClass(@Unredacted val name: String, val phoneNumber: String) + @Test fun userExample() { val user = User("Bob", "2815551234")