1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.processor 18 19 import android.processor.immutability.IMMUTABLE_ANNOTATION_NAME 20 import android.processor.immutability.ImmutabilityProcessor 21 import android.processor.immutability.MessageUtils 22 import com.google.common.truth.Expect 23 import com.google.testing.compile.CompilationSubject.assertThat 24 import com.google.testing.compile.Compiler.javac 25 import com.google.testing.compile.JavaFileObjects 26 import org.junit.Rule 27 import org.junit.Test 28 import java.util.* 29 import javax.tools.JavaFileObject 30 31 class ImmutabilityProcessorTest { 32 33 companion object { 34 private const val PACKAGE_PREFIX = "android.processor.immutability" 35 private const val DATA_CLASS_NAME = "DataClass" 36 private val ANNOTATION = JavaFileObjects.forResource("Immutable.java") 37 38 private val FINAL_CLASSES = listOf( 39 JavaFileObjects.forSourceString( 40 "$PACKAGE_PREFIX.NonFinalClassFinalFields", 41 /* language=JAVA */ """ 42 package $PACKAGE_PREFIX; 43 44 public class NonFinalClassFinalFields { 45 private final String finalField; 46 public NonFinalClassFinalFields(String value) { 47 this.finalField = value; 48 } 49 } 50 """.trimIndent() 51 ), 52 JavaFileObjects.forSourceString( 53 "$PACKAGE_PREFIX.NonFinalClassNonFinalFields", 54 /* language=JAVA */ """ 55 package $PACKAGE_PREFIX; 56 57 public class NonFinalClassNonFinalFields { 58 private String nonFinalField; 59 } 60 """.trimIndent() 61 ), 62 JavaFileObjects.forSourceString( 63 "$PACKAGE_PREFIX.FinalClassFinalFields", 64 /* language=JAVA */ """ 65 package $PACKAGE_PREFIX; 66 67 public final class FinalClassFinalFields { 68 private final String finalField; 69 public FinalClassFinalFields(String value) { 70 this.finalField = value; 71 } 72 } 73 """.trimIndent() 74 ), 75 JavaFileObjects.forSourceString( 76 "$PACKAGE_PREFIX.FinalClassNonFinalFields", 77 /* language=JAVA */ """ 78 package $PACKAGE_PREFIX; 79 80 public final class FinalClassNonFinalFields { 81 private String nonFinalField; 82 } 83 """.trimIndent() 84 ) 85 ) 86 } 87 88 @get:Rule 89 val expect = Expect.create() 90 91 @Test 92 fun validInterface() = test( 93 source = JavaFileObjects.forSourceString( 94 "$PACKAGE_PREFIX.$DATA_CLASS_NAME", 95 /* language=JAVA */ """ 96 package $PACKAGE_PREFIX; 97 98 import $IMMUTABLE_ANNOTATION_NAME; 99 import java.util.ArrayList; 100 import java.util.Collections; 101 import java.util.List; 102 103 @Immutable 104 public interface $DATA_CLASS_NAME { 105 InnerInterface DEFAULT = new InnerInterface() { 106 @Override 107 public String getValue() { 108 return ""; 109 } 110 @Override 111 public List<String> getArray() { 112 return Collections.emptyList(); 113 } 114 }; 115 116 String getValue(); 117 ArrayList<String> getArray(); 118 InnerInterface getInnerInterface(); 119 120 @Immutable 121 interface InnerInterface { 122 String getValue(); 123 List<String> getArray(); 124 } 125 } 126 """.trimIndent() 127 ), errors = emptyList() 128 ) 129 130 @Test 131 fun abstractClass() = test( 132 JavaFileObjects.forSourceString( 133 "$PACKAGE_PREFIX.$DATA_CLASS_NAME", 134 /* language=JAVA */ """ 135 package $PACKAGE_PREFIX; 136 137 import $IMMUTABLE_ANNOTATION_NAME; 138 import java.util.Map; 139 140 @Immutable 141 public abstract class $DATA_CLASS_NAME { 142 public static final String IMMUTABLE = ""; 143 public static final InnerClass NOT_IMMUTABLE = null; 144 public static InnerClass NOT_FINAL = null; 145 146 // Field finality doesn't matter, methods are always enforced so that future 147 // field compaction or deprecation is possible 148 private final String fieldFinal = ""; 149 private String fieldNonFinal; 150 public abstract void sideEffect(); 151 public abstract String[] getArray(); 152 public abstract InnerClass getInnerClassOne(); 153 public abstract InnerClass getInnerClassTwo(); 154 @Immutable.Ignore 155 public abstract InnerClass getIgnored(); 156 public abstract InnerInterface getInnerInterface(); 157 158 public abstract Map<String, String> getValidMap(); 159 public abstract Map<InnerClass, InnerClass> getInvalidMap(); 160 161 public static final class InnerClass { 162 public String innerField; 163 public String[] getArray() { return null; } 164 } 165 166 public interface InnerInterface { 167 String[] getArray(); 168 InnerClass getInnerClass(); 169 } 170 } 171 """.trimIndent() 172 ), errors = listOf( 173 nonInterfaceClassFailure(line = 7), 174 nonInterfaceReturnFailure(line = 9), 175 staticNonFinalFailure(line = 10), 176 nonInterfaceReturnFailure(line = 10), 177 memberNotMethodFailure(line = 14), 178 memberNotMethodFailure(line = 15), 179 voidReturnFailure(line = 16), 180 arrayFailure(line = 17), 181 nonInterfaceReturnFailure(line = 18), 182 nonInterfaceReturnFailure(line = 19), 183 classNotImmutableFailure(line = 22, className = "InnerInterface"), 184 nonInterfaceReturnFailure(line = 25, prefix = "Key InnerClass"), 185 nonInterfaceReturnFailure(line = 25, prefix = "Value InnerClass"), 186 classNotImmutableFailure(line = 27, className = "InnerClass"), 187 nonInterfaceClassFailure(line = 27), 188 memberNotMethodFailure(line = 28), 189 arrayFailure(line = 29), 190 arrayFailure(line = 33), 191 nonInterfaceReturnFailure(line = 34), 192 ) 193 ) 194 195 @Test 196 fun finalClasses() = test( 197 JavaFileObjects.forSourceString( 198 "$PACKAGE_PREFIX.$DATA_CLASS_NAME", 199 /* language=JAVA */ """ 200 package $PACKAGE_PREFIX; 201 202 import java.util.List; 203 204 @Immutable 205 public interface $DATA_CLASS_NAME { 206 NonFinalClassFinalFields getNonFinalFinal(); 207 List<NonFinalClassNonFinalFields> getNonFinalNonFinal(); 208 FinalClassFinalFields getFinalFinal(); 209 List<FinalClassNonFinalFields> getFinalNonFinal(); 210 211 @Immutable.Policy(exceptions = {Immutable.Policy.Exception.FINAL_CLASSES_WITH_FINAL_FIELDS}) 212 NonFinalClassFinalFields getPolicyNonFinalFinal(); 213 214 @Immutable.Policy(exceptions = {Immutable.Policy.Exception.FINAL_CLASSES_WITH_FINAL_FIELDS}) 215 List<NonFinalClassNonFinalFields> getPolicyNonFinalNonFinal(); 216 217 @Immutable.Policy(exceptions = {Immutable.Policy.Exception.FINAL_CLASSES_WITH_FINAL_FIELDS}) 218 FinalClassFinalFields getPolicyFinalFinal(); 219 220 @Immutable.Policy(exceptions = {Immutable.Policy.Exception.FINAL_CLASSES_WITH_FINAL_FIELDS}) 221 List<FinalClassNonFinalFields> getPolicyFinalNonFinal(); 222 } 223 """.trimIndent() 224 ), errors = listOf( 225 nonInterfaceReturnFailure(line = 7), 226 nonInterfaceReturnFailure(line = 8, index = 0), 227 nonInterfaceReturnFailure(line = 9), 228 nonInterfaceReturnFailure(line = 10, index = 0), 229 classNotFinalFailure(line = 13, "NonFinalClassFinalFields"), 230 ), otherErrors = mapOf( 231 FINAL_CLASSES[1] to listOf( 232 memberNotMethodFailure(line = 4), 233 ), 234 FINAL_CLASSES[3] to listOf( 235 memberNotMethodFailure(line = 4), 236 ), 237 ) 238 ) 239 240 @Test 241 fun superClass() { 242 val superClass = JavaFileObjects.forSourceString( 243 "$PACKAGE_PREFIX.SuperClass", 244 /* language=JAVA */ """ 245 package $PACKAGE_PREFIX; 246 247 import java.util.List; 248 249 public interface SuperClass { 250 InnerClass getInnerClassOne(); 251 252 final class InnerClass { 253 public String innerField; 254 } 255 } 256 """.trimIndent() 257 ) 258 259 val dataClass = JavaFileObjects.forSourceString( 260 "$PACKAGE_PREFIX.$DATA_CLASS_NAME", 261 /* language=JAVA */ """ 262 package $PACKAGE_PREFIX; 263 264 import java.util.List; 265 266 @Immutable 267 public interface $DATA_CLASS_NAME extends SuperClass { 268 String[] getArray(); 269 } 270 """.trimIndent() 271 ) 272 273 test( 274 sources = arrayOf(superClass, dataClass), 275 fileToErrors = mapOf( 276 superClass to listOf( 277 classNotImmutableFailure(line = 5, className = "SuperClass"), 278 nonInterfaceReturnFailure(line = 6), 279 nonInterfaceClassFailure(8), 280 classNotImmutableFailure(line = 8, className = "InnerClass"), 281 memberNotMethodFailure(line = 9), 282 ), 283 dataClass to listOf( 284 arrayFailure(line = 7), 285 ) 286 ) 287 ) 288 } 289 290 @Test 291 fun ignoredClass() = test( 292 JavaFileObjects.forSourceString( 293 "$PACKAGE_PREFIX.$DATA_CLASS_NAME", 294 /* language=JAVA */ """ 295 package $PACKAGE_PREFIX; 296 297 import java.util.List; 298 import java.util.Map; 299 300 @Immutable 301 public interface $DATA_CLASS_NAME { 302 IgnoredClass getInnerClassOne(); 303 NotIgnoredClass getInnerClassTwo(); 304 Map<String, IgnoredClass> getInnerClassThree(); 305 Map<String, NotIgnoredClass> getInnerClassFour(); 306 307 @Immutable.Ignore 308 final class IgnoredClass { 309 public String innerField; 310 } 311 312 final class NotIgnoredClass { 313 public String innerField; 314 } 315 } 316 """.trimIndent() 317 ), errors = listOf( 318 nonInterfaceReturnFailure(line = 9), 319 nonInterfaceReturnFailure(line = 11, prefix = "Value NotIgnoredClass"), 320 classNotImmutableFailure(line = 18, className = "NotIgnoredClass"), 321 nonInterfaceClassFailure(line = 18), 322 memberNotMethodFailure(line = 19), 323 ) 324 ) 325 326 private fun test( 327 source: JavaFileObject, 328 errors: List<CompilationError>, 329 otherErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(), 330 ) = test( 331 sources = arrayOf(source), 332 fileToErrors = otherErrors + (source to errors), 333 ) 334 335 private fun test( 336 vararg sources: JavaFileObject, 337 fileToErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(), 338 ) { 339 val compilation = javac() 340 .withProcessors(ImmutabilityProcessor()) 341 .compile(FINAL_CLASSES + ANNOTATION + sources) 342 343 fileToErrors.forEach { (file, errors) -> 344 errors.forEach { error -> 345 try { 346 assertThat(compilation) 347 .hadErrorContaining(error.message) 348 .inFile(file) 349 .onLine(error.line) 350 } catch (e: AssertionError) { 351 // Wrap the exception so that the line number is logged 352 val wrapped = AssertionError("Expected $error, ${e.message}").apply { 353 stackTrace = e.stackTrace 354 } 355 356 // Wrap again with Expect so that all errors are reported. This is very bad code 357 // but can only be fixed by updating compile-testing with a better Truth Subject 358 // implementation. 359 expect.that(wrapped).isNull() 360 } 361 } 362 } 363 364 expect.that(compilation.errors().size).isEqualTo(fileToErrors.values.sumOf { it.size }) 365 366 if (expect.hasFailures()) { 367 expect.withMessage( 368 compilation.errors() 369 .sortedBy { it.lineNumber } 370 .joinToString(separator = "\n") { 371 "${it.lineNumber}: ${it.getMessage(Locale.ENGLISH)?.trim()}" 372 } 373 ).fail() 374 } 375 } 376 377 private fun classNotImmutableFailure(line: Long, className: String) = 378 CompilationError(line = line, message = MessageUtils.classNotImmutableFailure(className)) 379 380 private fun nonInterfaceClassFailure(line: Long) = 381 CompilationError(line = line, message = MessageUtils.nonInterfaceClassFailure()) 382 383 private fun nonInterfaceReturnFailure(line: Long) = 384 CompilationError(line = line, message = MessageUtils.nonInterfaceReturnFailure()) 385 386 private fun nonInterfaceReturnFailure(line: Long, prefix: String = "", index: Int = -1) = 387 CompilationError( 388 line = line, 389 message = MessageUtils.nonInterfaceReturnFailure(prefix = prefix, index = index) 390 ) 391 392 private fun memberNotMethodFailure(line: Long) = 393 CompilationError(line = line, message = MessageUtils.memberNotMethodFailure()) 394 395 private fun voidReturnFailure(line: Long) = 396 CompilationError(line = line, message = MessageUtils.voidReturnFailure()) 397 398 private fun staticNonFinalFailure(line: Long) = 399 CompilationError(line = line, message = MessageUtils.staticNonFinalFailure()) 400 401 private fun arrayFailure(line: Long) = 402 CompilationError(line = line, message = MessageUtils.arrayFailure()) 403 404 private fun classNotFinalFailure(line: Long, className: String) = 405 CompilationError(line = line, message = MessageUtils.classNotFinalFailure(className)) 406 407 data class CompilationError( 408 val line: Long, 409 val message: String, 410 ) 411 } 412