| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * 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 |
| * |
| * http://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 com.android.tools.metalava |
| |
| import org.junit.Test |
| |
| class KotlinInteropChecksTest : DriverTest() { |
| @Test |
| fun `Hard Kotlin keywords`() { |
| check( |
| apiLint = "", |
| expectedIssues = """ |
| src/test/pkg/Test.java:7: error: Avoid method names that are Kotlin hard keywords ("fun"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword] |
| src/test/pkg/Test.java:8: error: Avoid parameter names that are Kotlin hard keywords ("typealias"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword] |
| src/test/pkg/Test.java:10: error: Avoid field names that are Kotlin hard keywords ("object"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword] |
| """, |
| expectedFail = DefaultLintErrorMessage, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.ParameterName; |
| |
| public class Test { |
| public void fun() { } |
| public void foo(int fun, @ParameterName("typealias") int internalName) { } |
| @NonNull |
| public final Object object = null; |
| } |
| """ |
| ), |
| supportParameterName, |
| androidxNonNullSource |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Sam-compatible parameters should be last`() { |
| check( |
| apiLint = "", |
| expectedIssues = """ |
| src/test/pkg/Test.java:20: warning: SAM-compatible parameters (such as parameter 1, "run", in test.pkg.Test.error1) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions [SamShouldBeLast] [See https://s.android.com/api-guidelines#placement-of-sam-parameters] |
| src/test/pkg/Test.java:23: warning: SAM-compatible parameters (such as parameter 2, "callback", in test.pkg.Test.error2) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions [SamShouldBeLast] [See https://s.android.com/api-guidelines#placement-of-sam-parameters] |
| 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] [See https://s.android.com/api-guidelines#placement-of-sam-parameters] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.NonNull; |
| import java.lang.Runnable; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| |
| public class Test { |
| public void ok1() { } |
| public void ok1(int x) { } |
| public void ok2(int x, int y) { } |
| public void ok3(@Nullable Runnable run) { } |
| public void ok4(int x, @Nullable Runnable run) { } |
| public void ok5(@Nullable Runnable run1, @Nullable Runnable run2) { } |
| public void ok6(@NonNull java.util.List<String> list, boolean b) { } |
| // Consumer declares exactly one non-default method (accept), other methods are default. |
| public void ok7(@NonNull String packageName, @NonNull Executor executor, |
| @NonNull Consumer<Boolean> callback) {} |
| public void error1(@NonNull Runnable run, int x) { } |
| // Executors, while they have a single method are not considered to be SAM that we want to be |
| // the last argument |
| public void error2(@NonNull String packageName, @NonNull Consumer<Boolean> callback, |
| @NonNull Executor executor) {} |
| // Iterables, while they have a single method are not considered to be SAM that we want to be |
| // the last argument |
| public void ok8(@Nullable Iterable<String> iterable, int x) { } |
| } |
| """ |
| ), |
| kotlin( |
| """ |
| package test.pkg |
| |
| fun ok1(bar: (Int) -> Int) { } |
| fun ok2(foo: Int) { } |
| fun ok3(foo: Int, bar: (Int) -> Int) { } |
| fun ok4(foo: Int, bar: (Int) -> Int, baz: (Int) -> Int) { } |
| fun error(bar: (Int) -> Int, foo: Int) { } |
| """ |
| ), |
| androidxNullableSource, |
| androidxNonNullSource |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Companion object methods should be marked with JvmStatic`() { |
| check( |
| apiLint = "", |
| extraArguments = arrayOf( |
| ARG_HIDE, "AllUpper", |
| ARG_HIDE, "AcronymName", |
| ARG_HIDE, "CompileTimeConstant" |
| ), |
| expectedIssues = """ |
| src/test/pkg/Foo.kt:8: warning: Companion object constants like BIG_INTEGER_ONE should be marked @JvmField for Java interoperability; see https://developer.android.com/kotlin/interop#companion_constants [MissingJvmstatic] |
| src/test/pkg/Foo.kt:11: warning: Companion object constants like WRONG should be using @JvmField, not @JvmStatic; see https://developer.android.com/kotlin/interop#companion_constants [MissingJvmstatic] |
| src/test/pkg/Foo.kt:12: warning: Companion object constants like WRONG2 should be using @JvmField, not @JvmStatic; see https://developer.android.com/kotlin/interop#companion_constants [MissingJvmstatic] |
| src/test/pkg/Foo.kt:15: warning: Companion object methods like missing should be marked @JvmStatic for Java interoperability; see https://developer.android.com/kotlin/interop#companion_functions [MissingJvmstatic] |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| @SuppressWarnings("all") |
| class Foo { |
| fun ok1() { } |
| companion object { |
| const val INTEGER_ONE = 1 |
| val BIG_INTEGER_ONE = BigInteger.ONE |
| private val PRIVATE_BIG_INTEGER = BigInteger.ONE |
| var ok = 1 |
| @JvmStatic val WRONG = 2 |
| @JvmStatic @JvmField val WRONG2 = 2 |
| @JvmField val ok3 = 3 |
| |
| fun missing() { } |
| |
| @JvmStatic |
| fun ok2() { } |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Methods with default parameters should specify JvmOverloads`() { |
| check( |
| apiLint = "", |
| expectedIssues = """ |
| src/test/pkg/Bar.kt:12: 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] |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| interface Bar { |
| fun ok(int: Int = 0, int2: Int = 0) { } |
| } |
| |
| class Foo { |
| fun ok1() { } |
| fun ok2(int: Int) { } |
| fun ok3(int: Int, int2: Int) { } |
| @JvmOverloads fun ok4(int: Int = 0, int2: Int = 0) { } |
| fun error(int: Int = 0, int2: Int = 0) { } |
| fun String.ok4(int: Int = 0, int2: Int = 0) { } |
| inline fun ok5(int: Int, int2: Int) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Methods which throw exceptions should document them`() { |
| check( |
| apiLint = "", |
| extraArguments = arrayOf(ARG_HIDE, "BannedThrow", ARG_HIDE, "GenericException"), |
| expectedIssues = """ |
| 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] [See https://s.android.com/api-guidelines#docs-throws] |
| src/test/pkg/Foo.kt:17: 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] [See https://s.android.com/api-guidelines#docs-throws] |
| 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] [See https://s.android.com/api-guidelines#docs-throws] |
| src/test/pkg/Foo.kt:44: 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] [See https://s.android.com/api-guidelines#docs-throws] |
| src/test/pkg/Foo.kt:76: error: Method Foo.getErrorVar 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] [See https://s.android.com/api-guidelines#docs-throws] |
| src/test/pkg/Foo.kt:77: error: Method Foo.setErrorVar 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] [See https://s.android.com/api-guidelines#docs-throws] |
| """, |
| expectedFail = DefaultLintErrorMessage, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| import java.io.FileNotFoundException |
| import java.lang.UnsupportedOperationException |
| |
| class Foo { |
| fun error_throws_multiple_times(x: Int) { |
| if (x < 0) { |
| throw FileNotFoundException("Something") |
| } |
| if (x > 10) { // make sure we don't list this twice |
| throw FileNotFoundException("Something") |
| } |
| } |
| |
| |
| @Throws(Exception::class) |
| fun error_throwsCheckedExceptionWithWrongExceptionClassInThrows(x: Int) { |
| if (x < 0) { |
| throw FileNotFoundException("Something") |
| } |
| } |
| |
| @Throws(FileNotFoundException::class) |
| fun ok_hasThrows1(x: Int) { |
| if (x < 0) { |
| throw FileNotFoundException("Something") |
| } |
| } |
| |
| @Throws(UnsupportedOperationException::class, FileNotFoundException::class) |
| fun ok_hasThrows2(x: Int) { |
| if (x < 0) { |
| throw FileNotFoundException("Something") |
| } |
| } |
| |
| fun error_throwsRuntimeExceptionDocsMissing(x: Int) { |
| if (x < 0) { |
| throw UnsupportedOperationException("Something") |
| } |
| } |
| |
| /** This method throws FileNotFoundException if blah blah blah */ |
| fun error_missingSpecificAnnotation(x: Int) { |
| if (x < 0) { |
| throw UnsupportedOperationException("Something") |
| } |
| } |
| |
| /** This method throws UnsupportedOperationException if blah blah blah */ |
| fun ok_docsPresent(x: Int) { |
| if (x < 0) { |
| throw UnsupportedOperationException("Something") |
| } |
| } |
| |
| fun ok_exceptionCaught(x: Int) { |
| try { |
| if (s.startsWith(" ")) { |
| throw NumberFormatException() |
| } |
| println("Hello") |
| } catch (e: NumberFormatException) {} |
| } |
| |
| fun ok_exceptionCaught2(x: Int) { |
| try { |
| if (s.startsWith(" ")) { |
| throw NumberFormatException() |
| } |
| println("Hello") |
| } catch (e: Exception) {} |
| } |
| |
| var errorVar: Int |
| get() { throw UnsupportedOperationException() } |
| set(value) { throw UnsupportedOperationException() } |
| |
| @get:Throws(FileNotFoundException::class) |
| var okValAnnotation: Int |
| get() { throw FileNotFoundException("Something") } |
| |
| /** Throws [UnsupportedOperationException] */ |
| val okValDocumented: Int |
| get() { throw UnsupportedOperationException() } |
| |
| /** Throws [UnsupportedOperationException] */ |
| var okVarDocumented: Int = 0 |
| set(value) { throw UnsupportedOperationException() } |
| |
| // TODO: What about something where you call in Java a method |
| // known to throw something (e.g. Integer.parseInt) and you don't catch it; should you |
| // pass it on? Hard to say; if the logic is complicated it may |
| // be the case that it can never happen, and this might be an annoying false positive. |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| } |