1 /*
2  * Copyright 2019 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 @file:Suppress("KDocUnresolvedReference", "UnstableApiUsage")
18 
19 package androidx.build.lint
20 
21 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
22 import com.android.tools.lint.checks.infrastructure.ProjectDescription
23 import com.android.tools.lint.checks.infrastructure.TestFile
24 import com.android.tools.lint.checks.infrastructure.TestMode
25 import org.junit.Test
26 import org.junit.runner.RunWith
27 import org.junit.runners.JUnit4
28 
29 /**
30  * Test for [SampledAnnotationDetector]
31  *
32  * This tests the following module setup:
33  *
34  * Module 'foo', which lives in foo Module 'foo:samples', which lives in foo/samples, and depends on
35  * 'foo'
36  */
37 @RunWith(JUnit4::class)
38 class SampledAnnotationDetectorTest : LintDetectorTest() {
39 
getDetectornull40     override fun getDetector() = SampledAnnotationDetector()
41 
42     override fun getIssues() =
43         mutableListOf(
44             SampledAnnotationDetector.OBSOLETE_SAMPLED_ANNOTATION,
45             SampledAnnotationDetector.UNRESOLVED_SAMPLE_LINK,
46             SampledAnnotationDetector.MULTIPLE_FUNCTIONS_FOUND,
47             SampledAnnotationDetector.INVALID_SAMPLES_LOCATION
48         )
49 
50     private val fooModuleName = "foo"
51     private val sampleModuleName = "samples"
52 
53     private val barFilePath = "src/foo/Bar.kt"
54     private val sampleFilePath = "samples/src/foo/samples/test.kt"
55 
56     private val sampledStub =
57         compiled(
58             "libs/sampled.jar",
59             kotlin(
60                     """
61             package androidx.annotation
62 
63             @Target(AnnotationTarget.FUNCTION)
64             @Retention(AnnotationRetention.SOURCE)
65             annotation class Sampled
66         """
67                 )
68                 .to("androidx/annotation/Sampled.kt"),
69             0x9dd4162c,
70             """
71         META-INF/main.kotlin_module:
72         H4sIAAAAAAAAAGNgYGBmYGBgBGJWKM3AJcrFnZafr1ecmFuQk1osxBaSWlzi
73         XaLEoMUAAClM+YMvAAAA
74         """,
75             """
76         androidx/annotation/Sampled.class:
77         H4sIAAAAAAAAAIVSyU4CQRB9NYggbriDuGv0Jki8cXLBSKJiEL1wapmOGWlm
78         CNOg3rj5Tx4M8ehHGatdgMNE+1D9qvq96qrqfv94eQWwj01CSrh203Psx7Rw
79         XU8L7Xhu+krUG0raERAhfi/aIq2Ee5cu3t7Lqo4gRFjtRwd0Bz0YQZiweFbz
80         tHLcQUpZNO+kzhEmhFLeg7S/Az5hK4DdT9jTRU+uL47KheIFYTlAUZJaugYx
81         NdwWqiUJO39mHlQMXxWvS0d5wtpZYIOD3O1/KJeecqpPOVNmILHX0UbweV7J
82         OmcqPzWkqew8Xz4tHhOmfps5l1rYQgs+tOrtED8oGcNzpxqHHh3jZRjZe4Rk
83         txONWQkrZsVT0bdnK9HtZK0MHXY7hpA1Rf7xEfgKEGI/3m5NG8drNavyxFE8
84         4GSpxS3X5Y3jO7dK9mfrb3N6DLF4GGZZWP+ya9jgvYow+I8hWgFJjCCGUfbG
85         JMYxgclvGGc4ZeAXZxozmGXVXAWhAuYLWGCLhDHJAhaRYpaPJSxXYPlY8bH6
86         CeBn7eLsAgAA
87         """
88         )
89 
90     private val emptySampleFile =
91         kotlin(
92             """
93             package foo.samples
94         """
95         )
96 
97     private val unannotatedSampleFile =
98         kotlin(
99             """
100             package foo.samples
101 
102             fun sampleBar() {}
103         """
104         )
105 
106     private val multipleMatchingSampleFile =
107         kotlin(
108             """
109             package foo.samples
110 
111             import androidx.annotation.Sampled
112 
113             @Sampled
114             fun sampleBar() {}
115 
116             @Sampled
117             fun sampleBar(param: Boolean = false) {}
118         """
119         )
120 
121     private val correctlyAnnotatedSampleFile =
122         kotlin(
123             """
124             package foo.samples
125 
126             import androidx.annotation.Sampled
127 
128             @Sampled
129             fun sampleBar() {}
130         """
131         )
132 
133     private fun checkKotlin(
134         fooFile: TestFile? = null,
135         sampleFile: TestFile,
136         expectedText: String? = null,
137         requiresPartialAnalysis: Boolean = true
138     ) {
139         val projectDescriptions = mutableListOf<ProjectDescription>()
140         val fooProject =
141             ProjectDescription().apply {
142                 name = fooModuleName
143                 type = ProjectDescription.Type.LIBRARY
144                 fooFile?.let { addFile(fooFile) }
145             }
146         projectDescriptions += fooProject
147 
148         val sampleProject =
149             ProjectDescription().apply {
150                 name = sampleModuleName
151                 type = ProjectDescription.Type.LIBRARY
152                 under = fooProject
153                 files = arrayOf(sampleFile, sampledStub)
154                 dependsOn(fooProject)
155             }
156         projectDescriptions += sampleProject
157 
158         lint().run {
159             projects(*projectDescriptions.toTypedArray())
160             if (requiresPartialAnalysis) {
161                 // The lint check will only work in partial analysis, so we expect non-partial
162                 // test modes to have no errors. This has to be called before run().
163                 expectIdenticalTestModeOutput(false)
164             }
165             run().run {
166                 if (expectedText == null) {
167                     expectClean()
168                 } else {
169                     if (requiresPartialAnalysis) {
170                         // expectClean doesn't accept a testMode param
171                         expect("No warnings.", testMode = TestMode.DEFAULT)
172                         expect(expectedText, testMode = TestMode.PARTIAL)
173                     } else {
174                         expect(expectedText)
175                     }
176                 }
177             }
178         }
179     }
180 
181     @Test
unresolvedSampleLink_Functionnull182     fun unresolvedSampleLink_Function() {
183         val fooFile =
184             kotlin(
185                 """
186             package foo
187 
188             class Bar {
189               /**
190                * @sample foo.samples.sampleBar
191                */
192               fun bar() {}
193             }
194         """
195             )
196 
197         val sampleFile = emptySampleFile
198 
199         val expected =
200             """$barFilePath:6: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
201                * @sample foo.samples.sampleBar
202                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
203 1 errors, 0 warnings
204         """
205 
206         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
207     }
208 
209     @Test
unannotatedSampleFunction_Functionnull210     fun unannotatedSampleFunction_Function() {
211         val fooFile =
212             kotlin(
213                 """
214             package foo
215 
216             class Bar {
217               /**
218                * @sample foo.samples.sampleBar
219                */
220               fun bar() {}
221             }
222         """
223             )
224 
225         val sampleFile = unannotatedSampleFile
226 
227         val expected =
228             """$barFilePath:6: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
229                * @sample foo.samples.sampleBar
230                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
231 1 errors, 0 warnings
232         """
233 
234         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
235     }
236 
237     @Test
multipleMatchingSampleFunctions_Functionnull238     fun multipleMatchingSampleFunctions_Function() {
239         val fooFile =
240             kotlin(
241                 """
242             package foo
243 
244             class Bar {
245               /**
246                * @sample foo.samples.sampleBar
247                */
248               fun bar() {}
249             }
250         """
251             )
252 
253         val sampleFile = multipleMatchingSampleFile
254 
255         val expected =
256             """$sampleFilePath:10: Error: Found multiple functions matching foo.samples.sampleBar [MultipleSampledFunctions]
257             fun sampleBar(param: Boolean = false) {}
258                 ~~~~~~~~~
259 1 errors, 0 warnings
260         """
261 
262         checkKotlin(
263             fooFile = fooFile,
264             sampleFile = sampleFile,
265             expectedText = expected,
266             // Since this particular is all done inside the same module, partial analysis isn't
267             // required so it will still work in global analysis
268             requiresPartialAnalysis = false
269         )
270     }
271 
272     @Test
correctlyAnnotatedSampleFunction_Functionnull273     fun correctlyAnnotatedSampleFunction_Function() {
274         val fooFile =
275             kotlin(
276                 """
277             package foo
278 
279             class Bar {
280               /**
281                * @sample foo.samples.sampleBar
282                */
283               fun bar() {}
284             }
285         """
286             )
287 
288         val sampleFile = correctlyAnnotatedSampleFile
289 
290         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = null)
291     }
292 
293     @Test
unresolvedSampleLink_Classnull294     fun unresolvedSampleLink_Class() {
295         val fooFile =
296             kotlin(
297                 """
298             package foo
299 
300             /**
301              * @sample foo.samples.sampleBar
302              */
303             class Bar
304         """
305             )
306 
307         val sampleFile = emptySampleFile
308 
309         val expected =
310             """$barFilePath:5: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
311              * @sample foo.samples.sampleBar
312                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
313 1 errors, 0 warnings
314         """
315 
316         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
317     }
318 
319     @Test
unannotatedSampleFunction_Classnull320     fun unannotatedSampleFunction_Class() {
321         val fooFile =
322             kotlin(
323                 """
324             package foo
325 
326             /**
327              * @sample foo.samples.sampleBar
328              */
329             class Bar
330         """
331             )
332 
333         val sampleFile = unannotatedSampleFile
334 
335         val expected =
336             """$barFilePath:5: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
337              * @sample foo.samples.sampleBar
338                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
339 1 errors, 0 warnings
340         """
341 
342         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
343     }
344 
345     @Test
multipleMatchingSampleFunctions_Classnull346     fun multipleMatchingSampleFunctions_Class() {
347         val fooFile =
348             kotlin(
349                 """
350             package foo
351 
352             /**
353              * @sample foo.samples.sampleBar
354              */
355             class Bar
356         """
357             )
358 
359         val sampleFile = multipleMatchingSampleFile
360 
361         val expected =
362             """$sampleFilePath:10: Error: Found multiple functions matching foo.samples.sampleBar [MultipleSampledFunctions]
363             fun sampleBar(param: Boolean = false) {}
364                 ~~~~~~~~~
365 1 errors, 0 warnings
366         """
367 
368         checkKotlin(
369             fooFile = fooFile,
370             sampleFile = sampleFile,
371             expectedText = expected,
372             // Since this particular is all done inside the same module, partial analysis isn't
373             // required so it will still work in global analysis
374             requiresPartialAnalysis = false
375         )
376     }
377 
378     @Test
correctlyAnnotatedSampleFunction_Classnull379     fun correctlyAnnotatedSampleFunction_Class() {
380         val fooFile =
381             kotlin(
382                 """
383             package foo
384 
385             /**
386              * @sample foo.samples.sampleBar
387              */
388             class Bar
389         """
390             )
391 
392         val sampleFile = correctlyAnnotatedSampleFile
393 
394         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = null)
395     }
396 
397     @Test
unresolvedSampleLink_Fieldnull398     fun unresolvedSampleLink_Field() {
399         val fooFile =
400             kotlin(
401                 """
402             package foo
403 
404             class Bar {
405               /**
406                * @sample foo.samples.sampleBar
407                */
408               const val bar = 0
409             }
410         """
411             )
412 
413         val sampleFile = emptySampleFile
414 
415         val expected =
416             """$barFilePath:6: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
417                * @sample foo.samples.sampleBar
418                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
419 1 errors, 0 warnings
420         """
421 
422         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
423     }
424 
425     @Test
unannotatedSampleFunction_Fieldnull426     fun unannotatedSampleFunction_Field() {
427         val fooFile =
428             kotlin(
429                 """
430             package foo
431 
432             class Bar {
433               /**
434                * @sample foo.samples.sampleBar
435                */
436               const val bar = 0
437             }
438         """
439             )
440 
441         val sampleFile = unannotatedSampleFile
442 
443         val expected =
444             """$barFilePath:6: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
445                * @sample foo.samples.sampleBar
446                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
447 1 errors, 0 warnings
448         """
449 
450         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
451     }
452 
453     @Test
multipleMatchingSampleFunctions_Fieldnull454     fun multipleMatchingSampleFunctions_Field() {
455         val fooFile =
456             kotlin(
457                 """
458             package foo
459 
460             class Bar {
461               /**
462                * @sample foo.samples.sampleBar
463                */
464               const val bar = 0
465             }
466         """
467             )
468 
469         val sampleFile = multipleMatchingSampleFile
470 
471         val expected =
472             """$sampleFilePath:10: Error: Found multiple functions matching foo.samples.sampleBar [MultipleSampledFunctions]
473             fun sampleBar(param: Boolean = false) {}
474                 ~~~~~~~~~
475 1 errors, 0 warnings
476         """
477 
478         checkKotlin(
479             fooFile = fooFile,
480             sampleFile = sampleFile,
481             expectedText = expected,
482             // Since this particular is all done inside the same module, partial analysis isn't
483             // required so it will still work in global analysis
484             requiresPartialAnalysis = false
485         )
486     }
487 
488     @Test
correctlyAnnotatedSampleFunction_Fieldnull489     fun correctlyAnnotatedSampleFunction_Field() {
490         val fooFile =
491             kotlin(
492                 """
493             package foo
494 
495             class Bar {
496               /**
497                * @sample foo.samples.sampleBar
498                */
499               const val bar = 0
500             }
501         """
502             )
503 
504         val sampleFile = correctlyAnnotatedSampleFile
505 
506         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = null)
507     }
508 
509     @Test
unresolvedSampleLink_PropertyWithGetternull510     fun unresolvedSampleLink_PropertyWithGetter() {
511         val fooFile =
512             kotlin(
513                 """
514             package foo
515 
516             class Bar {
517               /**
518                * @sample foo.samples.sampleBar
519                */
520               val bar get() = 0
521             }
522         """
523             )
524 
525         val sampleFile = emptySampleFile
526 
527         val expected =
528             """$barFilePath:6: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
529                * @sample foo.samples.sampleBar
530                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
531 1 errors, 0 warnings
532         """
533 
534         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
535     }
536 
537     @Test
unannotatedSampleFunction_PropertyWithGetternull538     fun unannotatedSampleFunction_PropertyWithGetter() {
539         val fooFile =
540             kotlin(
541                 """
542             package foo
543 
544             class Bar {
545               /**
546                * @sample foo.samples.sampleBar
547                */
548               val bar get() = 0
549             }
550         """
551             )
552 
553         val sampleFile = unannotatedSampleFile
554 
555         val expected =
556             """$barFilePath:6: Error: Couldn't find a valid @Sampled function matching foo.samples.sampleBar [UnresolvedSampleLink]
557                * @sample foo.samples.sampleBar
558                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
559 1 errors, 0 warnings
560         """
561 
562         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = expected)
563     }
564 
565     @Test
multipleMatchingSampleFunctions_PropertyWithGetternull566     fun multipleMatchingSampleFunctions_PropertyWithGetter() {
567         val fooFile =
568             kotlin(
569                 """
570             package foo
571 
572             class Bar {
573               /**
574                * @sample foo.samples.sampleBar
575                */
576               val bar get() = 0
577             }
578         """
579             )
580 
581         val sampleFile = multipleMatchingSampleFile
582 
583         val expected =
584             """$sampleFilePath:10: Error: Found multiple functions matching foo.samples.sampleBar [MultipleSampledFunctions]
585             fun sampleBar(param: Boolean = false) {}
586                 ~~~~~~~~~
587 1 errors, 0 warnings
588         """
589 
590         checkKotlin(
591             fooFile = fooFile,
592             sampleFile = sampleFile,
593             expectedText = expected,
594             // Since this particular is all done inside the same module, partial analysis isn't
595             // required so it will still work in global analysis
596             requiresPartialAnalysis = false
597         )
598     }
599 
600     @Test
correctlyAnnotatedSampleFunction_PropertyWithGetternull601     fun correctlyAnnotatedSampleFunction_PropertyWithGetter() {
602         val fooFile =
603             kotlin(
604                 """
605             package foo
606 
607             class Bar {
608               /**
609                * @sample foo.samples.sampleBar
610                */
611               val bar get() = 0
612             }
613         """
614             )
615 
616         val sampleFile = correctlyAnnotatedSampleFile
617 
618         checkKotlin(fooFile = fooFile, sampleFile = sampleFile, expectedText = null)
619     }
620 
621     @Test
obsoleteSampledFunctionnull622     fun obsoleteSampledFunction() {
623         val sampleFile = correctlyAnnotatedSampleFile
624 
625         val expected =
626             """$sampleFilePath:7: Error: foo.samples.sampleBar is annotated with @Sampled, but is not linked to from a @sample tag. [ObsoleteSampledAnnotation]
627             fun sampleBar() {}
628                 ~~~~~~~~~
629 1 errors, 0 warnings
630 
631         """
632 
633         checkKotlin(sampleFile = sampleFile, expectedText = expected)
634     }
635 
636     @Test
invalidSampleLocationnull637     fun invalidSampleLocation() {
638         val sampleFile =
639             kotlin(
640                 """
641             package foo.wrong.location
642 
643             import androidx.annotation.Sampled
644 
645             @Sampled
646             fun sampleBar() {}
647         """
648             )
649 
650         val expected =
651             """samples/src/foo/wrong/location/test.kt:7: Error: foo.wrong.location.sampleBar is annotated with @Sampled, but is not linked to from a @sample tag. [ObsoleteSampledAnnotation]
652             fun sampleBar() {}
653                 ~~~~~~~~~
654 1 errors, 0 warnings
655 
656         """
657 
658         checkKotlin(sampleFile = sampleFile, expectedText = expected)
659     }
660 }
661