diff --git a/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/Cursor.kt b/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/Cursor.kt new file mode 100644 index 0000000000000..0a275ff99d70a --- /dev/null +++ b/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/Cursor.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2024 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 org.jetbrains.kotlin.library.abi.parser + +internal class Cursor +private constructor(private val lines: List, rowIndex: Int = 0, columnIndex: Int = 0) { + constructor(text: String) : this(text.split("\n")) + + var rowIndex: Int = rowIndex + private set + + var columnIndex: Int = columnIndex + private set + + val currentLine: String + get() = lines[rowIndex].slice(columnIndex until lines[rowIndex].length) + + val offset = lines.subList(0, rowIndex).sumOf { it.length } + columnIndex + + /** Check if we have passed the last line in [lines] and there is nothing left to parse */ + fun isFinished() = rowIndex >= lines.size + + fun nextLine() { + rowIndex++ + columnIndex = 0 + if (!isFinished()) { + skipInlineWhitespace() + } + } + + fun parseSymbol( + pattern: Regex, + peek: Boolean = false, + skipInlineWhitespace: Boolean = true, + ): String? { + val match = pattern.find(currentLine) + return match?.value?.also { + if (!peek) { + val offset = it.length + currentLine.indexOf(it) + setColumn(columnIndex + offset) + if (skipInlineWhitespace) { + skipInlineWhitespace() + } + } + } + } + + fun parseValidIdentifier(peek: Boolean = false): String? = + parseSymbol(validIdentifierRegex, peek) + + fun parseWord(peek: Boolean = false): String? = parseSymbol(wordRegex, peek) + + fun copy() = Cursor(lines, rowIndex, columnIndex) + + private fun setColumn(index: Int) { + columnIndex = index + } + + internal fun skipInlineWhitespace() { + while (currentLine.firstOrNull()?.isWhitespace() == true) { + setColumn(columnIndex + 1) + } + } +} + +// Match any '=' not followed by '...' because they're valid characters, but we don't want to +// parse part of the parameter default symbol (=...) by accident. Otherwise match all non-illegal +// characters +private val validIdentifierRegex = + Regex( + """ + ^((=(?!\s?\.\.\.)|[^.;\[\]/<>:\\(){}?=,&])+) + """ + .trimIndent() + ) +private val wordRegex = Regex("[a-zA-Z]+") diff --git a/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/KLibDumpParser.kt b/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/KLibDumpParser.kt new file mode 100644 index 0000000000000..8fe99b154299f --- /dev/null +++ b/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/KLibDumpParser.kt @@ -0,0 +1,359 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalLibraryAbiReader::class) + +package org.jetbrains.kotlin.library.abi.parser + +import org.jetbrains.kotlin.library.BaseKotlinLibrary +import org.jetbrains.kotlin.library.abi.AbiClass +import org.jetbrains.kotlin.library.abi.AbiCompoundName +import org.jetbrains.kotlin.library.abi.AbiDeclaration +import org.jetbrains.kotlin.library.abi.AbiEnumEntry +import org.jetbrains.kotlin.library.abi.AbiFunction +import org.jetbrains.kotlin.library.abi.AbiModality +import org.jetbrains.kotlin.library.abi.AbiProperty +import org.jetbrains.kotlin.library.abi.AbiQualifiedName +import org.jetbrains.kotlin.library.abi.AbiSignatureVersion +import org.jetbrains.kotlin.library.abi.AbiSignatures +import org.jetbrains.kotlin.library.abi.AbiValueParameterKind +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader +import org.jetbrains.kotlin.library.abi.LibraryAbi +import org.jetbrains.kotlin.library.abi.LibraryManifest +import org.jetbrains.kotlin.library.abi.LibraryTarget +import org.jetbrains.kotlin.library.abi.impl.AbiAnnotationListImpl +import org.jetbrains.kotlin.library.abi.impl.AbiClassImpl +import org.jetbrains.kotlin.library.abi.impl.AbiConstructorImpl +import org.jetbrains.kotlin.library.abi.impl.AbiEnumEntryImpl +import org.jetbrains.kotlin.library.abi.impl.AbiFunctionImpl +import org.jetbrains.kotlin.library.abi.impl.AbiPropertyImpl +import org.jetbrains.kotlin.library.abi.impl.AbiSignaturesImpl +import org.jetbrains.kotlin.library.abi.impl.AbiTopLevelDeclarationsImpl +import org.jetbrains.kotlin.library.abi.impl.AbiValueParameterImpl +import java.io.File +import java.text.ParseException + +/** + * Parser for klib dump format created by [org.jetbrains.kotlin.library.abi.LibraryAbiRenderer] + * + * @property klibDump The text of the dump file + * @property filePath The file path to add to parse exceptions for clearer debugging (optional) + */ +class KlibDumpParser(klibDump: String, private val filePath: String? = null) { + + constructor(file: File) : this(file.readText(), file.path) + + /** Cursor to keep track of current location within the dump */ + private val cursor = Cursor(klibDump) + private val declarations: MutableList = mutableListOf() + private var uniqueName: String = "" + private var signatureVersions: MutableSet = mutableSetOf() + + /** + * Parse the klib dump text. + * + * Returns [LibraryAbi] represented by the dump text. + * + * Throws [ParseException] when unable to parse for whatever reason + * */ + fun parse(): LibraryAbi { + while (!cursor.isFinished()) { + parseDeclaration(parentQualifiedName = null)?.let { abiDeclaration -> + declarations.add(abiDeclaration) + } + } + return LibraryAbi( + uniqueName = uniqueName, + signatureVersions = signatureVersions, + topLevelDeclarations = AbiTopLevelDeclarationsImpl(declarations), + manifest = + LibraryManifest( + platform = null, + platformTargets = emptyList(), + compilerVersion = null, + abiVersion = null, + irProviderName = null, + ), + ) + } + + + internal fun parseClass(parentQualifiedName: AbiQualifiedName? = null): AbiClass { + val modality = + cursor.parseAbiModality() ?: throw parseException("Failed to parse class modality") + val modifiers = cursor.parseClassModifiers() + val isInner = modifiers.contains("inner") + val isValue = modifiers.contains("value") + val isFunction = modifiers.contains("fun") + val kind = cursor.parseClassKind() ?: throw parseException("Failed to parse class kind") + val typeParams = cursor.parseTypeParams() ?: emptyList() + // if we are a nested class the name won't be qualified, and we will need to use the + // [parentQualifiedName] to complete it + val abiQualifiedName = parseAbiQualifiedName(parentQualifiedName) + val superTypes = cursor.parseSuperTypes() + + val childDeclarations = + if (cursor.parseOpenClassBody() != null) { + cursor.nextLine() + parseChildDeclarations(abiQualifiedName) + } else { + emptyList() + } + return AbiClassImpl( + qualifiedName = abiQualifiedName, + signatures = currentSignatures(), + annotations = AbiAnnotationListImpl.EMPTY, // annotations aren't part of klib dumps + modality = modality, + kind = kind, + isInner = isInner, + isValue = isValue, + isFunction = isFunction, + superTypes = superTypes.toList(), + declarations = childDeclarations, + typeParameters = typeParams, + ) + } + + internal fun parseFunction( + parentQualifiedName: AbiQualifiedName? = null, + isGetterOrSetter: Boolean = false, + ): AbiFunction { + val modality = cursor.parseAbiModality() + val isConstructor = cursor.parseFunctionKind(peek = true) == "constructor" + return when { + isConstructor -> parseConstructor(parentQualifiedName) + else -> + parseNonConstructorFunction( + parentQualifiedName, + isGetterOrSetter, + modality ?: throw parseException("Non constructor function must have modality"), + ) + } + } + + internal fun parseProperty(parentQualifiedName: AbiQualifiedName? = null): AbiProperty { + val modality = + cursor.parseAbiModality() + ?: throw parseException("Unable to parse modality for property") + val kind = + cursor.parsePropertyKind() ?: throw parseException("Unable to parse kind for property") + val qualifiedName = parseAbiQualifiedName(parentQualifiedName) + + cursor.nextLine() + var getter: AbiFunction? = null + var setter: AbiFunction? = null + while (cursor.hasGetterOrSetter()) { + if (cursor.hasGetter()) { + getter = parseFunction(qualifiedName, isGetterOrSetter = true) + } else { + setter = parseFunction(qualifiedName, isGetterOrSetter = true) + } + if (cursor.isFinished()) { + break + } + } + return AbiPropertyImpl( + qualifiedName = qualifiedName, + signatures = currentSignatures(), + annotations = AbiAnnotationListImpl.EMPTY, // annotations aren't part of klib dumps + modality = modality, + kind = kind, + getter = getter, + setter = setter, + backingField = null, + ) + } + + internal fun parseEnumEntry(parentQualifiedName: AbiQualifiedName?): AbiEnumEntry { + cursor.parseEnumEntryKind() + val enumName = cursor.parseEnumName() ?: throw parseException("Failed to parse enum name") + val relativeName = + parentQualifiedName?.let { it.relativeName.value + "." + enumName } + ?: throw parseException("Enum entry must have parent qualified name") + val qualifiedName = + AbiQualifiedName(parentQualifiedName.packageName, AbiCompoundName(relativeName)) + cursor.nextLine() + return AbiEnumEntryImpl( + qualifiedName = qualifiedName, + signatures = currentSignatures(), + annotations = AbiAnnotationListImpl.EMPTY, + ) + } + + private fun parseDeclaration(parentQualifiedName: AbiQualifiedName?): AbiDeclaration? { + // if the line begins with a comment, we may need to parse the current target list + if (cursor.parseCommentMarker() != null) { + parseCommentLine() + } else if (cursor.hasClassKind()) { + return parseClass(parentQualifiedName) + } else if (cursor.hasFunctionKind()) { + return parseFunction(parentQualifiedName) + } else if (cursor.hasPropertyKind()) { + return parseProperty(parentQualifiedName) + } else if (cursor.hasEnumEntry()) { + return parseEnumEntry(parentQualifiedName) + } else if (cursor.currentLine.isBlank()) { + cursor.nextLine() + } else { + throw parseException("Failed to parse unknown declaration") + } + return null + } + + private fun parseCommentLine() { + if (cursor.hasUniqueName()) { + uniqueName = cursor.parseUniqueName() + ?: throw parseException("Failed to parse library unique name") + } else if (cursor.hasSignatureVersion()) { + signatureVersions.add(cursor.parseSignatureVersion() + ?: throw parseException("Failed to parse signature version")) + } + cursor.nextLine() + } + + /** Parse all declarations which belong to a parent such as a class */ + private fun parseChildDeclarations( + parentQualifiedName: AbiQualifiedName? + ): List { + val childDeclarations = mutableListOf() + // end of parent container is marked by a closing bracket, collect all declarations + // until we see one. + while (cursor.parseCloseClassBody(peek = true) == null) { + parseDeclaration(parentQualifiedName)?.let { childDeclarations.add(it) } + } + cursor.nextLine() + return childDeclarations + } + + private fun parseNonConstructorFunction( + parentQualifiedName: AbiQualifiedName? = null, + isGetterOrSetter: Boolean = false, + modality: AbiModality, + ): AbiFunction { + val modifiers = cursor.parseFunctionModifiers() + val isInline = modifiers.contains("inline") + val isSuspend = modifiers.contains("suspend") + cursor.parseFunctionKind() + val typeParams = cursor.parseTypeParams() ?: emptyList() + val contextParams = cursor.parseContextParams() ?: emptyList() + val functionReceiver = cursor.parseFunctionReceiver() + val abiQualifiedName = + if (isGetterOrSetter) { + parseAbiQualifiedNameForGetterOrSetter(parentQualifiedName) + } else { + parseAbiQualifiedName(parentQualifiedName) + } + val valueParameters = + cursor.parseValueParameters() ?: throw parseException("Couldn't parse value params") + val allValueParameters = + contextParams + + if (null != functionReceiver) { + val functionReceiverAsValueParam = + AbiValueParameterImpl( + kind = AbiValueParameterKind.EXTENSION_RECEIVER, + type = functionReceiver, + isVararg = false, + hasDefaultArg = false, + isNoinline = false, + isCrossinline = false, + ) + listOf(functionReceiverAsValueParam) + valueParameters + } else { + valueParameters + } + val returnType = cursor.parseReturnType() + cursor.nextLine() + return AbiFunctionImpl( + qualifiedName = abiQualifiedName, + signatures = currentSignatures(), + annotations = AbiAnnotationListImpl.EMPTY, // annotations aren't part of klib dumps + modality = modality, + isInline = isInline, + isSuspend = isSuspend, + typeParameters = typeParams, + valueParameters = allValueParameters, + returnType = returnType, + ) + } + + private fun parseConstructor(parentQualifiedName: AbiQualifiedName?): AbiFunction { + val abiQualifiedName = + parentQualifiedName?.let { + AbiQualifiedName( + parentQualifiedName.packageName, + AbiCompoundName(parentQualifiedName.relativeName.value + "."), + ) + } ?: throw parseException("Cannot parse constructor outside of class context") + cursor.parseConstructorName() + val valueParameters = + cursor.parseValueParameters() + ?: throw parseException("Couldn't parse value parameters for constructor") + cursor.nextLine() + return AbiConstructorImpl( + qualifiedName = abiQualifiedName, + signatures = currentSignatures(), + annotations = AbiAnnotationListImpl.EMPTY, // annotations aren't part of klib dumps + isInline = false, // constructors cannot be inline + valueParameters = valueParameters, + ) + } + + private fun parseAbiQualifiedName(parentQualifiedName: AbiQualifiedName?): AbiQualifiedName { + val hasQualifiedName = cursor.parseAbiQualifiedName(peek = true) != null + return if (hasQualifiedName) { + cursor.parseAbiQualifiedName()!! + } else { + if (parentQualifiedName == null) { + throw parseException("Failed to parse qName") + } + val identifier = cursor.parseValidIdentifierAndMaybeTrim() + val relativeName = parentQualifiedName.relativeName.value + "." + identifier + return AbiQualifiedName(parentQualifiedName.packageName, AbiCompoundName(relativeName)) + } + } + + private fun parseAbiQualifiedNameForGetterOrSetter( + parentQualifiedName: AbiQualifiedName? + ): AbiQualifiedName { + if (parentQualifiedName == null) { + throw parseException("Failed to parse qName") + } + val identifier = + cursor.parseGetterOrSetterName() ?: throw parseException("Failed to parse qName") + val relativeName = parentQualifiedName.relativeName.value + "." + identifier + return AbiQualifiedName(parentQualifiedName.packageName, AbiCompoundName(relativeName)) + } + + private fun currentSignatures(): AbiSignatures { + val hasV1 = signatureVersions.any { it.versionNumber == 1} + val hasV2 = signatureVersions.any { it.versionNumber == 2} + return AbiSignaturesImpl( + if (hasV1) ("v1") else null, + if (hasV2) ("v2") else null, + ) + } + + private fun parseException(message: String): ParseException = + ParseException(formatErrorMessage(message), cursor.offset) + + private fun formatErrorMessage(message: String): String { + val maybeFilePath = filePath?.let { "$it:" } ?: "" + val location = "$maybeFilePath${cursor.rowIndex}:${cursor.columnIndex}" + return "$message at $location: '${cursor.currentLine}'" + } +} + + diff --git a/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/KlibParsingCursorExtensions.kt b/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/KlibParsingCursorExtensions.kt new file mode 100644 index 0000000000000..712d60fd3d33e --- /dev/null +++ b/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/parser/KlibParsingCursorExtensions.kt @@ -0,0 +1,505 @@ +/* + * Copyright 2024 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. + */ + +// Impl classes from kotlin.library.abi.impl are necessary to instantiate parsed declarations +@file:OptIn(ExperimentalLibraryAbiReader::class) + +package org.jetbrains.kotlin.library.abi.parser + +import kotlin.text.dropLast +import org.jetbrains.kotlin.library.abi.AbiClassKind +import org.jetbrains.kotlin.library.abi.AbiCompoundName +import org.jetbrains.kotlin.library.abi.AbiModality +import org.jetbrains.kotlin.library.abi.AbiPropertyKind +import org.jetbrains.kotlin.library.abi.AbiQualifiedName +import org.jetbrains.kotlin.library.abi.AbiSignatureVersion +import org.jetbrains.kotlin.library.abi.AbiType +import org.jetbrains.kotlin.library.abi.AbiTypeArgument +import org.jetbrains.kotlin.library.abi.AbiTypeNullability +import org.jetbrains.kotlin.library.abi.AbiTypeParameter +import org.jetbrains.kotlin.library.abi.AbiValueParameter +import org.jetbrains.kotlin.library.abi.AbiValueParameterKind +import org.jetbrains.kotlin.library.abi.AbiVariance +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader +import org.jetbrains.kotlin.library.abi.impl.AbiTypeParameterImpl +import org.jetbrains.kotlin.library.abi.impl.AbiValueParameterImpl +import org.jetbrains.kotlin.library.abi.impl.ClassReferenceImpl +import org.jetbrains.kotlin.library.abi.impl.SimpleTypeImpl +import org.jetbrains.kotlin.library.abi.impl.StarProjectionImpl +import org.jetbrains.kotlin.library.abi.impl.TypeParameterReferenceImpl +import org.jetbrains.kotlin.library.abi.impl.TypeProjectionImpl + +// This file contains Cursor methods specific to parsing klib dump files + +internal fun Cursor.parseAbiModality(): AbiModality? { + val parsed = parseAbiModalityString(peek = true)?.let { AbiModality.valueOf(it) } + if (parsed != null) { + parseAbiModalityString() + } + return parsed +} + +internal fun Cursor.parseClassKind(peek: Boolean = false): AbiClassKind? { + val parsed = parseClassKindString(peek = true)?.let { AbiClassKind.valueOf(it) } + if (parsed != null && !peek) { + parseClassKindString() + } + return parsed +} + +internal fun Cursor.parsePropertyKind(peek: Boolean = false): AbiPropertyKind? { + val parsed = parsePropertyKindString(peek = true)?.let { AbiPropertyKind.valueOf(it) } + if (parsed != null && !peek) { + parsePropertyKindString() + } + return parsed +} + +internal fun Cursor.hasClassKind(): Boolean { + val subCursor = copy() + subCursor.skipInlineWhitespace() + subCursor.parseAbiModality() + subCursor.parseClassModifiers() + return subCursor.parseClassKind() != null +} + +internal fun Cursor.hasFunctionKind(): Boolean { + val subCursor = copy() + subCursor.skipInlineWhitespace() + subCursor.parseAbiModality() + subCursor.parseFunctionModifiers() + return subCursor.parseFunctionKind() != null +} + +internal fun Cursor.hasPropertyKind(): Boolean { + val subCursor = copy() + subCursor.skipInlineWhitespace() + subCursor.parseAbiModality() + return subCursor.parsePropertyKind() != null +} + +internal fun Cursor.hasEnumEntry(): Boolean = parseEnumEntryKind(peek = true) != null + +internal fun Cursor.hasGetter() = hasPropertyAccessor(GetterOrSetter.GETTER) + +internal fun Cursor.hasSetter() = hasPropertyAccessor(GetterOrSetter.SETTER) + +internal fun Cursor.hasGetterOrSetter() = hasGetter() || hasSetter() + +internal fun Cursor.parseGetterName(peek: Boolean = false): String? { + val cursor = subCursor(peek) + cursor.parseContextParams() + cursor.parseFunctionReceiver() + cursor.parseSymbol(getterNameRegex) ?: return null + val name = cursor.parseValidIdentifier() ?: return null + cursor.parseSymbol(closeAngleBracketRegex) ?: return null + return "" +} + +internal fun Cursor.parseSetterName(peek: Boolean = false): String? { + val cursor = subCursor(peek) + cursor.parseContextParams() + cursor.parseFunctionReceiver() + cursor.parseSymbol(setterNameRegex) ?: return null + val name = cursor.parseValidIdentifier() ?: return null + cursor.parseSymbol(closeAngleBracketRegex) ?: return null + return "" +} + +internal fun Cursor.parseGetterOrSetterName(peek: Boolean = false) = + parseGetterName(peek) ?: parseSetterName(peek) + +internal fun Cursor.parseClassModifier(peek: Boolean = false): String? = + parseSymbol(classModifierRegex, peek) + +internal fun Cursor.parseClassModifiers(): Set { + val modifiers = mutableSetOf() + while (parseClassModifier(peek = true) != null) { + modifiers.add(parseClassModifier()!!) + } + return modifiers +} + +internal fun Cursor.parseFunctionKind(peek: Boolean = false) = parseSymbol(functionKindRegex, peek) + +internal fun Cursor.parseFunctionModifier(peek: Boolean = false): String? = + parseSymbol(functionModifierRegex, peek) + +internal fun Cursor.parseFunctionModifiers(): Set { + val modifiers = mutableSetOf() + while (parseFunctionModifier(peek = true) != null) { + modifiers.add(parseFunctionModifier()!!) + } + return modifiers +} + +internal fun Cursor.parseConstructorName() = parseSymbol(constructorNameRegex) + +// Valid identifiers can appear in a lot of places, some of them are followed by spaces, +// for example at the end of a class name ('class libname.Foo {'). But not at the end of a function +// name ('libname.foo()'). So we trim the whitespace only when we know it was inserted by the dump +// format and is not part of the identifier itself. +internal fun Cursor.parseValidIdentifierAndMaybeTrim(peek: Boolean = false) = + parseValidIdentifier(peek)?.let { + if (parseSymbol(symbolsFollowingIdentifiersWithSpaces, peek = true) != null) { + it.dropLast(1) + } else { + it + } + } + +internal fun Cursor.parseAbiQualifiedName(peek: Boolean = false): AbiQualifiedName? { + val cursor = subCursor(peek) + val packageName = cursor.parsePackageName() ?: "" + cursor.parseSymbol(slashRegex) ?: return null + val relativeNameBuilder = StringBuilder() + while (cursor.hasNextValidIdentifierPiece()) { + cursor.parseSymbol(dotRegex)?.let { relativeNameBuilder.append(it) } + relativeNameBuilder.append(cursor.parseValidIdentifierAndMaybeTrim()) + } + val relativeName = + relativeNameBuilder.toString().ifEmpty { + return null + } + return AbiQualifiedName(AbiCompoundName(packageName), AbiCompoundName(relativeName)) +} + +private fun Cursor.hasNextValidIdentifierPiece(): Boolean { + val cursor = subCursor(peek = true) + cursor.parseSymbol(dotRegex) + return cursor.parseValidIdentifier(peek = true) != null +} + +internal fun Cursor.parsePackageName() = parseSymbol(packageNameRegex) + +internal fun Cursor.parseAbiType(peek: Boolean = false): AbiType? { + val cursor = subCursor(peek) + // A type will either be a qualified name (kotlin/Array) or a type reference (#A) + // try to parse a qualified name and a type reference if it doesn't exist + val abiQualifiedName = cursor.parseAbiQualifiedName() ?: return cursor.parseTypeReference() + val typeArgs = cursor.parseTypeArgs() ?: emptyList() + val nullability = cursor.parseNullability(assumeNotNull = true) + return SimpleTypeImpl( + ClassReferenceImpl(abiQualifiedName), + arguments = typeArgs, + nullability = nullability, + ) +} + +internal fun Cursor.parseTypeArgs(): List? { + val typeArgsString = parseTypeParamsString() ?: return null + val subCursor = Cursor(typeArgsString) + subCursor.parseSymbol(openAngleBracketRegex) ?: return null + val typeArgs = mutableListOf() + while (subCursor.parseTypeArg(peek = true) != null) { + typeArgs.add(subCursor.parseTypeArg()!!) + subCursor.parseSymbol(commaRegex) + } + return typeArgs +} + +internal fun Cursor.parseTypeArg(peek: Boolean = false): AbiTypeArgument? { + val cursor = subCursor(peek) + val variance = cursor.parseAbiVariance() + cursor.parseSymbol(starProjectionRegex)?.let { + return StarProjectionImpl + } + val type = cursor.parseAbiType(peek) ?: return null + return TypeProjectionImpl(type = type, variance = variance) +} + +internal fun Cursor.parseAbiVariance(): AbiVariance { + val variance = parseSymbol(abiVarianceRegex) ?: return AbiVariance.INVARIANT + return AbiVariance.valueOf(variance.uppercase()) +} + +internal fun Cursor.parseTypeReference(): AbiType? { + val typeParamReference = parseTag() ?: return null + val typeArgs = parseTypeArgs() ?: emptyList() + val nullability = parseNullability() + return SimpleTypeImpl( + TypeParameterReferenceImpl(typeParamReference), + arguments = typeArgs, + nullability = nullability, + ) +} + +internal fun Cursor.parseTag() = parseSymbol(tagRegex)?.removePrefix("#") + +internal fun Cursor.parseNullability(assumeNotNull: Boolean = false): AbiTypeNullability { + val nullable = parseSymbol(nullableSymbolRegex) != null + val definitelyNotNull = parseSymbol(notNullSymbolRegex) != null + return when { + nullable -> AbiTypeNullability.MARKED_NULLABLE + definitelyNotNull -> AbiTypeNullability.DEFINITELY_NOT_NULL + else -> + if (assumeNotNull) { + AbiTypeNullability.DEFINITELY_NOT_NULL + } else { + AbiTypeNullability.NOT_SPECIFIED + } + } +} + +internal fun Cursor.parseSuperTypes(): MutableSet { + parseSymbol(colonRegex) + val superTypes = mutableSetOf() + while (parseAbiQualifiedName(peek = true) != null) { + superTypes.add(parseAbiType()!!) + parseSymbol(commaRegex) + } + return superTypes +} + +internal fun Cursor.parseTypeParams(peek: Boolean = false): List? { + val typeParamsString = parseTypeParamsString(peek) ?: return null + val subCursor = Cursor(typeParamsString) + subCursor.parseSymbol(openAngleBracketRegex) + val typeParams = mutableListOf() + while (subCursor.parseTypeParam(peek = true) != null) { + typeParams.add(subCursor.parseTypeParam()!!) + subCursor.parseSymbol(commaRegex) + } + return typeParams +} + +internal fun Cursor.parseTypeParam(peek: Boolean = false): AbiTypeParameter? { + val cursor = subCursor(peek) + val tag = cursor.parseTag() ?: return null + cursor.parseSymbol(colonRegex) + val variance = cursor.parseAbiVariance() + val isReified = cursor.parseSymbol(reifiedRegex) != null + val upperBounds = mutableListOf() + while (null != cursor.parseAbiType(peek = true)) { + upperBounds.add(cursor.parseAbiType()!!) + cursor.parseSymbol(ampersandRegex) + } + + return AbiTypeParameterImpl( + tag = tag, + variance = variance, + isReified = isReified, + upperBounds = upperBounds, + ) +} + +internal fun Cursor.parseValueParameters( + kind: AbiValueParameterKind = AbiValueParameterKind.REGULAR +): List? { + val valueParams = mutableListOf() + parseSymbol(openParenRegex) + while (null != parseValueParameter(kind, peek = true)) { + valueParams.add(parseValueParameter(kind)!!) + parseSymbol(commaRegex) + } + parseSymbol(closeParenRegex) + return valueParams +} + +internal fun Cursor.parseValueParameter( + kind: AbiValueParameterKind = AbiValueParameterKind.REGULAR, + peek: Boolean = false, +): AbiValueParameter? { + val cursor = subCursor(peek) + val modifiers = cursor.parseValueParameterModifiers() + val isNoInline = modifiers.contains("noinline") + val isCrossinline = modifiers.contains("crossinline") + val type = cursor.parseAbiType() ?: return null + val isVararg = cursor.parseVarargSymbol() != null + val hasDefaultArg = cursor.parseDefaultArg() != null + return AbiValueParameterImpl( + kind = kind, + type = type, + isVararg = isVararg, + hasDefaultArg = hasDefaultArg, + isNoinline = isNoInline, + isCrossinline = isCrossinline, + ) +} + +internal fun Cursor.parseValueParameterModifiers(): Set { + val modifiers = mutableSetOf() + while (parseValueParameterModifier(peek = true) != null) { + modifiers.add(parseValueParameterModifier()!!) + } + return modifiers +} + +internal fun Cursor.parseValueParameterModifier(peek: Boolean = false): String? = + parseSymbol(valueParameterModifierRegex, peek) + +internal fun Cursor.parseVarargSymbol() = parseSymbol(varargSymbolRegex) + +internal fun Cursor.parseDefaultArg() = parseSymbol(defaultArgSymbolRegex) + +internal fun Cursor.parseFunctionReceiver(): AbiType? { + parseSymbol(openParenRegex) ?: return null + val type = parseAbiType() + parseSymbol(closeParenRegex) + parseSymbol(dotRegex) + return type +} + +internal fun Cursor.parseContextParams(): List? { + parseSymbol(contextRegex) ?: return null + val params = parseValueParameters(AbiValueParameterKind.CONTEXT) + parseSymbol(closeParenRegex) + return params +} + +internal fun Cursor.parseReturnType(): AbiType? { + parseSymbol(colonRegex) + return parseAbiType() +} + +internal fun Cursor.hasUniqueName(): Boolean = + parseSymbol(uniqueNameMarkerRegex, peek = true) != null + +internal fun Cursor.parseUniqueName(): String? { + parseSymbol(uniqueNameMarkerRegex) + parseSymbol(openAngleBracketRegex) + return parseSymbol(uniqueNameRegex) +} + +internal fun Cursor.hasSignatureVersion(): Boolean = + parseSymbol(signatureMarkerRegex, peek = true) != null + +internal fun Cursor.parseSignatureVersion(): AbiSignatureVersion? { + parseSymbol(signatureMarkerRegex) + val versionString = parseSymbol(digitRegex) ?: return null + val versionNumber = versionString.toInt() + return AbiSignatureVersion.resolveByVersionNumber(versionNumber) +} + +internal fun Cursor.parseEnumEntryKind(peek: Boolean = false) = + parseSymbol(enumEntryKindRegex, peek) + +internal fun Cursor.parseEnumName() = parseSymbol(enumNameRegex) + +internal fun Cursor.parseCommentMarker() = parseSymbol(commentMarkerRegex) + +internal fun Cursor.parseOpenClassBody() = parseSymbol(openCurlyBraceRegex) + +internal fun Cursor.parseCloseClassBody(peek: Boolean = false) = + parseSymbol(closeCurlyBraceRegex, peek) + +/** + * Used to check if declarations after a property are getter / setter methods which should be + * attached to that property. + */ +private fun Cursor.hasPropertyAccessor(type: GetterOrSetter): Boolean { + val subCursor = copy() + subCursor.parseAbiModality() + subCursor.parseFunctionModifiers() + subCursor.parseFunctionKind() ?: return false // if it's not a function it's not a getter/setter + val mightHaveTypeParams = subCursor.parseGetterOrSetterName(peek = true) == null + if (mightHaveTypeParams) { + subCursor.parseTypeParams() + } + subCursor.parseContextParams() + subCursor.parseFunctionReceiver() + return when (type) { + GetterOrSetter.GETTER -> subCursor.parseGetterName() != null + GetterOrSetter.SETTER -> subCursor.parseSetterName() != null + } +} + +private fun Cursor.subCursor(peek: Boolean) = + if (peek) { + copy() + } else { + this + } + +private fun Cursor.parseTypeParamsString(peek: Boolean = false): String? { + if (parseSymbol(getterOrSetterSignalRegex, peek = true) != null) { + return null + } + val cursor = subCursor(peek) + val result = StringBuilder() + cursor.parseSymbol(openAngleBracketRegex)?.let { result.append(it) } ?: return null + var openBracketCount = 1 + while (openBracketCount > 0) { + val nextSymbol = + cursor.parseSymbol(anyCharRegex, skipInlineWhitespace = false).also { + result.append(it) + } + when (nextSymbol) { + "<" -> openBracketCount++ + ">" -> openBracketCount-- + } + } + cursor.skipInlineWhitespace() + return result.toString() +} + +private fun Cursor.parseAbiModalityString(peek: Boolean = false) = + parseSymbol(abiModalityRegex, peek)?.uppercase() + +private fun Cursor.parsePropertyKindString(peek: Boolean = false) = + parseSymbol(propertyKindRegex, peek)?.uppercase()?.replace(" ", "_") + +private fun Cursor.parseClassKindString(peek: Boolean = false) = + parseSymbol(classKindRegex, peek)?.uppercase()?.replace(" ", "_") + +private enum class GetterOrSetter() { + GETTER, + SETTER, +} + +private val packageNameRegex = Regex("^[a-zA-Z0-9.]+") +private val constructorNameRegex = Regex("^constructor\\s") +private val closeCurlyBraceRegex = Regex("^}") +private val uniqueNameMarkerRegex = Regex("^Library unique name: ") +private val uniqueNameRegex = Regex("[a-zA-Z\\-\\.:]+") +private val commentMarkerRegex = Regex("^\\/\\/") +private val anyCharRegex = Regex(".") +private val closeSquareBracketRegex = Regex("^\\]") +private val openSquareBracketRegex = Regex("^\\[") +private val defaultArgSymbolRegex = Regex("^=(\\s)?\\.\\.\\.") +private val varargSymbolRegex = Regex("^\\.\\.\\.") +private val openParenRegex = Regex("^\\(") +private val closeParenRegex = Regex("^\\)") +private val reifiedRegex = Regex("reified") +private val contextRegex = Regex("^\\(?context") +private val colonRegex = Regex("^:") +private val commaRegex = Regex("^,") +private val notNullSymbolRegex = Regex("^\\!\\!") +private val nullableSymbolRegex = Regex("^\\?") +private val tagRegex = Regex("^#[a-zA-Z0-9]+") +private val getterNameRegex = Regex("^") +private val openCurlyBraceRegex = Regex("^\\{") +private val starProjectionRegex = Regex("^\\*") +private val abiVarianceRegex = Regex("^(out|in)") +private val valueParameterModifierRegex = Regex("^(crossinline|noinline)") +private val abiModalityRegex = Regex("^(final|open|abstract|sealed)") +private val classKindRegex = Regex("^(class|interface|object|enum\\sclass|annotation\\sclass)") +private val propertyKindRegex = Regex("^(const\\sval|val|var)") +private val getterOrSetterSignalRegex = Regex("^<(get|set)\\-") +private val enumNameRegex = Regex("^[A-Z_]+") +private val enumEntryKindRegex = Regex("^enum\\sentry") +private val signatureMarkerRegex = Regex("-\\sSignature\\sversion:") +private val digitRegex = Regex("^\\d+") +private val dotRegex = Regex("^\\.") +private val slashRegex = Regex("^/") +private val symbolsFollowingIdentifiersWithSpaces = Regex("^[:|/={&]") +private val ampersandRegex = Regex("^&") diff --git a/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/AbiExtensions.kt b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/AbiExtensions.kt new file mode 100644 index 0000000000000..0e4338960c0f6 --- /dev/null +++ b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/AbiExtensions.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalLibraryAbiReader::class) + +package org.jetbrains.kotlin.library.abi.parser + +import org.jetbrains.kotlin.library.abi.AbiClassifierReference +import org.jetbrains.kotlin.library.abi.AbiQualifiedName +import org.jetbrains.kotlin.library.abi.AbiType +import org.jetbrains.kotlin.library.abi.AbiTypeArgument +import org.jetbrains.kotlin.library.abi.AbiTypeNullability +import org.jetbrains.kotlin.library.abi.AbiVariance +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader + +// Convenience extensions for accessing properties that may exist without have to cast repeatedly +// For sources with documentation see +// https://github.com/JetBrains/kotlin/blob/master/compiler/util-klib-abi/src/org/jetbrains/kotlin/library/abi/LibraryAbi.kt + +/** A classifier reference is either a simple class or a type reference */ +internal val AbiType.classifierReference: AbiClassifierReference? + get() = (this as? AbiType.Simple)?.classifierReference +/** The class name from a regular type e.g. 'Array' */ +internal val AbiType.className: AbiQualifiedName? + get() = classifierReference?.className +/** A tag from a type type parameter reference e.g. 'T' */ +internal val AbiType.tag: String? + get() = classifierReference?.tag +/** The string representation of a type, whether it is a simple type or a type reference */ +internal val AbiType.classNameOrTag: String? + get() = className?.toString() ?: tag +internal val AbiType.nullability: AbiTypeNullability? + get() = (this as? AbiType.Simple)?.nullability +internal val AbiType.arguments: List? + get() = (this as? AbiType.Simple)?.arguments +internal val AbiTypeArgument.type: AbiType? + get() = (this as? AbiTypeArgument.TypeProjection)?.type +internal val AbiTypeArgument.variance: AbiVariance? + get() = (this as? AbiTypeArgument.TypeProjection)?.variance +internal val AbiClassifierReference.className: AbiQualifiedName? + get() = (this as? AbiClassifierReference.ClassReference)?.className +internal val AbiClassifierReference.tag: String? + get() = (this as? AbiClassifierReference.TypeParameterReference)?.tag diff --git a/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/CursorTest.kt b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/CursorTest.kt new file mode 100644 index 0000000000000..ae71ecc91a7a0 --- /dev/null +++ b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/CursorTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2024 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 org.jetbrains.kotlin.library.abi.parser + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +class CursorTest { + + @Test + fun cursorShowsCurrentLine() { + val input = "one\ntwo\nthree" + val cursor = Cursor(input) + assertEquals("one", cursor.currentLine) + cursor.nextLine() + assertEquals("two", cursor.currentLine) + cursor.nextLine() + assertEquals("three", cursor.currentLine) + cursor.nextLine() + assertTrue(cursor.isFinished()) + assertTrue(cursor.isFinished()) + } + + @Test + fun cursorGetsNextWord() { + val input = "one two three" + val cursor = Cursor(input) + val word = cursor.parseWord() + assertEquals("one", word) + assertEquals(cursor.currentLine, "two three") + } + + @Test + fun parseIdentifiersThatArePossibleByEscaping() { + val validEscapedIdentifiers = listOf("123", "🙂", "identifiers can include spaces") + for (id in validEscapedIdentifiers) { + assertEquals(id, Cursor(id).parseValidIdentifier()) + } + } + + @Test + fun parseValidIdentifierDoesNotIncludeIllegalCharacters() { + val identifiersIncludingIllegalChar = + listOf( + "identifiers can't include :", + "identifiers can't include \\", + "identifiers can't include /", + "identifiers can't include ;", + "identifiers can't include (", + "identifiers can't include )", + "identifiers can't include <", + "identifiers can't include >", + "identifiers can't include [", + "identifiers can't include ]", + "identifiers can't include {", + "identifiers can't include }", + "identifiers can't include ?", + "identifiers can't include ,", + ) + for (id in identifiersIncludingIllegalChar) { + val cursor = Cursor(id) + assertEquals("identifiers can't include ", cursor.parseValidIdentifier()) + assertEquals(id.last().toString(), cursor.currentLine) + } + } + + @Test + fun parseIdentifierThatEndsWithASpaceAtEndOfLine() { + val cursor = Cursor("identifiers can include spaces at the end //") + val id = cursor.parseValidIdentifier() + assertEquals("identifiers can include spaces at the end ", id) + assertEquals("//", cursor.currentLine) + } + + @Test + fun parseIdentifierThatEndsWithASpaceAtEndOfFunctionName() { + val cursor = Cursor("identifiers can include spaces at the end ()") + val id = cursor.parseValidIdentifier() + assertEquals("identifiers can include spaces at the end ", id) + assertEquals("()", cursor.currentLine) + } + + @Test + fun skipWhitespace() { + val input = " test" + val cursor = Cursor(input) + cursor.skipInlineWhitespace() + assertEquals("test", cursor.currentLine) + } + + @Test + fun skipWhitespaceOnBlankLine() { + val input = "" + val cursor = Cursor(input) + cursor.skipInlineWhitespace() + assertEquals("", cursor.currentLine) + } + + @Test + fun skipWhitespaceSkipsEntireLine() { + val input = " " + val cursor = Cursor(input) + cursor.skipInlineWhitespace() + assertEquals("", cursor.currentLine) + } +} diff --git a/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/KLibDumpParserTest.kt b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/KLibDumpParserTest.kt new file mode 100644 index 0000000000000..1217ef1091d27 --- /dev/null +++ b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/KLibDumpParserTest.kt @@ -0,0 +1,624 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalLibraryAbiReader::class) + +package org.jetbrains.kotlin.library.abi.parser + +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertNotNull +import kotlin.test.assertFailsWith +import org.jetbrains.kotlin.library.abi.AbiClass +import org.jetbrains.kotlin.library.abi.AbiClassKind +import org.jetbrains.kotlin.library.abi.AbiCompoundName +import org.jetbrains.kotlin.library.abi.AbiEnumEntry +import org.jetbrains.kotlin.library.abi.AbiFunction +import org.jetbrains.kotlin.library.abi.AbiModality +import org.jetbrains.kotlin.library.abi.AbiProperty +import org.jetbrains.kotlin.library.abi.AbiQualifiedName +import org.jetbrains.kotlin.library.abi.AbiSignatureVersion +import org.jetbrains.kotlin.library.abi.AbiTypeArgument +import org.jetbrains.kotlin.library.abi.AbiValueParameter +import org.jetbrains.kotlin.library.abi.AbiValueParameterKind +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader +import org.jetbrains.kotlin.library.abi.LibraryAbi +import org.junit.Ignore +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.assertThrows +import java.text.ParseException +import kotlin.toString + +class KlibDumpParserTest { + + @Test + fun parseASimpleClass() { + val input = + "final class my.lib/Child : my.lib/Parent" + val parsed = KlibDumpParser(input).parseClass() + assertNotNull(parsed) + + assertEquals("my.lib/Child", parsed.qualifiedName.toString()) + } + + @Test + fun parseAClassWithTwoSuperTypes() { + val input = + "final class my.lib/ArraySet : kotlin.collections/MutableCollection<#A>, kotlin.collections/MutableSet<#A>" + val parsed = KlibDumpParser(input).parseClass() + assertNotNull(parsed) + + assertEquals("my.lib/ArraySet", parsed.qualifiedName.toString()) + assertEquals(2, parsed.superTypes.size) + } + + @Test + fun parseAClassWithTypeParams() { + val input = + "final class <#A: kotlin/Any?, #B: kotlin/Any?> my.lib/Child : my.lib/Parent<#A, #B>" + val parsed = KlibDumpParser(input).parseClass() + assertNotNull(parsed) + + assertEquals("my.lib/Child", parsed.qualifiedName.toString()) + assertEquals(2, parsed.typeParameters.size) + parsed.typeParameters.forEach { + assertEquals("kotlin/Any", it.upperBounds.single().className?.toString()) + } + assertEquals("A", parsed.typeParameters.first().tag) + assertEquals("B", parsed.typeParameters.last().tag) + } + + @Test + fun parseAClassWithATypeArg() { + val input = "final class my.lib/MySubClass : my.lib/MyClass" + val parsed = KlibDumpParser(input).parseClass() + + assertEquals(parsed.typeParameters.size, 0) + assertEquals(1, parsed.superTypes.size) + val superType = parsed.superTypes.single() + assertEquals("kotlin/Int", superType.arguments?.single()?.type?.classNameOrTag) + } + + @Test + fun parseAClassBWithProperties() { + val input = + """ + abstract interface example.lib/MutablePoint { // example.lib/MutablePoint|null[0] + abstract var x // example.lib/MutablePoint.x|{}x[0] + abstract fun (): kotlin/Float // example.lib/MutablePoint.x.|(){}[0] + abstract fun (kotlin/Float) // example.lib/MutablePoint.x.|(kotlin.Float){}[0] + abstract var y // example.lib/MutablePoint.y|{}y[0] + abstract fun (): kotlin/Float // example.lib/MutablePoint.y.|(){}[0] + abstract fun (kotlin/Float) // example.lib/MutablePoint.y.|(kotlin.Float){}[0] + } + """ + .trimIndent() + val parsed = KlibDumpParser(input).parseClass() + assertEquals(2, parsed.declarations.filterIsInstance().size) + } + + @Test + fun parseAnAnnotationClass() { + val input = "open annotation class my.lib/MyClass : kotlin/Annotation" + val parsed = KlibDumpParser(input).parseClass() + assertNotNull(parsed) + + assertEquals("my.lib/MyClass", parsed.qualifiedName.toString()) + assertEquals(AbiClassKind.ANNOTATION_CLASS, parsed.kind) + } + + @Test + fun parseASerializerClass() { + val input = + """ + final object ${'$'}serializer : kotlinx.serialization.internal/GeneratedSerializer { // example.lib/MyClass.${'$'}serializer|null[0] + final val descriptor // example.lib/MyClass.${'$'}serializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // example.lib/MyClass.${'$'}serializer.descriptor.|(){}[0] + + final fun childSerializers(): kotlin/Array> // example.lib/MyClass.${'$'}serializer.childSerializers|childSerializers(){}[0] + final fun deserialize(kotlinx.serialization.encoding/Decoder): example.lib/MyClass // example.lib/MyClass.${'$'}serializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, example.lib/MyClass) // example.lib/MyClass.${'$'}serializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;example.lib.MyClass){}[0] + } + """ + .trimIndent() + val parsed = + KlibDumpParser(input) + .parseClass( + AbiQualifiedName( + AbiCompoundName("example.lib"), + AbiCompoundName("MyClass"), + ) + ) + assertNotNull(parsed) + assertEquals("example.lib/MyClass.\$serializer", parsed.qualifiedName.toString()) + } + + @Test + fun parseASimpleFunction() { + val input = + "final fun myFun(kotlin/Int): kotlin/String" + val parentQName = + AbiQualifiedName(AbiCompoundName("my.lib"), AbiCompoundName("MyClass")) + val parsed = KlibDumpParser(input).parseFunction(parentQName) + assertNotNull(parsed) + + assertEquals("my.lib/MyClass.myFun", parsed.qualifiedName.toString()) + assertEquals("kotlin/String", parsed.returnType!!.classNameOrTag) + assertEquals(1, parsed.valueParameters.size) + assertEquals("kotlin/Int", parsed.valueParameters.single().type.classNameOrTag) + } + + @Test + fun parseAFunction() { + val input = + "final inline fun <#A1: kotlin/Any?> myFun(#A1, kotlin/Function<#A1, #A, #A1>): #A1" + val parentQName = + AbiQualifiedName(AbiCompoundName("my.lib"), AbiCompoundName("MyClass")) + val parsed = KlibDumpParser(input).parseFunction(parentQName) + assertNotNull(parsed) + + assertEquals("my.lib/MyClass.myFun", parsed.qualifiedName.toString()) + + assertEquals(1, parsed.typeParameters.size) + assertEquals("A1", parsed.typeParameters.single().tag) + + assertEquals("A1", parsed.returnType!!.classNameOrTag) + + assertEquals(2, parsed.valueParameters.size) + assertEquals("A1", parsed.valueParameters.first().type.classNameOrTag) + assertEquals("kotlin/Function", parsed.valueParameters.last().type.classNameOrTag) + } + + @Test + fun parseAFunctionWithTypeArgsOnParams() { + val input = + "final fun <#A: kotlin/Any?> my.lib/arraySetOf(kotlin/Array...): my.lib/ArraySet<#A>" + val parsed = KlibDumpParser(input).parseFunction() + assertNotNull(parsed) + + assertEquals("my.lib/arraySetOf", parsed.qualifiedName.toString()) + val param = parsed.valueParameters.single() + assertNotEquals(param.type.arguments?.size, 0) + } + + @Test + fun parseAFunctionWithQualifiedReceiver() { + val input = + "final fun <#A: kotlin/Any> (my.lib/MyClass.Builder.Inner).my.lib/myFunWithReceiver(my.lib/Other? = ..., kotlin/Function1): #A" + val parsed = KlibDumpParser(input).parseFunction() + assertNotNull(parsed) + + assertEquals("my.lib/myFunWithReceiver", parsed.qualifiedName.toString()) + assertTrue(parsed.hasExtensionReceiverParameter()) + } + + @Test + fun parseAFunctionWithSingleContextValue() { + val input = "final fun context(kotlin/Int) my.lib/bar()" + val parsed = KlibDumpParser(input).parseFunction() + assertNotNull(parsed) + + assertEquals(1, parsed.contextReceiverParametersCount()) + assertEquals("kotlin/Int", parsed.valueParameters.first().type.className.toString()) + } + + @Test + fun parseAFunctionWithSingleContextValueWithNewFormatting() { + val input = "final fun (context(kotlin/Int)).my.lib/bar()" + val parsed = KlibDumpParser(input).parseFunction() + assertNotNull(parsed) + + assertEquals(1, parsed.contextReceiverParametersCount()) + assertEquals("kotlin/Int", parsed.valueParameters.first().type.className.toString()) + } + + @Test + fun parseAFunctionWithMultipleContextValuesAndAReceiver() { + val input = + "final fun context(kotlin/Int, kotlin/String) (kotlin/Int).my.lib/bar(kotlin/Double)" + val parsed = KlibDumpParser(input).parseFunction() + assertNotNull(parsed) + + assertEquals(2, parsed.contextReceiverParametersCount()) + assertTrue(parsed.hasExtensionReceiverParameter()) + assertEquals(listOf("kotlin/Int", "kotlin/String", "kotlin/Int", "kotlin/Double"), parsed.valueParameters.map { it.type.className.toString() }) + } + + @Test + fun parseAGetterFunction() { + val input = "final inline fun (): kotlin.ranges/IntRange" + val parentQName = + AbiQualifiedName(AbiCompoundName("my.lib"), AbiCompoundName("ObjectList")) + val parsed = KlibDumpParser(input).parseFunction(parentQName, isGetterOrSetter = true) + assertEquals("my.lib/ObjectList.", parsed.qualifiedName.toString()) + } + + @Test + fun parseAGetterFunctionWithReceiver() { + val input = + "final inline fun <#A1: kotlin/Any?> (my.lib/MyCollection<#A1>).(): kotlin/Int" + val parentQName = + AbiQualifiedName(AbiCompoundName("my.lib"), AbiCompoundName("MyClass")) + val parsed = KlibDumpParser(input).parseFunction(parentQName, isGetterOrSetter = true) + assertEquals("my.lib/MyClass.", parsed.qualifiedName.toString()) + assertEquals("kotlin/Int", parsed.returnType!!.classNameOrTag) + val receiver = parsed.extensionReceiverParameter() + assertNotNull(receiver) + assertEquals("my.lib/MyCollection", receiver!!.type.classNameOrTag) + } + + @Test + fun parseAFunctionWithTypeArgAsReceiver() { + val input = + "final inline fun <#A: example.lib/Closeable, #B: kotlin/Any?> (#A).example.lib/use(kotlin/Function1<#A, #B>): #B" + val parsed = KlibDumpParser(input).parseFunction() + + assertEquals(2, parsed.typeParameters.size) + val receiver = parsed.extensionReceiverParameter() + assertNotNull(receiver) + assertEquals("A", receiver!!.type.classNameOrTag) + } + + @Test + fun parseAComplexFunction() { + val input = + "final inline fun <#A: kotlin/Any, #B: kotlin/Any> my.lib/myFun(kotlin/Int, crossinline kotlin/Function2<#A, #B, kotlin/Int> =..., crossinline kotlin/Function1<#A, #B?> =..., crossinline kotlin/Function4 =...): my.lib/LruCache<#A, #B>" + val parsed = KlibDumpParser(input).parseFunction() + assertEquals(AbiModality.FINAL, parsed.modality) + assertEquals(2, parsed.typeParameters.size) + assertEquals("my.lib/myFun", parsed.qualifiedName.toString()) + assertEquals(4, parsed.valueParameters.size) + } + + @Test + fun parseAComplexFunctionWithK2Formatting() { + val input = + "final inline fun <#A: kotlin/Any, #B: kotlin/Any> my.lib/lruCache(kotlin/Int, crossinline kotlin/Function2<#A, #B, kotlin/Int> = ..., crossinline kotlin/Function1<#A, #B?> = ..., crossinline kotlin/Function4 = ...): my.lib/LruCache<#A, #B>" + val parsed = KlibDumpParser(input).parseFunction() + assertEquals(AbiModality.FINAL, parsed.modality) + assertEquals(2, parsed.typeParameters.size) + assertEquals("my.lib/lruCache", parsed.qualifiedName.toString()) + assertEquals(4, parsed.valueParameters.size) + } + + @Test + fun parseAPropertyWithTheWordContextInIt() { + val input = + """ + final val my.lib/MyProp + final fun (): my.other.lib/SomethingElse + """ + .trimIndent() + val parsed = KlibDumpParser(input).parseProperty() + assertEquals( + "my.lib/MyProp" + , parsed.qualifiedName.toString()) + } + + @Test + fun parseANestedValProperty() { + val input = """ + final val size + final fun (): kotlin/Int + """.trimIndent() + val parsed = + KlibDumpParser(input) + .parseProperty( + AbiQualifiedName( + AbiCompoundName("my.lib"), + AbiCompoundName("ScatterMap"), + ) + ) + assertNotNull(parsed.getter) + assertNull(parsed.setter) + } + + @Test + fun parseAVarProperty() { + val input = """ + final var examples + final fun (): kotlin/Array + final fun (kotlin/Array) + """.trimIndent() + val parsed = + KlibDumpParser(input) + .parseProperty( + AbiQualifiedName( + AbiCompoundName("my.lib"), + AbiCompoundName("MyClass"), + ) + ) + assertEquals("my.lib/MyClass.examples", parsed.qualifiedName.toString()) + assertEquals("my.lib/MyClass.examples.", parsed.getter!!.qualifiedName.toString()) + assertEquals("my.lib/MyClass.examples.", parsed.setter!!.qualifiedName.toString()) + assertEquals("kotlin/Array", parsed.getter!!.returnType!!.classNameOrTag) + assertEquals("kotlin/Array", parsed.setter!!.valueParameters.single().type.classNameOrTag) + } + + @Test + fun parseAPropertyWithStarParamsInReceiver() { + val input = + """ + final val my.lib/isFinished + final fun (my.lib/Something<*, *>).(): kotlin/Boolean + """ + .trimIndent() + val parsed = KlibDumpParser(input).parseProperty() + assertNotNull(parsed.getter) + assertTrue( + parsed.getter?.valueParameters?.any { + it.kind == AbiValueParameterKind.EXTENSION_RECEIVER + }!! + ) + val receiver = parsed.getter!!.extensionReceiverParameter()!! + assertTrue(receiver.type.arguments!!.all { it is AbiTypeArgument.StarProjection }) + } + + @Test + fun parseAnEnumEntry() { + val input = "enum entry MY_ENUM" + val parsed = + KlibDumpParser(input) + .parseEnumEntry( + AbiQualifiedName( + AbiCompoundName("my.lib"), + AbiCompoundName("MyEnumClass"), + ) + ) + assertEquals("my.lib/MyEnumClass.MY_ENUM", parsed.qualifiedName.toString()) + } + + @Test + fun parseAnInvalidDeclaration() { + val input = + """ + final class my.lib/MyClass { + invalid + } + """ + .trimIndent() + val e = assertFailsWith { KlibDumpParser(input, "current.txt").parse() } + assertEquals("Failed to parse unknown declaration at current.txt:1:4: 'invalid'", e.message) + } + + @Test + fun parseSingleTopLevelDeclaration() { + val input = "$exampleMetadata\nfinal fun my.lib/foo(kotlin/Int, kotlin/Int): kotlin/Int" + val parsed = KlibDumpParser(input, "current.txt").parse() + assertEquals(1, parsed.topLevelDeclarations.declarations.size) + } + + @Test + fun parseAConstructorWithDefaultValue() { + val input = "constructor (kotlin/Int =..., kotlin/Int =...)" + val parsed = + KlibDumpParser(input, "current.txt") + .parseFunction( + parentQualifiedName = + AbiQualifiedName( + AbiCompoundName("my.lib"), + AbiCompoundName("MyClass"), + ) + ) + assertTrue(parsed.isConstructor) + assertIterableEquals(listOf("kotlin/Int", "kotlin/Int"), parsed.valueParameters.map { it.type.classNameOrTag }) + } + + @Test + fun parseAConstructorWithDefaultValueAlternate() { + val input = + "constructor (kotlin/Int = ...) // my.lib/MyClass.|(kotlin.Int){}[0]" + val parsed = + KlibDumpParser(input, "current.txt") + .parseFunction( + parentQualifiedName = + AbiQualifiedName( + AbiCompoundName("my.lib"), + AbiCompoundName("MyClass"), + ) + ) + assertTrue(parsed.isConstructor) + assertEquals("kotlin/Int", parsed.valueParameters.single().type.classNameOrTag) + } + + @Test + fun parseClassNameThatEndsWithASpace() { + val input = + """$exampleMetadata + open class my.lib/MyClass { // my.lib/MyClass |null[0] + constructor () // my.lib/MyClass .|(){}[0] + } + """ + .trimIndent() + val parsed = KlibDumpParser(input, "current.txt").parse() + val parsedClass = + parsed + .topLevelDeclarations + .declarations + .filterIsInstance() + .single() + assertEquals("my.lib/MyClass ", parsedClass.qualifiedName.toString()) + } + + @Test + fun parseAVeryAnnoyingClassName() { + val input = + """$exampleMetadata + final class my.lib/MyMaybeClass = = { // my.lib/MyMaybeClass = = |null[0] + constructor () // my.lib/MyMaybeClass = = .|(){}[0] + } + final fun my.lib/foo(my.lib/MyMaybeClass = = =...): kotlin/Int // my.lib/foo|foo(my.lib.MyMaybeClass = = ){}[0] + """ + .trimIndent() + val parsed = KlibDumpParser(input, "current.txt").parse() + val parsedFunc = + parsed + .topLevelDeclarations + .declarations + .filterIsInstance() + .single() + val parsedClass = + parsed + .topLevelDeclarations + .declarations + .filterIsInstance() + .single() + assertEquals("my.lib/MyMaybeClass = = ", parsedClass.qualifiedName.toString()) + assertEquals(1, parsedFunc.valueParameters.size) + assertEquals("my.lib/MyMaybeClass = = ", parsedFunc.valueParameters.single().type.classNameOrTag) + assertTrue(parsedFunc.valueParameters.single().hasDefaultArg) + } + + @Test + fun parsesSignatureVersionAndAssignsToDeclarations() { + val input = """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final fun my.lib/commonFun() // my.lib/commonFun|commonFun(){}[0] + """.trimIndent() + val parsed = KlibDumpParser(input).parse() + assertEquals(2, parsed.signatureVersions.single().versionNumber) + val commonFun = parsed.topLevelDeclarations.declarations.single() + assertEquals("v2", commonFun.signatures.get(AbiSignatureVersion.resolveByVersionNumber(2))) + } + + @Test + fun parseTopLevelDeclarationsFromRootPackage() { + val input = """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + final class /MyRootClass { // /MyRootClass|null[0] + constructor () // /MyRootClass.|(){}[0] + } + final fun /myRootFun(): kotlin/Int // /myRootFun|myRootFun(){}[0] + final val /myRootProp // /myRootProp|{}myRootProp[0] + final fun (): kotlin/Int // /myRootProp.|(){}[0] + """ + val parsed = KlibDumpParser(input).parse() + val myClass = parsed.topLevelDeclarations.declarations.filterIsInstance().single() + assertEquals("/MyRootClass", myClass.qualifiedName.toString()) + val myFun = parsed.topLevelDeclarations.declarations.filterIsInstance().single() + assertEquals("/myRootFun", myFun.qualifiedName.toString()) + val myProp = parsed.topLevelDeclarations.declarations.filterIsInstance().single() + assertEquals("/myRootProp", myProp.qualifiedName.toString()) + } + + @Test + fun parseAFullDumpWithVariousTypes() { + val input = """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final class <#A: kotlin/Any?> my.lib/MyClass { // my.lib/MyClass|null[0] + constructor () // my.lib/MyClass.|(){}[0] + final class InnerClass { // my.lib/MyClass.InnerClass|null[0] + constructor () // my.lib/MyClass.InnerClass.|(){}[0] + final fun <#A2: kotlin/Any?> innerFun(#A2): #A2 // my.lib/MyClass.InnerClass.innerFun|innerFun(0:0){0§}[0] + } + final fun <#A1: kotlin/Any?> myFun(#A1): #A1 // my.lib/MyClass.myFun|myFun(0:0){0§}[0] + final object Companion { // my.lib/MyClass.Companion|null[0] + final fun static(): kotlin/Int // my.lib/MyClass.Companion.static|static(){}[0] + final val example // my.lib/MyClass.Companion.example|{}example[0] + final fun (): kotlin/String // my.lib/MyClass.Companion.example.|(){}[0] + } + } + final enum class my.lib/MyEnum : kotlin/Enum { // my.lib/MyEnum|null[0] + enum entry ONE // my.lib/MyEnum.ONE|null[0] + enum entry TWO // my.lib/MyEnum.TWO|null[0] + final fun valueOf(kotlin/String): my.lib/MyEnum // my.lib/MyEnum.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // my.lib/MyEnum.values|values#static(){}[0] + final val entries // my.lib/MyEnum.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // my.lib/MyEnum.entries.|#static(){}[0] + } + final fun my.lib/topLevel(): kotlin/Int // my.lib/topLevel|topLevel(){}[0] + final object my.lib/MyObject // my.lib/MyObject|null[0] + final var my.lib/myProp // my.lib/myProp|{}myProp[0] + final fun (): kotlin/String // my.lib/myProp.|(){}[0] + final fun (kotlin/String) // my.lib/myProp.|(kotlin.String){}[0] + """.trimIndent() + val parsed = KlibDumpParser(input).parse() + val myClass = parsed.topLevelDeclarations.declarations.filterIsInstance().find { + it.qualifiedName.toString() == "my.lib/MyClass" + } + assertNotNull(myClass) + val myObject = parsed.topLevelDeclarations.declarations.filterIsInstance().find { + it.qualifiedName.toString() == "my.lib/MyObject" + } + + assertNotNull(myObject) + val myEnum = parsed.topLevelDeclarations.declarations.filterIsInstance().find { + it.qualifiedName.toString() == "my.lib/MyEnum" + } + assertNotNull(myEnum) + assertEquals(2, myEnum!!.declarations.filterIsInstance().size) + + val topLevelFun = parsed.topLevelDeclarations.declarations.filterIsInstance().single() + assertNotNull(topLevelFun) + val myProp = parsed.topLevelDeclarations.declarations.filterIsInstance().single() + assertNotNull(myProp) + + val innerClass = myClass!!.declarations.filterIsInstance().find { + it.qualifiedName.toString() == "my.lib/MyClass.InnerClass" + } + assertNotNull(innerClass) + + val companion = myClass.declarations.filterIsInstance().find { + it.qualifiedName.toString() == "my.lib/MyClass.Companion" + } + assertNotNull(companion) + + val innerFun = innerClass!!.declarations.filterIsInstance().filterNot { it.isConstructor }.single() + assertNotNull(innerFun) + + val companionFun = companion!!.declarations.filterIsInstance().single() + assertNotNull(companionFun) + val companionProp = companion.declarations.filterIsInstance().single() + assertNotNull(companionProp) + } + + companion object { + private val exampleMetadata = + """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + // Library unique name: + """ + .trimIndent() + } +} + +private fun AbiFunction.hasExtensionReceiverParameter(): Boolean = + valueParameters.any { it.kind == AbiValueParameterKind.EXTENSION_RECEIVER } + +private fun AbiFunction.contextReceiverParametersCount(): Int = + valueParameters.count { it.kind == AbiValueParameterKind.CONTEXT } + +private fun AbiFunction.extensionReceiverParameter(): AbiValueParameter? = + valueParameters.find { it.kind == AbiValueParameterKind.EXTENSION_RECEIVER } \ No newline at end of file diff --git a/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/KlibParsingCursorExtensionsTest.kt b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/KlibParsingCursorExtensionsTest.kt new file mode 100644 index 0000000000000..e190876bd92b7 --- /dev/null +++ b/compiler/util-klib-abi/test/org/jetbrains/kotlin/library/abi/parser/KlibParsingCursorExtensionsTest.kt @@ -0,0 +1,729 @@ +/* + * Copyright 2024 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. + */ + +@file:OptIn(ExperimentalLibraryAbiReader::class) + +package org.jetbrains.kotlin.library.abi.parser + + +import org.jetbrains.kotlin.library.abi.AbiClassKind +import org.jetbrains.kotlin.library.abi.AbiModality +import org.jetbrains.kotlin.library.abi.AbiPropertyKind +import org.jetbrains.kotlin.library.abi.AbiTypeArgument +import org.jetbrains.kotlin.library.abi.AbiTypeNullability +import org.jetbrains.kotlin.library.abi.AbiValueParameter +import org.jetbrains.kotlin.library.abi.AbiValueParameterKind +import org.jetbrains.kotlin.library.abi.AbiVariance +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertIterableEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue + +class KlibParsingCursorExtensionsTest { + + @Test + fun parseModalityFailure() { + val input = "something else" + val cursor = Cursor(input) + val modality = cursor.parseAbiModality() + assertNull(modality) + assertEquals("something else", cursor.currentLine) + } + + @Test + fun parseModalitySuccess() { + val input = "final whatever" + val cursor = Cursor(input) + val modality = cursor.parseAbiModality() + assertEquals(AbiModality.FINAL, modality) + assertEquals("whatever", cursor.currentLine) + } + + @Test + fun parseClassModifier() { + val input = "inner whatever" + val cursor = Cursor(input) + val modifier = cursor.parseClassModifier() + assertEquals("inner", modifier) + assertEquals("whatever", cursor.currentLine) + } + + @Test + fun parseClassModifiers() { + val input = "inner value fun whatever" + val cursor = Cursor(input) + val modifiers = cursor.parseClassModifiers() + assertIterableEquals(listOf("inner", "value", "fun"), modifiers) + assertEquals("whatever", cursor.currentLine) + } + + @Test + fun parseFunctionModifiers() { + val input = "final inline suspend fun myFun(): kotlin/Long" + val cursor = Cursor(input) + cursor.parseAbiModality() + val modifiers = cursor.parseFunctionModifiers() + assertIterableEquals(listOf("inline", "suspend"), modifiers) + assertEquals("fun myFun(): kotlin/Long", cursor.currentLine) + } + + @Test + fun parseClassKindSimple() { + val input = "class" + val cursor = Cursor(input) + val kind = cursor.parseClassKind() + assertEquals(AbiClassKind.CLASS, kind) + } + + @Test + fun parseClassKindFalsePositive() { + val input = "my.lib/notAClass" + val cursor = Cursor(input) + val kind = cursor.parseClassKind() + assertNull(kind) + } + + @Test + fun parseClassKindMultiPart() { + val input = "annotation class" + val cursor = Cursor(input) + val kind = cursor.parseClassKind() + assertEquals(AbiClassKind.ANNOTATION_CLASS, kind) + } + + @Test + fun hasClassKind() { + val input = "final class my.lib/MyClass" + val cursor = Cursor(input) + assertTrue(cursor.hasClassKind()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun parseFunctionKindSimple() { + val input = "fun hello" + val cursor = Cursor(input) + val kind = cursor.parseFunctionKind() + assertEquals("fun", kind) + assertEquals(cursor.currentLine, cursor.currentLine) + } + + @Test + fun hasFunctionKind() { + val input = " final fun myFun(): kotlin/String " + val cursor = Cursor(input) + assertTrue(cursor.hasFunctionKind()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun hasFunctionKindConstructor() { + val input = " constructor (kotlin/Int =...)" + val cursor = Cursor(input) + assertTrue(cursor.hasFunctionKind()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun parseGetterOrSetterName() { + val input = "()" + val cursor = Cursor(input) + val name = cursor.parseGetterOrSetterName() + assertEquals("", name) + assertEquals("()", cursor.currentLine) + } + + @Test + fun hasGetter() { + val input = "final inline fun (): kotlin.ranges/IntRange" + val cursor = Cursor(input) + assertTrue(cursor.hasGetter()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun hasSetter() { + val input = "final inline fun (): kotlin.ranges/IntRange" + val cursor = Cursor(input) + assertTrue(cursor.hasSetter()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun hasGetterOrSetter() { + val inputs = + listOf( + "final inline fun (): kotlin.ranges/IntRange", + "final inline fun (): kotlin.ranges/IntRange", + ) + inputs.forEach { input -> assertTrue(Cursor(input).hasGetterOrSetter()) } + } + + @Test + fun hasPropertyKind() { + val input = "final const val my.lib/myProp" + val cursor = Cursor(input) + assertTrue(cursor.hasPropertyKind()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun parsePropertyKindConstVal() { + val input = "const val something" + val cursor = Cursor(input) + val kind = cursor.parsePropertyKind() + assertEquals(AbiPropertyKind.CONST_VAL, kind) + assertEquals("something", cursor.currentLine) + } + + @Test + fun parsePropertyKindVal() { + val input = "val something" + val cursor = Cursor(input) + val kind = cursor.parsePropertyKind() + assertEquals(AbiPropertyKind.VAL, kind) + assertEquals("something", cursor.currentLine) + } + + @Test + fun parseNullability() { + val nullable = Cursor("?").parseNullability() + val notNull = Cursor("!!").parseNullability() + val unspecified = Cursor("another symbol").parseNullability() + assertEquals(AbiTypeNullability.MARKED_NULLABLE, nullable) + assertEquals(AbiTypeNullability.DEFINITELY_NOT_NULL, notNull) + assertEquals(AbiTypeNullability.NOT_SPECIFIED, unspecified) + } + + @Test + fun parseNullabilityWhenAssumingNotNullable() { + val unspecified = Cursor("").parseNullability(assumeNotNull = true) + assertEquals(AbiTypeNullability.DEFINITELY_NOT_NULL, unspecified) + } + + @Test + fun parseQualifiedName() { + val input = "my.lib/MyClass { something" + val cursor = Cursor(input) + val qName = cursor.parseAbiQualifiedName() + assertEquals("my.lib/MyClass", qName.toString()) + assertEquals("{ something", cursor.currentLine) + } + + @Test + fun parseQualifiedNameKotlin() { + val input = "kotlin/Function2<#A1, #A, #A1>" + val cursor = Cursor(input) + val qName = cursor.parseAbiQualifiedName() + assertEquals("kotlin/Function2", qName.toString()) + assertEquals("<#A1, #A, #A1>", cursor.currentLine) + } + + @Test + fun parseQualifie0dNameDoesNotGrabNullable() { + val input = "my.lib/MyClass? something" + val cursor = Cursor(input) + val qName = cursor.parseAbiQualifiedName() + assertEquals("my.lib/MyClass", qName.toString()) + assertEquals("? something", cursor.currentLine) + } + + @Test + fun parseQualifiedNameWithQualifiedReceiver() { + val input = "my.longer.lib/MyClass.Builder.Inner {" + val cursor = Cursor(input) + val qName = cursor.parseAbiQualifiedName() + assertEquals("my.longer.lib/MyClass.Builder.Inner", qName.toString()) + } + + @Test + fun parseQualifiedNameBeforeDefaultParameterSymbol() { + val input = "my.lib/MyClass =..." + val cursor = Cursor(input) + val qName = cursor.parseAbiQualifiedName() + assertEquals("my.lib/MyClass", qName.toString()) + } + + @Test + fun parseQualifiedNameWhenPackageIsBlank() { + val input = "my.lib/MyClass =..." + val cursor = Cursor(input) + val qName = cursor.parseAbiQualifiedName() + assertEquals("my.lib/MyClass", qName.toString()) + } + + @Test + fun parseAbiType() { + val input = "my.lib/MyClass<#A, #B> something" + val cursor = Cursor(input) + val type = cursor.parseAbiType()!! + assertEquals("my.lib/MyClass", type.className?.toString()) + assertEquals(type.arguments!!.size, 2) + assertEquals("something", cursor.currentLine) + } + + @Test + fun parseAbiTypeWithAnotherType() { + val input = + "my.lib/MyClass<#A, #B>, my.lib/Other<#A, #B> " + + "something" + val cursor = Cursor(input) + val type = cursor.parseAbiType() + assertEquals("my.lib/MyClass", type?.className?.toString()) + assertEquals(", my.lib/Other<#A, #B> something", cursor.currentLine) + } + + @Test + fun parseAbiTypeWithThreeParams() { + val input = "kotlin/Function2<#A1, #A, #A1>" + val cursor = Cursor(input) + val type = cursor.parseAbiType() + assertEquals("kotlin/Function2", type?.className?.toString()) + } + + @Test + fun parseSuperTypes() { + val input = + ": my.lib/MyClass<#A, #B>, my.lib/Other<#A, #B> something" + val cursor = Cursor(input) + val superTypes = cursor.parseSuperTypes().toList() + assertEquals(2, superTypes.size) + assertEquals("my.lib/MyClass", superTypes.first().className?.toString()) + assertEquals("my.lib/Other", superTypes.last().className?.toString()) + assertEquals("something", cursor.currentLine) + } + + @Test + fun parseReturnType() { + val input = ": my.lib/MyClass<#A, #B> stuff" + val cursor = Cursor(input) + val returnType = cursor.parseReturnType() + assertEquals("my.lib/MyClass", returnType?.className?.toString()) + assertEquals("stuff", cursor.currentLine) + } + + @Test + fun parseReturnTypeNullableWithTypeParamsNullable() { + val input = ": #B? stuff" + val cursor = Cursor(input) + val returnType = cursor.parseReturnType() + assertEquals("B", returnType?.tag) + assertEquals(AbiTypeNullability.MARKED_NULLABLE, returnType?.nullability) + assertEquals("stuff", cursor.currentLine) + } + + @Test + fun parseReturnTypeNullableWithTypeParamsNotSpecified() { + val input = ": #B stuff" + val cursor = Cursor(input) + val returnType = cursor.parseReturnType() + assertEquals("B", returnType?.tag) + assertEquals(AbiTypeNullability.NOT_SPECIFIED, returnType?.nullability) + assertEquals("stuff", cursor.currentLine) + } + + @Test + fun parseFunctionReceiver() { + val input = "(my.lib/LongSparseArray<#A>).my.lib/keyIterator()" + val cursor = Cursor(input) + val receiver = cursor.parseFunctionReceiver() + assertEquals("my.lib/LongSparseArray", receiver?.className.toString()) + assertEquals("my.lib/keyIterator()", cursor.currentLine) + } + + @Test + fun parseFunctionReceiver2() { + val input = "(my.lib/LongSparseArray<#A1>).(): kotlin/Int" + val cursor = Cursor(input) + val receiver = cursor.parseFunctionReceiver() + assertEquals("my.lib/LongSparseArray", receiver?.className.toString()) + assertEquals("(): kotlin/Int", cursor.currentLine) + } + + @Test + fun parseFunctionReceiverWithStarParams() { + val input = "(my.lib/MyClass<*, *>).()" + val cursor = Cursor(input) + val receiver = cursor.parseFunctionReceiver() + assertEquals("my.lib/MyClass", receiver?.className.toString()) + assertEquals("()", cursor.currentLine) + } + + @Test + fun parseFunctionReceiverWithNullableParam() { + val input = "(kotlin.collections/List<#A?>)." + val cursor = Cursor(input) + val receiver = cursor.parseFunctionReceiver() + assertEquals("kotlin.collections/List", receiver?.className.toString()) + val typeArg = receiver?.arguments?.single()?.type + assertEquals(AbiTypeNullability.MARKED_NULLABLE, typeArg?.nullability) + assertEquals("A", typeArg?.tag) + } + + @Test + fun parseSimpleContextParams() { + val input = "context(kotlin/Int)" + val cursor = Cursor(input) + val contextParams = cursor.parseContextParams() + assertEquals(1, contextParams!!.size) + assertEquals("kotlin/Int", contextParams?.single()?.type?.className.toString()) + } + + @Test + fun parseSimpleContextParamsWithNewFormatting() { + val input = "(context(kotlin/Int))" + val cursor = Cursor(input) + val contextParams = cursor.parseContextParams() + assertEquals(1, contextParams!!.size) + assertEquals("kotlin/Int", contextParams?.single()?.type?.className.toString()) + } + + @Test + fun parseContextWithMultipleParams() { + val input = "context(kotlin/Int, kotlin.collections/List)" + val cursor = Cursor(input) + val contextParams = cursor.parseContextParams() + assertEquals(2, contextParams!!.size) + assertEquals("kotlin/Int", contextParams?.first()?.type?.className.toString()) + assertEquals("kotlin.collections/List", contextParams?.last()?.type?.className.toString()) + assertEquals("kotlin/String", contextParams?.last()?.type?.arguments?.first()?.type?.className.toString()) + } + + @Test + fun parseContextWithMultipeParamsAndNewFormatting() { + val input = "(context(kotlin/Int, kotlin.collections/List))" + val cursor = Cursor(input) + val contextParams = cursor.parseContextParams() + assertEquals(2, contextParams!!.size) + assertEquals("kotlin/Int", contextParams?.first()?.type?.className.toString()) + assertEquals("kotlin.collections/List", contextParams?.last()?.type?.className.toString()) + assertEquals("kotlin/String", contextParams?.last()?.type?.arguments?.first()?.type?.className.toString()) + } + + @Test + fun parseValueParamCrossinlineDefault() { + val input = "crossinline kotlin/Function2<#A, #B, kotlin/Int> =..." + val cursor = Cursor(input) + val valueParam = cursor.parseValueParameter()!! + assertEquals("kotlin/Function2", valueParam.type.className.toString()) + assertTrue(valueParam.hasDefaultArg) + assertTrue(valueParam.isCrossinline) + assertFalse(valueParam.isVararg) + } + + @Test + fun parseValueParamVararg() { + val input = "kotlin/Array>..." + val cursor = Cursor(input) + val valueParam = cursor.parseValueParameter()!! + assertEquals("kotlin/Array", valueParam.type.className.toString()) + assertFalse(valueParam.hasDefaultArg) + assertFalse(valueParam.isCrossinline) + assertTrue(valueParam.isVararg) + } + + @Test + fun parseValueParametersWithTypeArgs() { + val input = "kotlin/Array..." + val cursor = Cursor(input) + val valueParam = cursor.parseValueParameter()!! + assertEquals(1, valueParam.type.arguments!!.size) + } + + @Test + fun parseValueParametersWithTwoTypeArgs() { + val input = "kotlin/Function1)" + val cursor = Cursor(input) + val valueParam = cursor.parseValueParameter()!! + assertEquals(2, valueParam.type.arguments!!.size) + } + + @Test + fun parseValueParametersEmpty() { + val input = "() thing" + val cursor = Cursor(input) + val params = cursor.parseValueParameters(AbiValueParameterKind.CONTEXT) + assertEquals(emptyList(), params) + assertEquals("thing", cursor.currentLine) + } + + @Test + fun parseValueParamsSimple() { + val input = "(kotlin/Function1<#A, kotlin/Boolean>)" + val cursor = Cursor(input) + val valueParams = cursor.parseValueParameters(AbiValueParameterKind.CONTEXT) + assertEquals(1, valueParams!!.size) + } + + @Test + fun parseValueParamsTwoArgs() { + val input = "(#A1, kotlin/Function2<#A1, #A, #A1>)" + val cursor = Cursor(input) + val valueParams = cursor.parseValueParameters(AbiValueParameterKind.CONTEXT)!! + assertEquals(2, valueParams.size) + assertEquals("A1", valueParams.first().type.tag) + } + + @Test + fun parseValueParamsWithHasDefaultArg() { + val input = "(kotlin/Int =...)" + val cursor = Cursor(input) + val valueParams = cursor.parseValueParameters(AbiValueParameterKind.CONTEXT) + assertEquals(1, valueParams!!.size) + assertTrue(valueParams.single().hasDefaultArg) + } + + @Test + fun parseValueParamsComplex2() { + val input = + "(kotlin/Int, crossinline kotlin/Function2<#A, #B, kotlin/Int> =..., crossinline kotlin/Function1<#A, #B?> =..., crossinline kotlin/Function4 =...)" + val cursor = Cursor(input) + val valueParams = cursor.parseValueParameters(AbiValueParameterKind.CONTEXT)!! + assertEquals(4, valueParams.size) + assertEquals("kotlin/Int", valueParams.first().type.className?.toString()) + val rest = valueParams.subList(1, valueParams.size) + assertEquals(3, rest.size) + assertTrue(rest.all { it.hasDefaultArg }) + assertTrue(rest.all { it.isCrossinline }) + } + + @Test + fun parseValueParamsComplex3() { + val input = "(kotlin/Array>...)" + val cursor = Cursor(input) + val valueParams = cursor.parseValueParameters(AbiValueParameterKind.CONTEXT)!! + assertEquals(1, valueParams.size) + + assertTrue(valueParams.single().isVararg) + val type = valueParams.single().type + assertEquals("kotlin/Array", type.className.toString()) + } + + @Test + fun parseValueParamsWithStarTypeParam() { + val input = "(my.lib/MyClass.Example<*>)" + val cursor = Cursor(input) + val valueParams = cursor.parseValueParameters(AbiValueParameterKind.CONTEXT)!! + assertEquals(1, valueParams.size) + val valueParam = valueParams.single() + val type = valueParam.type + assertEquals("my.lib/MyClass.Example", type.className.toString()) + assertEquals(1, type.arguments!!.size) + assertInstanceOf(AbiTypeArgument.StarProjection::class.java, type.arguments?.single()) + } + + @Test + fun parseTypeParams() { + val input = "<#A1: kotlin/Any?>" + val cursor = Cursor(input) + val typeParams = cursor.parseTypeParams()!! + assertEquals(1, typeParams.size) + val type = typeParams.single().upperBounds.single() + assertEquals("A1", typeParams.single().tag) + assertEquals("kotlin/Any", type.className.toString()) + assertEquals(AbiTypeNullability.MARKED_NULLABLE, type.nullability) + assertEquals(AbiVariance.INVARIANT, typeParams.single().variance) + } + + @Test + fun parseTypeParamsWithVariance() { + val input = "<#A1: out kotlin/Any?>" + val cursor = Cursor(input) + val typeParams = cursor.parseTypeParams()!! + assertEquals(1, typeParams.size) + val type = typeParams.single().upperBounds.single() + assertEquals("A1", typeParams.single().tag) + assertEquals("kotlin/Any", type.className?.toString()) + assertEquals(AbiTypeNullability.MARKED_NULLABLE, type.nullability) + assertEquals(AbiVariance.OUT, typeParams.single().variance) + } + + @Test + fun parseTypeParamsWithTwo() { + val input = "<#A: kotlin/Any?, #B: kotlin/Any?>" + val cursor = Cursor(input) + val typeParams = cursor.parseTypeParams()!! + assertEquals(2, typeParams.size) + val type1 = typeParams.first().upperBounds.single() + val type2 = typeParams.first().upperBounds.single() + assertEquals("A", typeParams.first().tag) + assertEquals("B", typeParams.last().tag) + assertEquals("kotlin/Any", type1?.className?.toString()) + assertEquals(AbiTypeNullability.MARKED_NULLABLE, type1?.nullability) + assertEquals("kotlin/Any", type2?.className?.toString()) + assertEquals(AbiTypeNullability.MARKED_NULLABLE, type2?.nullability) + } + + @Test + fun parseTypeParamsWithMultipleUpperBounds() { + val input = "<#A: my.lib/A & my.lib/B>" + val cursor = Cursor(input) + val typeParams = cursor.parseTypeParams()!! + assertEquals(1, typeParams.size) + val upperBounds = typeParams.single().upperBounds + assertEquals(2, upperBounds.size) + assertEquals("my.lib/A", upperBounds.first().className.toString()) + assertEquals("my.lib/B", upperBounds.last().className.toString()) + } + + @Test + fun parseTypeParamsIsReified() { + val input = "<#A1: reified kotlin/Any?>" + val cursor = Cursor(input) + val typeParam = cursor.parseTypeParams()?.single() + assertNotNull(typeParam) + assertTrue(typeParam!!.isReified) + } + + @Test + fun parseTypeParamsDoesNotMatchGetter() { + val input = "" + val cursor = Cursor(input) + val typeParams = cursor.parseTypeParams() + assertNull(typeParams) + } + + @Test + fun parseTypeArgs() { + val input = "" + val cursor = Cursor(input) + val typeArgs = cursor.parseTypeArgs() + assertEquals(1, typeArgs!!.size) + val typeArg = typeArgs.single() + assertEquals("A", typeArg.type?.tag) + assertEquals(AbiVariance.OUT, typeArg.variance) + } + + @Test + fun parseTwoTypeArgs() { + val input = "" + val cursor = Cursor(input) + val typeArgs = cursor.parseTypeArgs() + assertEquals(2, typeArgs!!.size) + assertEquals("kotlin/Double", typeArgs.first().type!!.className.toString()) + assertEquals("kotlin/Boolean", typeArgs.last().type!!.className.toString()) + } + + @Test + fun parseTypeArgsWithNestedBrackets() { + val input = + ", my.lib/Other<#A, #B>>, something else" + val cursor = Cursor(input) + val typeArgs = cursor.parseTypeArgs() + assertEquals(2, typeArgs!!.size) + assertEquals(", something else", cursor.currentLine) + } + + @Test + fun parseVarargSymbol() { + val input = "..." + val cursor = Cursor(input) + val vararg = cursor.parseVarargSymbol() + assertNotNull(vararg) + } + + @Test + fun hasSignatureVersion() { + val input = "// - Signature version: 2" + val cursor = Cursor(input) + assertTrue(cursor.hasSignatureVersion()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun hasSignatureVersionFalsePositive() { + val input = "// - Show manifest properties: true" + val cursor = Cursor(input) + assertFalse(cursor.hasSignatureVersion()) + } + + @Test + fun parseSignatureVersion() { + val input = "// - Signature version: 2" + val cursor = Cursor(input) + val signatureVersion = cursor.parseSignatureVersion()!! + assertTrue(signatureVersion.isSupportedByAbiReader) + assertEquals(2, signatureVersion.versionNumber) + } + + @Test + fun parseInvalidSignatureVersion() { + val input = "// - Signature version: 101" + val cursor = Cursor(input) + val signatureVersion = cursor.parseSignatureVersion()!! + assertFalse(signatureVersion.isSupportedByAbiReader) + assertEquals(101, signatureVersion.versionNumber) + } + + @Test + fun parseEnumEntryName() { + val input = "SOME_ENUM something else" + val cursor = Cursor(input) + val enumName = cursor.parseEnumName() + assertEquals("SOME_ENUM", enumName) + assertEquals("something else", cursor.currentLine) + } + + @Test + fun parseEnumEntryKind() { + val input = "enum entry SOME_ENUM" + val cursor = Cursor(input) + val enumName = cursor.parseEnumEntryKind() + assertEquals("enum entry", enumName) + assertEquals("SOME_ENUM", cursor.currentLine) + } + + @Test + fun hasEnumEntry() { + val input = "enum entry SOME_ENUM" + val cursor = Cursor(input) + assertTrue(cursor.hasEnumEntry()) + assertEquals(input, cursor.currentLine) + } + + @Test + fun parseValidIdentifierAndMaybeTrimForFunctionName() { + val input = "myFun ()" + val cursor = Cursor(input) + assertEquals("myFun ", cursor.parseValidIdentifierAndMaybeTrim()) + } + + @Test + fun parseValidIdentifierAndMaybeTrimForClassName() { + val input = "MyClass {" + val cursor = Cursor(input) + assertEquals("MyClass ", cursor.parseValidIdentifierAndMaybeTrim()) + } + + @Test + fun parseValidIdentifierAndMaybeTrimForClassWithSuper() { + val input = "MyClass = : my.lib/MyClass {" + val cursor = Cursor(input) + assertEquals("MyClass =", cursor.parseValidIdentifierAndMaybeTrim()) + } + + @Test + fun parseValidIdentifierAndMaybeTrimForForValueParameter() { + val input = "MyClass ," + val cursor = Cursor(input) + assertEquals("MyClass ", cursor.parseValidIdentifierAndMaybeTrim()) + } +}