1 /*
2  * Copyright (C) 2018 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 com.android.tools.metalava
18 
19 import org.junit.Test
20 
21 class KotlinInteropChecksTest : DriverTest() {
22     @Test
Hard Kotlin keywordsnull23     fun `Hard Kotlin keywords`() {
24         check(
25             extraArguments = arrayOf("--check-kotlin-interop"),
26             warnings = """
27                 src/test/pkg/Test.java:5: warning: Avoid method names that are Kotlin hard keywords ("fun"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword:141]
28                 src/test/pkg/Test.java:6: warning: Avoid parameter names that are Kotlin hard keywords ("typealias"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword:141]
29                 src/test/pkg/Test.java:7: warning: Avoid field names that are Kotlin hard keywords ("object"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword:141]
30                 """,
31             sourceFiles = *arrayOf(
32                 java(
33                     """
34                     package test.pkg;
35                     import androidx.annotation.ParameterName;
36 
37                     public class Test {
38                         public void fun() { }
39                         public void foo(int fun, @ParameterName("typealias") int internalName) { }
40                         public Object object = null;
41                     }
42                     """
43                 ),
44                 supportParameterName
45             )
46         )
47     }
48 
49     @Test
Sam-compatible parameters should be lastnull50     fun `Sam-compatible parameters should be last`() {
51         check(
52             extraArguments = arrayOf("--check-kotlin-interop"),
53             warnings = """
54                 src/test/pkg/Test.java:10: warning: SAM-compatible parameters (such as parameter 1, "run", in test.pkg.Test.error) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions [SamShouldBeLast:142]
55                 src/test/pkg/test.kt:7: warning: lambda parameters (such as parameter 1, "bar", in test.pkg.TestKt.error) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions [SamShouldBeLast:142]
56                 """,
57             sourceFiles = *arrayOf(
58                 java(
59                     """
60                     package test.pkg;
61                     public class Test {
62                         public void ok1() { }
63                         public void ok1(int x) { }
64                         public void ok2(int x, int y) { }
65                         public void ok3(Runnable run) { }
66                         public void ok4(int x, Runnable run) { }
67                         public void ok5(Runnable run1, Runnable run2) { }
68                         public void ok6(java.util.List list, boolean b) { }
69                         public void error(Runnable run, int x) { }
70                     }
71                     """
72                 ),
73                 kotlin(
74                     """
75                     package test.pkg
76 
77                     fun ok1(bar: (Int) -> Int) { }
78                     fun ok2(foo: Int) { }
79                     fun ok3(foo: Int, bar: (Int) -> Int) { }
80                     fun ok4(foo: Int, bar: (Int) -> Int, baz: (Int) -> Int) { }
81                     fun error(bar: (Int) -> Int, foo: Int) { }
82                 """
83                 )
84             )
85         )
86     }
87 
88     @Test
Companion object methods should be marked with JvmStaticnull89     fun `Companion object methods should be marked with JvmStatic`() {
90         check(
91             extraArguments = arrayOf("--check-kotlin-interop"),
92             warnings = """
93                 src/test/pkg/Foo.kt:7: warning: Companion object constants like INTEGER_ONE should be marked @JvmField for Java interoperability; see https://android.github.io/kotlin-guides/interop.html#companion-constants [MissingJvmstatic:143]
94                 src/test/pkg/Foo.kt:13: warning: Companion object methods like missing should be marked @JvmStatic for Java interoperability; see https://android.github.io/kotlin-guides/interop.html#companion-functions [MissingJvmstatic:143]
95                 """,
96             sourceFiles = *arrayOf(
97                 kotlin(
98                     """
99                     package test.pkg
100 
101                     @SuppressWarnings("all")
102                     class Foo {
103                         fun ok1() { }
104                         companion object {
105                             const val INTEGER_ONE = 1
106                             var BIG_INTEGER_ONE = BigInteger.ONE
107                             @JvmStatic val WRONG = 2 // not yet flagged
108                             @JvmStatic @JvmField val WRONG2 = 2 // not yet flagged
109                             @JvmField val ok3 = 3
110 
111                             fun missing() { }
112 
113                             @JvmStatic
114                             fun ok2() { }
115                         }
116                     }
117                     """
118                 )
119             )
120         )
121     }
122 
123     @Test
Methods with default parameters should specify JvmOverloadsnull124     fun `Methods with default parameters should specify JvmOverloads`() {
125         check(
126             extraArguments = arrayOf("--check-kotlin-interop"),
127             warnings = """
128                 src/test/pkg/Foo.kt:8: warning: A Kotlin method with default parameter values should be annotated with @JvmOverloads for better Java interoperability; see https://android.github.io/kotlin-guides/interop.html#function-overloads-for-defaults [MissingJvmstatic:143]
129                 """,
130             sourceFiles = *arrayOf(
131                 kotlin(
132                     """
133                     package test.pkg
134 
135                     class Foo {
136                         fun ok1() { }
137                         fun ok2(int: Int) { }
138                         fun ok3(int: Int, int2: Int) { }
139                         @JvmOverloads fun ok4(int: Int = 0, int2: Int = 0) { }
140                         fun error(int: Int = 0, int2: Int = 0) { }
141                         fun String.ok4(int: Int = 0, int2: Int = 0) { }
142                         inline fun ok5(int: Int, int2: Int) { }
143                     }
144                     """
145                 )
146             )
147         )
148     }
149 
150     @Test
Methods which throw exceptions should document themnull151     fun `Methods which throw exceptions should document them`() {
152         check(
153             extraArguments = arrayOf("--check-kotlin-interop"),
154             warnings = """
155                 src/test/pkg/Foo.kt:6: error: Method Foo.error_throws_multiple_times appears to be throwing java.io.FileNotFoundException; this should be recorded with a @Throws annotation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions [DocumentExceptions:145]
156                 src/test/pkg/Foo.kt:16: error: Method Foo.error_throwsCheckedExceptionWithWrongExceptionClassInThrows appears to be throwing java.io.FileNotFoundException; this should be recorded with a @Throws annotation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions [DocumentExceptions:145]
157                 src/test/pkg/Foo.kt:37: error: Method Foo.error_throwsRuntimeExceptionDocsMissing appears to be throwing java.lang.UnsupportedOperationException; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions [DocumentExceptions:145]
158                 src/test/pkg/Foo.kt:43: error: Method Foo.error_missingSpecificAnnotation appears to be throwing java.lang.UnsupportedOperationException; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions [DocumentExceptions:145]
159                 """,
160             sourceFiles = *arrayOf(
161                 kotlin(
162                     """
163                     package test.pkg
164                     import java.io.FileNotFoundException
165                     import java.lang.UnsupportedOperationException
166 
167                     class Foo {
168                         fun error_throws_multiple_times(x: Int) {
169                             if (x < 0) {
170                                 throw java.io.FileNotFoundException("Something")
171                             }
172                             if (x > 10) { // make sure we don't list this twice
173                                 throw FileNotFoundException("Something")
174                             }
175                         }
176 
177 
178                         @Throws(Exception::class)
179                         fun error_throwsCheckedExceptionWithWrongExceptionClassInThrows(x: Int) {
180                             if (x < 0) {
181                                 throw java.io.FileNotFoundException("Something")
182                             }
183                         }
184 
185                         @Throws(FileNotFoundException::class)
186                         fun ok_hasThrows1(x: Int) {
187                             if (x < 0) {
188                                 throw java.io.FileNotFoundException("Something")
189                             }
190                         }
191 
192                         @Throws(UnsupportedOperationException::class, FileNotFoundException::class)
193                         fun ok_hasThrows2(x: Int) {
194                             if (x < 0) {
195                                 throw java.io.FileNotFoundException("Something")
196                             }
197                         }
198 
199                         fun error_throwsRuntimeExceptionDocsMissing(x: Int) {
200                             if (x < 0) {
201                                 throw UnsupportedOperationException("Something")
202                             }
203                         }
204 
205                         /** This method throws FileNotFoundException if blah blah blah */
206                         fun error_missingSpecificAnnotation(x: Int) {
207                             if (x < 0) {
208                                 throw UnsupportedOperationException("Something")
209                             }
210                         }
211 
212                         /** This method throws UnsupportedOperationException if blah blah blah */
213                         fun ok_docsPresent(x: Int) {
214                             if (x < 0) {
215                                 throw UnsupportedOperationException("Something")
216                             }
217                         }
218 
219                         fun ok_exceptionCaught(x: Int) {
220                             try {
221                                 if (s.startsWith(" ")) {
222                                     throw NumberFormatException()
223                                 }
224                                 println("Hello")
225                             } catch (e: NumberFormatException) {}
226                         }
227 
228                         fun ok_exceptionCaught2(x: Int) {
229                             try {
230                                 if (s.startsWith(" ")) {
231                                     throw NumberFormatException()
232                                 }
233                                 println("Hello")
234                             } catch (e: Exception) {}
235                         }
236 
237                         // TODO: What about something where you call in Java a method
238                         // known to throw something (e.g. Integer.parseInt) and you don't catch it; should you
239                         // pass it on? Hard to say; if the logic is complicated it may
240                         // be the case that it can never happen, and this might be an annoying false positive.
241                     }
242                     """
243                 )
244             )
245         )
246     }
247 }
248