Add @Immutable
This annotation marks an interface as effectively immutable,
enforcing through an annotation processor that it follow these
guidelines:
- Only exposes methods and/or static final constants
- Every exposed type is an @Immutable interface or otherwise immutable class
- Every method must return a type (no void methods allowed)
- All inner classes must be @Immutable interfaces
This is in preparation of making PackageState fully immutable.
As a test case it has been marked in this change but has its
errors suppressed using @Immutable.Ignore to have the build work.
Test: atest --host ImmutabilityAnnotationProcessorUnitTests
Test: m services.core.unboosted
Change-Id: I59c615a97d5a30b83a5bcace0e2cee270ea5d1d2
diff --git a/tools/processors/immutability/Android.bp b/tools/processors/immutability/Android.bp
new file mode 100644
index 0000000..01e7a31
--- /dev/null
+++ b/tools/processors/immutability/Android.bp
@@ -0,0 +1,58 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+ name: "ImmutabilityAnnotationProcessorHostLibrary",
+ srcs: [
+ "src/**/*.kt",
+ "src/**/*.java",
+ ],
+ use_tools_jar: true,
+ // The --add-modules/exports flags below don't work for kotlinc yet, so pin this module to Java
+ // language level 8 (see b/139342589):
+ java_version: "1.8",
+ openjdk9: {
+ javacflags: [
+ "--add-modules=jdk.compiler",
+ "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ ],
+ },
+}
+
+java_plugin {
+ name: "ImmutabilityAnnotationProcessor",
+ processor_class: "android.processor.immutability.ImmutabilityProcessor",
+ static_libs: ["ImmutabilityAnnotationProcessorHostLibrary"],
+}
+
+java_library {
+ name: "ImmutabilityAnnotation",
+ srcs: ["src/**/Immutable.java"],
+ sdk_version: "core_current",
+ host_supported: true,
+}
+
+java_test_host {
+ name: "ImmutabilityAnnotationProcessorUnitTests",
+
+ srcs: ["test/**/*.kt"],
+
+ static_libs: [
+ "compile-testing-prebuilt",
+ "truth-prebuilt",
+ "junit",
+ "kotlin-reflect",
+ "ImmutabilityAnnotationProcessorHostLibrary",
+ ],
+
+ test_suites: ["general-tests"],
+}
diff --git a/tools/processors/immutability/OWNERS b/tools/processors/immutability/OWNERS
new file mode 100644
index 0000000..86ae581
--- /dev/null
+++ b/tools/processors/immutability/OWNERS
@@ -0,0 +1 @@
+include /PACKAGE_MANAGER_OWNERS
diff --git a/tools/processors/immutability/TEST_MAPPING b/tools/processors/immutability/TEST_MAPPING
new file mode 100644
index 0000000..4e8e238
--- /dev/null
+++ b/tools/processors/immutability/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "ImmutabilityAnnotationProcessorUnitTests"
+ }
+ ]
+}
diff --git a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
new file mode 100644
index 0000000..f7690d2
--- /dev/null
+++ b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2022 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 android.processor.immutability
+
+import com.sun.tools.javac.code.Symbol
+import com.sun.tools.javac.code.Type
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic
+
+val IMMUTABLE_ANNOTATION_NAME = Immutable::class.qualifiedName
+
+class ImmutabilityProcessor : AbstractProcessor() {
+
+ companion object {
+ /**
+ * Types that are already immutable.
+ */
+ private val IGNORED_TYPES = listOf(
+ "java.io.File",
+ "java.lang.Boolean",
+ "java.lang.Byte",
+ "java.lang.CharSequence",
+ "java.lang.Character",
+ "java.lang.Double",
+ "java.lang.Float",
+ "java.lang.Integer",
+ "java.lang.Long",
+ "java.lang.Short",
+ "java.lang.String",
+ "java.lang.Void",
+ )
+ }
+
+ private lateinit var collectionType: TypeMirror
+ private lateinit var mapType: TypeMirror
+
+ private lateinit var ignoredTypes: List<TypeMirror>
+
+ private val seenTypes = mutableSetOf<Type>()
+
+ override fun getSupportedSourceVersion() = SourceVersion.latest()!!
+
+ override fun getSupportedAnnotationTypes() = setOf(Immutable::class.qualifiedName)
+
+ override fun init(processingEnv: ProcessingEnvironment) {
+ super.init(processingEnv)
+ collectionType = processingEnv.erasedType("java.util.Collection")
+ mapType = processingEnv.erasedType("java.util.Map")
+ ignoredTypes = IGNORED_TYPES.map { processingEnv.elementUtils.getTypeElement(it).asType() }
+ }
+
+ override fun process(
+ annotations: MutableSet<out TypeElement>,
+ roundEnvironment: RoundEnvironment
+ ): Boolean {
+ annotations.find {
+ it.qualifiedName.toString() == IMMUTABLE_ANNOTATION_NAME
+ } ?: return false
+ roundEnvironment.getElementsAnnotatedWith(Immutable::class.java)
+ .forEach { visitClass(emptyList(), seenTypes, it, it as Symbol.TypeSymbol) }
+ return true
+ }
+
+ private fun visitClass(
+ parentChain: List<String>,
+ seenTypes: MutableSet<Type>,
+ elementToPrint: Element,
+ classType: Symbol.TypeSymbol,
+ ) {
+ if (!seenTypes.add(classType.asType())) return
+ if (classType.getAnnotation(Immutable.Ignore::class.java) != null) return
+
+ if (classType.getAnnotation(Immutable::class.java) == null) {
+ printError(parentChain, elementToPrint,
+ MessageUtils.classNotImmutableFailure(classType.simpleName.toString()))
+ }
+
+ if (classType.getKind() != ElementKind.INTERFACE) {
+ printError(parentChain, elementToPrint, MessageUtils.nonInterfaceClassFailure())
+ }
+
+ val filteredElements = classType.enclosedElements
+ .filterNot(::isIgnored)
+
+ filteredElements
+ .filter { it.getKind() == ElementKind.FIELD }
+ .forEach {
+ if (it.isStatic) {
+ if (!it.isPrivate) {
+ if (!it.modifiers.contains(Modifier.FINAL)) {
+ printError(parentChain, it, MessageUtils.staticNonFinalFailure())
+ }
+
+ visitType(parentChain, seenTypes, it, it.type)
+ }
+ } else {
+ printError(parentChain, it, MessageUtils.memberNotMethodFailure())
+ }
+ }
+
+ // Scan inner classes before methods so that any violations isolated to the file prints
+ // the error on the class declaration rather than on the method that returns the type.
+ // Although it doesn't matter too much either way.
+ filteredElements
+ .filter { it.getKind() == ElementKind.CLASS }
+ .map { it as Symbol.ClassSymbol }
+ .forEach {
+ visitClass(parentChain, seenTypes, it, it)
+ }
+
+ val newChain = parentChain + "$classType"
+
+ filteredElements
+ .filter { it.getKind() == ElementKind.METHOD }
+ .map { it as Symbol.MethodSymbol }
+ .forEach {
+ visitMethod(newChain, seenTypes, it)
+ }
+ }
+
+ private fun visitMethod(
+ parentChain: List<String>,
+ seenTypes: MutableSet<Type>,
+ method: Symbol.MethodSymbol,
+ ) {
+ val returnType = method.returnType
+ val typeName = returnType.toString()
+ when (returnType.kind) {
+ TypeKind.BOOLEAN,
+ TypeKind.BYTE,
+ TypeKind.SHORT,
+ TypeKind.INT,
+ TypeKind.LONG,
+ TypeKind.CHAR,
+ TypeKind.FLOAT,
+ TypeKind.DOUBLE,
+ TypeKind.NONE,
+ TypeKind.NULL -> {
+ // Do nothing
+ }
+ TypeKind.VOID -> {
+ if (!method.isConstructor) {
+ printError(parentChain, method, MessageUtils.voidReturnFailure())
+ }
+ }
+ TypeKind.ARRAY -> {
+ printError(parentChain, method, MessageUtils.arrayFailure())
+ }
+ TypeKind.DECLARED -> {
+ visitType(parentChain, seenTypes, method, method.returnType)
+ }
+ TypeKind.ERROR,
+ TypeKind.TYPEVAR,
+ TypeKind.WILDCARD,
+ TypeKind.PACKAGE,
+ TypeKind.EXECUTABLE,
+ TypeKind.OTHER,
+ TypeKind.UNION,
+ TypeKind.INTERSECTION,
+ // Java 9+
+ // TypeKind.MODULE,
+ null -> printError(parentChain, method,
+ MessageUtils.genericTypeKindFailure(typeName = typeName))
+ else -> printError(parentChain, method,
+ MessageUtils.genericTypeKindFailure(typeName = typeName))
+ }
+ }
+
+ private fun visitType(
+ parentChain: List<String>,
+ seenTypes: MutableSet<Type>,
+ symbol: Symbol,
+ type: Type,
+ nonInterfaceClassFailure: () -> String = { MessageUtils.nonInterfaceReturnFailure() },
+ ) {
+ if (type.isPrimitive) return
+ if (type.isPrimitiveOrVoid) {
+ printError(parentChain, symbol, MessageUtils.voidReturnFailure())
+ return
+ }
+
+ if (ignoredTypes.any { processingEnv.typeUtils.isSameType(it, type) }) {
+ return
+ }
+
+ // Collection (and Map) types are ignored for the interface check as they have immutability
+ // enforced through a runtime exception which must be verified in a separate runtime test
+ val isMap = processingEnv.typeUtils.isAssignable(type, mapType)
+ if (!processingEnv.typeUtils.isAssignable(type, collectionType) && !isMap) {
+ if (type.isInterface) {
+ visitClass(parentChain, seenTypes, symbol,
+ processingEnv.typeUtils.asElement(type) as Symbol.TypeSymbol)
+ } else {
+ printError(parentChain, symbol, nonInterfaceClassFailure())
+ // If the type already isn't an interface, don't scan deeper children
+ // to avoid printing an excess amount of errors for a known bad type.
+ return
+ }
+ }
+
+ type.typeArguments.forEachIndexed { index, typeArg ->
+ visitType(parentChain, seenTypes, symbol, typeArg) {
+ MessageUtils.nonInterfaceReturnFailure(prefix = when {
+ !isMap -> ""
+ index == 0 -> "Key " + typeArg.asElement().simpleName
+ else -> "Value " + typeArg.asElement().simpleName
+ }, index = index)
+ }
+ }
+ }
+
+ private fun printError(
+ parentChain: List<String>,
+ element: Element,
+ message: String,
+ ) = processingEnv.messager.printMessage(
+ Diagnostic.Kind.ERROR,
+ // Drop one from the parent chain so that the directly enclosing class isn't logged.
+ // It exists in the list at this point in the traversal so that further children can
+ // include the right reference.
+ parentChain.dropLast(1).joinToString() + "\n\t" + message,
+ element,
+ )
+
+ private fun ProcessingEnvironment.erasedType(typeName: String) =
+ typeUtils.erasure(elementUtils.getTypeElement(typeName).asType())
+
+ private fun isIgnored(symbol: Symbol) =
+ symbol.getAnnotation(Immutable.Ignore::class.java) != null
+}
\ No newline at end of file
diff --git a/tools/processors/immutability/src/android/processor/immutability/Immutable.java b/tools/processors/immutability/src/android/processor/immutability/Immutable.java
new file mode 100644
index 0000000..0470faf
--- /dev/null
+++ b/tools/processors/immutability/src/android/processor/immutability/Immutable.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 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 android.processor.immutability;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Marks a class as immutable. When used with the Immutability processor, verifies at compile that
+ * the class is truly immutable. Immutable is defined as:
+ * <ul>
+ * <li>Only exposes methods and/or static final constants</li>
+ * <li>Every exposed type is an @Immutable interface or otherwise immutable class</li>
+ * <ul>
+ * <li>Implicitly immutable types like {@link String} are ignored</li>
+ * <li>{@link Collection} and {@link Map} and their subclasses where immutability is
+ * enforced at runtime are ignored</li>
+ * </ul>
+ * <li>Every method must return a type (no void methods allowed)</li>
+ * <li>All inner classes must be @Immutable interfaces</li>
+ * </ul>
+ */
+public @interface Immutable {
+
+ /**
+ * Marks a specific class, field, or method as ignored for immutability validation.
+ */
+ @Retention(RetentionPolicy.CLASS) // Not SOURCE as that isn't retained for some reason
+ @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
+ @interface Ignore {
+ String reason() default "";
+ }
+}
diff --git a/tools/processors/immutability/src/android/processor/immutability/MessageUtils.kt b/tools/processors/immutability/src/android/processor/immutability/MessageUtils.kt
new file mode 100644
index 0000000..63f5641
--- /dev/null
+++ b/tools/processors/immutability/src/android/processor/immutability/MessageUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 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 android.processor.immutability
+
+object MessageUtils {
+
+ fun classNotImmutableFailure(className: String) = "$className should be marked @Immutable"
+
+ fun memberNotMethodFailure() = "Member must be a method"
+
+ fun nonInterfaceClassFailure() = "Class was not an interface"
+
+ fun nonInterfaceReturnFailure(prefix: String, index: Int = -1) =
+ if (prefix.isEmpty()) {
+ "Type at index $index was not an interface"
+ } else {
+ "$prefix was not an interface"
+ }
+
+ fun genericTypeKindFailure(typeName: CharSequence) = "TypeKind $typeName unsupported"
+
+ fun arrayFailure() = "Array types are not supported as they can be mutated by callers"
+
+ fun nonInterfaceReturnFailure() = "Must return an interface"
+
+ fun voidReturnFailure() = "Cannot return void"
+
+ fun staticNonFinalFailure() = "Static member must be final"
+}
\ No newline at end of file
diff --git a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
new file mode 100644
index 0000000..7e1e8e1
--- /dev/null
+++ b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2022 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 android.processor
+
+import android.processor.immutability.IMMUTABLE_ANNOTATION_NAME
+import android.processor.immutability.ImmutabilityProcessor
+import android.processor.immutability.MessageUtils
+import com.google.common.truth.Expect
+import com.google.testing.compile.CompilationSubject.assertThat
+import com.google.testing.compile.Compiler.javac
+import com.google.testing.compile.JavaFileObjects
+import org.junit.Rule
+import org.junit.Test
+import javax.tools.JavaFileObject
+
+class ImmutabilityProcessorTest {
+
+ companion object {
+ private const val PACKAGE_PREFIX = "android.processor.immutability"
+ private const val DATA_CLASS_NAME = "DataClass"
+ private val ANNOTATION = JavaFileObjects.forSourceString(IMMUTABLE_ANNOTATION_NAME,
+ /* language=JAVA */ """
+ package $PACKAGE_PREFIX;
+
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Immutable {
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Ignore {}
+ }
+ """.trimIndent()
+ )
+ }
+
+ @get:Rule
+ val expect = Expect.create()
+
+ @Test
+ fun validInterface() = test(
+ JavaFileObjects.forSourceString("$PACKAGE_PREFIX.$DATA_CLASS_NAME",
+ /* language=JAVA */ """
+ package $PACKAGE_PREFIX;
+
+ import $IMMUTABLE_ANNOTATION_NAME;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.List;
+
+ @Immutable
+ public interface $DATA_CLASS_NAME {
+ InnerInterface DEFAULT = new InnerInterface() {
+ @Override
+ public String getValue() {
+ return "";
+ }
+ @Override
+ public List<String> getArray() {
+ return Collections.emptyList();
+ }
+ };
+
+ String getValue();
+ ArrayList<String> getArray();
+ InnerInterface getInnerInterface();
+
+ @Immutable
+ interface InnerInterface {
+ String getValue();
+ List<String> getArray();
+ }
+ }
+ """.trimIndent()
+ ), errors = emptyList())
+
+ @Test
+ fun abstractClass() = test(
+ JavaFileObjects.forSourceString("$PACKAGE_PREFIX.$DATA_CLASS_NAME",
+ /* language=JAVA */ """
+ package $PACKAGE_PREFIX;
+
+ import $IMMUTABLE_ANNOTATION_NAME;
+ import java.util.Map;
+
+ @Immutable
+ public abstract class $DATA_CLASS_NAME {
+ public static final String IMMUTABLE = "";
+ public static final InnerClass NOT_IMMUTABLE = null;
+ public static InnerClass NOT_FINAL = null;
+
+ // Field finality doesn't matter, methods are always enforced so that future
+ // field compaction or deprecation is possible
+ private final String fieldFinal = "";
+ private String fieldNonFinal;
+ public abstract void sideEffect();
+ public abstract String[] getArray();
+ public abstract InnerClass getInnerClassOne();
+ public abstract InnerClass getInnerClassTwo();
+ @Immutable.Ignore
+ public abstract InnerClass getIgnored();
+ public abstract InnerInterface getInnerInterface();
+
+ public abstract Map<String, String> getValidMap();
+ public abstract Map<InnerClass, InnerClass> getInvalidMap();
+
+ public static final class InnerClass {
+ public String innerField;
+ public String[] getArray() { return null; }
+ }
+
+ public interface InnerInterface {
+ String[] getArray();
+ InnerClass getInnerClass();
+ }
+ }
+ """.trimIndent()
+ ), errors = listOf(
+ nonInterfaceClassFailure(line = 7),
+ nonInterfaceReturnFailure(line = 9),
+ staticNonFinalFailure(line = 10),
+ nonInterfaceReturnFailure(line = 10),
+ memberNotMethodFailure(line = 14),
+ memberNotMethodFailure(line = 15),
+ voidReturnFailure(line = 16),
+ arrayFailure(line = 17),
+ nonInterfaceReturnFailure(line = 18),
+ nonInterfaceReturnFailure(line = 19),
+ nonInterfaceReturnFailure(line = 25, prefix = "Key InnerClass"),
+ nonInterfaceReturnFailure(line = 25, prefix = "Value InnerClass"),
+ classNotImmutableFailure(line = 27, className = "InnerClass"),
+ nonInterfaceClassFailure(line = 27),
+ memberNotMethodFailure(line = 28),
+ arrayFailure(line = 29),
+ classNotImmutableFailure(line = 22, className = "InnerInterface"),
+ arrayFailure(line = 33),
+ nonInterfaceReturnFailure(line = 34),
+ ))
+
+ private fun test(source: JavaFileObject, errors: List<CompilationError>) {
+ val compilation = javac()
+ .withProcessors(ImmutabilityProcessor())
+ .compile(listOf(source) + ANNOTATION)
+ errors.forEach {
+ try {
+ assertThat(compilation)
+ .hadErrorContaining(it.message)
+ .inFile(source)
+ .onLine(it.line)
+ } catch (e: AssertionError) {
+ // Wrap the exception so that the line number is logged
+ val wrapped = AssertionError("Expected $it, ${e.message}").apply {
+ stackTrace = e.stackTrace
+ }
+
+ // Wrap again with Expect so that all errors are reported. This is very bad code
+ // but can only be fixed by updating compile-testing with a better Truth Subject
+ // implementation.
+ expect.that(wrapped).isNull()
+ }
+ }
+
+ try {
+ assertThat(compilation).hadErrorCount(errors.size)
+ } catch (e: AssertionError) {
+ if (expect.hasFailures()) {
+ expect.that(e).isNull()
+ } else throw e
+ }
+ }
+
+ private fun classNotImmutableFailure(line: Long, className: String) =
+ CompilationError(line = line, message = MessageUtils.classNotImmutableFailure(className))
+
+ private fun nonInterfaceClassFailure(line: Long) =
+ CompilationError(line = line, message = MessageUtils.nonInterfaceClassFailure())
+
+ private fun nonInterfaceReturnFailure(line: Long) =
+ CompilationError(line = line, message = MessageUtils.nonInterfaceReturnFailure())
+
+ private fun nonInterfaceReturnFailure(line: Long, prefix: String, index: Int = -1) =
+ CompilationError(
+ line = line,
+ message = MessageUtils.nonInterfaceReturnFailure(prefix = prefix, index = index)
+ )
+
+ private fun memberNotMethodFailure(line: Long) =
+ CompilationError(line = line, message = MessageUtils.memberNotMethodFailure())
+
+ private fun voidReturnFailure(line: Long) =
+ CompilationError(line = line, message = MessageUtils.voidReturnFailure())
+
+ private fun staticNonFinalFailure(line: Long) =
+ CompilationError(line = line, message = MessageUtils.staticNonFinalFailure())
+
+ private fun arrayFailure(line: Long) =
+ CompilationError(line = line, message = MessageUtils.arrayFailure())
+
+ data class CompilationError(
+ val line: Long,
+ val message: String,
+ )
+}
\ No newline at end of file