From ce44f7d4d381972b25d4d60b3470fc2aaa1e7b99 Mon Sep 17 00:00:00 2001 From: Victor Petukhov Date: Fri, 9 Oct 2020 14:54:18 +0300 Subject: [PATCH] JSpecify: implement kotlin use sites to java ones converter --- compiler/tests-java8/build.gradle.kts | 1 + ...linUseSitesFromJavaOnesForJspecifyTests.kt | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 compiler/tests-java8/tests/org/jetbrains/kotlin/generators/tests/GenerateKotlinUseSitesFromJavaOnesForJspecifyTests.kt diff --git a/compiler/tests-java8/build.gradle.kts b/compiler/tests-java8/build.gradle.kts index 49e5fd3f2ae..977be68f45a 100644 --- a/compiler/tests-java8/build.gradle.kts +++ b/compiler/tests-java8/build.gradle.kts @@ -33,5 +33,6 @@ projectTest(parallel = true) { } val generateTests by generator("org.jetbrains.kotlin.generators.tests.GenerateJava8TestsKt") +val generateKotlinUseSiteFromJavaOnesForJspecifyTests by generator("org.jetbrains.kotlin.generators.tests.GenerateKotlinUseSitesFromJavaOnesForJspecifyTestsKt") testsJar() diff --git a/compiler/tests-java8/tests/org/jetbrains/kotlin/generators/tests/GenerateKotlinUseSitesFromJavaOnesForJspecifyTests.kt b/compiler/tests-java8/tests/org/jetbrains/kotlin/generators/tests/GenerateKotlinUseSitesFromJavaOnesForJspecifyTests.kt new file mode 100644 index 00000000000..412867efd30 --- /dev/null +++ b/compiler/tests-java8/tests/org/jetbrains/kotlin/generators/tests/GenerateKotlinUseSitesFromJavaOnesForJspecifyTests.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.generators.tests + +import org.jetbrains.kotlin.checkers.JSPECIFY_NULLABLE_ANNOTATION +import org.jetbrains.kotlin.checkers.JSPECIFY_NULLNESS_MISMATCH_MARK +import org.jetbrains.kotlin.checkers.JSPECIFY_NULLNESS_UNSPECIFIED_ANNOTATION +import org.jetbrains.kotlin.util.removeSuffixIfPresent +import java.io.File +import java.lang.Exception + +private data class ParsedMethod( + val returnType: String, + val funName: String, + val typeParameters: String, + val valueParameters: String, + val body: String +) + +private data class ParsedCallComponent( + val callee: String, + val typeArguments: String?, + val valueArguments: String? +) + +private data class ParsedCall( + val jspecifyMark: String?, + val receiver: String, + val callComponents: Sequence, + val indent: String +) + +class GenerateKotlinUseSitesFromJavaOnesForJspecifyTests { + companion object { + private const val TESTS_DIRECTORY = "compiler/testData/foreignAnnotationsJava8/tests/jspecify/java" + + private const val JSPECIFY_ANNOTATIONS = """(?:$JSPECIFY_NULLABLE_ANNOTATION|$JSPECIFY_NULLNESS_UNSPECIFIED_ANNOTATION)""" + private const val JSPECIFY_MARK = """(?:\/\/ $JSPECIFY_NULLNESS_MISMATCH_MARK)\n\s*""" + private const val TYPE_COMPONENT = """(?:$JSPECIFY_ANNOTATIONS )?(?:\w+)(?:\.\w+)*""" + private const val TYPE_ARGUMENTS = """<(?:$TYPE_COMPONENT)(?:\s*,\s*$TYPE_COMPONENT)*>""" + private const val TYPE_PARAMETER = """\w(?: (?:extends|super) $TYPE_COMPONENT)?""" + private const val TYPE_PARAMETERS = """<(?:$TYPE_PARAMETER)(?:\s*,\s*$TYPE_PARAMETER)*>\s*""" + private const val TYPE = """$TYPE_COMPONENT(?:$TYPE_ARGUMENTS)?""" + private const val VALUE_PARAMETER = """\s*(?:$TYPE) (?:\w+)""" + private const val VALUE_PARAMETERS = """\((?:$VALUE_PARAMETER)(?:\s*,\s*$VALUE_PARAMETER)*\s*\)""" + private const val VALUE_ARGUMENT = """\w+""" + private const val VALUE_ARGUMENTS = """\((?:(?:$VALUE_ARGUMENT)(?:\s*,\s*$VALUE_ARGUMENT)*\s*)?\)""" + private const val CALL_COMPONENT = """\.($TYPE_ARGUMENTS)?(?:\w+)(?:$VALUE_ARGUMENTS)?""" + private const val CALL = """\s*(?:$JSPECIFY_MARK)?(?:\w+)(?:(?:$CALL_COMPONENT)+);(\n+)\s*""" + private const val FUN_BODY = """\{((?:$CALL)+)\s*}""" + + val methodRegex = Regex("""(?:public )?($TYPE_PARAMETERS)?(void|$TYPE) (\w+)($VALUE_PARAMETERS)? $FUN_BODY""") + val valueParameterRegex = Regex("""($TYPE) (\w+)(,|\s*$)""") + val callRegex = Regex(CALL.replace("?:", "")) + val callComponentRegex = Regex(CALL_COMPONENT.replace("?:", "")) + } + + private fun parseMethod(sourceCode: String): ParsedMethod? { + val parsedResult = methodRegex.find(sourceCode) ?: return null + val (typeParameters, returnType, funName, arguments, body) = parsedResult.destructured + + return ParsedMethod( + transformTypesByAnnotationsIfNeeded(returnType), + funName, + typeParameters, + arguments.removeSurrounding("(", ")"), + body + ) + } + + private fun parseCalls(funBody: String) = + callRegex.findAll(funBody).map { call -> + val jspecifyMark = call.groups[2]?.value + val receiver = call.groups[3]?.value ?: throw Exception("Unable to parse receiver of $call") + val callComponents = call.groups[4] ?: throw Exception("Unable to parse call components of $call") + val indent = call.groups[22]?.value ?: throw Exception("Unable to parse indent of $call") + val parsedCallComponents = callComponentRegex.findAll(callComponents.value).map { callComponent -> + val calle = callComponent.groups[12]?.value + ?: throw Exception("Unable to parse calle of $callComponent") + val typeArguments = callComponent.groups[1]?.value + val valueArguments = callComponent.groups[13]?.value?.removeSurrounding("(", ")") + ?.let { transformTypesByAnnotationsIfNeeded(it) } + ParsedCallComponent(calle, typeArguments, valueArguments) + } + ParsedCall(jspecifyMark, receiver, parsedCallComponents, indent) + } + + private fun transformTypesByAnnotationsIfNeeded(valueArguments: String) = + valueArguments + .replace(Regex("""$JSPECIFY_NULLABLE_ANNOTATION (\w+)"""), "$1?") + .replace(Regex("""$JSPECIFY_NULLNESS_UNSPECIFIED_ANNOTATION (\w+)"""), "$1") + .replace("Object", "Any") + .replace("void", "Unit") + + private fun generateKotlinCode(parsedMethod: ParsedMethod, parsedCalls: Sequence) = buildString { + val methodValueParameters = + transformTypesByAnnotationsIfNeeded(parsedMethod.valueParameters.replace(valueParameterRegex, "$2: $1$3")) + .removeSuffixIfPresent(" ".repeat(4)) + val methodTypeParameters = parsedMethod.typeParameters.replace(" extends ", " : ") + + appendLine("fun $methodTypeParameters${parsedMethod.funName}($methodValueParameters): ${parsedMethod.returnType} {") + + for (call in parsedCalls) { + if (call.jspecifyMark != null) { + appendLine(" ".repeat(4) + call.jspecifyMark) + } + append(" ".repeat(4) + call.receiver) + for (callComponent in call.callComponents) { + val typeArguments = callComponent.typeArguments?.let { transformTypesByAnnotationsIfNeeded(it) } ?: "" + val valueArguments = callComponent.valueArguments?.let { "($it)" } ?: "" + append(".${callComponent.callee}${typeArguments}$valueArguments") + } + append(call.indent) + } + + append("}") + } + + private fun writeKotlinCode(javaFile: File, mode: String, code: String, modeDirective: String = "") { + File("${javaFile.parentFile.parent}/kotlin/$mode/${javaFile.nameWithoutExtension}.kt") + .writeText("// JAVA_SOURCES: ${javaFile.name}\n$modeDirective\n$code") + } + + fun main() { + val javaFiles = File(TESTS_DIRECTORY).walkTopDown() + + for (file in javaFiles) { + if (!file.isFile || file.extension != "java") continue + + val parsedMethod = parseMethod(file.readText()) ?: continue + val parsedCalls = parseCalls(parsedMethod.body) + val generatedKotlinCode = generateKotlinCode(parsedMethod, parsedCalls) + + writeKotlinCode(file, "strictMode", generatedKotlinCode) + writeKotlinCode(file, "warnMode", generatedKotlinCode, "// JSPECIFY_STATE warn\n") + } + } +} + +fun main() = GenerateKotlinUseSitesFromJavaOnesForJspecifyTests().main()