Add cursor and cursor extensions for parsing klib dump files
Test: CursorTest / CursorExtensionsTest
Change-Id: Iba1cddd8103f79ad60c3cb605f1cb1a163529aae
diff --git a/binarycompatibilityvalidator/OWNERS b/binarycompatibilityvalidator/OWNERS
new file mode 100644
index 0000000..a43f7c1
--- /dev/null
+++ b/binarycompatibilityvalidator/OWNERS
@@ -0,0 +1,3 @@
[email protected]
[email protected]
[email protected]
\ No newline at end of file
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle b/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle
new file mode 100644
index 0000000..aa18e365
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+
+import androidx.build.LibraryType
+
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ implementation(libs.kotlinCompiler)
+ testImplementation(libs.truth)
+ testImplementation(libs.junit)
+}
+
+androidx {
+ name = "AndroidX Binary Compatibility Validator"
+ type = LibraryType.INTERNAL_HOST_TEST_LIBRARY
+ inceptionYear = "2024"
+ description = "Enforce binary compatibility for klibs"
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt
new file mode 100644
index 0000000..871247d
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/AbiExtensions.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.binarycompatibilityvalidator
+
+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<AbiTypeArgument>?
+ 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
+private val AbiClassifierReference.className: AbiQualifiedName?
+ get() = (this as? AbiClassifierReference.ClassReference)?.className
+private val AbiClassifierReference.tag: String?
+ get() = (this as? AbiClassifierReference.TypeParameterReference)?.tag
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt
new file mode 100644
index 0000000..52f0e9b
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/Cursor.kt
@@ -0,0 +1,62 @@
+/*
+ * 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 androidx.binarycompatibilityvalidator
+
+class Cursor private constructor(
+ private val lines: List<String>,
+ private var rowIndex: Int = 0,
+ private var columnIndex: Int = 0
+) {
+ constructor(text: String) : this(text.split("\n"))
+ val currentLine: String
+ get() = lines[rowIndex].slice(columnIndex until lines[rowIndex].length)
+ fun hasNext() = rowIndex < (lines.size - 1)
+ fun nextLine() {
+ rowIndex++
+ columnIndex = 0
+ }
+
+ fun parseSymbol(
+ pattern: String,
+ peek: Boolean = false,
+ skipInlineWhitespace: Boolean = true
+ ): String? {
+ val match = Regex(pattern).find(currentLine)
+ return match?.value?.also {
+ if (!peek) {
+ val offset = it.length + currentLine.indexOf(it)
+ columnIndex += offset
+ if (skipInlineWhitespace) {
+ skipInlineWhitespace()
+ }
+ }
+ }
+ }
+
+ fun parseValidIdentifier(peek: Boolean = false): String? =
+ parseSymbol("^[a-zA-Z_][a-zA-Z0-9_]+", peek)
+
+ fun parseWord(peek: Boolean = false): String? = parseSymbol("[a-zA-Z]+", peek)
+
+ fun copy() = Cursor(lines, rowIndex, columnIndex)
+
+ internal fun skipInlineWhitespace() {
+ while (currentLine.firstOrNull()?.isWhitespace() == true) {
+ columnIndex++
+ }
+ }
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt
new file mode 100644
index 0000000..2afb33a
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/CursorExtensions.kt
@@ -0,0 +1,396 @@
+/*
+ * 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:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+@file:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+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.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.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.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.parseSymbol("^<get\\-") ?: return null
+ val name = cursor.parseValidIdentifier() ?: return null
+ cursor.parseSymbol("^>") ?: return null
+ return "<get-$name>"
+}
+
+internal fun Cursor.parseSetterName(peek: Boolean = false): String? {
+ val cursor = subCursor(peek)
+ cursor.parseSymbol("^<set\\-") ?: return null
+ val name = cursor.parseValidIdentifier() ?: return null
+ cursor.parseSymbol("^>") ?: return null
+ return "<set-$name>"
+}
+
+internal fun Cursor.parseGetterOrSetterName(peek: Boolean = false) =
+ parseGetterName(peek) ?: parseSetterName(peek)
+
+internal fun Cursor.parseClassModifier(peek: Boolean = false): String? =
+ parseSymbol("^(inner|value|fun|open|annotation|enum)", peek)
+
+internal fun Cursor.parseClassModifiers(): Set<String> {
+ val modifiers = mutableSetOf<String>()
+ while (parseClassModifier(peek = true) != null) {
+ modifiers.add(parseClassModifier()!!)
+ }
+ return modifiers
+}
+
+internal fun Cursor.parseFunctionKind(peek: Boolean = false) =
+ parseSymbol("^(constructor|fun)", peek)
+
+internal fun Cursor.parseFunctionModifier(peek: Boolean = false): String? =
+ parseSymbol("^(inline|suspend)", peek)
+
+internal fun Cursor.parseFunctionModifiers(): Set<String> {
+ val modifiers = mutableSetOf<String>()
+ while (parseFunctionModifier(peek = true) != null) {
+ modifiers.add(parseFunctionModifier()!!)
+ }
+ return modifiers
+}
+
+internal fun Cursor.parseAbiQualifiedName(peek: Boolean = false): AbiQualifiedName? {
+ val symbol = parseSymbol("^[a-zA-Z0-9\\.]+\\/[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?", peek)
+ ?: return null
+ val (packageName, relativeName) = symbol.split("/")
+ return AbiQualifiedName(
+ AbiCompoundName(packageName),
+ AbiCompoundName(relativeName)
+ )
+}
+
+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<AbiTypeArgument>? {
+ val typeArgsString = parseTypeParamsString() ?: return null
+ val subCursor = Cursor(typeArgsString)
+ subCursor.parseSymbol("<") ?: return null
+ val typeArgs = mutableListOf<AbiTypeArgument>()
+ while (subCursor.parseTypeArg(peek = true) != null) {
+ typeArgs.add(subCursor.parseTypeArg()!!)
+ subCursor.parseSymbol(",")
+ }
+ return typeArgs
+}
+
+internal fun Cursor.parseTypeArg(peek: Boolean = false): AbiTypeArgument? {
+ val cursor = subCursor(peek)
+ val variance = cursor.parseAbiVariance()
+ cursor.parseSymbol("\\*")?.let {
+ return StarProjectionImpl
+ }
+ val type = cursor.parseAbiType(peek) ?: return null
+ return TypeProjectionImpl(
+ type = type,
+ variance = variance
+ )
+}
+
+internal fun Cursor.parseAbiVariance(): AbiVariance {
+ val variance = parseSymbol("^(out|in)") ?: 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("^#[a-zA-Z0-9]+")?.removePrefix("#")
+
+internal fun Cursor.parseNullability(assumeNotNull: Boolean = false): AbiTypeNullability {
+ val nullable = parseSymbol("^\\?") != null
+ val definitelyNotNull = parseSymbol("^\\!\\!") != 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<AbiType> {
+ parseSymbol(":")
+ val superTypes = mutableSetOf<AbiType>()
+ while (parseAbiQualifiedName(peek = true) != null) {
+ superTypes.add(parseAbiType()!!)
+ parseSymbol(",")
+ }
+ return superTypes
+}
+
+fun Cursor.parseTypeParams(peek: Boolean = false): List<AbiTypeParameter>? {
+ val typeParamsString = parseTypeParamsString(peek) ?: return null
+ val subCursor = Cursor(typeParamsString)
+ subCursor.parseSymbol("^<")
+ val typeParams = mutableListOf<AbiTypeParameter>()
+ while (subCursor.parseTypeParam(peek = true) != null) {
+ typeParams.add(subCursor.parseTypeParam()!!)
+ subCursor.parseSymbol("^,")
+ }
+ return typeParams
+}
+
+fun Cursor.parseTypeParam(peek: Boolean = false): AbiTypeParameter? {
+ val cursor = subCursor(peek)
+ val tag = cursor.parseTag() ?: return null
+ cursor.parseSymbol("^:")
+ val variance = cursor.parseAbiVariance()
+ val isReified = cursor.parseSymbol("reified") != null
+ val upperBounds = mutableListOf<AbiType>()
+ if (null != cursor.parseAbiType(peek = true)) {
+ upperBounds.add(cursor.parseAbiType()!!)
+ }
+
+ return AbiTypeParameterImpl(
+ tag = tag,
+ variance = variance,
+ isReified = isReified,
+ upperBounds = upperBounds
+ )
+}
+
+internal fun Cursor.parseValueParameters(): List<AbiValueParameter>? {
+ val valueParamString = parseValueParametersString() ?: return null
+ val subCursor = Cursor(valueParamString)
+ val valueParams = mutableListOf<AbiValueParameter>()
+ subCursor.parseSymbol("\\(")
+ while (null != subCursor.parseValueParameter(peek = true)) {
+ valueParams.add(subCursor.parseValueParameter()!!)
+ subCursor.parseSymbol("^,")
+ }
+ return valueParams
+}
+
+internal fun Cursor.parseValueParameter(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(
+ type = type,
+ isVararg = isVararg,
+ hasDefaultArg = hasDefaultArg,
+ isNoinline = isNoInline,
+ isCrossinline = isCrossinline
+ )
+}
+
+internal fun Cursor.parseValueParameterModifiers(): Set<String> {
+ val modifiers = mutableSetOf<String>()
+ while (parseValueParameterModifier(peek = true) != null) {
+ modifiers.add(parseValueParameterModifier()!!)
+ }
+ return modifiers
+}
+
+internal fun Cursor.parseValueParameterModifier(peek: Boolean = false): String? =
+ parseSymbol("^(crossinline|noinline)", peek)
+
+internal fun Cursor.parseVarargSymbol() = parseSymbol("^\\.\\.\\.")
+
+internal fun Cursor.parseDefaultArg() = parseSymbol("^=\\.\\.\\.")
+
+internal fun Cursor.parseFunctionReceiver(): AbiType? {
+ val string = parseFunctionReceiverString() ?: return null
+ val subCursor = Cursor(string)
+ subCursor.parseSymbol("\\(")
+ return subCursor.parseAbiType()
+}
+
+internal fun Cursor.parseReturnType(): AbiType? {
+ parseSymbol("^:\\s")
+ return parseAbiType()
+}
+
+internal fun Cursor.parseTargets(): List<String> {
+ parseSymbol("^Targets:")
+ parseSymbol("^\\[")
+ val targets = mutableListOf<String>()
+ while (parseValidIdentifier(peek = true) != null) {
+ targets.add(parseValidIdentifier()!!)
+ parseSymbol("^,")
+ }
+ parseSymbol("^\\]")
+ return targets
+}
+
+/**
+ * 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.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? {
+ val cursor = subCursor(peek)
+ val result = StringBuilder()
+ cursor.parseSymbol("^<")?.let { result.append(it) } ?: return null
+ var openBracketCount = 1
+ while (openBracketCount > 0) {
+ val nextSymbol = cursor.parseSymbol(".", skipInlineWhitespace = false).also {
+ result.append(it)
+ }
+ when (nextSymbol) {
+ "<" -> openBracketCount++
+ ">" -> openBracketCount--
+ }
+ }
+ cursor.skipInlineWhitespace()
+ return result.toString()
+}
+
+private fun Cursor.parseFunctionReceiverString() =
+ parseSymbol("^\\([a-zA-Z0-9,\\/<>,#\\.\\s]+?\\)\\.")
+
+private fun Cursor.parseValueParametersString() =
+ parseSymbol("^\\(([a-zA-Z0-9,\\/<>,#\\.\\s\\?=]+)?\\)")
+
+private fun Cursor.parseAbiModalityString(peek: Boolean = false) =
+ parseSymbol("^(final|open|abstract|sealed)", peek)?.uppercase()
+
+private fun Cursor.parsePropertyKindString(peek: Boolean = false) =
+ parseSymbol("^(const\\sval|val|var)", peek)?.uppercase()?.replace(" ", "_")
+
+private fun Cursor.parseClassKindString(peek: Boolean = false) =
+ parseSymbol("^(class|interface|object|enum_class|annotation_class)", peek)?.uppercase()
+
+private enum class GetterOrSetter() {
+ GETTER,
+ SETTER
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt
new file mode 100644
index 0000000..6390ea9
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorExtensionsTest.kt
@@ -0,0 +1,509 @@
+/*
+ * 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 androidx.binarycompatibilityvalidator
+
+import com.google.common.truth.Truth.assertThat
+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.AbiTypeNullability
+import org.jetbrains.kotlin.library.abi.AbiValueParameter
+import org.jetbrains.kotlin.library.abi.AbiVariance
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.junit.Test
+
+class CursorExtensionsTest {
+
+ @Test
+ fun parseModalityFailure() {
+ val input = "something else"
+ val cursor = Cursor(input)
+ val modality = cursor.parseAbiModality()
+ assertThat(modality).isNull()
+ assertThat(cursor.currentLine).isEqualTo("something else")
+ }
+
+ @Test
+ fun parseModalitySuccess() {
+ val input = "final whatever"
+ val cursor = Cursor(input)
+ val modality = cursor.parseAbiModality()
+ assertThat(modality).isEqualTo(AbiModality.FINAL)
+ assertThat(cursor.currentLine).isEqualTo("whatever")
+ }
+
+ @Test
+ fun parseClassModifier() {
+ val input = "inner whatever"
+ val cursor = Cursor(input)
+ val modifier = cursor.parseClassModifier()
+ assertThat(modifier).isEqualTo("inner")
+ assertThat(cursor.currentLine).isEqualTo("whatever")
+ }
+
+ @Test
+ fun parseClassModifiers() {
+ val input = "inner value fun whatever"
+ val cursor = Cursor(input)
+ val modifiers = cursor.parseClassModifiers()
+ assertThat(modifiers).containsExactly("inner", "fun", "value")
+ assertThat(cursor.currentLine).isEqualTo("whatever")
+ }
+
+ @Test
+ fun parseFunctionModifiers() {
+ val input = "final inline suspend fun component1(): kotlin/Long"
+ val cursor = Cursor(input)
+ cursor.parseAbiModality()
+ val modifiers = cursor.parseFunctionModifiers()
+ assertThat(modifiers).containsExactly("inline", "suspend")
+ assertThat(cursor.currentLine).isEqualTo("fun component1(): kotlin/Long")
+ }
+
+ @Test
+ fun parseClassKindSimple() {
+ val input = "class"
+ val cursor = Cursor(input)
+ val kind = cursor.parseClassKind()
+ assertThat(kind).isEqualTo(AbiClassKind.CLASS)
+ }
+
+ @Test
+ fun parseClassKindFalsePositive() {
+ val input = "androidx.collection/objectFloatMap"
+ val cursor = Cursor(input)
+ val kind = cursor.parseClassKind()
+ assertThat(kind).isNull()
+ }
+
+ @Test
+ fun hasClassKind() {
+ val input = "final class my.lib/MyClass"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasClassKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test
+ fun parseFunctionKindSimple() {
+ val input = "fun hello"
+ val cursor = Cursor(input)
+ val kind = cursor.parseFunctionKind()
+ assertThat(kind).isEqualTo("fun")
+ assertThat(cursor.currentLine).isEqualTo(cursor.currentLine)
+ }
+
+ @Test fun hasFunctionKind() {
+ val input = " final fun myFun(): kotlin/String "
+ val cursor = Cursor(input)
+ assertThat(cursor.hasFunctionKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun hasFunctionKindConstructor() {
+ val input = " constructor <init>(kotlin/Int =...)"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasFunctionKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun parseGetterOrSetterName() {
+ val input = "<get-indices>()"
+ val cursor = Cursor(input)
+ val name = cursor.parseGetterOrSetterName()
+ assertThat(name).isEqualTo("<get-indices>")
+ assertThat(cursor.currentLine).isEqualTo("()")
+ }
+
+ @Test fun hasGetter() {
+ val input = "final inline fun <get-indices>(): kotlin.ranges/IntRange"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasGetter()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun hasSetter() {
+ val input = "final inline fun <set-indices>(): kotlin.ranges/IntRange"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasSetter()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test fun hasGetterOrSetter() {
+ val inputs = listOf(
+ "final inline fun <set-indices>(): kotlin.ranges/IntRange",
+ "final inline fun <get-indices>(): kotlin.ranges/IntRange"
+ )
+ inputs.forEach { input ->
+ assertThat(Cursor(input).hasGetterOrSetter()).isTrue()
+ }
+ }
+
+ @Test
+ fun hasPropertyKind() {
+ val input = "final const val my.lib/myProp"
+ val cursor = Cursor(input)
+ assertThat(cursor.hasPropertyKind()).isTrue()
+ assertThat(cursor.currentLine).isEqualTo(input)
+ }
+
+ @Test
+ fun parsePropertyKindConstVal() {
+ val input = "const val something"
+ val cursor = Cursor(input)
+ val kind = cursor.parsePropertyKind()
+ assertThat(kind).isEqualTo(AbiPropertyKind.CONST_VAL)
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test
+ fun parsePropertyKindVal() {
+ val input = "val something"
+ val cursor = Cursor(input)
+ val kind = cursor.parsePropertyKind()
+ assertThat(kind).isEqualTo(AbiPropertyKind.VAL)
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test
+ fun parseNullability() {
+ val nullable = Cursor("?").parseNullability()
+ val notNull = Cursor("!!").parseNullability()
+ val unspecified = Cursor("another symbol").parseNullability()
+ assertThat(nullable).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(notNull).isEqualTo(AbiTypeNullability.DEFINITELY_NOT_NULL)
+ assertThat(unspecified).isEqualTo(AbiTypeNullability.NOT_SPECIFIED)
+ }
+
+ @Test fun parseNullabilityWhenAssumingNotNullable() {
+ val unspecified = Cursor("").parseNullability(assumeNotNull = true)
+ assertThat(unspecified).isEqualTo(AbiTypeNullability.DEFINITELY_NOT_NULL)
+ }
+
+ @Test fun parseQualifiedName() {
+ val input = "androidx.collection/MutableScatterMap something"
+ val cursor = Cursor(input)
+ val qName = cursor.parseAbiQualifiedName()
+ assertThat(qName.toString()).isEqualTo("androidx.collection/MutableScatterMap")
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test fun parseQualifiedNameKotlin() {
+ val input = "kotlin/Function2<#A1, #A, #A1>"
+ val cursor = Cursor(input)
+ val qName = cursor.parseAbiQualifiedName()
+ assertThat(qName.toString()).isEqualTo("kotlin/Function2")
+ assertThat(cursor.currentLine).isEqualTo("<#A1, #A, #A1>",)
+ }
+
+ @Test fun parseQualifie0dNameDoesNotGrabNullable() {
+ val input = "androidx.collection/MutableScatterMap? something"
+ val cursor = Cursor(input)
+ val qName = cursor.parseAbiQualifiedName()
+ assertThat(qName.toString()).isEqualTo("androidx.collection/MutableScatterMap")
+ assertThat(cursor.currentLine).isEqualTo("? something")
+ }
+
+ @Test
+ fun parseAbiType() {
+ val input = "androidx.collection/ScatterMap<#A, #B> something"
+ val cursor = Cursor(input)
+ val type = cursor.parseAbiType()
+ assertThat(type?.className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test
+ fun parseAbiTypeWithAnotherType() {
+ val input = "androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B> " +
+ "something"
+ val cursor = Cursor(input)
+ val type = cursor.parseAbiType()
+ assertThat(type?.className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(cursor.currentLine).isEqualTo(
+ ", androidx.collection/Other<#A, #B> something"
+ )
+ }
+
+ @Test fun parseAbiTypeWithThreeParams() {
+ val input = "kotlin/Function2<#A1, #A, #A1>"
+ val cursor = Cursor(input)
+ val type = cursor.parseAbiType()
+ assertThat(type?.className?.toString()).isEqualTo("kotlin/Function2")
+ }
+
+ @Test
+ fun parseSuperTypes() {
+ val input = ": androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B> " +
+ "something"
+ val cursor = Cursor(input)
+ val superTypes = cursor.parseSuperTypes().toList()
+ assertThat(superTypes).hasSize(2)
+ assertThat(superTypes.first().className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(superTypes.last().className?.toString()).isEqualTo(
+ "androidx.collection/Other"
+ )
+ assertThat(cursor.currentLine).isEqualTo("something")
+ }
+
+ @Test fun parseReturnType() {
+ val input = ": androidx.collection/ScatterMap<#A, #B> stuff"
+ val cursor = Cursor(input)
+ val returnType = cursor.parseReturnType()
+ assertThat(returnType?.className?.toString()).isEqualTo(
+ "androidx.collection/ScatterMap"
+ )
+ assertThat(cursor.currentLine).isEqualTo("stuff")
+ }
+
+ @Test fun parseReturnTypeNullableWithTypeParamsNullable() {
+ val input = ": #B? stuff"
+ val cursor = Cursor(input)
+ val returnType = cursor.parseReturnType()
+ assertThat(returnType?.tag).isEqualTo("B")
+ assertThat(returnType?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(cursor.currentLine).isEqualTo("stuff")
+ }
+
+ @Test fun parseReturnTypeNullableWithTypeParamsNotSpecified() {
+ val input = ": #B stuff"
+ val cursor = Cursor(input)
+ val returnType = cursor.parseReturnType()
+ assertThat(returnType?.tag).isEqualTo("B")
+ assertThat(returnType?.nullability).isEqualTo(AbiTypeNullability.NOT_SPECIFIED)
+ assertThat(cursor.currentLine).isEqualTo("stuff")
+ }
+
+ @Test
+ fun parseFunctionReceiver() {
+ val input = "(androidx.collection/LongSparseArray<#A>).androidx.collection/keyIterator()"
+ val cursor = Cursor(input)
+ val receiver = cursor.parseFunctionReceiver()
+ assertThat(receiver?.className.toString()).isEqualTo(
+ "androidx.collection/LongSparseArray"
+ )
+ assertThat(cursor.currentLine).isEqualTo("androidx.collection/keyIterator()")
+ }
+
+ @Test
+ fun parseFunctionReceiver2() {
+ val input = "(androidx.collection/LongSparseArray<#A1>).<get-size>(): kotlin/Int"
+ val cursor = Cursor(input)
+ val receiver = cursor.parseFunctionReceiver()
+ assertThat(receiver?.className.toString()).isEqualTo(
+ "androidx.collection/LongSparseArray"
+ )
+ assertThat(cursor.currentLine).isEqualTo("<get-size>(): kotlin/Int")
+ }
+
+ @Test fun parseValueParamCrossinlineDefault() {
+ val input = "crossinline kotlin/Function2<#A, #B, kotlin/Int> =..."
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()!!
+ assertThat(
+ valueParam.type.className.toString()
+ ).isEqualTo("kotlin/Function2")
+ assertThat(valueParam.hasDefaultArg).isTrue()
+ assertThat(valueParam.isCrossinline).isTrue()
+ assertThat(valueParam.isVararg).isFalse()
+ }
+
+ @Test
+ fun parseValueParamVararg() {
+ val input = "kotlin/Array<out kotlin/Pair<#A, #B>>..."
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()
+ assertThat(
+ valueParam?.type?.className?.toString()
+ ).isEqualTo("kotlin/Array")
+ assertThat(valueParam?.hasDefaultArg).isFalse()
+ assertThat(valueParam?.isCrossinline).isFalse()
+ assertThat(valueParam?.isVararg).isTrue()
+ }
+
+ @Test fun parseValueParametersWithTypeArgs() {
+ val input = "kotlin/Array<out #A>..."
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()
+ assertThat(valueParam?.type?.arguments).hasSize(1)
+ }
+
+ @Test fun parseValueParametersWithTwoTypeArgs() {
+ val input = "kotlin/Function1<kotlin/Double, kotlin/Boolean>)"
+ val cursor = Cursor(input)
+ val valueParam = cursor.parseValueParameter()
+ assertThat(valueParam?.type?.arguments).hasSize(2)
+ }
+
+ @Test fun parseValueParametersEmpty() {
+ val input = "() thing"
+ val cursor = Cursor(input)
+ val params = cursor.parseValueParameters()
+ assertThat(params).isEqualTo(emptyList<AbiValueParameter>())
+ assertThat(cursor.currentLine).isEqualTo("thing")
+ }
+
+ @Test fun parseValueParamsSimple() {
+ val input = "(kotlin/Function1<#A, kotlin/Boolean>)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()
+ assertThat(valueParams).hasSize(1)
+ }
+
+ @Test fun parseValueParamsTwoArgs() {
+ val input = "(#A1, kotlin/Function2<#A1, #A, #A1>)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()
+ assertThat(valueParams).hasSize(2)
+ assertThat(valueParams?.first()?.type?.tag).isEqualTo("A1")
+ }
+
+ @Test
+ fun parseValueParamsWithHasDefaultArg() {
+ val input = "(kotlin/Int =...)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()
+ assertThat(valueParams).hasSize(1)
+ assertThat(valueParams?.single()?.hasDefaultArg).isTrue()
+ }
+
+ @Test
+ fun parseValueParamsComplex2() {
+ val input = "(kotlin/Int, crossinline kotlin/Function2<#A, #B, kotlin/Int> =..., " +
+ "crossinline kotlin/Function1<#A, #B?> =..., " +
+ "crossinline kotlin/Function4<kotlin/Boolean, #A, #B, #B?, kotlin/Unit> =...)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()!!
+ assertThat(valueParams).hasSize(4)
+ assertThat(valueParams.first().type.className?.toString()).isEqualTo("kotlin/Int")
+ val rest = valueParams.subList(1, valueParams.size)
+ assertThat(rest).hasSize(3)
+ assertThat(rest.all { it.hasDefaultArg }).isTrue()
+ assertThat(rest.all { it.isCrossinline }).isTrue()
+ }
+
+ @Test fun parseValueParamsComplex3() {
+ val input = "(kotlin/Array<out kotlin/Pair<#A, #B>>...)"
+ val cursor = Cursor(input)
+ val valueParams = cursor.parseValueParameters()!!
+ assertThat(valueParams).hasSize(1)
+
+ assertThat(valueParams.single().isVararg).isTrue()
+ val type = valueParams.single().type
+ assertThat(type.className.toString()).isEqualTo("kotlin/Array")
+ }
+
+ @Test fun parseTypeParams() {
+ val input = "<#A1: kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParams = cursor.parseTypeParams()
+ assertThat(typeParams).hasSize(1)
+ val type = typeParams?.single()?.upperBounds?.single()
+ assertThat(typeParams?.single()?.tag).isEqualTo("A1")
+ assertThat(type?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(typeParams?.single()?.variance).isEqualTo(AbiVariance.INVARIANT)
+ }
+
+ @Test fun parseTypeParamsWithVariance() {
+ val input = "<#A1: out kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParams = cursor.parseTypeParams()
+ assertThat(typeParams).hasSize(1)
+ val type = typeParams?.single()?.upperBounds?.single()
+ assertThat(typeParams?.single()?.tag).isEqualTo("A1")
+ assertThat(type?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(typeParams?.single()?.variance).isEqualTo(AbiVariance.OUT)
+ }
+
+ @Test fun parseTypeParamsWithTwo() {
+ val input = "<#A: kotlin/Any?, #B: kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParams = cursor.parseTypeParams()
+ assertThat(typeParams).hasSize(2)
+ val type1 = typeParams?.first()?.upperBounds?.single()
+ val type2 = typeParams?.first()?.upperBounds?.single()
+ assertThat(typeParams?.first()?.tag).isEqualTo("A")
+ assertThat(typeParams?.last()?.tag).isEqualTo("B")
+ assertThat(type1?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type1?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ assertThat(type2?.className?.toString()).isEqualTo("kotlin/Any")
+ assertThat(type2?.nullability).isEqualTo(AbiTypeNullability.MARKED_NULLABLE)
+ }
+
+ @Test fun parseTypeParamsReifed() {
+ val input = "<#A1: reified kotlin/Any?>"
+ val cursor = Cursor(input)
+ val typeParam = cursor.parseTypeParams()?.single()
+ assertThat(typeParam).isNotNull()
+ assertThat(typeParam?.isReified).isTrue()
+ }
+
+ @Test
+ fun parseTypeArgs() {
+ val input = "<out #A>"
+ val cursor = Cursor(input)
+ val typeArgs = cursor.parseTypeArgs()
+ assertThat(typeArgs).hasSize(1)
+ val typeArg = typeArgs?.single()
+ assertThat(typeArg?.type?.tag).isEqualTo("A")
+ assertThat(typeArg?.variance).isEqualTo(AbiVariance.OUT)
+ }
+
+ @Test
+ fun parseTwoTypeArgs() {
+ val input = "<kotlin/Double, kotlin/Boolean>"
+ val cursor = Cursor(input)
+ val typeArgs = cursor.parseTypeArgs()
+ assertThat(typeArgs).hasSize(2)
+ assertThat(typeArgs?.first()?.type?.className?.toString()).isEqualTo("kotlin/Double")
+ assertThat(typeArgs?.last()?.type?.className?.toString()).isEqualTo("kotlin/Boolean")
+ }
+
+ @Test
+ fun parseTypeArgsWithNestedBrackets() {
+ val input = "<androidx.collection/ScatterMap<#A, #B>, androidx.collection/Other<#A, #B>>," +
+ " something else"
+ val cursor = Cursor(input)
+ val typeArgs = cursor.parseTypeArgs()
+ assertThat(typeArgs).hasSize(2)
+ assertThat(cursor.currentLine).isEqualTo(", something else")
+ }
+
+ @Test fun parseVarargSymbol() {
+ val input = "..."
+ val cursor = Cursor(input)
+ val vararg = cursor.parseVarargSymbol()
+ assertThat(vararg).isNotNull()
+ }
+
+ @Test fun parseTargets() {
+ val input = "Targets: [iosX64, linuxX64]"
+ val cursor = Cursor(input)
+ val targets = cursor.parseTargets()
+ assertThat(targets).containsExactly("linuxX64", "iosX64")
+ }
+}
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt
new file mode 100644
index 0000000..f37f6c5
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/CursorTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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 androidx.binarycompatibilityvalidator
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class CursorTest {
+
+ @Test
+ fun cursorShowsCurrentLine() {
+ val input = "one\ntwo\nthree"
+ val cursor = Cursor(input)
+ assertThat(cursor.currentLine).isEqualTo("one")
+ cursor.nextLine()
+ assertThat(cursor.currentLine).isEqualTo("two")
+ cursor.nextLine()
+ assertThat(cursor.currentLine).isEqualTo("three")
+ assertThat(cursor.hasNext()).isFalse()
+ }
+
+ @Test
+ fun cursorGetsNextWord() {
+ val input = "one two three"
+ val cursor = Cursor(input)
+ val word = cursor.parseWord()
+ assertThat(word).isEqualTo("one")
+ assertThat("two three").isEqualTo(cursor.currentLine)
+ }
+
+ @Test
+ fun parseValidIdentifierValid() {
+ val input = "oneTwo3 four"
+ val cursor = Cursor(input)
+ val symbol = cursor.parseValidIdentifier()
+ assertThat(symbol).isEqualTo("oneTwo3")
+ assertThat("four").isEqualTo(cursor.currentLine)
+ }
+
+ @Test
+ fun parseValidIdentifierValidStartsWithUnderscore() {
+ val input = "_one_Two3 four"
+ val cursor = Cursor(input)
+ val symbol = cursor.parseValidIdentifier()
+ assertThat(symbol).isEqualTo("_one_Two3")
+ assertThat("four").isEqualTo(cursor.currentLine)
+ }
+
+ @Test
+ fun parseValidIdentifierInvalid() {
+ val input = "1twothree"
+ val cursor = Cursor(input)
+ val symbol = cursor.parseValidIdentifier()
+ assertThat(symbol).isNull()
+ }
+
+ @Test
+ fun skipWhitespace() {
+ val input = " test"
+ val cursor = Cursor(input)
+ cursor.skipInlineWhitespace()
+ assertThat(cursor.currentLine).isEqualTo("test")
+ }
+
+ @Test
+ fun skipWhitespaceOnBlankLine() {
+ val input = ""
+ val cursor = Cursor(input)
+ cursor.skipInlineWhitespace()
+ assertThat(cursor.currentLine).isEqualTo("")
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 20624d7..8473061 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -405,6 +405,7 @@
includeProject(":benchmark:integration-tests:macrobenchmark", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:startup-benchmark", [BuildType.MAIN])
+includeProject(":binarycompatibilityvalidator:binarycompatibilityvalidator", [BuildType.MAIN])
includeProject(":biometric:biometric", [BuildType.MAIN])
includeProject(":biometric:biometric-ktx", [BuildType.MAIN])
includeProject(":biometric:biometric-ktx-samples", "biometric/biometric-ktx/samples", [BuildType.MAIN])