From 5f45b41d5361dc01a08288234de93fa8a1deabf8 Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Tue, 25 Oct 2016 23:34:37 +0300 Subject: [PATCH] Kapt3: Initial implementation of JCTreeConverter --- generators/generators.iml | 1 + .../kotlin/generators/tests/GenerateTests.kt | 7 + plugins/kapt3/kapt3.iml | 1 + .../jetbrains/kotlin/kapt3/JCTreeConverter.kt | 274 ++++++++++++++++++ .../Kapt3AnalysisCompletedHandlerExtension.kt | 2 +- .../org/jetbrains/kotlin/kapt3/KaptRunner.kt | 2 +- .../kapt3/test/AbstractJCTreeConverterTest.kt | 46 +++ .../test/JCTreeConverterTestGenerated.java | 43 +++ .../kotlin/kapt3/test}/KaptRunnerTest.kt | 19 +- plugins/kapt3/testData/converter/dataClass.kt | 1 + .../kapt3/testData/converter/dataClass.txt | 49 ++++ 11 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 plugins/kapt3/src/org/jetbrains/kotlin/kapt3/JCTreeConverter.kt create mode 100644 plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/AbstractJCTreeConverterTest.kt create mode 100644 plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/JCTreeConverterTestGenerated.java rename plugins/kapt3/test/{ => org/jetbrains/kotlin/kapt3/test}/KaptRunnerTest.kt (86%) create mode 100644 plugins/kapt3/testData/converter/dataClass.kt create mode 100644 plugins/kapt3/testData/converter/dataClass.txt diff --git a/generators/generators.iml b/generators/generators.iml index d590b297bfd..4584b2ac1c2 100644 --- a/generators/generators.iml +++ b/generators/generators.iml @@ -40,5 +40,6 @@ + \ No newline at end of file diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index f4664f80b37..20d07fca238 100755 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -136,6 +136,7 @@ import org.jetbrains.kotlin.js.test.semantics.* import org.jetbrains.kotlin.jvm.compiler.* import org.jetbrains.kotlin.jvm.runtime.AbstractJvm8RuntimeDescriptorLoaderTest import org.jetbrains.kotlin.jvm.runtime.AbstractJvmRuntimeDescriptorLoaderTest +import org.jetbrains.kotlin.kapt3.test.AbstractJCTreeConverterTest import org.jetbrains.kotlin.kdoc.AbstractKDocLexerTest import org.jetbrains.kotlin.lang.resolve.android.test.AbstractAndroidBoxTest import org.jetbrains.kotlin.lang.resolve.android.test.AbstractAndroidBytecodeShapeTest @@ -1137,6 +1138,12 @@ fun main(args: Array) { } } + testGroup("plugins/kapt3/test", "plugins/kapt3/testData") { + testClass { + model("converter") + } + } + testGroup("plugins/android-extensions/android-extensions-idea/tests", "plugins/android-extensions/android-extensions-idea/testData") { testClass() { model("android/completion", recursive = false, extension = null) diff --git a/plugins/kapt3/kapt3.iml b/plugins/kapt3/kapt3.iml index 00a9aed3a04..08584ed92ed 100644 --- a/plugins/kapt3/kapt3.iml +++ b/plugins/kapt3/kapt3.iml @@ -15,5 +15,6 @@ + \ No newline at end of file diff --git a/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/JCTreeConverter.kt b/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/JCTreeConverter.kt new file mode 100644 index 00000000000..98f72c0f3d5 --- /dev/null +++ b/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/JCTreeConverter.kt @@ -0,0 +1,274 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.kapt3 + +import com.sun.tools.javac.tree.JCTree +import com.sun.tools.javac.tree.JCTree.* +import com.sun.tools.javac.tree.TreeMaker +import com.sun.tools.javac.util.Context +import com.sun.tools.javac.util.Name +import com.sun.tools.javac.util.Names +import com.sun.tools.javac.util.SharedNameTable +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin +import org.jetbrains.org.objectweb.asm.Opcodes +import org.jetbrains.org.objectweb.asm.Type +import org.jetbrains.org.objectweb.asm.tree.* +import com.sun.tools.javac.util.List as JavacList + +class JCTreeConverter(context: Context, val classes: List, val origins: Map) { + private companion object { + private val BLACKLISTED_ANNOTATATIONS = listOf( + "java.lang.Deprecated", "kotlin.Deprecated", // Deprecated annotations + "java.lang.annotation.", // Java annotations + "org.jetbrains.annotations.", // Nullable/NotNull, ReadOnly, Mutable + "kotlin.jvm.", "kotlin.Metadata" // Kotlin annotations from runtime + ) + } + + private val treeMaker = TreeMaker.instance(context) + private val nameTable = SharedNameTable(Names.instance(context)) + + private var done = false + + fun convert(): JavacList { + if (done) error(JCTreeConverter::class.java.simpleName + " can convert classes only once") + done = true + return mapValues(classes) { convertTopLevelClass(it) } + } + + private fun convertTopLevelClass(clazz: ClassNode): JCCompilationUnit? { + val origin = origins[clazz] ?: return null + val ktFile = origin.element?.containingFile as? KtFile ?: return null + val descriptor = origin.descriptor as? ClassDescriptor ?: return null + + // Nested classes will be processed during the outer classes conversion + if (descriptor.containingDeclaration is ClassDescriptor) return null + + val classDeclaration = convert(clazz) + + val packageAnnotations = JavacList.nil() + val packageName = ktFile.packageFqName.asString() + val packageClause = if (packageName.isEmpty()) null else convertFqName(packageName) + + val imports = JavacList.nil() + val classes = JavacList.of(classDeclaration) + + return treeMaker.TopLevel(packageAnnotations, packageClause, imports + classes) + } + + /** + * Returns false for the inner classe or if the origin for the class was not found. + */ + private fun convert(clazz: ClassNode): JCClassDecl? { + if (isSynthetic(clazz.access)) return null + + val descriptor = origins[clazz]?.descriptor as? ClassDescriptor ?: return null + val modifiers = convertModifiers(clazz.access, clazz.visibleAnnotations, clazz.invisibleAnnotations) + val simpleName = name(descriptor.name.asString()) + val typeParams = JavacList.nil() + val extending = if (clazz.superName == "java/lang/Object") null else convertFqName(clazz.superName) + val implementing = mapValues(clazz.interfaces) { convertFqName(it) } + + val fields = mapValues(clazz.fields) { convertField(it) } + val methods = mapValues(clazz.methods) { convertMethod(it, simpleName) } + val nestedClasses = mapValues(clazz.innerClasses) { innerClass -> + if (innerClass.outerName != clazz.name) return@mapValues null + val innerClassNode = classes.firstOrNull { it.name == innerClass.name } ?: return@mapValues null + convert(innerClassNode) + } + + return treeMaker.ClassDef(modifiers, simpleName, typeParams, extending, implementing, fields + methods + nestedClasses) + } + + private fun convertField(field: FieldNode): JCVariableDecl? { + if (isSynthetic(field.access)) return null + + val modifiers = convertModifiers(field.access, field.visibleAnnotations, field.invisibleAnnotations) + val name = name(field.name) + val type = convertFqName(Type.getType(field.desc).className) + val value = field.value + val initializer = when (value) { + is Byte, is Boolean, is Char, is Short, is Int, is Long, is Float, is Double, is String -> treeMaker.Literal(value) + else -> null + } + return treeMaker.VarDef(modifiers, name, type, initializer) + } + + private fun convertMethod(method: MethodNode, containingClassSimpleName: Name): JCMethodDecl? { + if (isSynthetic(method.access)) return null + + val modifiers = convertModifiers(method.access, method.visibleAnnotations, method.invisibleAnnotations) + val name = if (method.name == "") containingClassSimpleName else name(method.name) + val typeParameters = JavacList.nil() + val receiverParameter = null + + val returnType = Type.getReturnType(method.desc) + val returnTypeExpr = convertFqName(returnType.className) + + val parametersInfo = method.getParametersInfo() + val parameters = mapValues(parametersInfo) { info -> + val modifiers = convertModifiers(info.access, info.visibleAnnotations, info.invisibleAnnotations) + val name = name(info.name) + val type = convertFqName(info.type) + treeMaker.VarDef(modifiers, name, type, null) + } + + val thrown = mapValues(method.exceptions) { convertFqName(it) } + + val defaultValue = method.annotationDefault?.let { convertLiteralExpression(it) } + + val body = if (defaultValue != null) { + null + } else { + val returnStatement = treeMaker.Return(convertLiteralExpression(getDefaultValue(returnType))) + treeMaker.Block(0, JavacList.of(returnStatement)) + } + + return treeMaker.MethodDef(modifiers, name, returnTypeExpr, + typeParameters, receiverParameter, parameters, thrown, body, defaultValue) + } + + private fun convertModifiers( + access: Int, + visibleAnnotations: List?, + invisibleAnnotations: List? + ): JCModifiers { + var annotations = visibleAnnotations?.fold(JavacList.nil()) { list, anno -> + convertAnnotation(anno)?.let { list.prepend(it) } ?: list + } ?: JavacList.nil() + annotations = invisibleAnnotations?.fold(annotations) { list, anno -> + convertAnnotation(anno)?.let { list.prepend(it) } ?: list + } ?: annotations + + return treeMaker.Modifiers(access.toLong(), annotations) + } + + private fun convertAnnotation(annotation: AnnotationNode): JCAnnotation? { + val fqName = Type.getType(annotation.desc).className + if (BLACKLISTED_ANNOTATATIONS.any { fqName.startsWith(it) }) return null + + val name = convertFqName(fqName) + val values = mapPairedValues(annotation.values) { key, value -> + treeMaker.Assign(convertSimpleName(key), convertLiteralExpression(value)) + } + return treeMaker.Annotation(name, values) + } + + private fun convertLiteralExpression(value: Any?): JCExpression { + return when (value) { + null -> convertSimpleName("null") + is Byte, is Boolean, is Char, is Short, is Int, is Long, is Float, is Double, is String -> treeMaker.Literal(value) + + is ByteArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is BooleanArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is CharArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is ShortArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is IntArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is LongArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is FloatArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is DoubleArray -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is Array<*> -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value.asIterable()) { convertLiteralExpression(it) }) + is List<*> -> treeMaker.NewArray(null, JavacList.nil(), mapValues(value) { convertLiteralExpression(it) }) + + is Type -> treeMaker.Select(convertFqName(value.className), name("class")) + is AnnotationNode -> convertAnnotation(value) ?: error("Annotation is filtered out") + else -> throw IllegalArgumentException("Illegal literal expression value: $value (${value.javaClass.canonicalName})") + } + } + + private fun getDefaultValue(type: Type): Any? = when (type) { + Type.BYTE_TYPE -> 0.toByte() + Type.BOOLEAN_TYPE -> false + Type.CHAR_TYPE -> '\u0000' + Type.SHORT_TYPE -> 0.toShort() + Type.INT_TYPE -> 0 + Type.LONG_TYPE -> 0L + Type.FLOAT_TYPE -> 0.0F + Type.DOUBLE_TYPE -> 0.0 + else -> null + } + + private fun convertFqName(internalOrFqName: String): JCExpression { + val path = internalOrFqName.replace('/', '.').split('.') + assert(path.isNotEmpty()) + if (path.size == 1) return convertSimpleName(path.single()) + + var expr = treeMaker.Select(convertSimpleName(path[0]), name(path[1])) + for (index in 2..path.lastIndex) { + expr = treeMaker.Select(expr, name(path[index])) + } + return expr + } + + private fun convertSimpleName(name: String): JCIdent = treeMaker.Ident(name(name)) + + private fun name(name: String) = nameTable.fromString(name) +} + +private class ParameterInfo( + val access: Int, + val name: String, + val type: String, + val visibleAnnotations: List?, + val invisibleAnnotations: List?) + +private fun MethodNode.getParametersInfo(): List { + val localVariables = this.localVariables ?: emptyList() + val parameters = this.parameters ?: emptyList() + val isStatic = (access and Opcodes.ACC_STATIC) > 0 + val parameterTypes = Type.getArgumentTypes(desc) + return parameterTypes.mapIndexed { index, type -> + val name = parameters.getOrNull(index)?.name ?: localVariables.getOrNull(index + (if (isStatic) 0 else 1))?.name ?: "p$index" + val visibleAnnotations = visibleParameterAnnotations?.get(index) + val invisibleAnnotations = invisibleParameterAnnotations?.get(index) + ParameterInfo(0, name, type.className, visibleAnnotations, invisibleAnnotations) + } +} + +private inline fun mapValues(values: Iterable?, f: (T) -> R?): JavacList { + if (values == null) return JavacList.nil() + + var result = JavacList.nil() + for (item in values) { + f(item)?.let { result = result.prepend(it) } + } + return result.reverse() +} + +private inline fun mapPairedValues(valuePairs: List?, f: (String, Any) -> T?): JavacList { + if (valuePairs == null || valuePairs.isEmpty()) return JavacList.nil() + + val size = valuePairs.size + var result = JavacList.nil() + assert(size % 2 == 0) + var index = 0 + while (index < size) { + val key = valuePairs[index] as String + val value = valuePairs[index + 1] + f(key, value)?.let { result = result.prepend(it) } + index += 2 + } + return result.reverse() +} + +private operator fun JavacList.plus(other: JavacList): JavacList { + return this.appendList(other) +} + +private fun isSynthetic(access: Int) = (access and Opcodes.ACC_SYNTHETIC) > 0 \ No newline at end of file diff --git a/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/Kapt3AnalysisCompletedHandlerExtension.kt b/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/Kapt3AnalysisCompletedHandlerExtension.kt index 931d82b6aca..2f314a11877 100644 --- a/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/Kapt3AnalysisCompletedHandlerExtension.kt +++ b/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/Kapt3AnalysisCompletedHandlerExtension.kt @@ -77,7 +77,7 @@ class Kapt3AnalysisCompletedHandlerExtension( } } -private class Kapt3BuilderFactory : ClassBuilderFactory { +class Kapt3BuilderFactory : ClassBuilderFactory { val compiledClasses = mutableListOf() val origins = mutableMapOf() diff --git a/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/KaptRunner.kt b/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/KaptRunner.kt index 78e79788f71..3169ae82f19 100644 --- a/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/KaptRunner.kt +++ b/plugins/kapt3/src/org/jetbrains/kotlin/kapt3/KaptRunner.kt @@ -51,7 +51,7 @@ class KaptError : RuntimeException { } class KaptRunner { - private val context = Context() + val context = Context() private val options: Options init { diff --git a/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/AbstractJCTreeConverterTest.kt b/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/AbstractJCTreeConverterTest.kt new file mode 100644 index 00000000000..a1906e56dd8 --- /dev/null +++ b/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/AbstractJCTreeConverterTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.kapt3.test + +import org.jetbrains.kotlin.codegen.CodegenTestCase +import org.jetbrains.kotlin.codegen.CodegenTestUtil +import org.jetbrains.kotlin.kapt3.JCTreeConverter +import org.jetbrains.kotlin.kapt3.Kapt3BuilderFactory +import org.jetbrains.kotlin.kapt3.KaptRunner +import org.jetbrains.kotlin.test.ConfigurationKind +import org.jetbrains.kotlin.test.KotlinTestUtils +import java.io.File + +abstract class AbstractJCTreeConverterTest : CodegenTestCase() { + override fun doMultiFileTest(wholeFile: File, files: List, javaFilesDir: File?) { + val javaSources = javaFilesDir?.let { arrayOf(it) } ?: emptyArray() + + createEnvironmentWithMockJdkAndIdeaAnnotations(ConfigurationKind.ALL, *javaSources) + loadMultiFiles(files) + + val txtFile = File(wholeFile.parentFile, wholeFile.nameWithoutExtension + ".txt") + val classBuilderFactory = Kapt3BuilderFactory() + CodegenTestUtil.generateFiles(myEnvironment, myFiles, classBuilderFactory) + + val kaptRunner = KaptRunner() + val converter = JCTreeConverter(kaptRunner.context, classBuilderFactory.compiledClasses, classBuilderFactory.origins) + val javaFiles = converter.convert() + + KotlinTestUtils.assertEqualsToFile(txtFile, javaFiles.joinToString("\n\n////////////////////\n\n")) + } +} + diff --git a/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/JCTreeConverterTestGenerated.java b/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/JCTreeConverterTestGenerated.java new file mode 100644 index 00000000000..81baee8323c --- /dev/null +++ b/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/JCTreeConverterTestGenerated.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.kapt3.test; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("plugins/kapt3/testData/converter") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class JCTreeConverterTestGenerated extends AbstractJCTreeConverterTest { + public void testAllFilesPresentInConverter() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("plugins/kapt3/testData/converter"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("dataClass.kt") + public void testDataClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("plugins/kapt3/testData/converter/dataClass.kt"); + doTest(fileName); + } +} diff --git a/plugins/kapt3/test/KaptRunnerTest.kt b/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/KaptRunnerTest.kt similarity index 86% rename from plugins/kapt3/test/KaptRunnerTest.kt rename to plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/KaptRunnerTest.kt index c90c9e9ce78..3acd688842c 100644 --- a/plugins/kapt3/test/KaptRunnerTest.kt +++ b/plugins/kapt3/test/org/jetbrains/kotlin/kapt3/test/KaptRunnerTest.kt @@ -1,6 +1,23 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * 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 org.jetbrains.kotlin.kapt3.test + import org.jetbrains.kotlin.kapt3.KaptError import org.jetbrains.kotlin.kapt3.KaptRunner -import org.junit.Assert import org.junit.Assert.* import org.junit.Test import java.io.File diff --git a/plugins/kapt3/testData/converter/dataClass.kt b/plugins/kapt3/testData/converter/dataClass.kt new file mode 100644 index 00000000000..6466e746d38 --- /dev/null +++ b/plugins/kapt3/testData/converter/dataClass.kt @@ -0,0 +1 @@ +data class User(val firstName: String, val secondName: String, val age: Int) \ No newline at end of file diff --git a/plugins/kapt3/testData/converter/dataClass.txt b/plugins/kapt3/testData/converter/dataClass.txt new file mode 100644 index 00000000000..9f5b59bd126 --- /dev/null +++ b/plugins/kapt3/testData/converter/dataClass.txt @@ -0,0 +1,49 @@ +public final synchronized class User { + private final java.lang.String firstName; + private final java.lang.String secondName; + private final int age; + + public final java.lang.String getFirstName() { + return null; + } + + public final java.lang.String getSecondName() { + return null; + } + + public final int getAge() { + return 0; + } + + public void User(java.lang.String firstName, java.lang.String secondName, int age) { + return null; + } + + public final java.lang.String component1() { + return null; + } + + public final java.lang.String component2() { + return null; + } + + public final int component3() { + return 0; + } + + public final User copy(java.lang.String firstName, java.lang.String secondName, int age) { + return null; + } + + public java.lang.String toString() { + return null; + } + + public int hashCode() { + return 0; + } + + public boolean equals(java.lang.Object p0) { + return false; + } +} \ No newline at end of file