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