diff --git a/artifacts.json b/artifacts.json index 1026ffc690..ffaa8b6247 100644 --- a/artifacts.json +++ b/artifacts.json @@ -87,6 +87,22 @@ "packaging": "jar", "javaVersion": "11" }, + { + "gradlePath": ":modulecheck-name:api", + "group": "com.rickbusarow.modulecheck", + "artifactId": "modulecheck-name-api", + "description": "Fast dependency graph linting for Gradle projects", + "packaging": "jar", + "javaVersion": "11" + }, + { + "gradlePath": ":modulecheck-name:testing", + "group": "com.rickbusarow.modulecheck", + "artifactId": "modulecheck-name-testing", + "description": "Fast dependency graph linting for Gradle projects", + "packaging": "jar", + "javaVersion": "11" + }, { "gradlePath": ":modulecheck-parsing:android", "group": "com.rickbusarow.modulecheck", diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties index 6e8a73fe44..e99cde2004 100644 --- a/build-logic/gradle.properties +++ b/build-logic/gradle.properties @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.gradle.jvmargs=-Xmx12g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx12g -XX:MaxMetaspaceSize=4g -XX:+HeapDumpOnOutOfMemoryError org.gradle.daemon=true org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle.properties b/gradle.properties index 6e8a73fe44..e99cde2004 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.gradle.jvmargs=-Xmx12g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx12g -XX:MaxMetaspaceSize=4g -XX:+HeapDumpOnOutOfMemoryError org.gradle.daemon=true org.gradle.caching=true org.gradle.parallel=true diff --git a/modulecheck-name/api/api/api.api b/modulecheck-name/api/api/api.api new file mode 100644 index 0000000000..842973bfaf --- /dev/null +++ b/modulecheck-name/api/api/api.api @@ -0,0 +1,169 @@ +public final class modulecheck/name/AndroidDataBindingName : modulecheck/name/AndroidName, modulecheck/name/NameWithPackageName { + public synthetic fun (Lmodulecheck/name/AndroidResourceName;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getPackageName-A9AyiEE ()Ljava/lang/String; + public fun getSimpleNames ()Ljava/util/List; +} + +public abstract interface class modulecheck/name/AndroidName : modulecheck/name/HasSimpleNames, modulecheck/name/Name { + public static final field Companion Lmodulecheck/name/AndroidName$Companion; +} + +public final class modulecheck/name/AndroidName$Companion { + public final fun dataBinding-9PpwmVA (Lmodulecheck/name/UnqualifiedAndroidResourceName;Ljava/lang/String;)Lmodulecheck/name/AndroidDataBindingName; + public final fun qualifiedAndroidResource (Lmodulecheck/name/AndroidRName;Lmodulecheck/name/UnqualifiedAndroidResourceName;)Lmodulecheck/name/AndroidResourceNameWithRName; + public final fun r-p04zEWQ (Ljava/lang/String;)Lmodulecheck/name/AndroidRName; +} + +public final class modulecheck/name/AndroidRName : modulecheck/name/AndroidName, modulecheck/name/NameWithPackageName { + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getPackageName-A9AyiEE ()Ljava/lang/String; + public fun getSimpleNames ()Ljava/util/List; +} + +public abstract interface class modulecheck/name/AndroidResourceName : modulecheck/name/AndroidName, modulecheck/name/HasSimpleNames, modulecheck/name/Name { + public abstract fun getIdentifier-yyTc5LE ()Ljava/lang/String; + public abstract fun getPrefix-yyTc5LE ()Ljava/lang/String; +} + +public final class modulecheck/name/AndroidResourceNameWithRName : modulecheck/name/AndroidResourceName, modulecheck/name/NameWithPackageName { + public fun (Lmodulecheck/name/AndroidRName;Lmodulecheck/name/UnqualifiedAndroidResourceName;)V + public final fun getAndroidRName ()Lmodulecheck/name/AndroidRName; + public fun getIdentifier-yyTc5LE ()Ljava/lang/String; + public fun getPackageName-A9AyiEE ()Ljava/lang/String; + public fun getPrefix-yyTc5LE ()Ljava/lang/String; + public final fun getResourceName ()Lmodulecheck/name/UnqualifiedAndroidResourceName; + public fun getSimpleNames ()Ljava/util/List; +} + +public abstract interface class modulecheck/name/HasPackageName { + public abstract fun getPackageName-A9AyiEE ()Ljava/lang/String; +} + +public abstract interface class modulecheck/name/HasSimpleNames { + public static final field Companion Lmodulecheck/name/HasSimpleNames$Companion; + public abstract fun getSimpleNames ()Ljava/util/List; + public fun getSimplestName-yyTc5LE ()Ljava/lang/String; +} + +public final class modulecheck/name/HasSimpleNames$Companion { +} + +public abstract interface class modulecheck/name/Name : java/io/Serializable, java/lang/Comparable { + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun compareTo (Lmodulecheck/name/Name;)I + public abstract fun getAsString ()Ljava/lang/String; + public abstract fun getSegments ()Ljava/util/List; + public fun getSimpleName-yyTc5LE ()Ljava/lang/String; + public fun getSimpleNameString ()Ljava/lang/String; +} + +public abstract interface class modulecheck/name/NameWithPackageName : modulecheck/name/HasPackageName, modulecheck/name/HasSimpleNames, modulecheck/name/ResolvableName { + public static final field Companion Lmodulecheck/name/NameWithPackageName$Companion; + public fun getAsString ()Ljava/lang/String; + public fun getSegments ()Ljava/util/List; + public fun isTopLevel ()Z +} + +public final class modulecheck/name/NameWithPackageName$Companion { + public final fun invoke-Q6FKjUA (Ljava/lang/String;Ljava/util/List;)Lmodulecheck/name/NameWithPackageName; +} + +public final class modulecheck/name/NameWithPackageNameKt { + public static final fun asNameWithPackageName-23wzK4s (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static final fun asNameWithPackageName-9PpwmVA (Ljava/lang/Iterable;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static final fun asNameWithPackageName-9PpwmVA (Lorg/jetbrains/kotlin/name/FqName;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; +} + +public final class modulecheck/name/PackageName : modulecheck/name/Name { + public static final field Companion Lmodulecheck/name/PackageName$Companion; + public static final fun append-f585xGA (Ljava/lang/String;Ljava/lang/Iterable;)Ljava/lang/String; + public static final fun appendAsString-impl (Ljava/lang/String;Ljava/lang/Iterable;)Ljava/lang/String; + public static final synthetic fun box-impl (Ljava/lang/String;)Lmodulecheck/name/PackageName; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z + public fun getAsString ()Ljava/lang/String; + public fun getSegments ()Ljava/util/List; + public static fun getSegments-impl (Ljava/lang/String;)Ljava/util/List; + public fun hashCode ()I + public static fun hashCode-impl (Ljava/lang/String;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Ljava/lang/String; +} + +public final class modulecheck/name/PackageName$Companion { + public final fun asPackageName-f585xGA (Ljava/lang/String;)Ljava/lang/String; + public final fun getDEFAULT-A9AyiEE ()Ljava/lang/String; + public final fun invoke-f585xGA (Ljava/lang/String;)Ljava/lang/String; +} + +public final class modulecheck/name/PackageNameKt { + public static final fun append-Q6FKjUA (Ljava/lang/String;Ljava/lang/Iterable;)Ljava/lang/String; + public static final fun appendAsString-Q6FKjUA (Ljava/lang/String;Ljava/lang/Iterable;)Ljava/lang/String; + public static final fun appendAsString-Q6FKjUA (Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String; +} + +public abstract interface class modulecheck/name/ResolvableName : modulecheck/name/Name { +} + +public final class modulecheck/name/SimpleName : modulecheck/name/Name { + public static final field Companion Lmodulecheck/name/SimpleName$Companion; + public static final synthetic fun box-impl (Ljava/lang/String;)Lmodulecheck/name/SimpleName; + public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String; + public fun equals (Ljava/lang/Object;)Z + public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z + public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z + public fun getAsString ()Ljava/lang/String; + public fun getSegments ()Ljava/util/List; + public static fun getSegments-impl (Ljava/lang/String;)Ljava/util/List; + public fun getSimpleName-yyTc5LE ()Ljava/lang/String; + public static fun getSimpleName-yyTc5LE (Ljava/lang/String;)Ljava/lang/String; + public fun getSimpleNameString ()Ljava/lang/String; + public static fun getSimpleNameString-impl (Ljava/lang/String;)Ljava/lang/String; + public fun hashCode ()I + public static fun hashCode-impl (Ljava/lang/String;)I + public fun toString ()Ljava/lang/String; + public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String; + public final synthetic fun unbox-impl ()Ljava/lang/String; +} + +public final class modulecheck/name/SimpleName$Companion { + public final fun asSimpleName-d3qGUlM (Ljava/lang/String;)Ljava/lang/String; + public final fun asString (Ljava/util/List;)Ljava/lang/String; + public final fun stripPackageNameFromFqName-9PpwmVA (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List; +} + +public final class modulecheck/name/UnqualifiedAndroidResourceName : modulecheck/name/AndroidResourceName { + public static final field Companion Lmodulecheck/name/UnqualifiedAndroidResourceName$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getAsString ()Ljava/lang/String; + public fun getIdentifier-yyTc5LE ()Ljava/lang/String; + public fun getPrefix-yyTc5LE ()Ljava/lang/String; + public fun getSegments ()Ljava/util/List; + public fun getSimpleNames ()Ljava/util/List; + public final fun toAndroidNameWithRName (Lmodulecheck/name/AndroidRName;)Lmodulecheck/name/AndroidResourceNameWithRName; +} + +public final class modulecheck/name/UnqualifiedAndroidResourceName$Companion { + public final fun anim-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun animator-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun array-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun bool-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun color-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun dimen-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun drawable-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun font-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun fromFile (Ljava/io/File;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun fromValuePair (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun fromXmlString (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun id-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun integer-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun layout-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun menu-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun mipmap-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun raw-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun string-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; + public final fun style-tQbDKes (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; +} + diff --git a/modulecheck-name/api/build.gradle.kts b/modulecheck-name/api/build.gradle.kts new file mode 100644 index 0000000000..b95fbd9cf5 --- /dev/null +++ b/modulecheck-name/api/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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. + */ + +plugins { + id("mcbuild") +} + +mcbuild { + published( + artifactId = "modulecheck-name-api" + ) +} + +dependencies { + + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.coroutines.jvm) + + implementation(project(path = ":modulecheck-utils:lazy")) + implementation(project(path = ":modulecheck-utils:stdlib")) + + testImplementation(libs.bundles.junit) + testImplementation(libs.bundles.kotest) + + testImplementation(project(path = ":modulecheck-internal-testing")) + testImplementation(project(path = ":modulecheck-name:testing")) + testImplementation(project(path = ":modulecheck-parsing:source:api")) +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidDataBindingName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidDataBindingName.kt new file mode 100644 index 0000000000..5c848b4ae8 --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidDataBindingName.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import modulecheck.name.HasSimpleNames.Companion.checkSimpleNames +import modulecheck.name.SimpleName.Companion.asSimpleName +import modulecheck.utils.capitalize +import modulecheck.utils.lazy.unsafeLazy + +/** example: `com.example.databinding.FragmentListBinding` */ +class AndroidDataBindingName( + sourceLayout: AndroidResourceName, + override val packageName: PackageName +) : NameWithPackageName, AndroidName { + + override val simpleNames: List by unsafeLazy { + + val simpleBindingName = sourceLayout.identifier.asString + .split("_") + .joinToString("") { it.capitalize() } + .plus("Binding") + .asSimpleName() + + listOf( + "databinding".asSimpleName(), + simpleBindingName + ) + } + + init { + checkSimpleNames() + } +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidName.kt new file mode 100644 index 0000000000..4dcc11beea --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidName.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import modulecheck.name.SimpleName.Companion.asSimpleName + +/** + * - fully qualified generated resources like `com.example.R.string.app_name` + * - generated data-/view-binding declarations like `com.example.databinding.FragmentListBinding` + * - unqualified resources which can be consumed in downstream projects, like `R.string.app_name` + * - R names, like `com.example.R` + */ +sealed interface AndroidName : Name, HasSimpleNames { + + companion object { + /** @return example: `com.example.app.R` */ + fun r(packageName: PackageName): AndroidRName = AndroidRName(packageName) + + /** @return `com.example.R.string.app_name` */ + fun qualifiedAndroidResource( + sourceR: AndroidRName, + sourceResource: UnqualifiedAndroidResourceName + ): AndroidResourceNameWithRName = AndroidResourceNameWithRName( + androidRName = sourceR, + resourceName = sourceResource + ) + + /** @return `com.example.databinding.FragmentListBinding` */ + fun dataBinding( + sourceLayout: UnqualifiedAndroidResourceName, + packageName: PackageName + ): AndroidDataBindingName = AndroidDataBindingName( + sourceLayout = sourceLayout, + packageName = packageName + ) + } +} + +/** example: `com.example.app.R` */ +class AndroidRName( + override val packageName: PackageName +) : NameWithPackageName, AndroidName { + + override val simpleNames: List by lazy { listOf("R".asSimpleName()) } +} + +/** + * Models fully qualified names like `com.example.R.string.app_name` + * or unqualified ones like `string.app_name`. + */ +sealed interface AndroidResourceName : AndroidName, Name, HasSimpleNames { + /** example: 'string' in `R.string.app_name` */ + val prefix: SimpleName + + /** example: 'app_name' in `R.string.app_name` */ + val identifier: SimpleName +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidResourceNameWithRName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidResourceNameWithRName.kt new file mode 100644 index 0000000000..60bd358995 --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/AndroidResourceNameWithRName.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import modulecheck.utils.lazy.unsafeLazy + +/** + * example: `com.example.R.string.app_name` + * + * @property androidRName the R declaration used when AGP generates this fully qualified resource + * @property resourceName the resource declaration, like `string.app_name` + */ +class AndroidResourceNameWithRName( + val androidRName: AndroidRName, + val resourceName: UnqualifiedAndroidResourceName +) : NameWithPackageName, AndroidResourceName { + override val packageName: PackageName + get() = androidRName.packageName + + override val simpleNames: List by unsafeLazy { + androidRName.simpleNames + resourceName.simpleNames + } + override val prefix: SimpleName get() = resourceName.prefix + override val identifier: SimpleName get() = resourceName.identifier +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/Name.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/Name.kt new file mode 100644 index 0000000000..321adc2cea --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/Name.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import modulecheck.name.SimpleName.Companion.asSimpleName +import java.io.Serializable + +/** + * Fundamentally, this is a version of `Name` or `FqName` (such as Kotlin's + * [Name][org.jetbrains.kotlin.name.Name] and [FqName][org.jetbrains.kotlin.name.FqName]) + * with syntactic sugar for complex matching requirements. + */ +sealed interface Name : Comparable, Serializable { + + /** The raw String value of this name, such as `com.example.lib1.Lib1Class`. */ + val asString: String + + /** ex: 'com.example.Subject' has the segments ['com', 'example', 'Subject'] */ + val segments: List + + /** The simplest name. For an inner class like `com.example.Outer.Inner`, this will be 'Inner'. */ + val simpleName: SimpleName + get() = segments.last().asSimpleName() + + /** The simplest name. For an inner class like `com.example.Outer.Inner`, this will be 'Inner'. */ + val simpleNameString: String + get() = segments.last() + + override fun compareTo(other: Name): Int { + return compareValuesBy(this, other, { it.asString }, { it::class.qualifiedName }) + } +} + +sealed interface TypeName : Name, HasSimpleNames { + val nullable: Boolean + + fun copy(nullable: Boolean): TypeName +} + +class ClassName( + override val packageName: PackageName, + override val simpleNames: List, + override val nullable: Boolean +) : TypeName, NameWithPackageName { + override fun copy(nullable: Boolean): ClassName { + return ClassName( + packageName = packageName, + simpleNames = simpleNames, + nullable = nullable + ) + } +} + +class ParameterizedTypeName( + val rawType: ClassName, + val typeParameters: List +) : TypeName, NameWithPackageName { + + override val packageName: PackageName get() = rawType.packageName + override val simpleNames: List get() = rawType.simpleNames + override val nullable: Boolean get() = rawType.nullable + + override fun copy(nullable: Boolean): ParameterizedTypeName { + return ParameterizedTypeName( + rawType = rawType.copy(nullable), + typeParameters = typeParameters + ) + } +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/NameWithPackageName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/NameWithPackageName.kt new file mode 100644 index 0000000000..29ab6023de --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/NameWithPackageName.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import modulecheck.name.HasSimpleNames.Companion.checkSimpleNames +import modulecheck.name.SimpleName.Companion.asString +import modulecheck.name.SimpleName.Companion.stripPackageNameFromFqName +import modulecheck.utils.asList +import modulecheck.utils.lazy.unsafeLazy +import modulecheck.utils.singletonList +import org.jetbrains.kotlin.name.FqName + +/** Represents a "declaration" -- a named object which can be referenced elsewhere. */ +sealed interface NameWithPackageName : + HasPackageName, + HasSimpleNames, + ResolvableName { + + override val asString: String + get() = packageName.appendAsString(simpleNames.asString()) + + override val segments: List + get() = asString.split('.') + + /** + * `true` if a declaration is top-level in a file, otherwise `false` + * such as if the declaration is a nested type or a member declaration + */ + val isTopLevel: Boolean + get() = simpleNames.size == 1 + + companion object { + /** */ + operator fun invoke( + packageName: PackageName, + simpleNames: List + ): NameWithPackageName = NameWithPackageNameImpl(packageName, simpleNames) + } +} + +internal data class NameWithPackageNameImpl( + override val packageName: PackageName, + override val simpleNames: List +) : NameWithPackageName { + init { + checkSimpleNames() + } + + override val segments: List by unsafeLazy { asString.split('.') } +} + +/** + * @return a [NameWithPackageName], where the String after [packageName] + * is split and treated as the collection of [SimpleNames][SimpleName]. + */ +fun FqName.asNameWithPackageName(packageName: PackageName): NameWithPackageName = asString() + .stripPackageNameFromFqName(packageName) + .asNameWithPackageName(packageName) + +/** + * @return a [NameWithPackageName] from the [packageName] + * argument, appending the receiver [SimpleNames][SimpleName] + */ +fun Iterable.asNameWithPackageName(packageName: PackageName): NameWithPackageName { + return NameWithPackageNameImpl(packageName, this.asList()) +} + +/** + * @return a [NameWithPackageName] from the [packageName] + * argument, appending the receiver [SimpleNames][SimpleName] + */ +fun SimpleName.asNameWithPackageName(packageName: PackageName): NameWithPackageName { + return singletonList().asNameWithPackageName(packageName) +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/PackageName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/PackageName.kt new file mode 100644 index 0000000000..b9b1f6fb81 --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/PackageName.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import modulecheck.name.PackageName.Companion.asPackageName +import modulecheck.name.SimpleName.Companion.asSimpleName + +/** + * Represents a package name. + * + * Note that a java/kotlin file without a package declaration will have a `null` _declaration_, but + * it still has a "default" package. Files with a default package should use [PackageName.DEFAULT]. + * + * @property asString the full name of this package + * @see Name + * @throws IllegalArgumentException if the [asString] parameter is empty or blank + */ +@JvmInline +value class PackageName private constructor( + override val asString: String +) : Name { + + /** + * Safe function for appending a simple name to the "end" of a package name. + * + * If the package name is default/empty, this function will + * return just the simple name without a preceding period. + * + * If the package name is not blank, this function will append + * a period to the package name, then add the simple name. + */ + fun appendAsString(simpleNames: Iterable): String { + return "$asString.${simpleNames.joinToString(".") { it.asString }}" + } + + /** + * Safe function for appending a simple name to the "end" of a package name. + * + * If the package name is default/empty, this function will + * return just the simple name without a preceding period. + * + * If the package name is not blank, this function will append + * a period to the package name, then add the simple name. + */ + fun append(simpleNames: Iterable): PackageName { + return appendAsString(simpleNames).asPackageName() + } + + override val segments: List + get() = asString.split('.') + + companion object { + + /** + * Represents a [PackageName] when there isn't actually a package name, meaning that + * top-level declarations in that file are at the root of source without qualifiers. + * + * @see Name + * @see DEFAULT + */ + val DEFAULT = PackageName("") + + /** Shorthand for calling [PackageName.invoke] in-line. */ + fun String?.asPackageName(): PackageName = PackageName(this) + + /** + * Shorthand for calling [PackageName.invoke] in-line. + * + * @return A `PackageName` wrapper around [nameOrNull]. If [nameOrNull] + * is null or blank, this will return [PackageName.DEFAULT]. + */ + operator fun invoke(nameOrNull: String?): PackageName { + return when { + nameOrNull.isNullOrBlank() -> DEFAULT + else -> PackageName(nameOrNull) + } + } + } +} + +/** Convenience interface for providing a [PackageName]. */ +interface HasPackageName { + val packageName: PackageName +} + +/** + * Safe function for appending a simple name to the "end" of a package name. + * + * If the package name is default/empty, this function will + * return just the simple name without a preceding period. + * + * If the package name is not blank, this function will append + * a period to the package name, then add the simple name. + */ +fun PackageName.appendAsString(simpleNames: Iterable): String { + return appendAsString(simpleNames.map { it.asSimpleName() }) +} + +/** + * Safe function for appending a simple name to the "end" of a package name. + * + * If the package name is default/empty, this function will + * return just the simple name without a preceding period. + * + * If the package name is not blank, this function will append + * a period to the package name, then add the simple name. + */ +fun PackageName.append(simpleNames: Iterable): PackageName { + return appendAsString(simpleNames).asPackageName() +} + +/** + * Safe function for appending a simple name to the "end" of a package name. + * + * If the package name is default/empty, this function will + * return just the simple name without a preceding period. + * + * If the package name is not blank, this function will append + * a period to the package name, then add the simple name. + */ +fun PackageName.appendAsString(vararg simpleNames: String): String { + return appendAsString(simpleNames.toList()) +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/ResolvableName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/ResolvableName.kt new file mode 100644 index 0000000000..38c8795a6c --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/ResolvableName.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +/** An [Name] which has the potential to be resolved -- meaning any [NameWithPackageName] */ +sealed interface ResolvableName : Name diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/SimpleName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/SimpleName.kt new file mode 100644 index 0000000000..5d567e0cb4 --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/SimpleName.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +/** + * A name which is not fully qualified, like `Foo` in `com.example.Foo` + * + * @property asString the string value of this name + */ +@JvmInline +value class SimpleName(override val asString: String) : Name { + + init { + require(asString.matches("""^([a-zA-Z_$][a-zA-Z\d_$]*)|(`.*`)$""".toRegex())) { + "SimpleName names must be valid Java identifier " + + "without a dot qualifier or whitespace. This name was: `$asString`" + } + } + + override val segments: List + get() = listOf(asString) + + override val simpleName: SimpleName + get() = this + + override val simpleNameString: String + get() = asString + + companion object { + /** shorthand for `joinToString(".") { it.name.trim() }` */ + fun List.asString(): String = joinToString(".") { it.asString.trim() } + + /** wraps this String in a [SimpleName] */ + fun String.asSimpleName(): SimpleName = SimpleName(this) + + /** + * Removes the prefix of [packageName]'s value and a subsequent period, + * then splits the remainder by dots and returns that list as [SimpleName] + * + * example: `com.example.Outer.Inner` becomes `[Outer, Inner]` + */ + fun String.stripPackageNameFromFqName(packageName: PackageName): List { + return removePrefix("${packageName.asString}.").split('.') + .map { it.asSimpleName() } + } + } +} + +/** Convenience interface for providing a [SimpleName]. */ +interface HasSimpleNames { + /** The contained [SimpleNames][SimpleName] */ + val simpleNames: List + + /** + * If the collection in [simpleNames] has more than one name, this value will be the last. + * + * example: Given a full name of `com.example.Outer.Inner`, with + * the [simpleNames] `[Outer, Inner]`, this value will be `Inner`. + */ + val simplestName: SimpleName + get() = simpleNames.last() + + companion object { + + internal fun HasSimpleNames.checkSimpleNames() { + check(simpleNames.isNotEmpty()) { + "`simpleNames` must have at least one name, but this list is empty." + } + } + } +} diff --git a/modulecheck-name/api/src/main/kotlin/modulecheck/name/UnqualifiedAndroidResourceName.kt b/modulecheck-name/api/src/main/kotlin/modulecheck/name/UnqualifiedAndroidResourceName.kt new file mode 100644 index 0000000000..2f77859e4e --- /dev/null +++ b/modulecheck-name/api/src/main/kotlin/modulecheck/name/UnqualifiedAndroidResourceName.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import modulecheck.name.SimpleName.Companion.asSimpleName +import modulecheck.name.SimpleName.Companion.asString +import modulecheck.utils.lazy.unsafeLazy +import java.io.File +import kotlin.io.path.name + +/** + * example: `string.app_name` + * + * @property prefix 'string' in `R.string.app_name` + * @property identifier 'app_name' in `R.string.app_name` + */ +class UnqualifiedAndroidResourceName private constructor( + override val prefix: SimpleName, + override val identifier: SimpleName +) : AndroidResourceName { + + override val simpleNames: List by unsafeLazy { + listOf("R".asSimpleName(), prefix, identifier) + } + override val segments: List by unsafeLazy { simpleNames.map { it.asString } } + override val asString: String by unsafeLazy { simpleNames.asString() } + + /** + * @return the fully qualified name of a generated Android resource, like + * `com.example.R.string.app_name` from the combination of `com.example.R` and `string.app_name` + */ + fun toAndroidResourceNameWithRName(androidRDeclaration: AndroidRName): AndroidResourceNameWithRName { + return AndroidName.qualifiedAndroidResource( + sourceR = AndroidRName(androidRDeclaration.packageName), + sourceResource = this + ) + } + + companion object { + + /** `anim.foo` */ + fun anim(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("anim".asSimpleName(), identifier = identifier) + + /** `animator.foo` */ + fun animator(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("animator".asSimpleName(), identifier = identifier) + + /** `array.foo` */ + fun array(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("array".asSimpleName(), identifier = identifier) + + /** `bool.foo` */ + fun bool(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("bool".asSimpleName(), identifier = identifier) + + /** `color.foo` */ + fun color(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("color".asSimpleName(), identifier = identifier) + + /** `dimen.foo` */ + fun dimen(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("dimen".asSimpleName(), identifier = identifier) + + /** `drawable.foo` */ + fun drawable(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("drawable".asSimpleName(), identifier = identifier) + + /** `font.foo` */ + fun font(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("font".asSimpleName(), identifier = identifier) + + /** `id.foo` */ + fun id(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("id".asSimpleName(), identifier = identifier) + + /** `integer.foo` */ + fun integer(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("integer".asSimpleName(), identifier = identifier) + + /** `layout.foo` */ + fun layout(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("layout".asSimpleName(), identifier = identifier) + + /** `menu.foo` */ + fun menu(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("menu".asSimpleName(), identifier = identifier) + + /** `mipmap.foo` */ + fun mipmap(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("mipmap".asSimpleName(), identifier = identifier) + + /** `raw.foo` */ + fun raw(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("raw".asSimpleName(), identifier = identifier) + + /** `string.foo` */ + fun string(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("string".asSimpleName(), identifier = identifier) + + /** `style.foo` */ + fun style(identifier: SimpleName): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName("style".asSimpleName(), identifier = identifier) + + /** @return all resources declared within the given [file] */ + fun fromFile(file: File): UnqualifiedAndroidResourceName? { + val dir = file.toPath().parent?.name ?: return null + val name = file.nameWithoutExtension + + return when { + dir.startsWith("anim") -> anim(name.asSimpleName()) + dir.startsWith("animator") -> animator(name.asSimpleName()) + dir.startsWith("color") -> color(name.asSimpleName()) + dir.startsWith("dimen") -> dimen(name.asSimpleName()) + dir.startsWith("drawable") -> drawable(name.asSimpleName()) + dir.startsWith("font") -> font(name.asSimpleName()) + dir.startsWith("layout") -> layout(name.asSimpleName()) + dir.startsWith("menu") -> menu(name.asSimpleName()) + dir.startsWith("mipmap") -> mipmap(name.asSimpleName()) + dir.startsWith("raw") -> raw(name.asSimpleName()) + else -> null + } + } + + /** @return `id.foo` for [type] `id` and [name] `foo` */ + fun fromValuePair(type: String, name: String): UnqualifiedAndroidResourceName? { + val fixedName = name.replace('.', '_') + return when (type.removePrefix("android:")) { + "anim" -> anim(fixedName.asSimpleName()) + "animator" -> animator(fixedName.asSimpleName()) + "array" -> array(fixedName.asSimpleName()) + "bool" -> bool(fixedName.asSimpleName()) + "color" -> color(fixedName.asSimpleName()) + "dimen" -> dimen(fixedName.asSimpleName()) + "drawable" -> drawable(fixedName.asSimpleName()) + "font" -> font(fixedName.asSimpleName()) + "id" -> id(fixedName.asSimpleName()) + "integer" -> integer(fixedName.asSimpleName()) + "integer-array" -> array(fixedName.asSimpleName()) + "layout" -> layout(fixedName.asSimpleName()) + "menu" -> menu(fixedName.asSimpleName()) + "mipmap" -> mipmap(fixedName.asSimpleName()) + "raw" -> raw(fixedName.asSimpleName()) + "string" -> string(fixedName.asSimpleName()) + "style" -> style(fixedName.asSimpleName()) + else -> null + } + } + + private val REGEX = """"?@\+?(.*)/(.*)"?""".toRegex() + + /** @return a resource declaration from a string in XML, like `@+id/______` */ + fun fromXmlString(str: String): UnqualifiedAndroidResourceName? { + val (prefix, name) = REGEX.find(str)?.destructured ?: return null + + return fromValuePair(prefix, name) + } + } +} diff --git a/modulecheck-name/api/src/test/kotlin/modulecheck/name/AsNameWithPackageBaseNameTest.kt b/modulecheck-name/api/src/test/kotlin/modulecheck/name/AsNameWithPackageBaseNameTest.kt new file mode 100644 index 0000000000..2526ef1b3e --- /dev/null +++ b/modulecheck-name/api/src/test/kotlin/modulecheck/name/AsNameWithPackageBaseNameTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2021-2024 Rick Busarow + * 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 modulecheck.name + +import io.kotest.matchers.shouldBe +import modulecheck.name.PackageName.Companion.asPackageName +import modulecheck.name.SimpleName.Companion.asSimpleName +import org.jetbrains.kotlin.name.FqName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +internal class AsNameWithPackageBaseNameTest { + + @Nested + inner class `FqName` { + + @Test + fun `FqName asDeclaredName with nested type treats outer type as simple name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Outer".asSimpleName(), "Inner".asSimpleName()) + + val asString = packageName.appendAsString(simpleNames.map { it.asString }) + + FqName(asString).asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + + @Test + fun `asNameWithPackageName with no language creates agnostic declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + val asString = packageName.appendAsString(simpleNames.map { it.asString }) + + FqName(asString).asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + + @Test + fun `asNameWithPackageName with Kotlin language creates Kotlin declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + val asString = packageName.appendAsString(simpleNames.map { it.asString }) + + FqName(asString).asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + + @Test + fun `asNameWithPackageName with Java language creates Java declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + val asString = packageName.appendAsString(simpleNames.map { it.asString }) + + FqName(asString).asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + + @Test + fun `asNameWithPackageName with Java and Kotlin languages creates agnostic declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + val asString = packageName.appendAsString(simpleNames.map { it.asString }) + + FqName(asString).asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + } + + @Nested + inner class `iterable receiver` { + + @Test + fun `asNameWithPackageName with no language creates agnostic declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + simpleNames.asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + + @Test + fun `asNameWithPackageName with Kotlin language creates Kotlin declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + simpleNames.asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + + @Test + fun `asNameWithPackageName with Java language creates Java declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + simpleNames.asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + + @Test + fun `asNameWithPackageName with Java and Kotlin languages creates agnostic declared name`() { + + val packageName = "com.test".asPackageName() + val simpleNames = listOf("Subject".asSimpleName()) + + simpleNames.asNameWithPackageName(packageName) shouldBe NameWithPackageNameImpl( + packageName = packageName, + simpleNames = simpleNames + ) + } + } +} diff --git a/modulecheck-name/api/src/test/kotlin/modulecheck/name/NameTest.kt b/modulecheck-name/api/src/test/kotlin/modulecheck/name/NameTest.kt new file mode 100644 index 0000000000..6f172bbca4 --- /dev/null +++ b/modulecheck-name/api/src/test/kotlin/modulecheck/name/NameTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021-2024 Rick Busarow + * 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 modulecheck.name + +import modulecheck.name.SimpleName.Companion.asSimpleName +import modulecheck.name.testing.BaseNameTest +import org.junit.jupiter.api.Test + +class NameTest : BaseNameTest { + + @Test + fun `sorting should be by name first, then the name of the Name class`() { + val packageNames = listOf("a", "b", "c", "d") + val simpleNames = listOf("X", "Y", "Z") + + val instances = packageNames + .reversed() + .flatMap { packageName -> + simpleNames + .reversed() + .flatMap { simpleName -> + + listOf( + NameWithPackageName( + PackageName(packageName), + listOf(simpleName.asSimpleName()) + ), + NameWithPackageName( + PackageName(packageName), + listOf(simpleName.asSimpleName()) + ), + NameWithPackageName( + PackageName(packageName), + listOf(simpleName.asSimpleName()) + ), + AndroidRName(PackageName(packageName)) + ) + } + } + .shuffled() + // Android R names will be duplicated, so clean those up + .distinctBy { it.asString to it::class } + + val prettySorted = instances.sorted() + .joinToString("\n") { "${it::class.java.simpleName.padStart(28)} ${it.asString}" } + + prettySorted shouldBe """ + AndroidRName a.R + NameWithPackageNameImpl a.X + NameWithPackageNameImpl a.Y + NameWithPackageNameImpl a.Z + AndroidRName b.R + NameWithPackageNameImpl b.X + NameWithPackageNameImpl b.Y + NameWithPackageNameImpl b.Z + AndroidRName c.R + NameWithPackageNameImpl c.X + NameWithPackageNameImpl c.Y + NameWithPackageNameImpl c.Z + AndroidRName d.R + NameWithPackageNameImpl d.X + NameWithPackageNameImpl d.Y + NameWithPackageNameImpl d.Z + """.trimIndent() + } +} diff --git a/modulecheck-name/api/src/test/kotlin/modulecheck/name/SimpleNameTest.kt b/modulecheck-name/api/src/test/kotlin/modulecheck/name/SimpleNameTest.kt new file mode 100644 index 0000000000..deb4e9ffee --- /dev/null +++ b/modulecheck-name/api/src/test/kotlin/modulecheck/name/SimpleNameTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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 modulecheck.name + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrowWithMessage +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import modulecheck.parsing.source.PackageName +import modulecheck.parsing.source.SimpleName +import modulecheck.testing.forAllBlocking +import org.junit.jupiter.api.Test + +internal class SimpleNameTest { + + @Test + fun `any valid name string just becomes wrapped by the class`() { + + Arb.stringPattern("""^([a-zA-Z_$][a-zA-Z\d_$]*)|(`.*`)$""") + .forAllBlocking { name -> + + shouldNotThrow { + SimpleName(name) + } + } + } + + @Test + fun `a name with whitespaces is allowed if wrapped in backticks`() { + + val name = "`a name with whitespaces is allowed if wrapped in backticks`" + + shouldNotThrow { + SimpleName(name).name shouldBe name + } + } + + @Test + fun `a name with a white space throws exception with message`() { + + Arb.stringPattern("""\s+""") + .forAllBlocking { name -> + + shouldThrowWithMessage( + "SimpleName names must be valid Java identifier " + + "without a dot qualifier or whitespace. This name was: `$name`" + ) { + SimpleName(name) + } + } + } + + @Test + fun `a name with a dot throws exception with message`() { + + Arb.stringPattern("""\.+""") + .forAllBlocking { name -> + + shouldThrowWithMessage( + "SimpleName names must be valid Java identifier " + + "without a dot qualifier or whitespace. This name was: `$name`" + ) { + SimpleName(name) + } + } + } + + @Test + fun `an empty name throws exception with message`() { + + shouldThrowWithMessage( + "SimpleName names must be valid Java identifier " + + "without a dot qualifier or whitespace. This name was: ``" + ) { + SimpleName("") + } + } + + @Test + fun `a blank name throws exception with message`() { + + Arb.stringPattern("\\s*") + .forAllBlocking { name -> + + shouldThrowWithMessage( + "SimpleName names must be valid Java identifier " + + "without a dot qualifier or whitespace. This name was: `$name`" + ) { + SimpleName(name) + } + } + } + + @Test + fun `an empty package name becomes DEFAULT`() { + + PackageName("") shouldBe PackageName.DEFAULT + } + + @Test + fun `a blank package name becomes DEFAULT`() { + + Arb.stringPattern("\\s*") + .forAllBlocking { name -> + + PackageName(name) shouldBe PackageName.DEFAULT + } + } +} diff --git a/modulecheck-name/testing/api/testing.api b/modulecheck-name/testing/api/testing.api new file mode 100644 index 0000000000..ffc3cbe98d --- /dev/null +++ b/modulecheck-name/testing/api/testing.api @@ -0,0 +1,63 @@ +public abstract interface class NameTest : modulecheck/testing/assert/TrimmedAsserts { + public fun agnostic-9PpwmVA (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static synthetic fun agnostic-9PpwmVA$default (LNameTest;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/NameWithPackageName; + public fun androidR-p04zEWQ (Ljava/lang/String;)Lmodulecheck/name/AndroidRName; + public static synthetic fun androidR-p04zEWQ$default (LNameTest;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/AndroidRName; + public fun java-9PpwmVA (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static synthetic fun java-9PpwmVA$default (LNameTest;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/NameWithPackageName; + public fun kotlin-9PpwmVA (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static synthetic fun kotlin-9PpwmVA$default (LNameTest;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/NameWithPackageName; + public fun shouldBe (Ljava/util/Collection;Ljava/util/Collection;)V + public fun shouldBe (Ljava/util/List;Ljava/util/Collection;)V + public fun shouldBe (Lmodulecheck/utils/lazy/LazyDeferred;Ljava/util/Collection;)V + public fun shouldBe (Lmodulecheck/utils/lazy/LazySet;Ljava/util/Collection;)V + public fun shouldBeJvmFile (Lmodulecheck/parsing/source/JvmFile;Lkotlin/jvm/functions/Function1;)V +} + +public final class NameTest$JvmFileBuilder { + public fun ()V + public final fun apiReferences (Lkotlin/jvm/functions/Function1;)V + public final fun declarations (Lkotlin/jvm/functions/Function1;)V + public final fun getApiNames ()Ljava/util/List; + public final fun getDeclarations ()Ljava/util/List; + public final fun getNames ()Ljava/util/List; + public final fun references (Lkotlin/jvm/functions/Function1;)V +} + +public final class NameTest$JvmFileBuilder$ApiReferenceBuilder : NameTest$JvmFileBuilder$ReferenceBuilder { + public fun (LNameTest$JvmFileBuilder;)V +} + +public final class NameTest$JvmFileBuilder$DeclarationsBuilder { + public fun (LNameTest$JvmFileBuilder;)V + public final fun agnostic-9PpwmVA (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static synthetic fun agnostic-9PpwmVA$default (LNameTest$JvmFileBuilder$DeclarationsBuilder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/NameWithPackageName; + public final fun java-9PpwmVA (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static synthetic fun java-9PpwmVA$default (LNameTest$JvmFileBuilder$DeclarationsBuilder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/NameWithPackageName; + public final fun kotlin-9PpwmVA (Ljava/lang/String;Ljava/lang/String;)Lmodulecheck/name/NameWithPackageName; + public static synthetic fun kotlin-9PpwmVA$default (LNameTest$JvmFileBuilder$DeclarationsBuilder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/NameWithPackageName; +} + +public final class NameTest$JvmFileBuilder$NormalReferenceBuilder : NameTest$JvmFileBuilder$ReferenceBuilder { + public fun (LNameTest$JvmFileBuilder;)V +} + +public class NameTest$JvmFileBuilder$ReferenceBuilder { + public fun (Ljava/util/List;)V + public final fun androidDataBinding (Ljava/lang/String;)Lmodulecheck/name/AndroidDataBindingName; + public final fun androidR-p04zEWQ (Ljava/lang/String;)Lmodulecheck/name/AndroidRName; + public static synthetic fun androidR-p04zEWQ$default (LNameTest$JvmFileBuilder$ReferenceBuilder;Ljava/lang/String;ILjava/lang/Object;)Lmodulecheck/name/AndroidRName; + public final fun java (Ljava/lang/String;)Lmodulecheck/name/Name; + public final fun kotlin (Ljava/lang/String;)Lmodulecheck/name/Name; + public final fun qualifiedAndroidResource (Ljava/lang/String;)Lmodulecheck/name/AndroidResourceNameWithRName; + public final fun unqualifiedAndroidResource (Ljava/lang/String;)Lmodulecheck/name/UnqualifiedAndroidResourceName; +} + +public final class NameTestKt { + public static final fun prettyPrint (Ljava/util/Collection;)Ljava/lang/String; +} + +public abstract class modulecheck/name/testing/BaseNameTest : modulecheck/testing/BaseTest { + public fun ()V +} + diff --git a/modulecheck-name/testing/build.gradle.kts b/modulecheck-name/testing/build.gradle.kts new file mode 100644 index 0000000000..965e433530 --- /dev/null +++ b/modulecheck-name/testing/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021-2023 Rick Busarow + * 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. + */ + +plugins { + id("mcbuild") +} + +mcbuild { + published( + artifactId = "modulecheck-name-testing" + ) +} + +dependencies { + + api(project(path = ":modulecheck-internal-testing")) + api(project(path = ":modulecheck-name:api")) + api(project(path = ":modulecheck-parsing:source:api")) + api(project(path = ":modulecheck-utils:lazy")) + + implementation(libs.bundles.junit) + implementation(libs.bundles.kotest) + implementation(libs.kotlin.reflect) + + implementation(project(path = ":modulecheck-utils:trace")) +} diff --git a/modulecheck-name/testing/src/main/kotlin/modulecheck/name/testing/BaseNameTest.kt b/modulecheck-name/testing/src/main/kotlin/modulecheck/name/testing/BaseNameTest.kt new file mode 100644 index 0000000000..d50f30c37a --- /dev/null +++ b/modulecheck-name/testing/src/main/kotlin/modulecheck/name/testing/BaseNameTest.kt @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2021-2024 Rick Busarow + * 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 modulecheck.name.testing + +import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import modulecheck.name.AndroidDataBindingName +import modulecheck.name.AndroidRName +import modulecheck.name.AndroidResourceNameWithRName +import modulecheck.name.Name +import modulecheck.name.NameWithPackageName +import modulecheck.name.PackageName +import modulecheck.name.SimpleName +import modulecheck.name.SimpleName.Companion.asSimpleName +import modulecheck.name.SimpleName.Companion.stripPackageNameFromFqName +import modulecheck.name.UnqualifiedAndroidResourceName +import modulecheck.name.asNameWithPackageName +import modulecheck.parsing.source.JvmFile +import modulecheck.testing.assertions.TrimmedAsserts +import modulecheck.utils.lazy.LazyDeferred +import modulecheck.utils.lazy.LazySet +import modulecheck.utils.trace.Trace + +interface BaseNameTest : TrimmedAsserts { + + class JvmFileBuilder { + + val names: MutableList = mutableListOf() + val apiNames: MutableList = mutableListOf() + val declarations: MutableList = mutableListOf() + + fun references(builder: NormalReferenceBuilder.() -> Unit) { + NormalReferenceBuilder().builder() + } + + fun apiReferences(builder: ApiReferenceBuilder.() -> Unit) { + ApiReferenceBuilder().builder() + } + + fun declarations(builder: DeclarationsBuilder.() -> Unit) { + DeclarationsBuilder().builder() + } + + open class ReferenceBuilder( + private val target: MutableList + ) { + + fun androidR(packageName: PackageName = PackageName("com.test")): AndroidRName = + AndroidRName(packageName) + .also { target.add(it) } + + fun androidDataBinding(name: String): AndroidDataBindingName = + AndroidDataBindingName(TODO(), TODO()) + .also { target.add(it) } + + fun qualifiedAndroidResource(name: String): AndroidResourceNameWithRName = + AndroidResourceNameWithRName(TODO(), TODO()) + .also { target.add(it) } + + fun unqualifiedAndroidResource(name: String): UnqualifiedAndroidResourceName = + UnqualifiedAndroidResourceName.layout(name.asSimpleName()) + .also { target.add(it) } + + fun kotlin(name: String): Name = name.asSimpleName().also { target.add(it) } + + fun java(name: String): Name = name.asSimpleName().also { target.add(it) } + } + + inner class NormalReferenceBuilder : ReferenceBuilder(names) + + inner class ApiReferenceBuilder : ReferenceBuilder(apiNames) + + inner class DeclarationsBuilder { + fun kotlin( + name: String, + packageName: PackageName = PackageName("com.subject") + ): NameWithPackageName = NameWithPackageName( + packageName, + name.stripPackageNameFromFqName(packageName) + ) + .also { declarations.add(it) } + + fun java( + name: String, + packageName: PackageName = PackageName("com.subject") + ): NameWithPackageName = NameWithPackageName( + packageName, + name.stripPackageNameFromFqName(packageName) + ) + .also { declarations.add(it) } + + fun agnostic( + name: String, + packageName: PackageName = PackageName("com.subject") + ): NameWithPackageName = name.stripPackageNameFromFqName(packageName) + .asNameWithPackageName(packageName) + .also { declarations.add(it) } + } + } + + infix fun JvmFile.shouldBeJvmFile(config: JvmFileBuilder.() -> Unit) { + + val other = JvmFileBuilder().also { it.config() } + + assertSoftly { + "references".asClue { + references shouldBe other.names + } + "api references".asClue { + apiReferences shouldBe other.apiNames + } + "declarations".asClue { + declarations shouldBe other.declarations + } + } + } + + infix fun Collection.shouldBe(other: Collection) { + prettyPrint().trimmedShouldBe(other.prettyPrint(), BaseNameTest::class) + } + + infix fun LazySet.shouldBe(other: Collection) { + runBlocking(Trace.start(BaseNameTest::class)) { + toList() + .distinct() + .prettyPrint().trimmedShouldBe(other.prettyPrint(), BaseNameTest::class) + } + } + + infix fun LazyDeferred>.shouldBe(other: Collection) { + runBlocking(Trace.start(BaseNameTest::class)) { + await() + .distinct() + .prettyPrint().trimmedShouldBe(other.prettyPrint(), BaseNameTest::class) + } + } + + infix fun List>.shouldBe(other: Collection) { + runBlocking(Trace.start(BaseNameTest::class)) { + flatMap { it.get() } + .distinct() + .prettyPrint() + .trimmedShouldBe(other.prettyPrint(), BaseNameTest::class) + } + } + + fun kotlin( + name: String, + packageName: PackageName = PackageName("com.subject") + ): NameWithPackageName = + name.stripPackageNameFromFqName(packageName).asNameWithPackageName(packageName) + + fun java( + name: String, + packageName: PackageName = PackageName("com.subject") + ): NameWithPackageName = + name.stripPackageNameFromFqName(packageName).asNameWithPackageName(packageName) + + fun agnostic( + name: String, + packageName: PackageName = PackageName("com.subject") + ): NameWithPackageName = + name.stripPackageNameFromFqName(packageName).asNameWithPackageName(packageName) + + fun androidR(packageName: PackageName = PackageName("com.test")): AndroidRName = + AndroidRName(packageName) +} + +fun Collection.prettyPrint(): String = asSequence() + .map { name -> + val typeName = when (name) { + // references + is UnqualifiedAndroidResourceName -> "unqualifiedAndroidResource" + is AndroidRName -> "androidR" + is AndroidResourceNameWithRName -> "qualifiedAndroidResource" + is AndroidDataBindingName -> "androidDataBinding" + // is Name -> when { + // Name.isJava() -> "java" + // Name.isKotlin() -> "kotlin" + // Name.isXml() -> "xml" + // else -> throw IllegalArgumentException("???") + // } + + // is AndroidRDeclaredName -> "androidR" + // is UnqualifiedAndroidResource -> Name.prefix.name + // is QualifiedAndroidResourceDeclaredName -> "qualifiedAndroidResource" + // is AndroidDataBindingDeclaredName -> "androidDataBinding" + + // declarations + is NameWithPackageName -> { + when { + // Name.languages.containsAll(setOf(KOTLIN, JAVA)) -> "agnostic" + // Name.languages.contains(KOTLIN) -> "kotlin" + // Name.languages.contains(JAVA) -> "java" + // Name.languages.contains(XML) -> "xml" + else -> "throw IllegalArgumentException(???)" + } + } + // package + is PackageName -> "packageName" + is SimpleName -> "simpleName" + } + typeName to name + } + .groupBy { it.first } + .toList() + .sortedBy { it.first } + .joinToString("\n") { (typeName, pairs) -> + + pairs.map { it.second } + .sortedBy { it.asString } + .joinToString("\n", "$typeName {\n", "\n}") { "\t${it.asString}" } + } diff --git a/modulecheck-parsing/source/api/src/main/kotlin/modulecheck/parsing/source/SimpleName.kt b/modulecheck-parsing/source/api/src/main/kotlin/modulecheck/parsing/source/SimpleName.kt index 046072a484..edbdcb5840 100644 --- a/modulecheck-parsing/source/api/src/main/kotlin/modulecheck/parsing/source/SimpleName.kt +++ b/modulecheck-parsing/source/api/src/main/kotlin/modulecheck/parsing/source/SimpleName.kt @@ -15,13 +15,13 @@ package modulecheck.parsing.source -@JvmInline /** * A name which is not fully qualified, like `Foo` in `com.example.Foo` * * @property name the string value of this name * @since 0.12.0 */ +@JvmInline value class SimpleName(val name: String) : Comparable { init { diff --git a/modulecheck-utils/stdlib/src/main/kotlin/modulecheck/utils/collection.kt b/modulecheck-utils/stdlib/src/main/kotlin/modulecheck/utils/collection.kt index 69ead21bb7..5786c4d920 100644 --- a/modulecheck-utils/stdlib/src/main/kotlin/modulecheck/utils/collection.kt +++ b/modulecheck-utils/stdlib/src/main/kotlin/modulecheck/utils/collection.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2023 Rick Busarow + * Copyright (C) 2021-2024 Rick Busarow * 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 diff --git a/settings.gradle.kts b/settings.gradle.kts index f828346eb0..0640b154d2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -110,6 +110,8 @@ include( ":modulecheck-gradle:platforms:internal-jvm", ":modulecheck-gradle:plugin", ":modulecheck-internal-testing", + ":modulecheck-name:api", + ":modulecheck-name:testing", ":modulecheck-model:dependency:api", ":modulecheck-model:dependency:impl", ":modulecheck-model:sourceset:api",