From 5f17c0c4dd70e5489ffd43d635ba8c5674c25be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Ku=C4=8Dera?= Date: Thu, 8 Aug 2024 21:50:09 +0200 Subject: [PATCH] feat: 1.2.0 (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: switch version, run workflow for upcoming version snapshots * feat(generator-web-cli): namespace configuration * perf: intern strings * fix(generator-web-cli): add missing CB version replacement call * feat: support for Quilt mappings (#33) * feat: support for Quilt Mappings * refactor(core): remove Fabric-only code from Quilt implementation * fix(core): log "quilt mappings" instead of just "quilt" * fix(core): fix missing license header * fix(generator-accessor-plugin): fix Quilt being missing from namespaceFriendlinessIndex * refactor(core): remove Fabric remnants from QuiltMetadataProvider and QuiltMappingResolver chore(core): lowercase Quilt mappings, fix typo * feat: support for Quilt Hashed mappings * fix: remove Hashed from default ancestry namespace list fix: add Hashed to namespace friendliness index * fix(accessor-generator-plugin): remove edit remnant --------- Co-authored-by: Matouš Kučera * feat(core): validate SHA1 hashes of Yarn and Quilt cached metadata (#35) * fix(generator-web-cli): mismatching joined output cache * refactor: improve concurrency model * chore: update license headers * refactor: misc * feat(generator-accessor): add `@since` and `@version` tags to generated javadocs (#38) * feat(generator-accessor): naming strategy (#39) * feat(generator-accessor): introduction of naming strategy * style(generator-accessor): this todo is useless since it has been fixed elsewhere Probably not the best way to fix that tho * feat(generator-accessor): additional naming strategies + licenses * fix(generator-accessor): fix generation * build: updated gradle, kotlin-conventions and -Xjvm-default flag * feat(generator-accessor): specifying custom package containing generator-accessor-runtime module * fix(generator-accessor): groovy dsl compatibility with StandardNamingStrategies enum * refactor: improve API, untested * fix: de-parallelize `AccessorGenerator` Fixes non-deterministic behavior of name conflict resolution, I don't think parallelization makes that much of a difference anyway. * deps: Kotlin, ASM, Jackson, KotlinPoet, SLF4J, Shadow * build: release 1.1.2 * refactor: re-add missing strategies, make them serializable * fix: pass correct input into `generateLookupClass` * feat(generator-accessor-plugin): add naming strategy shortcuts --------- Co-authored-by: Matouš Kučera * feat(generator-accessor-plugin): namespace configuration * [ci skip] fix: deprecated usage in example * fix(generator-accessor): prevent KotlinPoet from wrapping references * refactor(generator-accessor-plugin): `versions` -> `version` * chore(generator-web): bump Java API doc version * fix(generator-accessor-plugin): wrong source type * fix(generator-accessor): prevent KotlinPoet from wrapping references * fix(generator-accessor): escape Kotlin names in lookup class * feat(generator-accessor): merge chained accessors into single accessor field (#40) * feat(generator-accessor): merge chained accessors into single accessor field * fix(generator-accessor): fix javadoc format * fix(generator-accessor): squash mappings as well, standalone counter for put mappings and accessor fields * feat(generator-accessor): kotlin support * style(generator-accessor): use filter and mutableListOf * refactor(generator-accessor): extract some common code from Java and Kotlin generation context * fix(generator-accessor): Kdoc generation for *Mapping classes * refactor(generator-accessor): improve chain resolving logic Co-authored-by: Matouš Kučera * fix(generator-accessor): fucking whitespace * refactor: simplify --------- Co-authored-by: Matouš Kučera * refactor: prepare to make Jackson an implementation detail * deps: update `kotlin-logging-jvm` * deps: bump Kotlin Coroutines and KotlinPoet * [ci skip] fix: doc mistakes * fix(generator-accessor): improve chain tracing * fix(generator-accessor): group method descriptors exactly when tracing * fix(generator-accessor): this logic was needed * feat(generator-accessor): add mapping website link to accessors *doc (#45) * feat(generator-accessor): add mapping website link to accessors *doc * chore(generator-accessor): use toInternalName extension function --------- Co-authored-by: Florentin Schleuß Co-authored-by: Misat11 <20199703+Misat11@users.noreply.github.com> --- .github/workflows/build.yml | 1 + README.md | 33 +- build-logic/build.gradle.kts | 1 + .../takenaka.kotlin-conventions.gradle.kts | 9 + build.gradle.kts | 3 +- core/build.gradle.kts | 3 +- .../me/kcra/takenaka/core/VersionManifest.kt | 67 ++ .../kotlin/me/kcra/takenaka/core/Workspace.kt | 1 + .../adapter/MissingDescriptorFilter.kt | 2 +- .../mapping/adapter/StringPoolingAdapter.kt | 50 ++ .../analysis/impl/AbstractMappingAnalyzer.kt | 2 +- .../core/mapping/ancestry/AncestryTree.kt | 27 +- .../resolve/impl/HashedMappingResolver.kt | 117 ++++ .../impl/IntermediaryMappingResolver.kt | 29 +- .../impl/MojangManifestAttributeProvider.kt | 17 +- .../resolve/impl/MojangMappingResolver.kt | 87 ++- .../resolve/impl/QuiltMappingResolver.kt | 238 +++++++ .../resolve/impl/QuiltMetadataProvider.kt | 111 ++++ .../resolve/impl/SeargeMappingResolver.kt | 50 +- .../resolve/impl/SpigotManifestProvider.kt | 22 +- .../resolve/impl/SpigotMappingResolver.kt | 150 ++++- .../resolve/impl/VanillaMappingContributor.kt | 70 +- .../resolve/impl/YarnMappingResolver.kt | 118 ++-- .../resolve/impl/YarnMetadataProvider.kt | 16 +- .../me/kcra/takenaka/core/util/collections.kt | 12 + .../me/kcra/takenaka/core/util/httpClient.kt | 13 + .../kcra/takenaka/core/util/objectMapper.kt | 13 + .../takenaka/core/test/VersionManifestTest.kt | 30 +- .../plugin/AccessorGeneratorExtension.kt | 147 +++- .../plugin/AccessorGeneratorPlugin.kt | 45 +- .../plugin/tasks/GenerateAccessorsTask.kt | 8 +- .../accessor/plugin/tasks/GenerationTask.kt | 57 +- .../plugin/tasks/ResolveMappingsTask.kt | 100 ++- .../plugin/tasks/TraceAccessorsTask.kt | 42 +- generator/accessor-runtime/build.gradle.kts | 3 +- generator/accessor/build.gradle.kts | 3 +- .../accessor/AccessorConfiguration.kt | 20 +- .../generator/accessor/GeneratedClassType.kt | 41 ++ .../generator/accessor/GeneratedMemberType.kt | 40 ++ .../context/impl/AbstractGenerationContext.kt | 164 ++--- .../context/impl/JavaGenerationContext.kt | 306 ++++----- .../context/impl/KotlinGenerationContext.kt | 627 +++++++++--------- .../context/impl/ResolvedClassAccessor.kt | 49 -- .../accessor/context/impl/SourceTypes.kt | 58 +- .../context/impl/TracingGenerationContext.kt | 16 +- .../accessor/context/impl/elementKeys.kt | 15 +- .../context/impl/resolvedAccessors.kt | 92 +++ .../generator/accessor/model/ClassAccessor.kt | 17 +- .../naming/ForwardingNamingStrategy.kt | 46 ++ .../naming/FullyQualifiedNamingStrategy.kt | 43 ++ .../accessor/naming/NamingStrategy.kt | 107 +++ .../naming/SemiQualifiedNamingStrategy.kt | 46 ++ .../accessor/naming/SimpleNamingStrategy.kt | 92 +++ .../naming/StandardNamingStrategies.kt | 116 ++++ generator/common/build.gradle.kts | 3 +- .../provider/impl/BundledMappingProvider.kt | 60 +- .../provider/impl/ResolvingMappingProvider.kt | 56 +- generator/web-cli/build.gradle.kts | 3 +- .../kcra/takenaka/generator/web/cli/Main.kt | 128 +++- generator/web/build.gradle.kts | 3 +- .../generator/web/ClassSearchIndex.kt | 27 + .../generator/web/GenerationContext.kt | 4 +- .../generator/web/NamespaceDescription.kt | 13 +- .../generator/web/WebConfiguration.kt | 2 +- .../takenaka/generator/web/WebGenerator.kt | 170 ++--- .../web/src/main/resources/assets/main.css | 2 +- gradle/libs.versions.toml | 8 +- gradlew | 0 68 files changed, 2993 insertions(+), 1078 deletions(-) create mode 100644 build-logic/src/main/kotlin/takenaka.kotlin-conventions.gradle.kts create mode 100644 core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/StringPoolingAdapter.kt create mode 100644 core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/HashedMappingResolver.kt create mode 100644 core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMappingResolver.kt create mode 100644 core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMetadataProvider.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedClassType.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedMemberType.kt delete mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/ResolvedClassAccessor.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/resolvedAccessors.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/ForwardingNamingStrategy.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/FullyQualifiedNamingStrategy.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/NamingStrategy.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SemiQualifiedNamingStrategy.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SimpleNamingStrategy.kt create mode 100644 generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/StandardNamingStrategies.kt mode change 100644 => 100755 gradlew diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb2ec538738..9726e0854e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - 'ver/**' jobs: build: diff --git a/README.md b/README.md index 5f388df56b9..dd9bb31f164 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ The goal of this project is to improve the maintainability and performance of th - [x] Searge (Forge) mappings - [x] Spigot mappings - [x] Yarn (FabricMC) mappings -- [ ] Hashed (QuiltMC) mappings (PRs welcome!) -- [ ] QuiltMC mappings (PRs welcome!) +- [x] Hashed (QuiltMC) mappings +- [x] QuiltMC mappings ## Usage @@ -83,7 +83,7 @@ accessors { // if you don't, you will get accessors mapped for everything that the bundle offers, i.e. 1.8.8 to 1.20.6 basePackage("org.example.myplugin.accessors") // this is the base package of the generated output, probably somewhere in your plugin/library's namespace - accessedNamespaces("spigot", "mojang") // these are the "namespaces" that can be queried on runtime, i.e. "spigot" (for Spigot/CraftBukkit/Paper), "searge" (for Forge), "mojang" (for Mojang-mapped Paper - >1.20.4), "yarn" (not useful on runtime) or "intermediary" (for Fabric) + namespaces("spigot", "mojang") // these are the "namespaces" that can be queried on runtime, i.e. "spigot" (for Spigot/CraftBukkit/Paper), "searge" (for Forge), "mojang" (for Mojang-mapped Paper - >1.20.4), "yarn" (not useful on runtime), "intermediary" (for Fabric), "quilt" or "hashed" (for Quilt) accessorType("reflection") // this is the generated accessor type, can be "none" (no accessor breakout classes are generated, only a mapping class that can be queried), "reflection" or "method_handles" (self-explanatory, java.lang.reflect or java.lang.invoke accessors) // there are many more options, like mapping for clients, IntelliJ's source JAR view and auto-complete are your friends (Ctrl+Click) @@ -126,6 +126,8 @@ Usage: web-cli options_list Options: --output, -o [output] -> Output directory { String } --version, -v -> Target Minecraft version, can be specified multiple times (always required) { String } + --namespace, -n -> Target namespace, can be specified multiple times, order matters { String } + --ancestryNamespace, -a -> Target ancestry namespace, can be specified multiple times, has to be a subset of --namespace { String } --cache, -c [cache] -> Caching directory for mappings and other resources { String } --server [false] -> Include server mappings in the documentation --client [false] -> Include client mappings in the documentation @@ -141,7 +143,28 @@ Options: ``` The command-line to build a [mappings.dev](https://mappings.dev) clone would look something like this: -`java -jar generator-web-cli-.jar --client --server -v 1.20.2 -v 1.20.1 ... (more versions follow)` +`java -jar generator-web-cli-.jar --client --server -n mojang -n spigot -n yarn -n searge -n intermediary -v 1.20.2 -v 1.20.1 ... (more versions follow)` + +#### `--namespace` option + +This option allows you to specify a custom namespace subset and preference ordering. +Useful if you want to build an instance for only modding mappings for example. + +For the Fabric toolchain, you would specify something along the lines of `-n yarn -n intermediary`, +that resolves only the Yarn and Intermediary mappings, with Yarn names used for the links/overview pages/... +and Intermediary being a fallback that is used if a Yarn name is not present. +Both namespaces will still be shown as usual on class detail pages. + +*The `source` (obfuscated) namespace is always implicitly appended last.* + +By default, all available namespaces are used (`mojang, spigot, yarn, quilt, searge, intermediary, hashed`). + +#### `--ancestryNamespace` option + +This option allows you to select a subset of defined namespaces (`--namespace` choices or its default), which will be +used for ancestry computation. This option is more advanced and most users won't need to use it, the default suffices for most cases. + +By default, the `mojang, spigot, searge, intermediary` namespaces are used, minus ones that haven't been defined (`--namespace` choices or its default). #### `--javadoc` option @@ -149,7 +172,7 @@ The expected value can be: - a plus-sign delimited pair of a supported package and a link to the Javadoc root (Javadoc sites _with no modules_): `org.slf4j+https://www.slf4j.org/api` - a link to the Javadoc root (Javadoc sites _with modules_): `https://docs.oracle.com/en/java/javase/17/docs/api` -**Java 17 API is included automatically for indexing.** +**Java 21 API is included automatically for indexing.** ## Acknowledgements diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 392b2c685be..2e7a5f9a320 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -12,6 +12,7 @@ repositories { dependencies { implementation(libs.build.licenser) implementation(libs.build.gradle.versions) + implementation(libs.build.kotlin.jvm) } dependencies { diff --git a/build-logic/src/main/kotlin/takenaka.kotlin-conventions.gradle.kts b/build-logic/src/main/kotlin/takenaka.kotlin-conventions.gradle.kts new file mode 100644 index 00000000000..4e1b0e334e7 --- /dev/null +++ b/build-logic/src/main/kotlin/takenaka.kotlin-conventions.gradle.kts @@ -0,0 +1,9 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +apply(plugin = "org.jetbrains.kotlin.jvm") + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjvm-default=all-compatibility") + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 08d80ea6823..e08e02f28e4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,9 @@ plugins { id("takenaka.base-conventions") - alias(libs.plugins.kotlin.jvm) apply false } allprojects { group = "me.kcra.takenaka" - version = "1.1.4" + version = "1.2.0-SNAPSHOT" description = "A Kotlin library for reconciling multiple obfuscation mapping files from multiple versions of Minecraft: JE." } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 7f539e5f239..01605f1dc7e 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,10 +1,9 @@ plugins { id("takenaka.base-conventions") + id("takenaka.kotlin-conventions") id("takenaka.publish-conventions") } -apply(plugin = "org.jetbrains.kotlin.jvm") - dependencies { api(libs.bundles.asm) api(libs.bundles.jackson) diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/VersionManifest.kt b/core/src/main/kotlin/me/kcra/takenaka/core/VersionManifest.kt index 49de2b5c865..66123de763d 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/VersionManifest.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/VersionManifest.kt @@ -30,6 +30,7 @@ import java.nio.file.Path import java.time.Instant import kotlin.io.path.createDirectories import kotlin.io.path.fileSize +import kotlin.io.path.getLastModifiedTime import kotlin.io.path.isRegularFile /** @@ -42,8 +43,52 @@ const val VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_ * * @return the version manifest */ +@Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("versionManifestOf()", "me.kcra.takenaka.core.versionManifestOf") +) fun ObjectMapper.versionManifest(): VersionManifest = readValue(URL(VERSION_MANIFEST_V2)) +/** + * Fetches and deserializes the version manifest from Mojang's API. + * + * @param url the version manifest url, defaults to [VERSION_MANIFEST_V2] + * @return the version manifest + */ +fun versionManifestOf(url: String = VERSION_MANIFEST_V2): VersionManifest = MAPPER.readValue(URL(url)) + +/** + * Retrieves the version manifest from Mojang's API or a cache file, + * fetching it again if the cache missed, or it could not be deserialized. + * + * @param cacheFile the cache file, does not need to exist + * @param url the version manifest url, defaults to [VERSION_MANIFEST_V2] + * @return the version manifest + */ +fun versionManifestFrom(cacheFile: Path, url: String = VERSION_MANIFEST_V2): VersionManifest { + val url0 = URL(url) + if (cacheFile.isRegularFile()) { + if (url0.lastModified > cacheFile.getLastModifiedTime().toMillis()) { + try { + return MAPPER.readValue(cacheFile) + } catch (_: JacksonException) { + // failed to read cached file, corrupted? fetch it again + } + } + } + + url0.httpRequest { + if (it.ok) { + cacheFile.parent.createDirectories() + it.copyTo(cacheFile) + + return MAPPER.readValue(cacheFile) + } + + throw IOException("Failed to fetch v2 Mojang manifest, received ${it.responseCode}") + } +} + /** * Retrieves the version manifest from Mojang's API or a cache file, * fetching it if it could not be deserialized or the content length changed. @@ -51,6 +96,10 @@ fun ObjectMapper.versionManifest(): VersionManifest = readValue(URL(VERSION_MANI * @param cacheFile the cache file, does not need to exist * @return the version manifest */ +@Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("versionManifestFrom(cacheFile)", "me.kcra.takenaka.core.versionManifestFrom") +) fun ObjectMapper.cachedVersionManifest(cacheFile: Path): VersionManifest { val url = URL(VERSION_MANIFEST_V2) @@ -179,6 +228,24 @@ data class Version( } } +/** + * Fetches and deserializes version attributes from Mojang's API. + * + * @param url the version attributes url + * @return the version attributes + */ +fun versionAttributesOf(url: String): VersionAttributes { + return MAPPER.readValue(URL(url)) +} + +/** + * Fetches and deserializes version attributes from Mojang's API. + * + * @param version the version + * @return the version attributes + */ +fun versionAttributesOf(version: Version): VersionAttributes = versionAttributesOf(version.url) + /** * The version attributes from Mojang's v2 version manifest. */ diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/Workspace.kt b/core/src/main/kotlin/me/kcra/takenaka/core/Workspace.kt index 9f57b966410..7212f5fc9ac 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/Workspace.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/Workspace.kt @@ -97,6 +97,7 @@ open class WorkspaceBuilder { /** * The resolver options. */ + @Deprecated("Unused.") var options = 0 /** diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/MissingDescriptorFilter.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/MissingDescriptorFilter.kt index 5a77e250a8a..0ca29218923 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/MissingDescriptorFilter.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/MissingDescriptorFilter.kt @@ -17,7 +17,7 @@ package me.kcra.takenaka.core.mapping.adapter -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.MappingVisitor import net.fabricmc.mappingio.adapter.ForwardingMappingVisitor diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/StringPoolingAdapter.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/StringPoolingAdapter.kt new file mode 100644 index 00000000000..eaeb56d5ea6 --- /dev/null +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/adapter/StringPoolingAdapter.kt @@ -0,0 +1,50 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.core.mapping.adapter + +import net.fabricmc.mappingio.MappedElementKind +import net.fabricmc.mappingio.MappingVisitor +import net.fabricmc.mappingio.adapter.ForwardingMappingVisitor + +/** + * Pools element name and descriptor strings (using [String.intern]). + * + * @param next the visitor to delegate to + * @author Matouš Kučera + */ +class StringPoolingAdapter(next: MappingVisitor) : ForwardingMappingVisitor(next) { + override fun visitClass(srcName: String?): Boolean { + return super.visitClass(srcName?.intern()) + } + + override fun visitField(srcName: String?, srcDesc: String?): Boolean { + return super.visitField(srcName?.intern(), srcDesc?.intern()) + } + + override fun visitMethod(srcName: String?, srcDesc: String?): Boolean { + return super.visitMethod(srcName?.intern(), srcDesc?.intern()) + } + + override fun visitDstName(targetKind: MappedElementKind?, namespace: Int, name: String?) { + super.visitDstName(targetKind, namespace, name?.intern()) + } + + override fun visitDstDesc(targetKind: MappedElementKind?, namespace: Int, desc: String?) { + super.visitDstDesc(targetKind, namespace, desc?.intern()) + } +} diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/analysis/impl/AbstractMappingAnalyzer.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/analysis/impl/AbstractMappingAnalyzer.kt index 0309ac7f010..c5bf2e668ae 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/analysis/impl/AbstractMappingAnalyzer.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/analysis/impl/AbstractMappingAnalyzer.kt @@ -21,7 +21,7 @@ import me.kcra.takenaka.core.mapping.analysis.MappingAnalyzer import me.kcra.takenaka.core.mapping.analysis.Problem import me.kcra.takenaka.core.mapping.analysis.ProblemKind import me.kcra.takenaka.core.mapping.analysis.ProblemResolution -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.tree.MappingTree import java.util.* import kotlin.system.measureTimeMillis diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/ancestry/AncestryTree.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/ancestry/AncestryTree.kt index 008bda309af..b21dc4d1877 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/ancestry/AncestryTree.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/ancestry/AncestryTree.kt @@ -112,7 +112,7 @@ class AncestryTree( if (keys.isEmpty()) return null // return early, we're not searching for anything return if (keys.size == 1) { - val key = keys[0] + val key = keys.first() find { key in it.lastNames } } else { @@ -120,3 +120,28 @@ class AncestryTree( } } } + +/** + * Creates a merged node. + * + * Respects existing ordering in case of conflicts - only latest entries are kept, older are discarded. + * + * @param nodes the nodes to merge + * @return the merged node + */ +fun nodeOf(vararg nodes: AncestryTree.Node): AncestryTree.Node { + require(nodes.isNotEmpty()) { + "No nodes provided" + } + + if (nodes.size == 1) return nodes.first() + + // create merged node + val lastNode = nodes.last() + return AncestryTree.Node( + lastNode.tree, + nodes.flatMap { it.entries } + .associate { it.key to it.value }, + last = lastNode.last + ) +} diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/HashedMappingResolver.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/HashedMappingResolver.kt new file mode 100644 index 00000000000..59748a85c23 --- /dev/null +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/HashedMappingResolver.kt @@ -0,0 +1,117 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.core.mapping.resolve.impl + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import me.kcra.takenaka.core.VersionedWorkspace +import me.kcra.takenaka.core.mapping.MappingContributor +import me.kcra.takenaka.core.mapping.resolve.AbstractMappingResolver +import me.kcra.takenaka.core.mapping.resolve.Output +import me.kcra.takenaka.core.mapping.resolve.lazyOutput +import me.kcra.takenaka.core.util.contentLength +import me.kcra.takenaka.core.util.copyTo +import me.kcra.takenaka.core.util.httpRequest +import me.kcra.takenaka.core.util.ok +import io.github.oshai.kotlinlogging.KotlinLogging +import net.fabricmc.mappingio.MappingUtil +import net.fabricmc.mappingio.MappingVisitor +import net.fabricmc.mappingio.adapter.MappingNsRenamer +import net.fabricmc.mappingio.format.Tiny2Reader +import java.net.URL +import java.nio.file.Path +import kotlin.io.path.fileSize +import kotlin.io.path.isRegularFile +import kotlin.io.path.reader + +private val logger = KotlinLogging.logger {} + +/** + * A resolver for the Hashed mappings from QuiltMC. + * + * @property workspace the workspace + * @author Matouš Kučera + * @author Florentin Schleuß + */ +class HashedMappingResolver(override val workspace: VersionedWorkspace) : AbstractMappingResolver(), MappingContributor { + override val targetNamespace: String = "hashed" + override val outputs: List> + get() = listOf(mappingOutput) + + override val mappingOutput = lazyOutput { + resolver { + val file = workspace[MAPPINGS] + + val url = URL("https://maven.quiltmc.org/repository/release/org/quiltmc/hashed/${version.id}/hashed-${version.id}.tiny") + val length = url.contentLength + + if (length == -1L) { + logger.info { "did not find Hashed mappings for ${version.id}" } + return@resolver null + } + + if (MAPPINGS in workspace) { + if (file.fileSize() == length) { + logger.info { "matched same length for cached ${version.id} Hashed mappings" } + return@resolver file + } + + logger.warn { "length mismatch for ${version.id} Hashed mapping cache, fetching them again" } + } + + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + url.httpRequest { + if (it.ok) { + it.copyTo(file) + + logger.info { "fetched ${version.id} Hashed mappings" } + return@httpRequest file + } + + logger.warn { "failed to fetch ${version.id} Hashed mappings, received ${it.responseCode}" } + return@httpRequest null + } + } + } + + upToDateWhen { it == null || it.isRegularFile() } + } + + /** + * Visits the mappings to the supplied visitor. + * + * @param visitor the visitor + */ + override fun accept(visitor: MappingVisitor) { + val mappingPath by mappingOutput + + mappingPath?.reader()?.use { reader -> + // Hashed has official and Hashed namespaces + // official is the obfuscated one + Tiny2Reader.read(reader, MappingNsRenamer(visitor, mapOf("official" to MappingUtil.NS_SOURCE_FALLBACK))) + } + } + + companion object { + /** + * The file name of the cached mappings. + */ + const val MAPPINGS = "hashed_mappings.tiny" + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/IntermediaryMappingResolver.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/IntermediaryMappingResolver.kt index abc32cf7833..c7b715ae2ee 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/IntermediaryMappingResolver.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/IntermediaryMappingResolver.kt @@ -17,12 +17,15 @@ package me.kcra.takenaka.core.mapping.resolve.impl +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import me.kcra.takenaka.core.VersionedWorkspace import me.kcra.takenaka.core.Workspace import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.resolve.* import me.kcra.takenaka.core.util.* -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.MappingUtil import net.fabricmc.mappingio.MappingVisitor import net.fabricmc.mappingio.adapter.MappingNsRenamer @@ -59,7 +62,7 @@ class IntermediaryMappingResolver( val url = URL("https://raw.githubusercontent.com/FabricMC/intermediary/master/mappings/${version.id}.tiny") val length = url.contentLength - if (length == (-1).toLong()) { + if (length == -1L) { logger.info { "did not find Intermediary mappings for ${version.id}" } return@resolver null } @@ -73,18 +76,19 @@ class IntermediaryMappingResolver( logger.warn { "length mismatch for ${version.id} Intermediary mapping cache, fetching them again" } } - url.httpRequest { - if (it.ok) { - it.copyTo(file) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + url.httpRequest { + if (it.ok) { + it.copyTo(file) - logger.info { "fetched ${version.id} Intermediary mappings" } - return@resolver file - } + logger.info { "fetched ${version.id} Intermediary mappings" } + return@httpRequest file + } - logger.warn { "failed to fetch ${version.id} Intermediary mappings, received ${it.responseCode}" } + logger.warn { "failed to fetch ${version.id} Intermediary mappings, received ${it.responseCode}" } + return@httpRequest null + } } - - return@resolver null } upToDateWhen { it == null || it.isRegularFile() } @@ -94,13 +98,12 @@ class IntermediaryMappingResolver( resolver { licenseWorkspace.withLock(WORKSPACE_LOCK) { val file = licenseWorkspace[LICENSE] - if (LICENSE in licenseWorkspace) { logger.info { "found cached Intermediary license file" } return@withLock file } - URL(licenseSource).copyTo(file) + URL(licenseSource).copyTo(file) // TODO: use IO context logger.info { "fetched Intermediary license file" } return@withLock file diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangManifestAttributeProvider.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangManifestAttributeProvider.kt index a8e1d8f0134..8da43b9e05c 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangManifestAttributeProvider.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangManifestAttributeProvider.kt @@ -21,9 +21,10 @@ import com.fasterxml.jackson.core.JacksonException import com.fasterxml.jackson.databind.ObjectMapper import me.kcra.takenaka.core.VersionAttributes import me.kcra.takenaka.core.VersionedWorkspace +import me.kcra.takenaka.core.util.MAPPER import me.kcra.takenaka.core.util.copyTo import me.kcra.takenaka.core.util.readValue -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import java.net.URL private val logger = KotlinLogging.logger {} @@ -38,12 +39,24 @@ private val logger = KotlinLogging.logger {} * @property relaxedCache whether output cache verification constraints should be relaxed * @author Matouš Kučera */ -class MojangManifestAttributeProvider(val workspace: VersionedWorkspace, private val objectMapper: ObjectMapper, val relaxedCache: Boolean = true) { +class MojangManifestAttributeProvider @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("MojangManifestAttributeProvider(workspace, relaxedCache)") +) constructor(val workspace: VersionedWorkspace, private val objectMapper: ObjectMapper, val relaxedCache: Boolean = true) { /** * The version attributes. */ val attributes: VersionAttributes by lazy(::readAttributes) + /** + * Creates a new attribute provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + @Suppress("DEPRECATION") + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : this(workspace, MAPPER, relaxedCache) + /** * Reads the attributes of the targeted version from cache, fetching it if the cache missed. * diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangMappingResolver.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangMappingResolver.kt index 8c8e2cffed4..6babd906aa3 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangMappingResolver.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/MojangMappingResolver.kt @@ -18,11 +18,14 @@ package me.kcra.takenaka.core.mapping.resolve.impl import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import me.kcra.takenaka.core.VersionedWorkspace import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.resolve.* import me.kcra.takenaka.core.util.* -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.MappingUtil import net.fabricmc.mappingio.MappingVisitor import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch @@ -66,29 +69,30 @@ abstract class AbstractMojangMappingResolver( val fileName = "${mappingAttribute.name}.txt" val file = workspace[fileName] - if (fileName in workspace) { - val checksum = file.getChecksum(sha1Digest) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + if (fileName in workspace) { + val checksum = file.getChecksum(sha1Digest) - if (mappingAttribute.checksum == checksum) { - logger.info { "matched checksum for cached ${version.id} Mojang mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } - return@resolver file + if (mappingAttribute.checksum == checksum) { + logger.info { "matched checksum for cached ${version.id} Mojang mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } + return@withContext file + } + + logger.warn { "checksum mismatch for ${version.id} Mojang mapping cache, fetching them again (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } } - logger.warn { "checksum mismatch for ${version.id} Mojang mapping cache, fetching them again (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } - } + URL(mappingAttribute.value!!).httpRequest { + if (it.ok) { + it.copyTo(file) - URL(mappingAttribute.value!!).httpRequest { - if (it.ok) { - it.copyTo(file) + logger.info { "fetched ${version.id} Mojang mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } + return@httpRequest file + } - logger.info { "fetched ${version.id} Mojang mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } - return@resolver file + logger.info { "failed to fetch ${version.id} Mojang mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value}), received ${it.responseCode}" } + return@httpRequest null } - - logger.info { "failed to fetch ${version.id} Mojang mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value}), received ${it.responseCode}" } } - - return@resolver null } upToDateWhen { it == null || it.isRegularFile() } @@ -99,16 +103,18 @@ abstract class AbstractMojangMappingResolver( val file = workspace[LICENSE] val mappingPath by mappingOutput - mappingPath?.bufferedReader()?.use { - val line = it.readLine() + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + mappingPath?.bufferedReader()?.use { + val line = it.readLine() - if (line.startsWith("# ")) { - file.writeText(line.drop(2)) - return@resolver file + if (line.startsWith("# ")) { + file.writeText(line.drop(2)) + return@withContext file + } } - } - return@resolver null + return@withContext null + } } upToDateWhen { it == null || it.isRegularFile() } @@ -135,7 +141,6 @@ abstract class AbstractMojangMappingResolver( } val licensePath by licenseOutput - licensePath?.reader()?.use { visitor.visitMetadata(META_LICENSE, it.readText()) visitor.visitMetadata(META_LICENSE_SOURCE, licenseSource) @@ -178,9 +183,23 @@ class MojangClientMappingResolver( * @param objectMapper an [ObjectMapper] that can deserialize JSON data * @param relaxedCache whether output cache verification constraints should be relaxed */ + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("MojangClientMappingResolver(workspace, relaxedCache)") + ) + @Suppress("DEPRECATION") constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, relaxedCache: Boolean = true) : this(workspace, MojangManifestAttributeProvider(workspace, objectMapper, relaxedCache)) + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : + this(workspace, MojangManifestAttributeProvider(workspace, relaxedCache)) + /** * Resolves a Mojang manifest mapping attribute. * @@ -213,8 +232,22 @@ class MojangServerMappingResolver( * @param objectMapper an [ObjectMapper] that can deserialize JSON data * @param relaxedCache whether output cache verification constraints should be relaxed */ - constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, relaxedCache: Boolean = true) - : this(workspace, MojangManifestAttributeProvider(workspace, objectMapper, relaxedCache)) + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("MojangServerMappingResolver(workspace, relaxedCache)") + ) + @Suppress("DEPRECATION") + constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, relaxedCache: Boolean = true) : + this(workspace, MojangManifestAttributeProvider(workspace, objectMapper, relaxedCache)) + + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : + this(workspace, MojangManifestAttributeProvider(workspace, relaxedCache)) /** * Resolves a Mojang manifest mapping attribute. diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMappingResolver.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMappingResolver.kt new file mode 100644 index 00000000000..5f3a8f0d18e --- /dev/null +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMappingResolver.kt @@ -0,0 +1,238 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.core.mapping.resolve.impl + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import me.kcra.takenaka.core.VersionedWorkspace +import me.kcra.takenaka.core.mapping.MappingContributor +import me.kcra.takenaka.core.mapping.resolve.AbstractMappingResolver +import me.kcra.takenaka.core.mapping.resolve.LicenseResolver +import me.kcra.takenaka.core.mapping.resolve.Output +import me.kcra.takenaka.core.mapping.resolve.lazyOutput +import me.kcra.takenaka.core.util.* +import io.github.oshai.kotlinlogging.KotlinLogging +import net.fabricmc.mappingio.MappingReader +import net.fabricmc.mappingio.MappingUtil +import net.fabricmc.mappingio.MappingVisitor +import net.fabricmc.mappingio.adapter.MappingNsRenamer +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.zip.ZipFile +import kotlin.io.path.bufferedReader +import kotlin.io.path.fileSize +import kotlin.io.path.isRegularFile +import kotlin.io.path.reader + +private val logger = KotlinLogging.logger {} + +/** + * A resolver for the Quilt mappings from QuiltMC. + * + * @property workspace the workspace + * @property quiltProvider the Quilt metadata provider + * @property relaxedCache whether output cache verification constraints should be relaxed + * @author Matouš Kučera + * @author Florentin Schleuß + */ +class QuiltMappingResolver( + override val workspace: VersionedWorkspace, + val quiltProvider: QuiltMetadataProvider, + val relaxedCache: Boolean = true +) : AbstractMappingResolver(), MappingContributor, LicenseResolver { + override val licenseSource: String = "https://raw.githubusercontent.com/QuiltMC/quilt-mappings/${version.id}/LICENSE" + override val targetNamespace: String = "quilt" + override val outputs: List> + get() = listOf(mappingOutput, licenseOutput) + + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) + : this(workspace, QuiltMetadataProvider(workspace, relaxedCache), relaxedCache) + + override val mappingOutput = lazyOutput { + resolver { + val file = workspace[MAPPING_JAR] + + val builds = quiltProvider.versions[version.id] + if (builds == null) { + logger.info { "did not find Quilt mappings for ${version.id}" } + return@resolver null + } + + val targetBuild = builds.maxBy(QuiltBuild::buildNumber) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + var urlString = "https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-mappings/$targetBuild/quilt-mappings-$targetBuild-mergedv2.jar" + URL(urlString).httpRequest(method = "HEAD") { mergedv2 -> + if (!mergedv2.ok) { + logger.info { "mergedv2 Quilt mappings JAR for ${version.id} failed to fetch, falling back to no classifier" } + + urlString = "https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-mappings/$targetBuild/quilt-mappings-$targetBuild.jar" + } + } + + val url = URL(urlString) + val checksumUrl = URL("$urlString.sha1") + + if (MAPPING_JAR in workspace) { + checksumUrl.httpRequest { + if (it.ok) { + val checksum = file.getChecksum(sha1Digest) + + if (it.readText() == checksum) { + logger.info { "matched checksum for cached ${version.id} Quilt mappings" } + return@withContext findMappingFile(file) + } + } else if (file.fileSize() == url.contentLength) { + logger.info { "matched same length for cached ${version.id} Quilt mappings" } + return@withContext findMappingFile(file) + } + } + + logger.warn { "checksum/length mismatch for ${version.id} Quilt mappings cache, fetching them again" } + } + + url.httpRequest { + if (it.ok) { + it.copyTo(file) + + logger.info { "fetched ${version.id} Quilt mappings" } + return@httpRequest findMappingFile(file) + } + + logger.warn { "failed to fetch ${version.id} Quilt mappings, received ${it.responseCode}" } + return@httpRequest null + } + } + } + + upToDateWhen { it == null || it.isRegularFile() } + } + + override val licenseOutput = lazyOutput { + resolver { + val file = workspace[LICENSE] + if (LICENSE in workspace) { + logger.info { "found cached ${version.id} Quilt license file" } + return@resolver file + } + + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + URL(licenseSource).httpRequest { + if (it.ok) { + it.copyTo(file) + + logger.info { "fetched ${version.id} Quilt license file" } + return@httpRequest file + } else if (it.responseCode == 404) { + logger.info { "did not find ${version.id} Quilt mappings license file" } + } else { + logger.warn { "failed to fetch Quilt mappings license file, received ${it.responseCode}" } + } + + return@httpRequest null + } + } + } + + upToDateWhen { it == null || it.isRegularFile() } + } + + /** + * Visits the mappings to the supplied visitor. + * + * @param visitor the visitor + */ + override fun accept(visitor: MappingVisitor) { + val mappingPath by mappingOutput + + mappingPath?.reader()?.use { reader -> + // Quilt has official, named and hashed namespaces + // official is the obfuscated one + MappingReader.read(reader, MappingNsRenamer(visitor, mapOf( + "official" to MappingUtil.NS_SOURCE_FALLBACK, + "named" to targetNamespace + ))) + + val licensePath by licenseOutput + + // limit the license file to 12 lines for conciseness + licensePath?.bufferedReader()?.use { + visitor.visitMetadata(META_LICENSE, it.lineSequence().take(12).joinToString("\\n").replace("\t", " ")) + visitor.visitMetadata(META_LICENSE_SOURCE, licenseSource) + } + } + } + + /** + * Extracts the mapping file from the supplied zip file. + * + * @param file the zip file + * @return the file + */ + private fun findMappingFile(file: Path): Path { + val mappingFile = workspace[MAPPINGS] + + if (!relaxedCache || !mappingFile.isRegularFile()) { + ZipFile(file.toFile()).use { + val entry = it.stream() + .filter { e -> e.name == "mappings/mappings.tiny" } + .findFirst() + .orElseThrow { RuntimeException("Could not find mapping file in zip file (Quilt mappings, ${version.id})") } + + Files.copy(it.getInputStream(entry), mappingFile, StandardCopyOption.REPLACE_EXISTING) + } + } + + return mappingFile + } + + companion object { + /** + * The file name of the cached mapping JAR. + */ + const val MAPPING_JAR = "quilt_mappings.jar" + + /** + * The file name of the cached mappings. + */ + const val MAPPINGS = "quilt_mappings.tiny" + + /** + * The file name of the cached license file. + */ + const val LICENSE = "quilt_license.txt" + + /** + * The license metadata key. + */ + const val META_LICENSE = "quilt_license" + + /** + * The license source metadata key. + */ + const val META_LICENSE_SOURCE = "quilt_license_source" + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMetadataProvider.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMetadataProvider.kt new file mode 100644 index 00000000000..aa502e77009 --- /dev/null +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/QuiltMetadataProvider.kt @@ -0,0 +1,111 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.core.mapping.resolve.impl + +import com.fasterxml.jackson.core.JacksonException +import com.fasterxml.jackson.databind.JsonNode +import me.kcra.takenaka.core.Workspace +import me.kcra.takenaka.core.util.* +import io.github.oshai.kotlinlogging.KotlinLogging +import java.net.URL + +private val logger = KotlinLogging.logger {} + +/** + * A provider of Quilt's maven-metadata.xml file. + * + * This class is thread-safe, but presumes that only one instance will operate on a workspace at a time. + * + * @property workspace the workspace + * @property relaxedCache whether output cache verification constraints should be relaxed + * @author Matouš Kučera + * @author Florentin Schleuß + */ +class QuiltMetadataProvider(val workspace: Workspace, val relaxedCache: Boolean = true) { + /** + * A map of versions and their builds. + */ + val versions by lazy(::parseVersions) + + /** + * The XML node of the maven-metadata file. + */ + private val metadata by lazy(::readMetadata) + + /** + * Parses Quilt version strings in the metadata file. + * + * @return the version metadata + */ + private fun parseVersions(): Map> = buildMap> { + metadata["versioning"]["versions"]["version"].forEach { versionNode -> + val (version, buildNumber) = versionNode.asText().split("+build.", limit = 2) + getOrPut(version, ::mutableListOf) += QuiltBuild(version, buildNumber.toInt()) + } + } + + /** + * Reads the metadata file from cache, fetching it if the cache missed. + * + * @return the metadata XML node + */ + private fun readMetadata(): JsonNode { + val file = workspace[METADATA] + + val metadataLocation = "https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-mappings/maven-metadata.xml" + if (relaxedCache && METADATA in workspace) { + URL("$metadataLocation.sha1").httpRequest { + if (it.readText() == file.getChecksum(sha1Digest)) { + try { + return XML_MAPPER.readTree(file).apply { + logger.info { "read cached Quilt mappings metadata" } + } + } catch (e: JacksonException) { + logger.warn(e) { "failed to read cached Quilt mappings metadata, fetching it again" } + } + } else { + logger.warn { "cached Quilt mappings metadata is outdated or corrupt, fetching it again" } + } + } + } + + URL(metadataLocation).copyTo(file) + + logger.info { "fetched Quilt metadata" } + return XML_MAPPER.readTree(file) + } + + companion object { + /** + * The file name of the cached version metadata. + */ + const val METADATA = "quilt_metadata.xml" + } +} + +/** + * Information about one Quilt build. + * + * @property version the Minecraft version that this build is for + * @property buildNumber the Quilt build number, higher is newer + */ +data class QuiltBuild(val version: String, val buildNumber: Int) { + override fun toString(): String { + return "$version+build.$buildNumber" + } +} diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SeargeMappingResolver.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SeargeMappingResolver.kt index bd847e90e56..0f12cce6a7c 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SeargeMappingResolver.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SeargeMappingResolver.kt @@ -17,12 +17,15 @@ package me.kcra.takenaka.core.mapping.resolve.impl +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import me.kcra.takenaka.core.VersionedWorkspace import me.kcra.takenaka.core.Workspace import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.resolve.* import me.kcra.takenaka.core.util.* -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.MappingReader import net.fabricmc.mappingio.MappingUtil import net.fabricmc.mappingio.MappingVisitor @@ -61,7 +64,7 @@ class SeargeMappingResolver( val file = workspace[MCP_CONFIG] var url = URL("https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp_config/${version.id}/mcp_config-${version.id}.zip.sha1") - fun readMcpConfig(checksum: String): Path { + suspend fun readMcpConfig(checksum: String): Path { if (MCP_CONFIG in workspace) { val fileChecksum = file.getChecksum(sha1Digest) @@ -73,29 +76,33 @@ class SeargeMappingResolver( logger.warn { "checksum mismatch for ${version.id} Searge mapping cache, fetching them again" } } - URL(url.toString().removeSuffix(".sha1")).copyTo(file) + return withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + URL(url.toString().removeSuffix(".sha1")).copyTo(file) - logger.info { "fetched ${version.id} Searge mappings" } - return findMappingFile(file) + logger.info { "fetched ${version.id} Searge mappings" } + return@withContext findMappingFile(file) + } } - url.httpRequest { - if (it.ok) { - return@resolver readMcpConfig(it.inputStream.reader().use(Reader::readText)) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + url.httpRequest { + if (it.ok) { + return@withContext readMcpConfig(it.inputStream.reader().use(Reader::readText)) + } } - } - // let's try the second URL + // let's try the second URL - url = URL("https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp/${version.id}/mcp-${version.id}-srg.zip.sha1") - url.httpRequest { - if (it.ok) { - return@resolver readMcpConfig(it.inputStream.reader().use(Reader::readText)) + url = URL("https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp/${version.id}/mcp-${version.id}-srg.zip.sha1") + url.httpRequest { + if (it.ok) { + return@withContext readMcpConfig(it.inputStream.reader().use(Reader::readText)) + } } - } - logger.warn { "failed to fetch ${version.id} Searge mappings, didn't find a valid URL" } - return@resolver null + logger.warn { "failed to fetch ${version.id} Searge mappings, didn't find a valid URL" } + return@withContext null + } } upToDateWhen { it == null || it.isRegularFile() } @@ -111,7 +118,7 @@ class SeargeMappingResolver( return@withLock file } - URL(licenseSource).copyTo(file) + URL(licenseSource).copyTo(file) // TODO: use IO context logger.info { "fetched Searge license file" } return@withLock file @@ -133,14 +140,13 @@ class SeargeMappingResolver( // Searge has obf, srg and id namespaces; obf is the obfuscated one MappingReader.read(reader, MappingNsRenamer(visitor, mapOf( "obf" to MappingUtil.NS_SOURCE_FALLBACK, - "srg" to "searge", - "id" to "searge_id", + "srg" to targetNamespace, + "id" to "${targetNamespace}_id", // in older versions, there weren't any namespaces, so make sure to rename the fallback too - MappingUtil.NS_TARGET_FALLBACK to "searge" + MappingUtil.NS_TARGET_FALLBACK to targetNamespace ))) val licensePath by licenseOutput - licensePath.reader().use { visitor.visitMetadata(META_LICENSE, it.readLines().joinToString("\\n").replace("\t", " ")) visitor.visitMetadata(META_LICENSE_SOURCE, licenseSource) diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotManifestProvider.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotManifestProvider.kt index e6ade5aee6d..db13a9aaeaa 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotManifestProvider.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotManifestProvider.kt @@ -20,11 +20,9 @@ package me.kcra.takenaka.core.mapping.resolve.impl import com.fasterxml.jackson.core.JacksonException import com.fasterxml.jackson.databind.ObjectMapper import me.kcra.takenaka.core.* -import me.kcra.takenaka.core.util.copyTo -import me.kcra.takenaka.core.util.httpRequest -import me.kcra.takenaka.core.util.ok -import me.kcra.takenaka.core.util.readValue -import mu.KotlinLogging +import me.kcra.takenaka.core.util.* +import me.kcra.takenaka.core.util.MAPPER +import io.github.oshai.kotlinlogging.KotlinLogging import java.net.URL private val logger = KotlinLogging.logger {} @@ -39,7 +37,10 @@ private val logger = KotlinLogging.logger {} * @property relaxedCache whether output cache verification constraints should be relaxed * @author Matouš Kučera */ -class SpigotManifestProvider(val workspace: VersionedWorkspace, private val objectMapper: ObjectMapper, val relaxedCache: Boolean = true) { +class SpigotManifestProvider @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("SpigotManifestProvider(workspace, relaxedCache)") +) constructor(val workspace: VersionedWorkspace, private val objectMapper: ObjectMapper, val relaxedCache: Boolean = true) { /** * The version manifest. */ @@ -56,6 +57,15 @@ class SpigotManifestProvider(val workspace: VersionedWorkspace, private val obje val isAliased: Boolean get() = attributes?.let { workspace.version.id != it.minecraftVersion } ?: false + /** + * Creates a new manifest provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + @Suppress("DEPRECATION") + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : this(workspace, MAPPER, relaxedCache) + /** * Reads the manifest of the targeted version from cache, fetching it if the cache missed. * diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotMappingResolver.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotMappingResolver.kt index e20c120acdb..fd5d5220b8a 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotMappingResolver.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/SpigotMappingResolver.kt @@ -18,6 +18,9 @@ package me.kcra.takenaka.core.mapping.resolve.impl import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import me.kcra.takenaka.core.VersionedWorkspace import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.resolve.AbstractMappingResolver @@ -26,10 +29,11 @@ import me.kcra.takenaka.core.mapping.resolve.Output import me.kcra.takenaka.core.mapping.resolve.lazyOutput import me.kcra.takenaka.core.mapping.toInternalName import me.kcra.takenaka.core.mapping.util.unwrap +import me.kcra.takenaka.core.util.XML_MAPPER import me.kcra.takenaka.core.util.copyTo import me.kcra.takenaka.core.util.httpRequest import me.kcra.takenaka.core.util.ok -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.MappedElementKind import net.fabricmc.mappingio.MappingUtil import net.fabricmc.mappingio.MappingVisitor @@ -56,8 +60,12 @@ private val logger = KotlinLogging.logger {} * @property relaxedCache whether output cache verification constraints should be relaxed * @author Matouš Kučera */ -abstract class AbstractSpigotMappingResolver( +abstract class AbstractSpigotMappingResolver @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("AbstractSpigotMappingResolver(workspace, spigotProvider, relaxedCache)") +) constructor( override val workspace: VersionedWorkspace, + @Deprecated("Jackson will be an implementation detail in the future.") val xmlMapper: ObjectMapper, val spigotProvider: SpigotManifestProvider, val relaxedCache: Boolean = true @@ -73,6 +81,17 @@ abstract class AbstractSpigotMappingResolver( override val outputs: List> get() = listOf(mappingOutput, licenseOutput, pomOutput) + /** + * Creates a new resolver. + * + * @param workspace the workspace + * @param spigotProvider the Spigot manifest provider + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + @Suppress("DEPRECATION") + constructor(workspace: VersionedWorkspace, spigotProvider: SpigotManifestProvider, relaxedCache: Boolean = true) + : this(workspace, XML_MAPPER, spigotProvider, relaxedCache) + /** * The resolved mapping attribute. */ @@ -93,19 +112,20 @@ abstract class AbstractSpigotMappingResolver( return@resolver file } - // manifest is going to be non-null, since it's used to fetch mappingAttribute - URL("https://hub.spigotmc.org/stash/projects/SPIGOT/repos/builddata/raw/mappings/${mappingAttribute.value}?at=${spigotProvider.manifest!!.refs["BuildData"]}").httpRequest { - if (it.ok) { - it.copyTo(file) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + // manifest is going to be non-null, since it's used to fetch mappingAttribute + URL("https://hub.spigotmc.org/stash/projects/SPIGOT/repos/builddata/raw/mappings/${mappingAttribute.value}?at=${spigotProvider.manifest!!.refs["BuildData"]}").httpRequest { + if (it.ok) { + it.copyTo(file) - logger.info { "fetched ${version.id} Spigot mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } - return@resolver file - } + logger.info { "fetched ${version.id} Spigot mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value})" } + return@httpRequest file + } - logger.warn { "failed to fetch ${version.id} Spigot mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value}), received ${it.responseCode}" } + logger.warn { "failed to fetch ${version.id} Spigot mappings (name: ${mappingAttribute.name}, value: ${mappingAttribute.value}), received ${it.responseCode}" } + return@httpRequest null + } } - - return@resolver null } upToDateWhen { it == null || it.isRegularFile() } @@ -116,16 +136,18 @@ abstract class AbstractSpigotMappingResolver( val file = workspace[LICENSE] val mappingPath by mappingOutput - mappingPath?.bufferedReader()?.use { - val line = it.readLine() + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + mappingPath?.bufferedReader()?.use { + val line = it.readLine() - if (line.startsWith("# ")) { - file.writeText(line.drop(2)) - return@resolver file + if (line.startsWith("# ")) { + file.writeText(line.drop(2)) + return@withContext file + } } - } - return@resolver null + return@withContext null + } } upToDateWhen { it == null || it.isRegularFile() } @@ -146,18 +168,19 @@ abstract class AbstractSpigotMappingResolver( return@resolver file } - URL("https://hub.spigotmc.org/stash/projects/SPIGOT/repos/craftbukkit/raw/pom.xml?at=${manifest.refs["CraftBukkit"]}").httpRequest { - if (it.ok) { - it.copyTo(file) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + URL("https://hub.spigotmc.org/stash/projects/SPIGOT/repos/craftbukkit/raw/pom.xml?at=${manifest.refs["CraftBukkit"]}").httpRequest { + if (it.ok) { + it.copyTo(file) - logger.info { "fetched ${version.id} CraftBukkit pom.xml" } - return@resolver file - } + logger.info { "fetched ${version.id} CraftBukkit pom.xml" } + return@httpRequest file + } - logger.warn { "failed to fetch ${version.id} CraftBukkit pom.xml, received ${it.responseCode}" } + logger.warn { "failed to fetch ${version.id} CraftBukkit pom.xml, received ${it.responseCode}" } + return@httpRequest null + } } - - return@resolver null } upToDateWhen { it == null || it.isRegularFile() } @@ -189,7 +212,6 @@ abstract class AbstractSpigotMappingResolver( } val licensePath by licenseOutput - licensePath?.reader()?.use { visitor.visitMetadata(META_LICENSE, it.readText()) visitor.visitMetadata(META_LICENSE_SOURCE, licenseSource) @@ -198,7 +220,12 @@ abstract class AbstractSpigotMappingResolver( val pomPath by pomOutput // prepend "v" before the NMS version to match the package name - pomPath?.reader()?.use { xmlMapper.readTree(it)["properties"]["minecraft_version"].asText()?.let { v -> visitor.visitMetadata(META_CB_NMS_VERSION, "v$v") } } + pomPath?.reader()?.use { + @Suppress("DEPRECATION") + xmlMapper.readTree(it)["properties"]["minecraft_version"].asText()?.let { v -> + visitor.visitMetadata(META_CB_NMS_VERSION, "v$v") + } + } } companion object { @@ -258,7 +285,11 @@ inline val MappingTreeView.craftBukkitNmsVersion: String? * @param relaxedCache whether output cache verification constraints should be relaxed * @author Matouš Kučera */ -class SpigotClassMappingResolver( +@Suppress("DEPRECATION") +class SpigotClassMappingResolver @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("SpigotClassMappingResolver(workspace, spigotProvider, relaxedCache)") +) constructor( workspace: VersionedWorkspace, xmlMapper: ObjectMapper, spigotProvider: SpigotManifestProvider, @@ -272,8 +303,31 @@ class SpigotClassMappingResolver( * @param xmlMapper an [ObjectMapper] that can deserialize XML trees * @param relaxedCache whether output cache verification constraints should be relaxed */ + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("SpigotClassMappingResolver(workspace, relaxedCache)") + ) constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, xmlMapper: ObjectMapper, relaxedCache: Boolean = true) : - this(workspace, xmlMapper, SpigotManifestProvider(workspace, objectMapper), relaxedCache) + this(workspace, xmlMapper, SpigotManifestProvider(workspace, objectMapper, relaxedCache), relaxedCache) + + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : + this(workspace, XML_MAPPER, SpigotManifestProvider(workspace, relaxedCache), relaxedCache) + + /** + * Creates a new resolver with a supplied metadata provider. + * + * @param workspace the workspace + * @param spigotProvider the Spigot manifest provider + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, spigotProvider: SpigotManifestProvider, relaxedCache: Boolean = true) : + this(workspace, XML_MAPPER, spigotProvider, relaxedCache) /** * Resolves a BuildData mapping attribute. @@ -363,7 +417,11 @@ class SpigotClassMappingResolver( * @param relaxedCache whether output cache verification constraints should be relaxed * @author Matouš Kučera */ -class SpigotMemberMappingResolver( +@Suppress("DEPRECATION") +class SpigotMemberMappingResolver @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("SpigotMemberMappingResolver(workspace, spigotProvider, relaxedCache)") +) constructor( workspace: VersionedWorkspace, xmlMapper: ObjectMapper, spigotProvider: SpigotManifestProvider, @@ -377,9 +435,33 @@ class SpigotMemberMappingResolver( * @param workspace the workspace * @param objectMapper an [ObjectMapper] that can deserialize JSON data * @param xmlMapper an [ObjectMapper] that can deserialize XML trees + * @param relaxedCache whether output cache verification constraints should be relaxed */ + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("SpigotMemberMappingResolver(workspace, relaxedCache)") + ) constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, xmlMapper: ObjectMapper, relaxedCache: Boolean = true) : - this(workspace, xmlMapper, SpigotManifestProvider(workspace, objectMapper), relaxedCache) + this(workspace, xmlMapper, SpigotManifestProvider(workspace, objectMapper, relaxedCache), relaxedCache) + + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : + this(workspace, XML_MAPPER, SpigotManifestProvider(workspace, relaxedCache), relaxedCache) + + /** + * Creates a new resolver with a supplied metadata provider. + * + * @param workspace the workspace + * @param spigotProvider the Spigot manifest provider + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, spigotProvider: SpigotManifestProvider, relaxedCache: Boolean = true) : + this(workspace, XML_MAPPER, spigotProvider, relaxedCache) /** * Resolves a BuildData mapping attribute. diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/VanillaMappingContributor.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/VanillaMappingContributor.kt index 4fe382bc332..72606f4986c 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/VanillaMappingContributor.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/VanillaMappingContributor.kt @@ -18,13 +18,16 @@ package me.kcra.takenaka.core.mapping.resolve.impl import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import me.kcra.takenaka.core.VersionedWorkspace import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.resolve.AbstractOutputContainer import me.kcra.takenaka.core.mapping.resolve.Output import me.kcra.takenaka.core.mapping.resolve.lazyOutput import me.kcra.takenaka.core.util.* -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.MappedElementKind import net.fabricmc.mappingio.MappingUtil import net.fabricmc.mappingio.MappingVisitor @@ -74,29 +77,30 @@ abstract class AbstractVanillaMappingContributor( val fileName = "${jarAttribute.name}.jar" val file = workspace[fileName] - if (fileName in workspace) { - val checksum = file.getChecksum(sha1Digest) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + if (fileName in workspace) { + val checksum = file.getChecksum(sha1Digest) - if (jarAttribute.checksum == checksum) { - logger.info { "matched checksum for cached ${workspace.version.id} vanilla JAR (name: ${jarAttribute.name}, value: ${jarAttribute.value})" } - return@resolver file + if (jarAttribute.checksum == checksum) { + logger.info { "matched checksum for cached ${workspace.version.id} vanilla JAR (name: ${jarAttribute.name}, value: ${jarAttribute.value})" } + return@withContext file + } + + logger.warn { "checksum mismatch for ${workspace.version.id} vanilla JAR cache, fetching it again (name: ${jarAttribute.name}, value: ${jarAttribute.value})" } } - logger.warn { "checksum mismatch for ${workspace.version.id} vanilla JAR cache, fetching it again (name: ${jarAttribute.name}, value: ${jarAttribute.value})" } - } + URL(jarAttribute.value!!).httpRequest { + if (it.ok) { + it.copyTo(file) - URL(jarAttribute.value!!).httpRequest { - if (it.ok) { - it.copyTo(file) + logger.info { "fetched ${workspace.version.id} vanilla JAR (name: ${jarAttribute.name}, value: ${jarAttribute.value})" } + return@httpRequest file + } - logger.info { "fetched ${workspace.version.id} vanilla JAR (name: ${jarAttribute.name}, value: ${jarAttribute.value})" } - return@resolver file + logger.info { "failed to fetch ${workspace.version.id} vanilla JAR (name: ${jarAttribute.name}, value: ${jarAttribute.value}), received ${it.responseCode}" } + return@httpRequest null } - - logger.info { "failed to fetch ${workspace.version.id} vanilla JAR (name: ${jarAttribute.name}, value: ${jarAttribute.value}), received ${it.responseCode}" } } - - return@resolver null } upToDateWhen { it == null || it.isRegularFile() } @@ -308,9 +312,23 @@ class VanillaClientMappingContributor( * @param objectMapper an [ObjectMapper] that can deserialize JSON data * @param relaxedCache whether output cache verification constraints should be relaxed */ + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("VanillaClientMappingContributor(workspace, relaxedCache)") + ) + @Suppress("DEPRECATION") constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, relaxedCache: Boolean = true) : this(workspace, MojangManifestAttributeProvider(workspace, objectMapper, relaxedCache)) + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : + this(workspace, MojangManifestAttributeProvider(workspace, relaxedCache)) + /** * Resolves a Mojang manifest JAR attribute. * @@ -345,8 +363,22 @@ class VanillaServerMappingContributor( * @param objectMapper an [ObjectMapper] that can deserialize JSON data * @param relaxedCache whether output cache verification constraints should be relaxed */ - constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, relaxedCache: Boolean = true) - : this(workspace, MojangManifestAttributeProvider(workspace, objectMapper, relaxedCache), relaxedCache) + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("VanillaServerMappingContributor(workspace, relaxedCache)") + ) + @Suppress("DEPRECATION") + constructor(workspace: VersionedWorkspace, objectMapper: ObjectMapper, relaxedCache: Boolean = true) : + this(workspace, MojangManifestAttributeProvider(workspace, objectMapper, relaxedCache)) + + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) : + this(workspace, MojangManifestAttributeProvider(workspace, relaxedCache)) /** * Resolves a Mojang manifest JAR attribute. diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMappingResolver.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMappingResolver.kt index 6360ba939e9..cbb75fde41f 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMappingResolver.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMappingResolver.kt @@ -18,13 +18,16 @@ package me.kcra.takenaka.core.mapping.resolve.impl import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import me.kcra.takenaka.core.VersionedWorkspace import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.matchers.isConstructor import me.kcra.takenaka.core.mapping.resolve.* import me.kcra.takenaka.core.mapping.util.unwrap import me.kcra.takenaka.core.util.* -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.MappingReader import net.fabricmc.mappingio.MappingUtil import net.fabricmc.mappingio.MappingVisitor @@ -55,8 +58,7 @@ class YarnMappingResolver( val yarnProvider: YarnMetadataProvider, val relaxedCache: Boolean = true ) : AbstractMappingResolver(), MappingContributor, LicenseResolver { - override val licenseSource: String - get() = "https://raw.githubusercontent.com/FabricMC/yarn/${version.id}/LICENSE" + override val licenseSource: String = "https://raw.githubusercontent.com/FabricMC/yarn/${version.id}/LICENSE" override val targetNamespace: String = "yarn" override val outputs: List> get() = listOf(mappingOutput, licenseOutput) @@ -68,9 +70,23 @@ class YarnMappingResolver( * @param xmlMapper an [ObjectMapper] that can deserialize XML trees * @param relaxedCache whether output cache verification constraints should be relaxed */ + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("YarnMappingResolver(workspace, relaxedCache)") + ) + @Suppress("DEPRECATION") constructor(workspace: VersionedWorkspace, xmlMapper: ObjectMapper, relaxedCache: Boolean = true) : this(workspace, YarnMetadataProvider(workspace, xmlMapper), relaxedCache) + /** + * Creates a new resolver with a default metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + constructor(workspace: VersionedWorkspace, relaxedCache: Boolean = true) + : this(workspace, YarnMetadataProvider(workspace), relaxedCache) + override val mappingOutput = lazyOutput { resolver { val file = workspace[MAPPING_JAR] @@ -82,56 +98,56 @@ class YarnMappingResolver( } val targetBuild = builds.maxBy(YarnBuild::buildNumber) - - var urlString = "https://maven.fabricmc.net/net/fabricmc/yarn/$targetBuild/yarn-$targetBuild-mergedv2.jar" - URL(urlString).httpRequest(method = "HEAD") { mergedv2 -> - if (!mergedv2.ok) { - logger.info { "mergedv2 Yarn JAR for ${version.id} failed to fetch, falling back to v2" } - - urlString = "https://maven.fabricmc.net/net/fabricmc/yarn/$targetBuild/yarn-$targetBuild-v2.jar" - URL(urlString).httpRequest(method = "HEAD") { v2 -> - if (!v2.ok) { - logger.info { "v2 Yarn JAR for ${version.id} failed to fetch, falling back to no classifier" } - - urlString = "https://maven.fabricmc.net/net/fabricmc/yarn/$targetBuild/yarn-$targetBuild.jar" + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + var urlString = "https://maven.fabricmc.net/net/fabricmc/yarn/$targetBuild/yarn-$targetBuild-mergedv2.jar" + URL(urlString).httpRequest(method = "HEAD") { mergedv2 -> + if (!mergedv2.ok) { + logger.info { "mergedv2 Yarn JAR for ${version.id} failed to fetch, falling back to v2" } + + urlString = "https://maven.fabricmc.net/net/fabricmc/yarn/$targetBuild/yarn-$targetBuild-v2.jar" + URL(urlString).httpRequest(method = "HEAD") { v2 -> + if (!v2.ok) { + logger.info { "v2 Yarn JAR for ${version.id} failed to fetch, falling back to no classifier" } + + urlString = "https://maven.fabricmc.net/net/fabricmc/yarn/$targetBuild/yarn-$targetBuild.jar" + } } } } - } - val url = URL(urlString) - val checksumUrl = URL("$urlString.sha1") + val url = URL(urlString) + val checksumUrl = URL("$urlString.sha1") - if (MAPPING_JAR in workspace) { - checksumUrl.httpRequest { - if (it.ok) { - val checksum = file.getChecksum(sha1Digest) + if (MAPPING_JAR in workspace) { + checksumUrl.httpRequest { + if (it.ok) { + val checksum = file.getChecksum(sha1Digest) - if (it.readText() == checksum) { - logger.info { "matched checksum for cached ${version.id} Yarn mappings" } - return@resolver findMappingFile(file) + if (it.readText() == checksum) { + logger.info { "matched checksum for cached ${version.id} Yarn mappings" } + return@withContext findMappingFile(file) + } + } else if (file.fileSize() == url.contentLength) { + logger.info { "matched same length for cached ${version.id} Yarn mappings" } + return@withContext findMappingFile(file) } - } else if (file.fileSize() == url.contentLength) { - logger.info { "matched same length for cached ${version.id} Yarn mappings" } - return@resolver findMappingFile(file) } + + logger.warn { "checksum/length mismatch for ${version.id} Yarn mapping cache, fetching them again" } } - logger.warn { "checksum/length mismatch for ${version.id} Yarn mapping cache, fetching them again" } - } + url.httpRequest { + if (it.ok) { + it.copyTo(file) - url.httpRequest { - if (it.ok) { - it.copyTo(file) + logger.info { "fetched ${version.id} Yarn mappings" } + return@httpRequest findMappingFile(file) + } - logger.info { "fetched ${version.id} Yarn mappings" } - return@resolver findMappingFile(file) + logger.warn { "failed to fetch ${version.id} Yarn mappings, received ${it.responseCode}" } + return@httpRequest null } - - logger.warn { "failed to fetch ${version.id} Yarn mappings, received ${it.responseCode}" } } - - return@resolver null } upToDateWhen { it == null || it.isRegularFile() } @@ -146,20 +162,22 @@ class YarnMappingResolver( return@resolver file } - URL(licenseSource).httpRequest { - if (it.ok) { - it.copyTo(file) + withContext(Dispatchers.IO + CoroutineName("resolve-coro")) { + URL(licenseSource).httpRequest { + if (it.ok) { + it.copyTo(file) + + logger.info { "fetched ${version.id} Yarn license file" } + return@httpRequest file + } else if (it.responseCode == 404) { + logger.info { "did not find ${version.id} Yarn license file" } + } else { + logger.warn { "failed to fetch Yarn license file, received ${it.responseCode}" } + } - logger.info { "fetched ${version.id} Yarn license file" } - return@resolver file - } else if (it.responseCode == 404) { - logger.info { "did not find ${version.id} Yarn license file" } - } else { - logger.warn { "failed to fetch Yarn license file, received ${it.responseCode}" } + return@httpRequest null } } - - return@resolver null } upToDateWhen { it == null || it.isRegularFile() } diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMetadataProvider.kt b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMetadataProvider.kt index 3d336e78495..00c512bffb9 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMetadataProvider.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/mapping/resolve/impl/YarnMetadataProvider.kt @@ -22,7 +22,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import me.kcra.takenaka.core.Workspace import me.kcra.takenaka.core.util.* -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import java.net.URL private val logger = KotlinLogging.logger {} @@ -37,7 +37,10 @@ private val logger = KotlinLogging.logger {} * @property relaxedCache whether output cache verification constraints should be relaxed * @author Matouš Kučera */ -class YarnMetadataProvider(val workspace: Workspace, private val xmlMapper: ObjectMapper, val relaxedCache: Boolean = true) { +class YarnMetadataProvider @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("YarnMetadataProvider(workspace, relaxedCache)") +) constructor(val workspace: Workspace, private val xmlMapper: ObjectMapper, val relaxedCache: Boolean = true) { /** * A map of versions and their builds. */ @@ -48,6 +51,15 @@ class YarnMetadataProvider(val workspace: Workspace, private val xmlMapper: Obje */ private val metadata by lazy(::readMetadata) + /** + * Creates a new metadata provider. + * + * @param workspace the workspace + * @param relaxedCache whether output cache verification constraints should be relaxed + */ + @Suppress("DEPRECATION") + constructor(workspace: Workspace, relaxedCache: Boolean = true) : this(workspace, XML_MAPPER, relaxedCache) + /** * Parses Yarn version strings in the metadata file. * diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/util/collections.kt b/core/src/main/kotlin/me/kcra/takenaka/core/util/collections.kt index 9c08f579c07..a2a2bc00e47 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/util/collections.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/util/collections.kt @@ -25,3 +25,15 @@ package me.kcra.takenaka.core.util * @return the entry */ fun entryOf(key: K, value: V): Map.Entry = java.util.AbstractMap.SimpleImmutableEntry(key, value) + +/** + * Builds an ordered [Map] from a collection of pairs. + * + * Useful for doing in-place ordering changes before the map is created. + * + * @param action the builder action + * @return the built map + */ +fun buildOrderedMap(action: MutableList>.() -> Unit): Map { + return mutableListOf>().apply(action).toMap() +} diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/util/httpClient.kt b/core/src/main/kotlin/me/kcra/takenaka/core/util/httpClient.kt index d1498d93e57..581810a9af4 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/util/httpClient.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/util/httpClient.kt @@ -65,6 +65,19 @@ inline val URL.contentLength: Long } } +/** + * Fetches the URL and returns the value of the Last-Modified header of the response, returning -1 if the status code is not in the 2xx range. + */ +inline val URL.lastModified: Long + get() { + httpRequest(method = "HEAD") { + if (!it.ok) { + return -1 + } + return it.lastModified + } + } + /** * Returns whether the request's status code is in the 2xx range. */ diff --git a/core/src/main/kotlin/me/kcra/takenaka/core/util/objectMapper.kt b/core/src/main/kotlin/me/kcra/takenaka/core/util/objectMapper.kt index 403b10874c2..ed85780d635 100644 --- a/core/src/main/kotlin/me/kcra/takenaka/core/util/objectMapper.kt +++ b/core/src/main/kotlin/me/kcra/takenaka/core/util/objectMapper.kt @@ -20,17 +20,30 @@ package me.kcra.takenaka.core.util import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.readValue import java.nio.file.Path +/** + * An internal instance of an [ObjectMapper]. + */ +@Suppress("DEPRECATION") +internal val MAPPER = objectMapper() + +/** + * An internal instance of an [XmlMapper]. + */ +internal val XML_MAPPER = XmlMapper() + /** * Creates a new ObjectMapper with all modules necessary for deserializing manifests. * * @return the object mapper */ +@Deprecated("Jackson will be an implementation detail in the future.") fun objectMapper(): ObjectMapper = jsonMapper { addModule(kotlinModule()) addModule(JavaTimeModule()) diff --git a/core/src/test/kotlin/me/kcra/takenaka/core/test/VersionManifestTest.kt b/core/src/test/kotlin/me/kcra/takenaka/core/test/VersionManifestTest.kt index 58ab9a9de94..d890dc9f3cf 100644 --- a/core/src/test/kotlin/me/kcra/takenaka/core/test/VersionManifestTest.kt +++ b/core/src/test/kotlin/me/kcra/takenaka/core/test/VersionManifestTest.kt @@ -17,30 +17,25 @@ package me.kcra.takenaka.core.test -import com.fasterxml.jackson.module.kotlin.readValue -import me.kcra.takenaka.core.VersionAttributes -import me.kcra.takenaka.core.util.objectMapper -import me.kcra.takenaka.core.versionManifest -import java.net.URL +import me.kcra.takenaka.core.versionAttributesOf +import me.kcra.takenaka.core.versionManifestOf import kotlin.test.* class VersionManifestTest { - private val objectMapper = objectMapper() - @Test fun `version manifest can be fetched and deserialized`() { - objectMapper.versionManifest() + versionManifestOf() } @Test fun `version manifest can be fetched and deserialized, version list is not empty`() { - val versionManifest = objectMapper.versionManifest() + val versionManifest = versionManifestOf() assertTrue(versionManifest.versions.isNotEmpty()) } @Test fun `version can be found by id`() { - val versionManifest = objectMapper.versionManifest() + val versionManifest = versionManifestOf() val version = versionManifest["1.14.4"] assertNotNull(version) assertEquals("1.14.4", version.id) @@ -48,7 +43,7 @@ class VersionManifestTest { @Test fun `version list can be sorted by release time`() { - val versionManifest = objectMapper.versionManifest() + val versionManifest = versionManifestOf() val sortedVersions = versionManifest.versions.sorted() assertTrue(sortedVersions.isNotEmpty()) assertTrue(sortedVersions.first().releaseTime <= sortedVersions.last().releaseTime) @@ -56,27 +51,28 @@ class VersionManifestTest { @Test fun `version attributes can be deserialized`() { - val versionManifest = objectMapper.versionManifest() + val versionManifest = versionManifestOf() val version = versionManifest["1.14.4"] assertNotNull(version) - objectMapper.readValue(URL(version.url)) + + versionAttributesOf(version) } @Test fun `server mappings are available for 1_14_4`() { - val versionManifest = objectMapper.versionManifest() + val versionManifest = versionManifestOf() val version = versionManifest["1.14.4"] assertNotNull(version) - val versionAttributes = objectMapper.readValue(URL(version.url)) + val versionAttributes = versionAttributesOf(version) assertNotNull(versionAttributes.downloads.serverMappings) } @Test fun `server mappings are available for 1_12_2`() { - val versionManifest = objectMapper.versionManifest() + val versionManifest = versionManifestOf() val version = versionManifest["1.12.2"] assertNotNull(version) - val versionAttributes = objectMapper.readValue(URL(version.url)) + val versionAttributes = versionAttributesOf(version) assertNull(versionAttributes.downloads.serverMappings) } } diff --git a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorExtension.kt b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorExtension.kt index 9705d403da1..869c2b814fc 100644 --- a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorExtension.kt +++ b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorExtension.kt @@ -20,10 +20,15 @@ package me.kcra.takenaka.generator.accessor.plugin import me.kcra.takenaka.core.Version import me.kcra.takenaka.core.VersionManifest import me.kcra.takenaka.core.VersionRangeBuilder +import me.kcra.takenaka.generator.accessor.DEFAULT_RUNTIME_PACKAGE import me.kcra.takenaka.generator.accessor.AccessorConfiguration import me.kcra.takenaka.generator.accessor.AccessorType import me.kcra.takenaka.generator.accessor.CodeLanguage import me.kcra.takenaka.generator.accessor.model.* +import me.kcra.takenaka.generator.accessor.naming.NamingStrategy +import me.kcra.takenaka.generator.accessor.naming.StandardNamingStrategies +import me.kcra.takenaka.generator.accessor.naming.prefixed +import me.kcra.takenaka.generator.accessor.naming.resolveSimpleConflicts import me.kcra.takenaka.generator.accessor.plugin.tasks.DEFAULT_INDEX_NS import org.gradle.api.Action import org.gradle.api.Project @@ -77,8 +82,9 @@ abstract class AccessorGeneratorExtension(protected val project: Project, protec abstract val accessors: ListProperty /** - * Base package of the generated accessors, required. + * Base package of the generated accessors. */ + @Deprecated("The base package concept was superseded by naming strategies.") abstract val basePackage: Property /** @@ -92,9 +98,16 @@ abstract class AccessorGeneratorExtension(protected val project: Project, protec abstract val accessorType: Property /** - * Namespaces that should be used in accessors, empty if all namespaces should be used. + * Namespaces that should be used in accessors, defaults to all supported namespaces ("mojang", "spigot", "yarn", "quilt", "searge", "intermediary" and "hashed"). */ - abstract val accessedNamespaces: ListProperty + abstract val namespaces: ListProperty + + /** + * Alias for [namespaces]. + */ + @Deprecated("Use namespaces.", ReplaceWith("namespaces")) + val accessedNamespaces: ListProperty + get() = namespaces /** * Namespaces that should be used for computing history, defaults to "mojang", "spigot", "searge" and "intermediary". @@ -106,15 +119,43 @@ abstract class AccessorGeneratorExtension(protected val project: Project, protec */ abstract val historyIndexNamespace: Property + /** + * Strategy used to name generated classes and their members, defaults to a conflict-resolving variant of [StandardNamingStrategies.SIMPLE]. + */ + abstract val namingStrategy: Property + + /** + * Package containing the accessor runtime, defaults to [DEFAULT_RUNTIME_PACKAGE]. + */ + abstract val runtimePackage: Property + + /** + * Base URL of the mapping website including protocol, defaults to `null`. + */ + abstract val mappingWebsite: Property + init { outputDirectory.convention(project.layout.buildDirectory.dir("takenaka/output")) cacheDirectory.convention(project.layout.buildDirectory.dir("takenaka/cache")) codeLanguage.convention(CodeLanguage.JAVA) accessorType.convention(AccessorType.NONE) + namespaces.convention(listOf("mojang", "spigot", "yarn", "quilt", "searge", "intermediary", "hashed")) historyNamespaces.convention(listOf("mojang", "spigot", "searge", "intermediary")) historyIndexNamespace.convention(DEFAULT_INDEX_NS) relaxedCache.convention(true) platform.convention(PlatformTristate.SERVER) + @Suppress("DEPRECATION") + namingStrategy.convention(basePackage.map { pack -> StandardNamingStrategies.SIMPLE.prefixed(pack).resolveSimpleConflicts() }) + runtimePackage.convention(DEFAULT_RUNTIME_PACKAGE) + mappingWebsite.convention(null) + } + + /** + * Alias for [version]. + */ + @Deprecated("Use version(vararg String).", ReplaceWith("version(*versions)")) + fun versions(vararg versions: String) { + this.version(*versions) } /** @@ -122,7 +163,7 @@ abstract class AccessorGeneratorExtension(protected val project: Project, protec * * @param versions the versions */ - fun versions(vararg versions: String) { + fun version(vararg versions: String) { this.versions.addAll(*versions) } @@ -200,8 +241,9 @@ abstract class AccessorGeneratorExtension(protected val project: Project, protec * * @param basePackage the base package */ + @Suppress("DEPRECATION") fun basePackage(basePackage: String) { - this.basePackage.set(basePackage) + this.basePackage.set(basePackage) // TODO: immediately wrap the strategy instead } /** @@ -241,12 +283,20 @@ abstract class AccessorGeneratorExtension(protected val project: Project, protec } /** - * Adds new namespaces to the [accessedNamespaces] property. + * Adds new namespaces to the [namespaces] property. * - * @param accessedNamespaces the namespaces + * @param namespaces the namespaces + */ + fun namespaces(vararg namespaces: String) { + this.namespaces.addAll(*namespaces) + } + + /** + * Alias for [namespaces]. */ + @Deprecated("Use namespaces(vararg String).", ReplaceWith("namespaces(*accessedNamespaces)")) fun accessedNamespaces(vararg accessedNamespaces: String) { - this.accessedNamespaces.addAll(*accessedNamespaces) + this.namespaces(*accessedNamespaces) } /** @@ -267,6 +317,33 @@ abstract class AccessorGeneratorExtension(protected val project: Project, protec this.historyIndexNamespace.set(historyIndexNamespace) } + /** + * Sets the [namingStrategy] property. + * + * @param strategy the naming strategy + */ + fun namingStrategy(strategy: NamingStrategy) { + this.namingStrategy.set(strategy) + } + + /** + * Sets the [runtimePackage] property. + * + * @param runtimePackage the package + */ + fun runtimePackage(runtimePackage: String) { + this.runtimePackage.set(runtimePackage) + } + + /** + * Sets the [mappingWebsite] property. + * + * @param mappingWebsite base url of the website including protocol + */ + fun mappingWebsite(mappingWebsite: String?) { + this.mappingWebsite.set(mappingWebsite) + } + /** * Creates a new accessor model with the supplied name. * @@ -341,4 +418,58 @@ class GradleFlavoredClassAccessorBuilder(name: String) : AbstractClassAccessorBu fun methodChain(block: Action) { methodChain0(block::execute) } + + // naming strategy shortcuts + + /** + * Shortcut for [me.kcra.takenaka.generator.accessor.naming.prefixed]. + */ + fun prefixed(strategy: NamingStrategy, basePackage: String): NamingStrategy { + return strategy.prefixed(basePackage) + } + + /** + * Shortcut for [StandardNamingStrategies.SIMPLE]. + */ + fun simple(): NamingStrategy { + return StandardNamingStrategies.SIMPLE + } + + /** + * Shortcut for [me.kcra.takenaka.generator.accessor.naming.prefixed] and + * [me.kcra.takenaka.generator.accessor.naming.resolveSimpleConflicts] called on [StandardNamingStrategies.SIMPLE]. + */ + fun prefixedSimple(basePackage: String): NamingStrategy { + return StandardNamingStrategies.SIMPLE.prefixed(basePackage).resolveSimpleConflicts() + } + + /** + * Shortcut for [StandardNamingStrategies.SEMI_QUALIFIED]. + */ + fun semiQualified(): NamingStrategy { + return StandardNamingStrategies.SEMI_QUALIFIED + } + + /** + * Shortcut for [me.kcra.takenaka.generator.accessor.naming.prefixed] + * called on [StandardNamingStrategies.SEMI_QUALIFIED]. + */ + fun prefixedSemiQualified(basePackage: String): NamingStrategy { + return StandardNamingStrategies.SEMI_QUALIFIED.prefixed(basePackage) + } + + /** + * Shortcut for [StandardNamingStrategies.FULLY_QUALIFIED]. + */ + fun fullyQualified(): NamingStrategy { + return StandardNamingStrategies.FULLY_QUALIFIED + } + + /** + * Shortcut for [me.kcra.takenaka.generator.accessor.naming.prefixed] + * called on [StandardNamingStrategies.FULLY_QUALIFIED]. + */ + fun prefixedFullyQualified(basePackage: String): NamingStrategy { + return StandardNamingStrategies.FULLY_QUALIFIED.prefixed(basePackage) + } } diff --git a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorPlugin.kt b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorPlugin.kt index 6822bda7820..87852f4726c 100644 --- a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorPlugin.kt +++ b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/AccessorGeneratorPlugin.kt @@ -17,8 +17,7 @@ package me.kcra.takenaka.generator.accessor.plugin -import me.kcra.takenaka.core.cachedVersionManifest -import me.kcra.takenaka.core.util.objectMapper +import me.kcra.takenaka.core.versionManifestFrom import me.kcra.takenaka.generator.accessor.AccessorGenerator import me.kcra.takenaka.generator.accessor.plugin.tasks.GenerateAccessorsTask import me.kcra.takenaka.generator.accessor.plugin.tasks.ResolveMappingsTask @@ -49,35 +48,49 @@ class AccessorGeneratorPlugin : Plugin { override fun apply(target: Project) { val manifestCacheFile = target.layout.buildDirectory.file(System.getProperty("me.kcra.takenaka.manifest.file", "takenaka/cache/manifest.json")) - val manifest = objectMapper().cachedVersionManifest(manifestCacheFile.get().asFile.toPath()) // please switch to NIO paths, gradle!! + val manifest = versionManifestFrom(manifestCacheFile.get().asFile.toPath()) // please switch to NIO paths, gradle!! val config = target.extensions.create("accessors", target, manifest) // automatically adds tasks for basic Mojang-based accessor generation - val mappingBundle by target.configurations.creating - val resolveMappings by target.tasks.creating(ResolveMappingsTask::class) { + val mappingBundle by target.configurations.registering + val resolveMappings by target.tasks.registering(ResolveMappingsTask::class) { group = "takenaka" description = "Resolves a basic set of mappings for development on Mojang-based software." this.cacheDir.set(config.cacheDirectory) this.versions.set(config.versions) + this.namespaces.set(project.provider { config.namespaces.get() + config.historyNamespaces.get() }) this.relaxedCache.set(config.relaxedCache) this.platform.set(config.platform) this.manifest.set(manifest) + this.mappingBundle.fileProvider(mappingBundle.flatMap { mb -> + project.provider { // Kotlin doesn't like returning null from Provider#map + if (!mb.isEmpty) { + return@provider requireNotNull(mb.singleOrNull()) { + "mappingBundle configuration may only have a single file" + } + } + + return@provider null + } + }) } - val generateAccessors by target.tasks.creating(GenerateAccessorsTask::class) { + val generateAccessors by target.tasks.registering(GenerateAccessorsTask::class) { group = "takenaka" description = "Generates reflective accessors." dependsOn(resolveMappings) this.outputDir.set(config.outputDirectory) - this.mappingProvider.set(resolveMappings.mappings.map(::SimpleMappingProvider)) + this.mappingProvider.set(resolveMappings.flatMap { rm -> rm.mappings.map(::SimpleMappingProvider) }) this.accessors.set(config.accessors) - this.basePackage.set(config.basePackage) this.codeLanguage.set(config.codeLanguage) this.accessorType.set(config.accessorType) - this.accessedNamespaces.set(config.accessedNamespaces) + this.namespaces.set(config.namespaces) this.historyNamespaces.set(config.historyNamespaces) this.historyIndexNamespace.set(config.historyIndexNamespace) + this.namingStrategy.set(config.namingStrategy) + this.runtimePackage.set(config.runtimePackage) + this.mappingWebsite.set(config.mappingWebsite) } val traceAccessors by target.tasks.creating(TraceAccessorsTask::class) { group = "takenaka" @@ -85,14 +98,16 @@ class AccessorGeneratorPlugin : Plugin { dependsOn(resolveMappings) this.outputDir.set(config.outputDirectory) - this.mappingProvider.set(resolveMappings.mappings.map(::SimpleMappingProvider)) + this.mappingProvider.set(resolveMappings.flatMap { rm -> rm.mappings.map(::SimpleMappingProvider) }) this.accessors.set(config.accessors) - this.basePackage.set(config.basePackage) this.codeLanguage.set(config.codeLanguage) this.accessorType.set(config.accessorType) - this.accessedNamespaces.set(config.accessedNamespaces) + this.namespaces.set(config.namespaces) this.historyNamespaces.set(config.historyNamespaces) this.historyIndexNamespace.set(config.historyIndexNamespace) + this.namingStrategy.set(config.namingStrategy) + this.runtimePackage.set(config.runtimePackage) + this.mappingWebsite.set(config.mappingWebsite) } target.tasks.withType { @@ -114,11 +129,6 @@ class AccessorGeneratorPlugin : Plugin { if (config.outputDirectory.get().asFile == defaultLocation) { extensions.getByType().sourceSets["main"].java.srcDir(defaultLocation) } - - // set the task bundle file, if the mappingBundle configuration is not empty - if (!mappingBundle.isEmpty) { - resolveMappings.mappingBundle.set(mappingBundle.singleFile) - } } } } @@ -130,5 +140,6 @@ class AccessorGeneratorPlugin : Plugin { * @param version the dependency version, defaults to the version of this Gradle plugin * @return the dependency */ +@Suppress("UnusedReceiverParameter") // scope restriction fun DependencyHandler.accessorRuntime(group: String = BuildConfig.BUILD_MAVEN_GROUP, version: String = BuildConfig.BUILD_VERSION): Any = "$group:generator-accessor-runtime:$version" diff --git a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerateAccessorsTask.kt b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerateAccessorsTask.kt index bc457e3f48a..b4eaf9c0ff2 100644 --- a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerateAccessorsTask.kt +++ b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerateAccessorsTask.kt @@ -38,12 +38,14 @@ abstract class GenerateAccessorsTask : GenerationTask() { outputWorkspace, AccessorConfiguration( accessors = accessors.get(), - basePackage = basePackage.get(), codeLanguage = codeLanguage.get(), accessorType = accessorType.get(), namespaceFriendlinessIndex = namespaceFriendlinessIndex.get(), - accessedNamespaces = accessedNamespaces.get(), - craftBukkitVersionReplaceCandidates = craftBukkitVersionReplaceCandidates.get() + accessedNamespaces = namespaces.get(), + craftBukkitVersionReplaceCandidates = craftBukkitVersionReplaceCandidates.get(), + namingStrategy = namingStrategy.get(), + runtimePackage = runtimePackage.get(), + mappingWebsite = mappingWebsite.get() ) ) diff --git a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerationTask.kt b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerationTask.kt index 62cbfcc3bda..7b4834ffeec 100644 --- a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerationTask.kt +++ b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/GenerationTask.kt @@ -18,9 +18,14 @@ package me.kcra.takenaka.generator.accessor.plugin.tasks import me.kcra.takenaka.core.workspace +import me.kcra.takenaka.generator.accessor.DEFAULT_RUNTIME_PACKAGE import me.kcra.takenaka.generator.accessor.AccessorType import me.kcra.takenaka.generator.accessor.CodeLanguage import me.kcra.takenaka.generator.accessor.model.ClassAccessor +import me.kcra.takenaka.generator.accessor.naming.NamingStrategy +import me.kcra.takenaka.generator.accessor.naming.StandardNamingStrategies +import me.kcra.takenaka.generator.accessor.naming.prefixed +import me.kcra.takenaka.generator.accessor.naming.resolveSimpleConflicts import me.kcra.takenaka.generator.common.provider.MappingProvider import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty @@ -28,6 +33,7 @@ import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory /** @@ -64,16 +70,18 @@ abstract class GenerationTask : DefaultTask() { abstract val accessors: ListProperty /** - * Base package of the generated accessors, required. + * Base package of the generated accessors. * * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.basePackage */ + @Deprecated("The base package concept was superseded by naming strategies.") + @get:Optional @get:Input abstract val basePackage: Property /** * An ordered list of namespaces that will be considered when selecting a "friendly" name, - * defaults to "mojang", "spigot", "yarn", "searge", "intermediary" and "source". + * defaults to "mojang", "spigot", "yarn", "quilt", "searge", "intermediary", "hashed" and "source". */ @get:Input abstract val namespaceFriendlinessIndex: ListProperty @@ -95,12 +103,20 @@ abstract class GenerationTask : DefaultTask() { abstract val accessorType: Property /** - * Namespaces that should be used in accessors, empty if all namespaces should be used. + * Namespaces that should be used in accessors, defaults to all supported namespaces ("mojang", "spigot", "yarn", "quilt", "searge", "intermediary" and "hashed"). * - * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.accessedNamespaces + * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.namespaces */ @get:Input - abstract val accessedNamespaces: ListProperty + abstract val namespaces: ListProperty + + /** + * Alias for [namespaces]. + */ + @get:Internal + @Deprecated("Use namespaces.", ReplaceWith("namespaces")) + val accessedNamespaces: ListProperty + get() = namespaces /** * Namespaces that should have [me.kcra.takenaka.core.mapping.adapter.replaceCraftBukkitNMSVersion] applied @@ -123,6 +139,30 @@ abstract class GenerationTask : DefaultTask() { @get:Input abstract val historyIndexNamespace: Property + /** + * Strategy used to name generated classes and their members. + * + * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.namingStrategy + */ + @get:Input + abstract val namingStrategy: Property + + /** + * Package containing the accessor runtime, defaults to [DEFAULT_RUNTIME_PACKAGE]. + * + * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.runtimePackage + */ + @get:Input + abstract val runtimePackage: Property + + /** + * Base URL of the mapping website including protocol, defaults to `null`. + * + * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.mappingWebsite + */ + @get:Input + abstract val mappingWebsite: Property + /** * The output workspace ([outputDir]). */ @@ -135,11 +175,16 @@ abstract class GenerationTask : DefaultTask() { init { outputDir.convention(project.layout.buildDirectory.dir("takenaka/output")) - namespaceFriendlinessIndex.convention(listOf("mojang", "spigot", "yarn", "searge", "intermediary", "source")) + namespaceFriendlinessIndex.convention(listOf("mojang", "spigot", "yarn", "quilt", "searge", "intermediary", "hashed", "source")) codeLanguage.convention(CodeLanguage.JAVA) accessorType.convention(AccessorType.NONE) craftBukkitVersionReplaceCandidates.convention(listOf("spigot")) + namespaces.convention(listOf("mojang", "spigot", "yarn", "quilt", "searge", "intermediary", "hashed")) historyNamespaces.convention(listOf("mojang", "spigot", "searge", "intermediary")) historyIndexNamespace.convention(DEFAULT_INDEX_NS) + @Suppress("DEPRECATION") + namingStrategy.convention(basePackage.map { pack -> StandardNamingStrategies.SIMPLE.prefixed(pack).resolveSimpleConflicts() }) + runtimePackage.convention(DEFAULT_RUNTIME_PACKAGE) + mappingWebsite.convention(null) } } diff --git a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/ResolveMappingsTask.kt b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/ResolveMappingsTask.kt index cd2ab959322..bbd85f0f845 100644 --- a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/ResolveMappingsTask.kt +++ b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/ResolveMappingsTask.kt @@ -17,16 +17,17 @@ package me.kcra.takenaka.generator.accessor.plugin.tasks -import com.fasterxml.jackson.dataformat.xml.XmlMapper import kotlinx.coroutines.runBlocking import me.kcra.takenaka.core.Version import me.kcra.takenaka.core.VersionManifest import me.kcra.takenaka.core.compositeWorkspace +import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.adapter.* import me.kcra.takenaka.core.mapping.analysis.impl.AnalysisOptions import me.kcra.takenaka.core.mapping.analysis.impl.MappingAnalyzerImpl import me.kcra.takenaka.core.mapping.resolve.impl.* -import me.kcra.takenaka.core.util.objectMapper +import me.kcra.takenaka.core.util.md5Digest +import me.kcra.takenaka.core.util.updateAndHex import me.kcra.takenaka.generator.accessor.plugin.PlatformTristate import me.kcra.takenaka.generator.common.provider.impl.BundledMappingProvider import me.kcra.takenaka.generator.common.provider.impl.ResolvingMappingProvider @@ -35,6 +36,7 @@ import net.fabricmc.mappingio.tree.MappingTree import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty @@ -85,6 +87,17 @@ abstract class ResolveMappingsTask : DefaultTask() { @get:Input abstract val versions: SetProperty + /** + * Namespaces to be resolved, defaults to all supported namespaces ("mojang", "spigot", "yarn", "quilt", "searge", "intermediary" and "hashed"). + * + * *This is ignored if a [mappingBundle] is specified.* + * + * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.namespaces + * @see me.kcra.takenaka.generator.accessor.plugin.AccessorGeneratorExtension.historyNamespaces + */ + @get:Input + abstract val namespaces: ListProperty + /** * Whether output cache verification constraints should be relaxed, defaults to true. * @@ -144,6 +157,7 @@ abstract class ResolveMappingsTask : DefaultTask() { } init { + namespaces.convention(listOf("mojang", "spigot", "yarn", "quilt", "searge", "intermediary", "hashed")) cacheDir.convention(project.layout.buildDirectory.dir("takenaka/cache")) relaxedCache.convention(true) platform.convention(PlatformTristate.SERVER) @@ -181,18 +195,21 @@ abstract class ResolveMappingsTask : DefaultTask() { */ @TaskAction fun run() { - val objectMapper = objectMapper() - val requiredPlatform = platform.get() val requiredVersions = versions.get().toList() + + val requiredNamespaces = namespaces.get().toSet() + fun MutableCollection.addIfSupported(contributor: T) { + if (contributor.targetNamespace in requiredNamespaces) add(contributor) + } + val resolvedMappings = runBlocking { // resolve mappings on this system, if a bundle is not available if (mappingBundle.isPresent) { BundledMappingProvider(mappingBundle.get().asFile.toPath(), requiredVersions, manifest.get()).get() } else { - val xmlMapper = XmlMapper() - - val yarnProvider = YarnMetadataProvider(sharedCacheWorkspace, xmlMapper, relaxedCache.get()) + val yarnProvider = YarnMetadataProvider(sharedCacheWorkspace, relaxedCache.get()) + val quiltProvider = QuiltMetadataProvider(sharedCacheWorkspace, relaxedCache.get()) val mappingConfig = buildMappingConfig { version(requiredVersions) workspace(mappingCacheWorkspace) @@ -207,61 +224,92 @@ abstract class ResolveMappingsTask : DefaultTask() { intercept(::ObjectOverrideFilter) // remove obfuscated method parameter names, they are a filler from Searge intercept(::MethodArgSourceFilter) + // intern names to save memory + intercept(::StringPoolingAdapter) contributors { versionWorkspace -> - val mojangProvider = MojangManifestAttributeProvider(versionWorkspace, objectMapper, relaxedCache.get()) - val spigotProvider = SpigotManifestProvider(versionWorkspace, objectMapper, relaxedCache.get()) + val mojangProvider = MojangManifestAttributeProvider(versionWorkspace, relaxedCache.get()) + val spigotProvider = SpigotManifestProvider(versionWorkspace, relaxedCache.get()) buildList { if (requiredPlatform.wantsServer) { - add(VanillaServerMappingContributor(versionWorkspace, mojangProvider, relaxedCache.get())) - add(MojangServerMappingResolver(versionWorkspace, mojangProvider)) + add( + VanillaServerMappingContributor( + versionWorkspace, + mojangProvider, + relaxedCache.get() + ) + ) + addIfSupported(MojangServerMappingResolver(versionWorkspace, mojangProvider)) } if (requiredPlatform.wantsClient) { - add(VanillaClientMappingContributor(versionWorkspace, mojangProvider, relaxedCache.get())) - add(MojangClientMappingResolver(versionWorkspace, mojangProvider)) + add( + VanillaClientMappingContributor( + versionWorkspace, + mojangProvider, + relaxedCache.get() + ) + ) + addIfSupported(MojangClientMappingResolver(versionWorkspace, mojangProvider)) } - add(IntermediaryMappingResolver(versionWorkspace, sharedCacheWorkspace)) - add(YarnMappingResolver(versionWorkspace, yarnProvider, relaxedCache.get())) - add(SeargeMappingResolver(versionWorkspace, sharedCacheWorkspace, relaxedCache = relaxedCache.get())) + addIfSupported(IntermediaryMappingResolver(versionWorkspace, sharedCacheWorkspace)) + addIfSupported(HashedMappingResolver(versionWorkspace)) + addIfSupported(YarnMappingResolver(versionWorkspace, yarnProvider, relaxedCache.get())) + addIfSupported(QuiltMappingResolver(versionWorkspace, quiltProvider, relaxedCache.get())) + addIfSupported( + SeargeMappingResolver( + versionWorkspace, + sharedCacheWorkspace, + relaxedCache = relaxedCache.get() + ) + ) // Spigot resolvers have to be last if (requiredPlatform.wantsServer) { val link = LegacySpigotMappingPrepender.Link() - add( + addIfSupported( // 1.16.5 mappings have been republished with proper packages, even though the reobfuscated JAR does not have those // See: https://hub.spigotmc.org/stash/projects/SPIGOT/repos/builddata/commits/80d35549ec67b87a0cdf0d897abbe826ba34ac27 link.createPrependingContributor( - SpigotClassMappingResolver(versionWorkspace, xmlMapper, spigotProvider, relaxedCache.get()), + SpigotClassMappingResolver( + versionWorkspace, + spigotProvider, + relaxedCache.get() + ), prependEverything = versionWorkspace.version.id == "1.16.5" ) ) - add(link.createPrependingContributor(SpigotMemberMappingResolver(versionWorkspace, xmlMapper, spigotProvider, relaxedCache.get()))) + addIfSupported( + link.createPrependingContributor( + SpigotMemberMappingResolver( + versionWorkspace, + spigotProvider, + relaxedCache.get() + ) + ) + ) } } } joinedOutputPath { workspace -> - val fileName = when (requiredPlatform) { - PlatformTristate.CLIENT_SERVER -> "client+server.tiny" - PlatformTristate.CLIENT -> "client.tiny" - else -> "server.tiny" - } + val hash = md5Digest.updateAndHex(requiredNamespaces.sorted().joinToString(",")) - workspace[fileName] + workspace["$hash.$requiredPlatform.tiny"] } } val analyzer = MappingAnalyzerImpl( + // if the namespaces are missing, nothing happens anyway - no need to configure based on resolvedNamespaces AnalysisOptions( innerClassNameCompletionCandidates = setOf("spigot"), inheritanceAdditionalNamespaces = setOf("searge") // mojang could be here too for maximal parity, but that's in exchange for a little bit of performance ) ) - ResolvingMappingProvider(mappingConfig, manifest.get(), xmlMapper).get(analyzer) + ResolvingMappingProvider(mappingConfig, manifest.get()).get(analyzer) .apply { analyzer.acceptResolutions() } } } diff --git a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/TraceAccessorsTask.kt b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/TraceAccessorsTask.kt index a73490f9d6d..cb4cdbe7735 100644 --- a/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/TraceAccessorsTask.kt +++ b/generator/accessor-plugin/src/main/kotlin/me/kcra/takenaka/generator/accessor/plugin/tasks/TraceAccessorsTask.kt @@ -55,29 +55,31 @@ abstract class TraceAccessorsTask : GenerationTask() { tracingStream = Splitter(tracingStream, tracingFile0.outputStream()) } - val generator = TracingAccessorGenerator( - PrintStream(tracingStream), - outputWorkspace, - AccessorConfiguration( - accessors = accessors.get(), - basePackage = basePackage.get(), - codeLanguage = codeLanguage.get(), - accessorType = accessorType.get(), - namespaceFriendlinessIndex = namespaceFriendlinessIndex.get(), - accessedNamespaces = accessedNamespaces.get(), - craftBukkitVersionReplaceCandidates = craftBukkitVersionReplaceCandidates.get() + tracingStream.use { out -> + val generator = TracingAccessorGenerator( + PrintStream(out), + outputWorkspace, + AccessorConfiguration( + accessors = accessors.get(), + codeLanguage = codeLanguage.get(), + accessorType = accessorType.get(), + namespaceFriendlinessIndex = namespaceFriendlinessIndex.get(), + accessedNamespaces = namespaces.get(), + craftBukkitVersionReplaceCandidates = craftBukkitVersionReplaceCandidates.get(), + namingStrategy = namingStrategy.get(), + runtimePackage = runtimePackage.get() + ) ) - ) - runBlocking { - generator.generate( - mappingProvider.get(), - SimpleAncestryProvider(historyIndexNamespace.get(), historyNamespaces.get()) - ) - } + runBlocking { + generator.generate( + mappingProvider.get(), + SimpleAncestryProvider(historyIndexNamespace.get(), historyNamespaces.get()) + ) + } - tracingStream.close() - tracingFile0?.let { println("Report saved to ${it.absolutePath}.") } + tracingFile0?.let { println("Report saved to ${it.absolutePath}.") } + } } /** diff --git a/generator/accessor-runtime/build.gradle.kts b/generator/accessor-runtime/build.gradle.kts index ed9ed26b351..a081dee6271 100644 --- a/generator/accessor-runtime/build.gradle.kts +++ b/generator/accessor-runtime/build.gradle.kts @@ -1,10 +1,9 @@ plugins { id("takenaka.base-conventions") + id("takenaka.kotlin-conventions") id("takenaka.publish-conventions") } -apply(plugin = "org.jetbrains.kotlin.jvm") - dependencies { compileOnly(libs.bundles.kotlin) compileOnlyApi(libs.jb.annotations) diff --git a/generator/accessor/build.gradle.kts b/generator/accessor/build.gradle.kts index cc785bf7056..168b68aaee4 100644 --- a/generator/accessor/build.gradle.kts +++ b/generator/accessor/build.gradle.kts @@ -1,10 +1,9 @@ plugins { id("takenaka.base-conventions") + id("takenaka.kotlin-conventions") id("takenaka.publish-conventions") } -apply(plugin = "org.jetbrains.kotlin.jvm") - dependencies { api(project(":generator-common")) implementation(libs.javapoet) diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/AccessorConfiguration.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/AccessorConfiguration.kt index 51e35f895c2..519fd2eeb83 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/AccessorConfiguration.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/AccessorConfiguration.kt @@ -18,6 +18,15 @@ package me.kcra.takenaka.generator.accessor import me.kcra.takenaka.generator.accessor.model.ClassAccessor +import me.kcra.takenaka.generator.accessor.naming.NamingStrategy +import me.kcra.takenaka.generator.accessor.naming.StandardNamingStrategies +import me.kcra.takenaka.generator.accessor.naming.prefixed +import me.kcra.takenaka.generator.accessor.naming.resolveSimpleConflicts + +/** + * The default package of the `generator-accessor-plugin` module. + */ +const val DEFAULT_RUNTIME_PACKAGE = "me.kcra.takenaka.accessor" /** * Configuration for [AccessorGenerator]. @@ -29,14 +38,21 @@ import me.kcra.takenaka.generator.accessor.model.ClassAccessor * @property namespaceFriendlinessIndex an ordered list of namespaces that will be considered when selecting a "friendly" name * @property accessedNamespaces the namespaces that should be used in accessors * @property craftBukkitVersionReplaceCandidates namespaces that should have [me.kcra.takenaka.core.mapping.adapter.replaceCraftBukkitNMSVersion] applied (most likely Spigot mappings or a flavor of them) + * @property namingStrategy the strategy used to name generated classes and their members + * @property runtimePackage the package of the used accessor runtime + * @property mappingWebsite the base url of the mapping website * @author Matouš Kučera */ data class AccessorConfiguration( val accessors: List, // TODO: move to AccessorGenerator constructor - val basePackage: String, + @Deprecated("The base package concept was superseded by naming strategies.") + val basePackage: String = "", val codeLanguage: CodeLanguage = CodeLanguage.JAVA, val accessorType: AccessorType = AccessorType.NONE, val namespaceFriendlinessIndex: List = emptyList(), val accessedNamespaces: List = namespaceFriendlinessIndex, - val craftBukkitVersionReplaceCandidates: List = emptyList() + val craftBukkitVersionReplaceCandidates: List = emptyList(), + val namingStrategy: NamingStrategy = StandardNamingStrategies.SIMPLE.prefixed(basePackage).resolveSimpleConflicts(), + val runtimePackage: String = DEFAULT_RUNTIME_PACKAGE, + val mappingWebsite: String? = null ) diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedClassType.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedClassType.kt new file mode 100644 index 00000000000..76c31f8b54f --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedClassType.kt @@ -0,0 +1,41 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor + +/** + * Generated class types. + * + * @author Michal Turek + * @author Matouš Kučera + */ +enum class GeneratedClassType { + /** + * Mapping class. + */ + MAPPING, + + /** + * Accessor class. + */ + ACCESSOR, + + /** + * Mapping lookup class. + */ + LOOKUP +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedMemberType.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedMemberType.kt new file mode 100644 index 00000000000..4e110f2c760 --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/GeneratedMemberType.kt @@ -0,0 +1,40 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor + +/** + * Generated class member types. + * + * @author Matouš Kučera + */ +enum class GeneratedMemberType { + /** + * Type field ([java.lang.Class]). + */ + TYPE, + + /** + * Mapping field (`me.kcra.takenaka.accessor.ClassMapping`). + */ + MAPPING, + + /** + * Lookup field (`me.kcra.takenaka.accessor.MappingLookup`). + */ + LOOKUP +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/AbstractGenerationContext.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/AbstractGenerationContext.kt index 95a4e39a085..04b7f8c1582 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/AbstractGenerationContext.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/AbstractGenerationContext.kt @@ -27,13 +27,16 @@ import me.kcra.takenaka.core.mapping.fromInternalName import me.kcra.takenaka.core.mapping.resolve.impl.craftBukkitNmsVersion import me.kcra.takenaka.core.mapping.resolve.impl.modifiers import me.kcra.takenaka.core.mapping.util.dstNamespaceIds +import me.kcra.takenaka.core.util.buildOrderedMap import me.kcra.takenaka.generator.accessor.AccessorGenerator +import me.kcra.takenaka.generator.accessor.GeneratedClassType import me.kcra.takenaka.generator.accessor.context.GenerationContext import me.kcra.takenaka.generator.accessor.model.* +import me.kcra.takenaka.generator.accessor.naming.NamingStrategy import me.kcra.takenaka.generator.accessor.util.globAsRegex import me.kcra.takenaka.generator.accessor.util.isGlob import me.kcra.takenaka.generator.common.provider.AncestryProvider -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.tree.MappingTreeView.* import org.objectweb.asm.Opcodes import org.objectweb.asm.Type @@ -42,28 +45,13 @@ import java.util.* private val logger = KotlinLogging.logger {} -/** - * A field accessor and ancestry node pair. - */ -typealias ResolvedFieldPair = Pair - -/** - * A constructor accessor and ancestry node pair. - */ -typealias ResolvedConstructorPair = Pair - -/** - * A method accessor and ancestry node pair. - */ -typealias ResolvedMethodPair = Pair - /** * An implementation base for [GenerationContext]. * * @author Matouš Kučera */ abstract class AbstractGenerationContext( - override val generator: AccessorGenerator, + final override val generator: AccessorGenerator, val ancestryProvider: AncestryProvider, contextScope: CoroutineScope ) : GenerationContext, CoroutineScope by contextScope { @@ -73,9 +61,15 @@ abstract class AbstractGenerationContext( private val generatedClasses = Collections.synchronizedSet(mutableSetOf()) /** - * Accessor model simple class names and their renamed variants, used for detecting naming conflicts. + * The generator's naming strategy. + */ + protected val namingStrategy: NamingStrategy /*by generator.config::namingStrategy - KT-53799, can't use delegation */ + get() = generator.config.namingStrategy + + /** + * The source code types. */ - private val accessedSimpleNames = mutableMapOf>() // {simple name: {fq name: occurrence index}} + protected val types = SourceTypes(generator.config.runtimePackage) /** * The generation timestamp of this context's output. @@ -134,35 +128,42 @@ abstract class AbstractGenerationContext( logger.info { "generating accessors for class '${model.internalName}'" } val fieldTree = ancestryProvider.field<_, _, FieldMappingView>(node) - val fieldAccessors = model.fields.flatMap { resolveFieldChain(fieldTree, it) } + + + // fields can't be overloaded, but capitalization matters, which is a problem when making uppercase names from everything + val fieldOverloads = NameOverloads() + val fieldAccessors = model.fields + .map { resolveFieldChain(fieldTree, it, fieldOverloads.generate(it.upperName)) } + .plus( resolveRequiredFields(fieldTree, model.requiredTypes).map { fieldNode -> - FieldAccessor(getFriendlyName(fieldNode.last.value), getFriendlyDesc(fieldNode.last.value)) to fieldNode + val accessor = FieldAccessor(getFriendlyName(fieldNode.last.value), getFriendlyDesc(fieldNode.last.value)) + + ResolvedFieldAccessor(accessor, fieldNode, fieldOverloads.generate(accessor.upperName)) } + ) - // fields can't be overloaded, but capitalization matters, which is a problem when making uppercase names from everything - val fieldOverloadCount = mutableMapOf() - val fieldOverloads = fieldAccessors.associate { (fieldAccessor, _) -> - fieldAccessor to fieldOverloadCount.compute(fieldAccessor.upperName) { _, i -> i?.inc() ?: 0 }!! + val ctorTree = ancestryProvider.method<_, _, MethodMappingView>(node, constructorMode = ConstructorComputationMode.ONLY) + val ctorAccessors = model.constructors.mapIndexedTo(mutableListOf()) { i, accessor -> + ResolvedMemberAccessor(accessor, resolveConstructor(ctorTree, accessor), i) } - val ctorTree = ancestryProvider.method<_, _, MethodMappingView>(node, constructorMode = ConstructorComputationMode.ONLY) - val ctorAccessors = model.constructors.map { ResolvedConstructorPair(it, resolveConstructor(ctorTree, it)) } + - resolveRequiredConstructors(ctorTree, model.requiredTypes).map { ctorNode -> - ConstructorAccessor(getFriendlyDesc(ctorNode.last.value)) to ctorNode - } + resolveRequiredConstructors(ctorTree, model.requiredTypes).mapTo(ctorAccessors) { ctorNode -> + ResolvedConstructorAccessor(ConstructorAccessor(getFriendlyDesc(ctorNode.last.value)), ctorNode, ctorAccessors.size) + } val methodTree = ancestryProvider.method<_, _, MethodMappingView>(node) - val methodAccessors = model.methods.flatMap { resolveMethodChain(methodTree, it) } + + + val methodOverloads = NameOverloads() + val methodAccessors = model.methods + .map { resolveMethodChain(methodTree, it, methodOverloads.generate(it.upperName)) } + .plus( resolveRequiredMethods(methodTree, model.requiredTypes).map { methodNode -> - MethodAccessor(getFriendlyName(methodNode.last.value), getFriendlyDesc(methodNode.last.value)) to methodNode - } + val accessor = MethodAccessor(getFriendlyName(methodNode.last.value), getFriendlyDesc(methodNode.last.value)) - val methodOverloadCount = mutableMapOf() - val methodOverloads = methodAccessors.associate { (methodAccessor, _) -> - methodAccessor to methodOverloadCount.compute(methodAccessor.upperName) { _, i -> i?.inc() ?: 0 }!! - } + ResolvedMethodAccessor(accessor, methodNode, methodOverloads.generate(accessor.upperName)) + } + ) - generateClass(ResolvedClassAccessor(model, node, fieldAccessors, ctorAccessors, methodAccessors, fieldOverloads, methodOverloads)) + generateClass(ResolvedClassAccessor(model, node, fieldAccessors, ctorAccessors, methodAccessors)) } /** @@ -178,13 +179,14 @@ abstract class AbstractGenerationContext( * that have been generated in this context. */ override fun generateLookupClass() { - generateLookupClass(generatedClasses.toList()) + // try reconstructing necessary information for name generation + generateLookupClass(generatedClasses.map { cl -> namingStrategy.klass(ClassAccessor(cl), GeneratedClassType.MAPPING) }) } /** * Generates a mapping lookup class from class names. * - * @param names internal names of classes declared in accessor models + * @param names fully qualified names of generated mapping classes */ protected open fun generateLookupClass(names: List) { } @@ -219,16 +221,22 @@ abstract class AbstractGenerationContext( * * @param tree the ancestry tree * @param model the model - * @return the nodes + * @param overloadIndex the model overload index + * @return the resolved model */ - protected open fun resolveFieldChain(tree: FieldAncestryTree, model: FieldAccessor): List = buildList { - var nextNode: FieldAccessor? = model - while (nextNode != null) { - add(ResolvedFieldPair(nextNode, resolveField(tree, nextNode))) - nextNode = nextNode.chain - } + protected open fun resolveFieldChain(tree: FieldAncestryTree, model: FieldAccessor, overloadIndex: Int): ResolvedFieldAccessor { + return ResolvedFieldAccessor( + buildOrderedMap { + var nextNode: FieldAccessor? = model + while (nextNode != null) { + add(nextNode to resolveField(tree, nextNode)) + nextNode = nextNode.chain + } - reverse() // last chain member comes first + reverse() // last chain member comes first + }, + overloadIndex + ) } /** @@ -309,16 +317,22 @@ abstract class AbstractGenerationContext( * * @param tree the ancestry tree * @param model the model - * @return the nodes + * @param overloadIndex the model overload index + * @return the resolved model */ - protected open fun resolveMethodChain(tree: MethodAncestryTree, model: MethodAccessor): List = buildList { - var nextNode: MethodAccessor? = model - while (nextNode != null) { - add(ResolvedMethodPair(nextNode, resolveMethod(tree, nextNode))) - nextNode = nextNode.chain - } + protected open fun resolveMethodChain(tree: MethodAncestryTree, model: MethodAccessor, overloadIndex: Int): ResolvedMethodAccessor { + return ResolvedMethodAccessor( + buildOrderedMap { + var nextNode: MethodAccessor? = model + while (nextNode != null) { + add(nextNode to resolveMethod(tree, nextNode)) + nextNode = nextNode.chain + } - reverse() // last chain member comes first + reverse() // last chain member comes first + }, + overloadIndex + ) } /** @@ -429,9 +443,10 @@ abstract class AbstractGenerationContext( * Groups the generator's mappings by version. * * @param node the ancestry node + * @param exact whether descriptors should be matched exactly or without return types * @return the grouped method mappings */ - protected open fun groupMethodNames(node: MethodAncestryNode): Map> = buildMap> { + protected open fun groupMethodNames(node: MethodAncestryNode, exact: Boolean = false): Map> = buildMap> { node.forEach { (version, method) -> val nmsVersion = method.tree.craftBukkitNmsVersion @@ -439,33 +454,12 @@ abstract class AbstractGenerationContext( method.getDesc(ns)?.let { desc -> val name = method.getName(ns) ?: method.srcName - getOrPut(MethodKey(ns, name, desc.replaceCraftBukkitNMSVersion(nmsVersion)), ::mutableListOf) += version + getOrPut(MethodKey(ns, name, desc.replaceCraftBukkitNMSVersion(nmsVersion), exact), ::mutableListOf) += version } } } } - /** - * Sanitizes an accessor model name for generating an accessor class in this context. - * - * @param name a fully qualified name - * @return a non-conflicting simple name - */ - protected open fun generateNonConflictingName(name: String): String { - val simpleName = name.substringAfterLast('/') - - synchronized(accessedSimpleNames) { - val names = accessedSimpleNames.getOrPut(simpleName, ::mutableMapOf) - - val index = names.getOrPut(name) { names.size } - if (index > 0) { - return simpleName + index - } - - return simpleName // don't add a zero - } - } - companion object { /** * The file comment's generation timestamp date format. @@ -474,3 +468,15 @@ abstract class AbstractGenerationContext( } } +/** + * A utility class for managing name overloads. + */ +private class NameOverloads(delegate: MutableMap = mutableMapOf()) : MutableMap by delegate { + /** + * Generates an available index for the supplied name. + * + * @param key the name + * @return the index + */ + fun generate(key: String): Int = compute(key) { _, i -> i?.inc() ?: 0 }!! +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/JavaGenerationContext.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/JavaGenerationContext.kt index 41b6fdeb62d..2e07e76b47f 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/JavaGenerationContext.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/JavaGenerationContext.kt @@ -29,8 +29,11 @@ import kotlinx.coroutines.CoroutineScope import me.kcra.takenaka.core.Workspace import me.kcra.takenaka.core.mapping.fromInternalName import me.kcra.takenaka.core.mapping.resolve.impl.modifiers +import me.kcra.takenaka.core.mapping.toInternalName import me.kcra.takenaka.generator.accessor.AccessorGenerator import me.kcra.takenaka.generator.accessor.AccessorType +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.GeneratedMemberType import me.kcra.takenaka.generator.common.provider.AncestryProvider import org.objectweb.asm.Opcodes import org.objectweb.asm.Type @@ -57,28 +60,38 @@ open class JavaGenerationContext( */ override fun generateClass(resolvedAccessor: ResolvedClassAccessor) { val accessedQualifiedName = resolvedAccessor.model.name.fromInternalName() - val accessorSimpleName = generateNonConflictingName(resolvedAccessor.model.internalName) - val mappingClassName = JClassName.get(generator.config.basePackage, "${accessorSimpleName}Mapping") - val accessorClassName = JClassName.get(generator.config.basePackage, "${accessorSimpleName}Accessor") + val mappingClassName = namingStrategy.klass(resolvedAccessor.model, GeneratedClassType.MAPPING).toJClassName() + val accessorClassName = namingStrategy.klass(resolvedAccessor.model, GeneratedClassType.ACCESSOR).toJClassName() + val mappingFieldName = namingStrategy.member(GeneratedMemberType.MAPPING) + val accessorBuilder = JTypeSpec.interfaceBuilder(accessorClassName) .addModifiers(Modifier.PUBLIC) .addJavadoc( """ Accessors for the {@code ${'$'}L} class. + @since ${'$'}L + @version ${'$'}L @see ${'$'}L """.trimIndent(), accessedQualifiedName, + resolvedAccessor.node.first.key.id, + resolvedAccessor.node.last.key.id, mappingClassName.canonicalName() ) .addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.CLASS_WILDCARD), "TYPE", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addAnnotation(SourceTypes.NOT_NULL) - .initializer("\$T.of(\$T.MAPPING::getClazz)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.CLASS_WILDCARD), namingStrategy.member(GeneratedMemberType.TYPE), Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addAnnotation(types.NOT_NULL) + .initializer("\$T.of(\$T.\$L::getClazz)", types.LAZY_SUPPLIER, mappingClassName, mappingFieldName) .build() ) + if (generator.config.mappingWebsite != null) { + val link = "${generator.config.mappingWebsite}/${resolvedAccessor.node.last.key.id}/${getFriendlyName(resolvedAccessor.node.last.value).toInternalName()}.html" + accessorBuilder.addJavadoc("\n@see \$L", link, link) + } + JTypeSpec.interfaceBuilder(mappingClassName) .addModifiers(Modifier.PUBLIC) .addJavadoc( @@ -93,10 +106,10 @@ open class JavaGenerationContext( resolvedAccessor.node.last.key.id ) .addField( - FieldSpec.builder(SourceTypes.CLASS_MAPPING, "MAPPING", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addAnnotation(SourceTypes.NOT_NULL) + FieldSpec.builder(types.CLASS_MAPPING, mappingFieldName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addAnnotation(types.NOT_NULL) .initializer { - add("new \$T(\$S)", SourceTypes.CLASS_MAPPING, accessedQualifiedName) + add("new \$T(\$S)", types.CLASS_MAPPING, accessedQualifiedName) withIndent { groupClassNames(resolvedAccessor.node).forEach { (classKey, versions) -> val (ns, name) = classKey @@ -125,7 +138,7 @@ open class JavaGenerationContext( groupConstructorNames(ctorNode).forEach { (ctorKey, versions) -> val (ns, desc) = ctorKey - add("m.put(\$S, new \$T[] { \$L }", ns, SourceTypes.STRING, JCodeBlock.join(versions.map { JCodeBlock.of("\$S", it.id) }, ", ")) + add("m.put(\$S, new \$T[] { \$L }", ns, types.STRING, JCodeBlock.join(versions.map { JCodeBlock.of("\$S", it.id) }, ", ")) val args = Type.getArgumentTypes(desc) .map { JCodeBlock.of("\$S", it.className) } @@ -149,7 +162,7 @@ open class JavaGenerationContext( groupMethodNames(methodNode).forEach { (methodKey, versions) -> val (ns, name, desc) = methodKey - add("m.put(\$S, new \$T[] { \$L }, \$S", ns, SourceTypes.STRING, JCodeBlock.join(versions.map { JCodeBlock.of("\$S", it.id) }, ", "), name) + add("m.put(\$S, new \$T[] { \$L }, \$S", ns, types.STRING, JCodeBlock.join(versions.map { JCodeBlock.of("\$S", it.id) }, ", "), name) val args = Type.getArgumentTypes(desc) .map { JCodeBlock.of("\$S", it.className) } @@ -169,61 +182,82 @@ open class JavaGenerationContext( .build() ) .addFields( - resolvedAccessor.fields.map { (fieldAccessor, fieldNode) -> - val overloadIndex = resolvedAccessor.fieldOverloads[fieldAccessor] - val accessorName = "FIELD_${fieldAccessor.upperName}${overloadIndex?.let { if (it != 0) "_$it" else "" } ?: ""}" - val fieldType = fieldAccessor.type?.let(Type::getType) - ?: getFriendlyType(fieldNode.last.value) - - fun FieldSpec.Builder.addMeta(constant: Boolean = false): FieldSpec.Builder = apply { - addAnnotation(SourceTypes.NOT_NULL) - addJavadoc( - """ - Accessor for the {@code ${'$'}L ${'$'}L} ${'$'}L. - - @see ${'$'}L#${'$'}L - """.trimIndent(), - fieldType.className, - fieldAccessor.name, - if (constant) { - "constant field value" - } else { - "field" - }, - mappingClassName.canonicalName(), - accessorName - ) + resolvedAccessor.fields.map { mergedAccessor -> + val (fieldAccessor, fieldNode, overloadIndex) = mergedAccessor + + val mappingName = namingStrategy.field(fieldAccessor, overloadIndex) + fun FieldSpec.Builder.addMeta(constant: Boolean = false, mapping: Boolean = false): FieldSpec.Builder = apply { + addAnnotation(types.NOT_NULL) + + if (mergedAccessor.isChained) { + addJavadoc( + "\$L for the following \$L:\n
    \n", + if (mapping) "Mapping" else "Accessor", + if (constant) "constant field values" else "fields" + ) + + mergedAccessor.forEach { chainedAccessor, chainedFieldNode -> + val chainedType = chainedAccessor.type?.let(Type::getType) + ?: getFriendlyType(chainedFieldNode.last.value) + + addJavadoc( + "
  • {@code \$L \$L} (\$L - \$L)
  • \n", + chainedType.className, + chainedAccessor.name, + chainedFieldNode.first.key.id, + chainedFieldNode.last.key.id + ) + } + + addJavadoc("
\n") + } else { + val fieldType = fieldAccessor.type?.let(Type::getType) + ?: getFriendlyType(fieldNode.last.value) + + addJavadoc( + "\$L for the {@code \$L \$L} \$L.\n", + if (mapping) "Mapping" else "Accessor", + fieldType.className, + fieldAccessor.name, + if (constant) "constant field value" else "field" + ) + } + + addJavadoc("\n@since ${'$'}L\n@version ${'$'}L", fieldNode.first.key.id, fieldNode.last.key.id) + if (!mapping) { + addJavadoc("\n@see \$L#\$L", mappingClassName.canonicalName(), mappingName) + } } val mod = fieldNode.last.value.modifiers if ((mod and Opcodes.ACC_STATIC) != 0 && (mod and Opcodes.ACC_FINAL) != 0) { // constant accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, JClassName.OBJECT), accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, JClassName.OBJECT), namingStrategy.constant(fieldAccessor, overloadIndex), Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta(constant = true) - .initializer("\$T.of(\$T.$accessorName::getConstantValue)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getConstantValue)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) } else { when (generator.config.accessorType) { AccessorType.REFLECTION -> { accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.FIELD), accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.FIELD), mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta() - .initializer("\$T.of(\$T.$accessorName::getField)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getField)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) } AccessorType.METHOD_HANDLES -> { accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.METHOD_HANDLE), "${accessorName}_GETTER", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.METHOD_HANDLE), namingStrategy.fieldHandle(fieldAccessor, overloadIndex, false), Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta() - .initializer("\$T.of(\$T.$accessorName::getFieldGetter)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getFieldGetter)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.METHOD_HANDLE), "${accessorName}_SETTER", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.METHOD_HANDLE), namingStrategy.fieldHandle(fieldAccessor, overloadIndex, true), Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta() - .initializer("\$T.of(\$T.$accessorName::getFieldSetter)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getFieldSetter)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) } @@ -231,76 +265,57 @@ open class JavaGenerationContext( } } - FieldSpec.builder(SourceTypes.FIELD_MAPPING, accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addAnnotation(SourceTypes.NOT_NULL) - .addJavadoc( - """ - Mapping for the {@code ${'$'}L ${'$'}L} field. - - @since ${'$'}L - @version ${'$'}L - """.trimIndent(), - fieldType.className, - fieldAccessor.name, - fieldNode.first.key.id, - fieldNode.last.key.id - ) - .initializer { - add("MAPPING.getField(\$S, \$L)", fieldAccessor.name, overloadIndex) - if (fieldAccessor.chain != null) { - add( - ".chain(FIELD_\$L\$L)", - fieldAccessor.chain.upperName, - resolvedAccessor.fieldOverloads[fieldAccessor.chain] - ?.let { if (it != 0) "_$it" else "" } - ?: "" - ) - } - } + FieldSpec.builder(types.FIELD_MAPPING, mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addMeta(mapping = true) + .initializer("\$L.getField(\$S, \$L)", mappingFieldName, fieldAccessor.name, overloadIndex) .build() } ) .addFields( - resolvedAccessor.constructors.mapIndexed { i, (ctorAccessor, ctorNode) -> - val accessorName = "CONSTRUCTOR_$i" + resolvedAccessor.constructors.map { (ctorAccessor, ctorNode, overloadIndex) -> + val mappingName = namingStrategy.constructor(ctorAccessor, overloadIndex) val ctorArgs = Type.getArgumentTypes(ctorAccessor.type) fun FieldSpec.Builder.addMeta(): FieldSpec.Builder = apply { - addAnnotation(SourceTypes.NOT_NULL) + addAnnotation(types.NOT_NULL) addJavadoc( """ Accessor for the {@code (${'$'}L)} constructor. + @since ${'$'}L + @version ${'$'}L @see ${'$'}L#${'$'}L """.trimIndent(), ctorArgs.joinToString(transform = Type::getClassName), + ctorNode.first.key.id, + ctorNode.last.key.id, mappingClassName.canonicalName(), - accessorName + mappingName ) } when (generator.config.accessorType) { AccessorType.REFLECTION -> { accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.CONSTRUCTOR_WILDCARD), accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.CONSTRUCTOR_WILDCARD), mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta() - .initializer("\$T.of(\$T.$accessorName::getConstructor)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getConstructor)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) } AccessorType.METHOD_HANDLES -> { accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.METHOD_HANDLE), accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.METHOD_HANDLE), mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta() - .initializer("\$T.of(\$T.$accessorName::getConstructorHandle)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getConstructorHandle)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) } AccessorType.NONE -> {} } - FieldSpec.builder(SourceTypes.CONSTRUCTOR_MAPPING, accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addAnnotation(SourceTypes.NOT_NULL) + FieldSpec.builder(types.CONSTRUCTOR_MAPPING, mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addAnnotation(types.NOT_NULL) .addJavadoc( """ Mapping for the {@code (${'$'}L)} constructor. @@ -312,135 +327,128 @@ open class JavaGenerationContext( ctorNode.first.key.id, ctorNode.last.key.id ) - .initializer("MAPPING.getConstructor(\$L)", i) + .initializer("\$L.getConstructor(\$L)", mappingFieldName, overloadIndex) .build() } ) .addFields( - resolvedAccessor.methods.map { (methodAccessor, methodNode) -> - val overloadIndex = resolvedAccessor.methodOverloads[methodAccessor] - val accessorName = "METHOD_${methodAccessor.upperName}${overloadIndex?.let { if (it != 0) "_$it" else "" } ?: ""}" - val methodType = if (methodAccessor.isIncomplete) { - getFriendlyType(methodNode.last.value) - } else { - Type.getType(methodAccessor.type) - } + resolvedAccessor.methods.map { mergedAccessor -> + val (methodAccessor, methodNode, overloadIndex) = mergedAccessor - fun FieldSpec.Builder.addMeta(): FieldSpec.Builder = apply { - addAnnotation(SourceTypes.NOT_NULL) - addJavadoc( - """ - Accessor for the {@code ${'$'}L ${'$'}L(${'$'}L)} method. - - @see ${'$'}L#${'$'}L - """.trimIndent(), - methodType.returnType.className, - methodAccessor.name, - methodType.argumentTypes.joinToString(transform = Type::getClassName), - mappingClassName.canonicalName(), - accessorName - ) + val mappingName = namingStrategy.method(methodAccessor, overloadIndex) + fun FieldSpec.Builder.addMeta(mapping: Boolean = false): FieldSpec.Builder = apply { + addAnnotation(types.NOT_NULL) + + if (mergedAccessor.isChained) { + addJavadoc( + "\$L for the following methods:\n
    \n", + if (mapping) "Mapping" else "Accessor" + ) + + mergedAccessor.forEach { (chainedAccessor, chainedMethodNode) -> + val chainedType = if (chainedAccessor.isIncomplete) getFriendlyType(chainedMethodNode.last.value) else Type.getType(chainedAccessor.type) + + addJavadoc( + "
  • {@code \$L \$L(\$L)} (\$L - \$L)
  • \n", + chainedType.returnType.className, + chainedAccessor.name, + chainedType.argumentTypes.joinToString(transform = Type::getClassName), + chainedMethodNode.first.key.id, + chainedMethodNode.last.key.id + ) + } + + addJavadoc("
\n") + } else { + val methodType = if (methodAccessor.isIncomplete) getFriendlyType(methodNode.last.value) else Type.getType(methodAccessor.type) + + addJavadoc( + "\$L for the {@code \$L \$L(\$L)} method.\n", + if (mapping) "Mapping" else "Accessor", + methodType.returnType.className, + methodAccessor.name, + methodType.argumentTypes.joinToString(transform = Type::getClassName) + ) + } + + addJavadoc("\n@since ${'$'}L\n@version ${'$'}L", methodNode.first.key.id, methodNode.last.key.id) + if (!mapping) { + addJavadoc("\n@see \$L#\$L", mappingClassName.canonicalName(), mappingName) + } } when (generator.config.accessorType) { AccessorType.REFLECTION -> { accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.METHOD), accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.METHOD), mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta() - .initializer("\$T.of(\$T.$accessorName::getMethod)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getMethod)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) } AccessorType.METHOD_HANDLES -> { accessorBuilder.addField( - FieldSpec.builder(JParameterizedTypeName.get(SourceTypes.SUPPLIER, SourceTypes.METHOD_HANDLE), accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + FieldSpec.builder(JParameterizedTypeName.get(types.SUPPLIER, types.METHOD_HANDLE), mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addMeta() - .initializer("\$T.of(\$T.$accessorName::getMethodHandle)", SourceTypes.LAZY_SUPPLIER, mappingClassName) + .initializer("\$T.of(\$T.\$L::getMethodHandle)", types.LAZY_SUPPLIER, mappingClassName, mappingName) .build() ) } AccessorType.NONE -> {} } - FieldSpec.builder(SourceTypes.METHOD_MAPPING, accessorName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addAnnotation(SourceTypes.NOT_NULL) - .addJavadoc( - """ - Mapping for the {@code ${'$'}L ${'$'}L(${'$'}L)} method. - - @since ${'$'}L - @version ${'$'}L - """.trimIndent(), - methodType.returnType.className, - methodAccessor.name, - methodType.argumentTypes.joinToString(transform = Type::getClassName), - methodNode.first.key.id, - methodNode.last.key.id - ) - .initializer { - add("MAPPING.getMethod(\$S, \$L)", methodAccessor.name, overloadIndex) - if (methodAccessor.chain != null) { - add( - ".chain(METHOD_\$L\$L)", - methodAccessor.chain.upperName, - resolvedAccessor.methodOverloads[methodAccessor.chain] - ?.let { if (it != 0) "_$it" else "" } - ?: "" - ) - } - } + FieldSpec.builder(types.METHOD_MAPPING, mappingName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addMeta(mapping = true) + .initializer("\$L.getMethod(\$S, \$L)", mappingFieldName, methodAccessor.name, overloadIndex) .build() } ) .build() - .writeTo(generator.workspace) + .writeTo(mappingClassName, generator.workspace) if (generator.config.accessorType != AccessorType.NONE) { - accessorBuilder.build().writeTo(generator.workspace) + accessorBuilder.build() + .writeTo(accessorClassName, generator.workspace) } } /** * Generates a Java mapping lookup class from class names. * - * @param names internal names of classes declared in accessor models + * @param names fully qualified names of generated mapping classes */ override fun generateLookupClass(names: List) { - val poolClassName = JClassName.get(generator.config.basePackage, "Mappings") + val poolClassName = namingStrategy.klass(GeneratedClassType.LOOKUP).toJClassName() JTypeSpec.interfaceBuilder(poolClassName) .addModifiers(Modifier.PUBLIC) .addField( - FieldSpec.builder(SourceTypes.MAPPING_LOOKUP, "LOOKUP", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addAnnotation(SourceTypes.NOT_NULL) + FieldSpec.builder(types.MAPPING_LOOKUP, namingStrategy.member(GeneratedMemberType.LOOKUP), Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addAnnotation(types.NOT_NULL) .addJavadoc("Mapping lookup index generated on {@code \$L}.", DATE_FORMAT.format(generationTime)) .initializer { - add("new \$T()", SourceTypes.MAPPING_LOOKUP) + add("new \$T()", types.MAPPING_LOOKUP) withIndent { names.forEach { name -> - val mappingClassName = JClassName.get( - generator.config.basePackage, - "${name.substringAfterLast('/')}Mapping" - ) - - add("\n.put(\$T.MAPPING)", mappingClassName) + add("\n.put(\$T.\$L)", name.toJClassName(), namingStrategy.member(GeneratedMemberType.MAPPING)) } } } .build() ) .build() - .writeTo(generator.workspace) + .writeTo(poolClassName, generator.workspace) } /** * Writes a [JTypeSpec] to a workspace with default settings. * + * @param className the class name * @param workspace the workspace * @return the file where the source was actually written */ - fun JTypeSpec.writeTo(workspace: Workspace): Path = - JavaFile.builder(generator.config.basePackage, this) + fun JTypeSpec.writeTo(className: JClassName, workspace: Workspace): Path = + JavaFile.builder(className.packageName(), this) .skipJavaLangImports(true) .addFileComment("This file was generated by takenaka on ${DATE_FORMAT.format(generationTime)}. Do not edit, changes will be overwritten!") .indent(" ".repeat(4)) // 4 spaces diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/KotlinGenerationContext.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/KotlinGenerationContext.kt index 31fe3869ab9..1f1051ca356 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/KotlinGenerationContext.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/KotlinGenerationContext.kt @@ -27,13 +27,15 @@ import kotlinx.coroutines.CoroutineScope import me.kcra.takenaka.core.Workspace import me.kcra.takenaka.core.mapping.fromInternalName import me.kcra.takenaka.core.mapping.resolve.impl.modifiers +import me.kcra.takenaka.core.mapping.toInternalName import me.kcra.takenaka.generator.accessor.AccessorGenerator import me.kcra.takenaka.generator.accessor.AccessorType +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.GeneratedMemberType import me.kcra.takenaka.generator.accessor.util.escapeKotlinName import me.kcra.takenaka.generator.common.provider.AncestryProvider import org.objectweb.asm.Opcodes import org.objectweb.asm.Type -import java.util.* /** * A generation context that emits Kotlin code. @@ -55,379 +57,382 @@ open class KotlinGenerationContext( */ override fun generateClass(resolvedAccessor: ResolvedClassAccessor) { val accessedQualifiedName = resolvedAccessor.model.name.fromInternalName() - val accessorSimpleName = generateNonConflictingName(resolvedAccessor.model.internalName).escapeKotlinName() - - val typeSpecs = buildList { - val mappingClassName = KClassName(generator.config.basePackage, "${accessorSimpleName}Mapping") - val accessorClassName = KClassName(generator.config.basePackage, "${accessorSimpleName}Accessor") - val accessorBuilder = KTypeSpec.objectBuilder(accessorClassName) - .addKdoc( - """ - Accessors for the `%L` class. - - @see·%L - """.trimIndent(), - accessedQualifiedName, - mappingClassName.canonicalName - ) - .addProperty( - PropertySpec.builder("TYPE", SourceTypes.KT_NULLABLE_CLASS_WILDCARD) - .delegate("%M(%T::getClazz)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - - add( - KTypeSpec.objectBuilder(mappingClassName) - .addKdoc( - """ - Mappings for the `%L` class. - - `%L` - `%L` - """.trimIndent(), - accessedQualifiedName, - resolvedAccessor.node.last.key.id, - resolvedAccessor.node.first.key.id - ) - .superclass(SourceTypes.KT_CLASS_MAPPING) - .addSuperclassConstructorParameter("%S", accessedQualifiedName) - .addInitializerBlock( - buildCodeBlock { - groupClassNames(resolvedAccessor.node).forEach { (classKey, versions) -> - val (ns, name) = classKey - - addStatement( - "put(%S, %S, %L)", - ns, - name, - versions.map { KCodeBlock.of("%S", it.id) }.joinToCode() - ) - } - resolvedAccessor.fields.forEach { (fieldAccessor, fieldNode) -> - beginControlFlow("%M(%S)", SourceTypes.KT_CLASS_MAPPING_FIELD_DSL, fieldAccessor.name) + val mappingClassName = namingStrategy.klass(resolvedAccessor.model, GeneratedClassType.MAPPING).escapeKotlinName().toKClassName() + val accessorClassName = namingStrategy.klass(resolvedAccessor.model, GeneratedClassType.ACCESSOR).escapeKotlinName().toKClassName() + val accessorBuilder = KTypeSpec.objectBuilder(accessorClassName) + .addKdoc( + """ + Accessors for the `%L` class. + + `%L` - `%L` + """.trimIndent(), + accessedQualifiedName, + resolvedAccessor.node.first.key.id, + resolvedAccessor.node.last.key.id + ) + .addProperty( + PropertySpec.builder(namingStrategy.member(GeneratedMemberType.TYPE), types.KT_NULLABLE_CLASS_WILDCARD) + .delegate("%M(%T::getClazz)", types.KT_LAZY_DSL, mappingClassName) + .build() + ) - groupFieldNames(fieldNode).forEach { (fieldKey, versions) -> - val (ns, name) = fieldKey + if (generator.config.mappingWebsite != null) { + val link = "${generator.config.mappingWebsite}/${resolvedAccessor.node.last.key.id}/${getFriendlyName(resolvedAccessor.node.last.value).toInternalName()}.html" + accessorBuilder.addKdoc("\nSee: [%L](%L)", link, link) + } - addStatement( - "put(%S, %S, %L)", - ns, - name, - versions.map { KCodeBlock.of("%S", it.id) }.joinToCode() - ) - } + accessorBuilder.addKdoc("\n@see·%L", mappingClassName.canonicalName) + + KTypeSpec.objectBuilder(mappingClassName) + .addKdoc( + """ + Mappings for the `%L` class. + + `%L` - `%L` + """.trimIndent(), + accessedQualifiedName, + resolvedAccessor.node.first.key.id, + resolvedAccessor.node.last.key.id + ) + .superclass(types.KT_CLASS_MAPPING) + .addSuperclassConstructorParameter("%S", accessedQualifiedName) + .addInitializerBlock( + buildCodeBlock { + groupClassNames(resolvedAccessor.node).forEach { (classKey, versions) -> + val (ns, name) = classKey + + addStatement( + "put(%S, %S, %L)", + ns, + name, + versions.map { KCodeBlock.of("%S", it.id) }.joinToCode() + ) + } - endControlFlow() - } + resolvedAccessor.fields.forEach { (fieldAccessor, fieldNode) -> + beginControlFlow("%M(%S)", types.KT_CLASS_MAPPING_FIELD_DSL, fieldAccessor.name) + + groupFieldNames(fieldNode).forEach { (fieldKey, versions) -> + val (ns, name) = fieldKey - resolvedAccessor.constructors.forEach { (_, ctorNode) -> - beginControlFlow("%M", SourceTypes.KT_CLASS_MAPPING_CTOR_DSL) + addStatement( + "put(%S, %S, %L)", + ns, + name, + versions.map { KCodeBlock.of("%S", it.id) }.joinToCode() + ) + } - groupConstructorNames(ctorNode).forEach { (ctorKey, versions) -> - val (ns, desc) = ctorKey + endControlFlow() + } - val args = Type.getArgumentTypes(desc) - addStatement( - "put(%S, arrayOf(%L)%L)", - ns, - versions.map { KCodeBlock.of("%S", it.id) }.joinToCode(), - if (args.isEmpty()) "" else args.map { KCodeBlock.of("%S", it.className) }.joinToCode(prefix = ", ") - ) - } + resolvedAccessor.constructors.forEach { (_, ctorNode) -> + beginControlFlow("%M", types.KT_CLASS_MAPPING_CTOR_DSL) - endControlFlow() - } + groupConstructorNames(ctorNode).forEach { (ctorKey, versions) -> + val (ns, desc) = ctorKey - resolvedAccessor.methods.forEach { (methodAccessor, methodNode) -> - beginControlFlow("%M(%S)", SourceTypes.KT_CLASS_MAPPING_METHOD_DSL, methodAccessor.name) + val args = Type.getArgumentTypes(desc) + addStatement( + "put(%S, arrayOf(%L)%L)", + ns, + versions.map { KCodeBlock.of("%S", it.id) }.joinToCode(), + if (args.isEmpty()) "" else args.map { KCodeBlock.of("%S", it.className) }.joinToCode(prefix = ", ") + ) + } - groupMethodNames(methodNode).forEach { (methodKey, versions) -> - val (ns, name, desc) = methodKey + endControlFlow() + } - val args = Type.getArgumentTypes(desc) - addStatement( - "put(%S, arrayOf(%L), %S%L)", - ns, - versions.map { KCodeBlock.of("%S", it.id) }.joinToCode(), - name, - if (args.isEmpty()) "" else args.map { KCodeBlock.of("%S", it.className) }.joinToCode(prefix = ", ") - ) - } + resolvedAccessor.methods.forEach { (methodAccessor, methodNode) -> + beginControlFlow("%M(%S)", types.KT_CLASS_MAPPING_METHOD_DSL, methodAccessor.name) - endControlFlow() - } + groupMethodNames(methodNode).forEach { (methodKey, versions) -> + val (ns, name, desc) = methodKey + + val args = Type.getArgumentTypes(desc) + addStatement( + "put(%S, arrayOf(%L), %S%L)", + ns, + versions.map { KCodeBlock.of("%S", it.id) }.joinToCode(), + name, + if (args.isEmpty()) "" else args.map { KCodeBlock.of("%S", it.className) }.joinToCode(prefix = ", ") + ) } - ) - .addProperties( - resolvedAccessor.fields.map { (fieldAccessor, fieldNode) -> - val overloadIndex = resolvedAccessor.fieldOverloads[fieldAccessor] - val accessorName = "FIELD_${fieldAccessor.upperName.escapeKotlinName()}${overloadIndex?.let { if (it != 0) "_$it" else "" } ?: ""}" - val fieldType = fieldAccessor.type?.let(Type::getType) - ?: getFriendlyType(fieldNode.last.value) - fun PropertySpec.Builder.addMeta(constant: Boolean = false): PropertySpec.Builder = apply { + endControlFlow() + } + } + ) + .addProperties( + resolvedAccessor.fields.map { mergedAccessor -> + val (fieldAccessor, fieldNode, overloadIndex) = mergedAccessor + + val mappingName = namingStrategy.field(fieldAccessor, overloadIndex) + fun PropertySpec.Builder.addMeta(constant: Boolean = false, mapping: Boolean = false): PropertySpec.Builder = apply { + if (mergedAccessor.isChained) { + addKdoc( + "%L for the following %L:\n", + if (mapping) "Mapping" else "Accessor", + if (constant) "constant field values" else "fields" + ) + + mergedAccessor.forEach { chainedAccessor, chainedFieldNode -> + val chainedType = chainedAccessor.type?.let(Type::getType) + ?: getFriendlyType(chainedFieldNode.last.value) + addKdoc( - """ - Accessor for the `%L %L` %L. - - @see·%L.%L - """.trimIndent(), - fieldType.className, - fieldAccessor.name, - if (constant) { - "constant field value" - } else { - "field" - }, - mappingClassName.canonicalName, - accessorName + " - `%L %L` (`%L` - `%L`)\n", + chainedType.className, + chainedAccessor.name, + chainedFieldNode.first.key.id, + chainedFieldNode.last.key.id, ) } + } else { + val fieldType = fieldAccessor.type?.let(Type::getType) + ?: getFriendlyType(fieldNode.last.value) + + addKdoc( + "%L for the `%L %L` %L.\n", + if (mapping) "Mapping" else "Accessor", + fieldType.className, + fieldAccessor.name, + if (constant) "constant field value" else "field" + ) + } - val mod = fieldNode.last.value.modifiers - if ((mod and Opcodes.ACC_STATIC) != 0 && (mod and Opcodes.ACC_FINAL) != 0) { // constant + addKdoc("\n`%L` - `%L`", fieldNode.first.key.id, fieldNode.last.key.id) + if (!mapping) { + addKdoc("\n@see·%L.%L", mappingClassName.canonicalName, mappingName) + } + } + + val mod = fieldNode.last.value.modifiers + if ((mod and Opcodes.ACC_STATIC) != 0 && (mod and Opcodes.ACC_FINAL) != 0) { // constant + accessorBuilder.addProperty( + PropertySpec.builder(namingStrategy.constant(fieldAccessor, overloadIndex), types.KT_NULLABLE_ANY) + .addMeta(constant = true) + .delegate("%M(%T.%L::getConstantValue)", types.KT_LAZY_DSL, mappingClassName, mappingName) + .build() + ) + } else { + when (generator.config.accessorType) { + AccessorType.REFLECTION -> { accessorBuilder.addProperty( - PropertySpec.builder(accessorName, SourceTypes.KT_NULLABLE_ANY) - .addMeta(constant = true) - .delegate("%M(%T.$accessorName::getConstantValue)", SourceTypes.KT_LAZY_DSL, mappingClassName) + PropertySpec.builder(mappingName, types.KT_NULLABLE_FIELD) + .addMeta() + .delegate("%M(%T.%L::getField)", types.KT_LAZY_DSL, mappingClassName, mappingName) .build() ) - } else { - when (generator.config.accessorType) { - AccessorType.REFLECTION -> { - accessorBuilder.addProperty( - PropertySpec.builder(accessorName, SourceTypes.KT_NULLABLE_FIELD) - .addMeta() - .delegate("%M(%T.$accessorName::getField)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - } - AccessorType.METHOD_HANDLES -> { - accessorBuilder.addProperty( - PropertySpec.builder("${accessorName}_GETTER", SourceTypes.KT_NULLABLE_METHOD_HANDLE) - .addMeta() - .delegate("%M(%T.$accessorName::getFieldGetter)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - accessorBuilder.addProperty( - PropertySpec.builder("${accessorName}_SETTER", SourceTypes.KT_NULLABLE_METHOD_HANDLE) - .addMeta() - .delegate("%M(%T.$accessorName::getFieldSetter)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - } - AccessorType.NONE -> {} - } } - - PropertySpec.builder(accessorName, SourceTypes.KT_FIELD_MAPPING) - .addKdoc( - """ - Mapping for the `%L %L` field. - - `%L` - `%L` - """.trimIndent(), - fieldType.className, - fieldAccessor.name, - fieldNode.first.key.id, - fieldNode.last.key.id + AccessorType.METHOD_HANDLES -> { + accessorBuilder.addProperty( + PropertySpec.builder(namingStrategy.fieldHandle(fieldAccessor, overloadIndex, false), types.KT_NULLABLE_METHOD_HANDLE) + .addMeta() + .delegate("%M(%T.%L::getFieldGetter)", types.KT_LAZY_DSL, mappingClassName, mappingName) + .build() ) - .initializer { - add("getField(%S, %L)!!", fieldAccessor.name, overloadIndex) - if (fieldAccessor.chain != null) { - add( - ".chain(FIELD_%L%L)", - fieldAccessor.chain.upperName.escapeKotlinName(), - resolvedAccessor.fieldOverloads[fieldAccessor.chain] - ?.let { if (it != 0) "_$it" else "" } - ?: "" - ) - } - } - .build() - } - ) - .addProperties( - resolvedAccessor.constructors.mapIndexed { i, (ctorAccessor, ctorNode) -> - val accessorName = "CONSTRUCTOR_$i" - val ctorArgs = Type.getArgumentTypes(ctorAccessor.type) - - fun PropertySpec.Builder.addMeta(): PropertySpec.Builder = apply { - addKdoc( - """ - Accessor for the `(%L)` constructor. - - @see·%L.%L - """.trimIndent(), - ctorArgs.joinToString(transform = Type::getClassName), - mappingClassName.canonicalName, - accessorName + accessorBuilder.addProperty( + PropertySpec.builder(namingStrategy.fieldHandle(fieldAccessor, overloadIndex, true), types.KT_NULLABLE_METHOD_HANDLE) + .addMeta() + .delegate("%M(%T.%L::getFieldSetter)", types.KT_LAZY_DSL, mappingClassName, mappingName) + .build() ) } + AccessorType.NONE -> {} + } + } - when (generator.config.accessorType) { - AccessorType.REFLECTION -> { - accessorBuilder.addProperty( - PropertySpec.builder(accessorName, SourceTypes.KT_NULLABLE_CONSTRUCTOR_WILDCARD) - .addMeta() - .delegate("%M(%T.$accessorName::getConstructor)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - } - AccessorType.METHOD_HANDLES -> { - accessorBuilder.addProperty( - PropertySpec.builder(accessorName, SourceTypes.KT_NULLABLE_METHOD_HANDLE) - .addMeta() - .delegate("%M(%T.$accessorName::getConstructorHandle)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - } - AccessorType.NONE -> {} - } + PropertySpec.builder(mappingName, types.KT_FIELD_MAPPING) + .addMeta(mapping = true) + .initializer("getField(%S, %L)!!", fieldAccessor.name, overloadIndex) + .build() + } + ) + .addProperties( + resolvedAccessor.constructors.map { (ctorAccessor, ctorNode, overloadIndex) -> + val mappingName = namingStrategy.constructor(ctorAccessor, overloadIndex) + val ctorArgs = Type.getArgumentTypes(ctorAccessor.type) + + fun PropertySpec.Builder.addMeta(): PropertySpec.Builder = apply { + addKdoc( + """ + Accessor for the `(%L)` constructor. + + `%L` - `%L` + @see·%L.%L + """.trimIndent(), + ctorArgs.joinToString(transform = Type::getClassName), + ctorNode.first.key.id, + ctorNode.last.key.id, + mappingClassName.canonicalName, + mappingName + ) + } - PropertySpec.builder(accessorName, SourceTypes.KT_CONSTRUCTOR_MAPPING) - .addKdoc( - """ - Mapping for the `(%L)` constructor. - - `%L` - `%L` - """.trimIndent(), - ctorArgs.joinToString(transform = Type::getClassName), - ctorNode.first.key.id, - ctorNode.last.key.id - ) - .initializer("getConstructor(%L)!!", i) - .build() + when (generator.config.accessorType) { + AccessorType.REFLECTION -> { + accessorBuilder.addProperty( + PropertySpec.builder(mappingName, types.KT_NULLABLE_CONSTRUCTOR_WILDCARD) + .addMeta() + .delegate("%M(%T.%L::getConstructor)", types.KT_LAZY_DSL, mappingClassName, mappingName) + .build() + ) } - ) - .addProperties( - resolvedAccessor.methods.map { (methodAccessor, methodNode) -> - val overloadIndex = resolvedAccessor.methodOverloads[methodAccessor] - val accessorName = "METHOD_${methodAccessor.upperName.escapeKotlinName()}${overloadIndex?.let { if (it != 0) "_$it" else "" } ?: ""}" - val methodType = if (methodAccessor.isIncomplete) { - getFriendlyType(methodNode.last.value) - } else { - Type.getType(methodAccessor.type) - } + AccessorType.METHOD_HANDLES -> { + accessorBuilder.addProperty( + PropertySpec.builder(mappingName, types.KT_NULLABLE_METHOD_HANDLE) + .addMeta() + .delegate("%M(%T.%L::getConstructorHandle)", types.KT_LAZY_DSL, mappingClassName, mappingName) + .build() + ) + } + AccessorType.NONE -> {} + } + + PropertySpec.builder(mappingName, types.KT_CONSTRUCTOR_MAPPING) + .addKdoc( + """ + Mapping for the `(%L)` constructor. + + `%L` - `%L` + """.trimIndent(), + ctorArgs.joinToString(transform = Type::getClassName), + ctorNode.first.key.id, + ctorNode.last.key.id + ) + .initializer("getConstructor(%L)!!", overloadIndex) + .build() + } + ) + .addProperties( + resolvedAccessor.methods.map { mergedAccessor -> + val (methodAccessor, methodNode, overloadIndex) = mergedAccessor + + val mappingName = namingStrategy.method(methodAccessor, overloadIndex) + fun PropertySpec.Builder.addMeta(mapping: Boolean = false): PropertySpec.Builder = apply { + if (mergedAccessor.isChained) { + addKdoc( + "%L for the following methods:\n", + if (mapping) "Mapping" else "Accessor" + ) + + mergedAccessor.forEach { (chainedAccessor, chainedMethodNode) -> + val chainedType = if (chainedAccessor.isIncomplete) getFriendlyType(chainedMethodNode.last.value) else Type.getType(chainedAccessor.type) - fun PropertySpec.Builder.addMeta(): PropertySpec.Builder = apply { addKdoc( - """ - Accessor for the `%L %L(%L)` method. - - @see·%L.%L - """.trimIndent(), - methodType.returnType.className, - methodAccessor.name, - methodType.argumentTypes.joinToString(transform = Type::getClassName), - mappingClassName.canonicalName, - accessorName + " - `%L %L(%L)` (`%L` - `%L`)\n", + chainedType.returnType.className, + chainedAccessor.name, + chainedType.argumentTypes.joinToString(transform = Type::getClassName), + chainedMethodNode.first.key.id, + chainedMethodNode.last.key.id ) } + } else { + val methodType = if (methodAccessor.isIncomplete) getFriendlyType(methodNode.last.value) else Type.getType(methodAccessor.type) + + addKdoc( + "%L for the `%L %L(%L)` method.\n", + if (mapping) "Mapping" else "Accessor", + methodType.returnType.className, + methodAccessor.name, + methodType.argumentTypes.joinToString(transform = Type::getClassName) + ) + } - when (generator.config.accessorType) { - AccessorType.REFLECTION -> { - accessorBuilder.addProperty( - PropertySpec.builder(accessorName, SourceTypes.KT_NULLABLE_METHOD) - .addMeta() - .delegate("%M(%T.$accessorName::getMethod)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - } - AccessorType.METHOD_HANDLES -> { - accessorBuilder.addProperty( - PropertySpec.builder(accessorName, SourceTypes.KT_NULLABLE_METHOD_HANDLE) - .addMeta() - .delegate("%M(%T.$accessorName::getMethodHandle)", SourceTypes.KT_LAZY_DSL, mappingClassName) - .build() - ) - } - AccessorType.NONE -> {} - } + addKdoc("\n`%L` - `%L`", methodNode.first.key.id, methodNode.last.key.id) + if (!mapping) { + addKdoc("\n@see·%L.%L", mappingClassName.canonicalName, mappingName) + } + } - PropertySpec.builder(accessorName, SourceTypes.KT_METHOD_MAPPING) - .addKdoc( - """ - Mapping for the `%L %L(%L)` method. - - `%L` - `%L` - """.trimIndent(), - methodType.returnType.className, - methodAccessor.name, - methodType.argumentTypes.joinToString(transform = Type::getClassName), - methodNode.first.key.id, - methodNode.last.key.id - ) - .initializer { - add("getMethod(%S, %L)!!", methodAccessor.name, overloadIndex) - if (methodAccessor.chain != null) { - add( - ".chain(METHOD_%L%L)", - methodAccessor.chain.upperName.escapeKotlinName(), - resolvedAccessor.methodOverloads[methodAccessor.chain] - ?.let { if (it != 0) "_$it" else "" } - ?: "" - ) - } - } - .build() + when (generator.config.accessorType) { + AccessorType.REFLECTION -> { + accessorBuilder.addProperty( + PropertySpec.builder(mappingName, types.KT_NULLABLE_METHOD) + .addMeta() + .delegate("%M(%T.%L::getMethod)", types.KT_LAZY_DSL, mappingClassName, mappingName) + .build() + ) } - ) - .build() + AccessorType.METHOD_HANDLES -> { + accessorBuilder.addProperty( + PropertySpec.builder(mappingName, types.KT_NULLABLE_METHOD_HANDLE) + .addMeta() + .delegate("%M(%T.%L::getMethodHandle)", types.KT_LAZY_DSL, mappingClassName, mappingName) + .build() + ) + } + AccessorType.NONE -> {} + } + + PropertySpec.builder(mappingName, types.KT_METHOD_MAPPING) + .addMeta(mapping = true) + .initializer("getMethod(%S, %L)!!", methodAccessor.name, overloadIndex) + .build() + } ) + .build() + .writeTo(mappingClassName, generator.workspace) - if (generator.config.accessorType != AccessorType.NONE) { - add(accessorBuilder.build()) - } + if (generator.config.accessorType != AccessorType.NONE) { + accessorBuilder.build() + .writeTo(accessorClassName, generator.workspace, includeMappingDsl = true) } - - typeSpecs.writeTo( - generator.workspace, - accessorSimpleName.replaceFirstChar { it.lowercase(Locale.getDefault()) }, - // import LazySupplier delegate for accessors - includeMappingDsl = generator.config.accessorType != AccessorType.NONE - ) } /** * Generates a Kotlin mapping lookup class from class names. * - * @param names internal names of classes declared in accessor models + * @param names fully qualified names of generated mapping classes */ override fun generateLookupClass(names: List) { - PropertySpec.builder("LOOKUP", SourceTypes.KT_MAPPING_LOOKUP) + PropertySpec.builder(namingStrategy.member(GeneratedMemberType.LOOKUP), types.KT_MAPPING_LOOKUP) .addKdoc("Mapping lookup index.") .initializer { - beginControlFlow("%M", SourceTypes.KT_MAPPING_LOOKUP_DSL) + beginControlFlow("%M", types.KT_MAPPING_LOOKUP_DSL) names.forEach { name -> - val mappingClassName = KClassName( - generator.config.basePackage, - "${name.substringAfterLast('/').escapeKotlinName()}Mapping" - ) - - addStatement("put(%T)", mappingClassName) + addStatement("put(%T)", name.escapeKotlinName().toKClassName()) } endControlFlow() } .build() - .writeTo(generator.workspace, "lookup") + .writeTo(namingStrategy.klass(GeneratedClassType.LOOKUP).toKClassName(), generator.workspace) } /** * Writes a [PropertySpec] to a workspace with default settings. * - * @param workspace the workspace * @param name the file name + * @param workspace the workspace * @param includeMappingDsl whether imports for implicit mapping DSL should be added */ fun PropertySpec.writeTo( + name: KClassName, + workspace: Workspace, + includeMappingDsl: Boolean = false + ) { + listOf(this).writeTo(name, workspace, includeMappingDsl) + } + + /** + * Writes a [KTypeSpec] to a workspace with default settings. + * + * @param name the file name + * @param workspace the workspace + * @param includeMappingDsl whether imports for implicit mapping DSL should be added + */ + fun KTypeSpec.writeTo( + name: KClassName, workspace: Workspace, - name: String = requireNotNull(this.name) { "File name required, but type has no name" }, includeMappingDsl: Boolean = false ) { - listOf(this).writeTo(workspace, name, includeMappingDsl) + listOf(this).writeTo(name, workspace, includeMappingDsl) } /** @@ -437,14 +442,14 @@ open class KotlinGenerationContext( * @param name the file name * @param includeMappingDsl whether imports for implicit mapping DSL should be added */ - fun Iterable.writeTo(workspace: Workspace, name: String, includeMappingDsl: Boolean = false) { - FileSpec.builder(generator.config.basePackage, name) + fun Iterable.writeTo(name: KClassName, workspace: Workspace, includeMappingDsl: Boolean = false) { + FileSpec.builder(name.packageName, name.simpleName) .addFileComment("This file was generated by takenaka on ${DATE_FORMAT.format(generationTime)}. Do not edit, changes will be overwritten!") .addKotlinDefaultImports(includeJvm = true) .indent(" ".repeat(4)) // 4 spaces .apply { if (includeMappingDsl) { - addImport(SourceTypes.KT_LAZY_DELEGATE_DSL) + addImport(types.KT_LAZY_DELEGATE_DSL) } forEach { elem -> diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/ResolvedClassAccessor.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/ResolvedClassAccessor.kt deleted file mode 100644 index 32c41f78394..00000000000 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/ResolvedClassAccessor.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). - * - * Copyright (c) 2023-2024 Matous Kucera - * - * 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 me.kcra.takenaka.generator.accessor.context.impl - -import me.kcra.takenaka.core.mapping.ancestry.impl.ClassAncestryNode -import me.kcra.takenaka.generator.accessor.model.ClassAccessor -import me.kcra.takenaka.generator.accessor.model.FieldAccessor -import me.kcra.takenaka.generator.accessor.model.MethodAccessor - -/** - * A mapping of an accessor model to its overload index. - */ -typealias NameOverloads = Map - -/** - * A class accessor with members resolved from their ancestry trees. - * - * @property model the class accessor model - * @property node the class ancestry node - * @property fields the field accessor models and ancestry nodes - * @property constructors the constructor accessor models and ancestry nodes - * @property methods the method accessor models and ancestry nodes - * @property fieldOverloads the field accessor name overloads (fields can't be overloaded, but capitalization matters, which is a problem when making uppercase names from everything) - * @property methodOverloads the method accessor name overloads - */ -data class ResolvedClassAccessor( - val model: ClassAccessor, - val node: ClassAncestryNode, - val fields: List, - val constructors: List, - val methods: List, - val fieldOverloads: NameOverloads, - val methodOverloads: NameOverloads -) diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/SourceTypes.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/SourceTypes.kt index 39f093aca56..6f725e6191b 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/SourceTypes.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/SourceTypes.kt @@ -96,22 +96,52 @@ inline fun buildJLambdaBlock(vararg parameters: String, block: JCodeBlockBuilder add("}") } +/** + * Creates a [JClassName] of a fully qualified top-level class name. + * + * @return the [JClassName] + */ +fun String.toJClassName(): JClassName { + val dotIndex = lastIndexOf('.') + if (dotIndex == -1) { + return JClassName.get("", this) + } + + return JClassName.get(substring(0, dotIndex), substring(dotIndex + 1)) +} + +/** + * Creates a [KClassName] of a fully qualified top-level class name. + * + * @return the [KClassName] + */ +fun String.toKClassName(): KClassName { + val dotIndex = lastIndexOf('.') + if (dotIndex == -1) { + return KClassName("", this) + } + + return KClassName(substring(0, dotIndex), substring(dotIndex + 1)) +} + /** * JavaPoet/KotlinPoet types. + * + * @property runtimePackage the package of the accessor runtime */ -object SourceTypes { - val MAPPING_LOOKUP: JClassName = JClassName.get("me.kcra.takenaka.accessor.mapping", "MappingLookup") +class SourceTypes(val runtimePackage: String) { + val MAPPING_LOOKUP: JClassName = JClassName.get("$runtimePackage.mapping", "MappingLookup") val KT_MAPPING_LOOKUP = MAPPING_LOOKUP.toKClassName() - val CLASS_MAPPING: JClassName = JClassName.get("me.kcra.takenaka.accessor.mapping", "ClassMapping") + val CLASS_MAPPING: JClassName = JClassName.get("$runtimePackage.mapping", "ClassMapping") val KT_CLASS_MAPPING = CLASS_MAPPING.toKClassName() - val FIELD_MAPPING: JClassName = JClassName.get("me.kcra.takenaka.accessor.mapping", "FieldMapping") + val FIELD_MAPPING: JClassName = JClassName.get("$runtimePackage.mapping", "FieldMapping") val KT_FIELD_MAPPING = FIELD_MAPPING.toKClassName() - val CONSTRUCTOR_MAPPING: JClassName = JClassName.get("me.kcra.takenaka.accessor.mapping", "ConstructorMapping") + val CONSTRUCTOR_MAPPING: JClassName = JClassName.get("$runtimePackage.mapping", "ConstructorMapping") val KT_CONSTRUCTOR_MAPPING = CONSTRUCTOR_MAPPING.toKClassName() - val METHOD_MAPPING: JClassName = JClassName.get("me.kcra.takenaka.accessor.mapping", "MethodMapping") + val METHOD_MAPPING: JClassName = JClassName.get("$runtimePackage.mapping", "MethodMapping") val KT_METHOD_MAPPING = METHOD_MAPPING.toKClassName() val SUPPLIER: JClassName = JClassName.get(java.util.function.Supplier::class.java) - val LAZY_SUPPLIER: JClassName = JClassName.get("me.kcra.takenaka.accessor.util", "LazySupplier") + val LAZY_SUPPLIER: JClassName = JClassName.get("$runtimePackage.util", "LazySupplier") val KT_LAZY_SUPPLIER = LAZY_SUPPLIER.toKClassName() val NOT_NULL: JClassName = JClassName.get("org.jetbrains.annotations", "NotNull") val NULLABLE: JClassName = JClassName.get("org.jetbrains.annotations", "Nullable") @@ -135,11 +165,11 @@ object SourceTypes { val STRING: JClassName = JClassName.get(java.lang.String::class.java) val KT_NULLABLE_ANY = com.squareup.kotlinpoet.ANY.copy(nullable = true) - val KT_MAPPING_LOOKUP_DSL = MemberName("me.kcra.takenaka.accessor.util.kotlin", "mappingLookup") - val KT_CLASS_MAPPING_DSL = MemberName("me.kcra.takenaka.accessor.util.kotlin", "classMapping") - val KT_CLASS_MAPPING_FIELD_DSL = MemberName("me.kcra.takenaka.accessor.util.kotlin", "field", isExtension = true) - val KT_CLASS_MAPPING_CTOR_DSL = MemberName("me.kcra.takenaka.accessor.util.kotlin", "constructor", isExtension = true) - val KT_CLASS_MAPPING_METHOD_DSL = MemberName("me.kcra.takenaka.accessor.util.kotlin", "method", isExtension = true) - val KT_LAZY_DSL = MemberName("me.kcra.takenaka.accessor.util.kotlin", "lazy") - val KT_LAZY_DELEGATE_DSL = MemberName("me.kcra.takenaka.accessor.util.kotlin", "getValue", isExtension = true) + val KT_MAPPING_LOOKUP_DSL = MemberName("$runtimePackage.util.kotlin", "mappingLookup") + val KT_CLASS_MAPPING_DSL = MemberName("$runtimePackage.util.kotlin", "classMapping") + val KT_CLASS_MAPPING_FIELD_DSL = MemberName("$runtimePackage.util.kotlin", "field", isExtension = true) + val KT_CLASS_MAPPING_CTOR_DSL = MemberName("$runtimePackage.util.kotlin", "constructor", isExtension = true) + val KT_CLASS_MAPPING_METHOD_DSL = MemberName("$runtimePackage.util.kotlin", "method", isExtension = true) + val KT_LAZY_DSL = MemberName("$runtimePackage.util.kotlin", "lazy") + val KT_LAZY_DELEGATE_DSL = MemberName("$runtimePackage.util.kotlin", "getValue", isExtension = true) } diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/TracingGenerationContext.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/TracingGenerationContext.kt index baf5a8dfa34..fd1cd0844ef 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/TracingGenerationContext.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/TracingGenerationContext.kt @@ -94,13 +94,13 @@ open class TracingGenerationContext( output.println("-".repeat(classHeader.length)) - resolvedAccessor.fields.forEach { (fieldAccessor, fieldNode) -> - val header = "FIELD: ${resolvedAccessor.model.name}.${fieldAccessor.name}" + resolvedAccessor.fields.forEach { mergedAccessor -> + val header = "FIELD: ${resolvedAccessor.model.name}" + mergedAccessor.keys.joinToString(", ") { ".${it.name}" } output.println(header) - output.printNodeHistory(fieldNode) + output.printNodeHistory(mergedAccessor.mergedNode) - groupFieldNames(fieldNode).forEach { (fieldKey, versions) -> + groupFieldNames(mergedAccessor.mergedNode).forEach { (fieldKey, versions) -> val (ns, name) = fieldKey output.println("namespace: $ns, mapping: $name, versions: [${versions.joinToString(transform = Version::id)}]") @@ -124,13 +124,13 @@ open class TracingGenerationContext( output.println("-".repeat(header.length)) } - resolvedAccessor.methods.forEach { (methodAccessor, methodNode) -> - val header = "METHOD: ${resolvedAccessor.model.name}.${methodAccessor.name}${methodAccessor.type}" + resolvedAccessor.methods.forEach { mergedAccessor -> + val header = "METHOD: ${resolvedAccessor.model.name}" + mergedAccessor.keys.joinToString(", ") { ".${it.name}${it.type}" } output.println(header) - output.printNodeHistory(methodNode) + output.printNodeHistory(mergedAccessor.mergedNode) - groupMethodNames(methodNode).forEach { (methodKey, versions) -> + groupMethodNames(mergedAccessor.mergedNode, exact = true).forEach { (methodKey, versions) -> val (ns, name, desc) = methodKey output.println("namespace: $ns, mapping: $name, descriptor: $desc, versions: [${versions.joinToString(transform = Version::id)}]") diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/elementKeys.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/elementKeys.kt index 940d883b7c9..f3c0b8541e3 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/elementKeys.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/elementKeys.kt @@ -54,21 +54,20 @@ data class ConstructorKey(override val namespace: String, val descriptor: String /** * A method mapping key. * - * *The method return type is not used when checking equality, mimicking reflective lookups.* - * * @property namespace the mapping namespace * @property name the mapped method name * @property descriptor the mapped method descriptor + * @property exact whether descriptors should be matched exactly or without return types */ -data class MethodKey(override val namespace: String, val name: String, val descriptor: String) : NamespacedKey { +data class MethodKey(override val namespace: String, val name: String, val descriptor: String, val exact: Boolean) : NamespacedKey { /** - * [descriptor], but with the return type removed. + * The descriptor used for matching. */ - private val partialDescriptor: String + private val matchableDescriptor: String init { val parenthesisIndex = descriptor.lastIndexOf(')') - this.partialDescriptor = if (parenthesisIndex != -1) descriptor.substring(0, parenthesisIndex + 1) else descriptor + this.matchableDescriptor = if (!exact && parenthesisIndex != -1) descriptor.substring(0, parenthesisIndex + 1) else descriptor } override fun equals(other: Any?): Boolean { @@ -79,7 +78,7 @@ data class MethodKey(override val namespace: String, val name: String, val descr if (namespace != other.namespace) return false if (name != other.name) return false - if (partialDescriptor != other.partialDescriptor) return false + if (matchableDescriptor != other.matchableDescriptor) return false return true } @@ -87,7 +86,7 @@ data class MethodKey(override val namespace: String, val name: String, val descr override fun hashCode(): Int { var result = namespace.hashCode() result = 31 * result + name.hashCode() - result = 31 * result + partialDescriptor.hashCode() + result = 31 * result + matchableDescriptor.hashCode() return result } } diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/resolvedAccessors.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/resolvedAccessors.kt new file mode 100644 index 00000000000..85aff467027 --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/context/impl/resolvedAccessors.kt @@ -0,0 +1,92 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor.context.impl + +import me.kcra.takenaka.core.mapping.ancestry.AncestryTree +import me.kcra.takenaka.core.mapping.ancestry.impl.ClassAncestryNode +import me.kcra.takenaka.core.mapping.ancestry.nodeOf +import me.kcra.takenaka.generator.accessor.model.ClassAccessor +import me.kcra.takenaka.generator.accessor.model.ConstructorAccessor +import me.kcra.takenaka.generator.accessor.model.FieldAccessor +import me.kcra.takenaka.generator.accessor.model.MethodAccessor +import net.fabricmc.mappingio.tree.MappingTreeView +import net.fabricmc.mappingio.tree.MappingTreeView.* + +/** + * A class accessor with members resolved from their ancestry trees. + * + * @property model the class accessor model + * @property node the class ancestry node + * @property fields the field accessor models and ancestry nodes + * @property constructors the constructor accessor models and ancestry nodes + * @property methods the method accessor models and ancestry nodes + */ +data class ResolvedClassAccessor( + val model: ClassAccessor, + val node: ClassAncestryNode, + val fields: List, + val constructors: List, + val methods: List +) + +/** + * Member accessors with resolved ancestry nodes. + * + * @param nodes the ancestry nodes, multiple for chained models + * @property overloadIndex overload index of the accessor + */ +class ResolvedMemberAccessor( + nodes: Map>, + val overloadIndex: Int +) : Map> by nodes { + /** + * Creates a non-chained resolved accessor. + * + * @param model the accessor model + * @param node the ancestry node + * @param overloadIndex overload index of the accessor + */ + constructor(model: M, node: AncestryTree.Node, overloadIndex: Int) : this(mapOf(model to node), overloadIndex) + + /** + * Whether this member is a chained one, i.e. multiple ancestry nodes. + */ + val isChained: Boolean + get() = size > 1 + + /** + * The last accessor model, used for visual representation. + */ + val model: M by lazy(keys::last) + + /** + * The merged ancestry node. + */ + val mergedNode: AncestryTree.Node by lazy { + nodeOf(*values.toTypedArray()) + } + + // destructuring utilities + operator fun component1() = model + operator fun component2() = mergedNode + operator fun component3() = overloadIndex +} + +typealias ResolvedFieldAccessor = ResolvedMemberAccessor +typealias ResolvedConstructorAccessor = ResolvedMemberAccessor +typealias ResolvedMethodAccessor = ResolvedMemberAccessor \ No newline at end of file diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/model/ClassAccessor.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/model/ClassAccessor.kt index 9e02035b189..7957a08f3bc 100644 --- a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/model/ClassAccessor.kt +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/model/ClassAccessor.kt @@ -32,10 +32,10 @@ import java.io.Serializable */ data class ClassAccessor( val name: String, - val fields: List, - val constructors: List, - val methods: List, - val requiredTypes: RequiredMemberTypes + val fields: List = emptyList(), + val constructors: List = emptyList(), + val methods: List = emptyList(), + val requiredTypes: RequiredMemberTypes = 0 ) : Serializable { /** * Internalized variant of [name]. @@ -43,6 +43,15 @@ data class ClassAccessor( @Transient val internalName = name.toInternalName() + /** + * Creates a copy of this model, but with a different name. + * + * @param name the new model name + */ + fun withName(name: String): ClassAccessor { + return ClassAccessor(name, fields, constructors, methods, requiredTypes) + } + companion object { private const val serialVersionUID = 1L } diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/ForwardingNamingStrategy.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/ForwardingNamingStrategy.kt new file mode 100644 index 00000000000..65f107728f1 --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/ForwardingNamingStrategy.kt @@ -0,0 +1,46 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor.naming + +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.GeneratedMemberType +import me.kcra.takenaka.generator.accessor.model.ClassAccessor +import me.kcra.takenaka.generator.accessor.model.ConstructorAccessor +import me.kcra.takenaka.generator.accessor.model.FieldAccessor +import me.kcra.takenaka.generator.accessor.model.MethodAccessor + +/** + * A [NamingStrategy] implementation base that redirects calls to [next]. + * + * @property next the strategy to delegate to + * @author Matouš Kučera + */ +abstract class ForwardingNamingStrategy(private val next: NamingStrategy) : NamingStrategy { + override fun klass(model: ClassAccessor, type: GeneratedClassType): String = next.klass(model, type) + override fun field(model: FieldAccessor, index: Int): String = next.field(model, index) + override fun fieldHandle(model: FieldAccessor, index: Int, mutating: Boolean): String = next.fieldHandle(model, index, mutating) + override fun constant(model: FieldAccessor, index: Int): String = next.constant(model, index) + override fun constructor(model: ConstructorAccessor, index: Int): String = next.constructor(model, index) + override fun method(model: MethodAccessor, index: Int): String = next.method(model, index) + override fun klass(type: GeneratedClassType): String = next.klass(type) + override fun member(type: GeneratedMemberType): String = next.member(type) + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/FullyQualifiedNamingStrategy.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/FullyQualifiedNamingStrategy.kt new file mode 100644 index 00000000000..eb3c98df7a2 --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/FullyQualifiedNamingStrategy.kt @@ -0,0 +1,43 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor.naming + +import me.kcra.takenaka.core.mapping.fromInternalName +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.model.ClassAccessor + +/** + * A fully-qualified class name strategy. + * + * @author Matouš Kučera + */ +internal class FullyQualifiedNamingStrategy : SimpleNamingStrategy() { + override fun klass(model: ClassAccessor, type: GeneratedClassType): String { + val name = model.internalName.fromInternalName() // sanitize name + + return when (type) { + GeneratedClassType.MAPPING -> "${name}Mapping" + GeneratedClassType.ACCESSOR -> "${name}Accessor" + else -> throw UnsupportedOperationException("$type is not a contextual type") + } + } + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/NamingStrategy.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/NamingStrategy.kt new file mode 100644 index 00000000000..d78e428804c --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/NamingStrategy.kt @@ -0,0 +1,107 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor.naming + +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.GeneratedMemberType +import me.kcra.takenaka.generator.accessor.model.ClassAccessor +import me.kcra.takenaka.generator.accessor.model.ConstructorAccessor +import me.kcra.takenaka.generator.accessor.model.FieldAccessor +import me.kcra.takenaka.generator.accessor.model.MethodAccessor +import java.io.Serializable + +/** + * A strategy for naming generated classes and its members. + * + * @author Michal Turek + * @author Matouš Kučera + */ +interface NamingStrategy : Serializable { + /** + * Creates a new class name for the class type in the context of the specified class model. + * + * @param model the accessor model + * @param type the type of the class being generated + * @return the class name + */ + fun klass(model: ClassAccessor, type: GeneratedClassType): String + + /** + * Creates a new field name for the specified field model. + * + * @param model the accessor model + * @param index the overload index of the model declaration + * @return the field name + */ + fun field(model: FieldAccessor, index: Int): String + + /** + * Creates a new field name for the specified field model ([java.lang.invoke.MethodHandle] variant). + * + * @param model the accessor model + * @param index the overload index of the model declaration + * @param mutating whether a setter handle name should be generated + * @return the field name + */ + fun fieldHandle(model: FieldAccessor, index: Int, mutating: Boolean): String + + /** + * Creates a new field name for the specified constant field model. + * + * @param model the accessor model + * @param index the overload index of the model declaration + * @return the field name + */ + fun constant(model: FieldAccessor, index: Int): String + + /** + * Creates a new field name for the specified constructor model. + * + * @param model the accessor model + * @param index the index of the model declaration + * @return the field name + */ + fun constructor(model: ConstructorAccessor, index: Int): String + + /** + * Creates a new field name for the specified method model. + * + * @param model the accessor model + * @param index the overload index of the model declaration + * @return the field name + */ + fun method(model: MethodAccessor, index: Int): String + + // context-free cases + + /** + * Creates a new class name for the specified type. + * + * @param type the type of the class being generated + * @return the class name + */ + fun klass(type: GeneratedClassType): String + + /** + * Creates a new member name for the specified type. + * + * @param type the type of the member being generated + * @return the member name + */ + fun member(type: GeneratedMemberType): String +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SemiQualifiedNamingStrategy.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SemiQualifiedNamingStrategy.kt new file mode 100644 index 00000000000..0cabacf4102 --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SemiQualifiedNamingStrategy.kt @@ -0,0 +1,46 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor.naming + +import me.kcra.takenaka.core.mapping.fromInternalName +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.model.ClassAccessor + +/** + * A semi-qualified class name strategy. + * + * @author Matouš Kučera + */ +internal class SemiQualifiedNamingStrategy : SimpleNamingStrategy() { + override fun klass(model: ClassAccessor, type: GeneratedClassType): String { + val name = model.internalName + .fromInternalName() // sanitize name + .removePrefix("net.minecraft.") + .removePrefix("com.mojang.") + + return when (type) { + GeneratedClassType.MAPPING -> "${name}Mapping" + GeneratedClassType.ACCESSOR -> "${name}Accessor" + else -> throw UnsupportedOperationException("$type is not a contextual type") + } + } + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SimpleNamingStrategy.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SimpleNamingStrategy.kt new file mode 100644 index 00000000000..d995f4096d1 --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/SimpleNamingStrategy.kt @@ -0,0 +1,92 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor.naming + +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.GeneratedMemberType +import me.kcra.takenaka.generator.accessor.model.ClassAccessor +import me.kcra.takenaka.generator.accessor.model.ConstructorAccessor +import me.kcra.takenaka.generator.accessor.model.FieldAccessor +import me.kcra.takenaka.generator.accessor.model.MethodAccessor + +/** + * A simple, package-less strategy. + * + * @author Matouš Kučera + */ +internal open class SimpleNamingStrategy : NamingStrategy { + override fun klass(model: ClassAccessor, type: GeneratedClassType): String { + val simpleName = model.internalName.substringAfterLast('/') + + return when (type) { + GeneratedClassType.MAPPING -> "${simpleName}Mapping" + GeneratedClassType.ACCESSOR -> "${simpleName}Accessor" + else -> throw UnsupportedOperationException("$type is not a contextual type") + } + } + + override fun klass(type: GeneratedClassType): String { + return when (type) { + GeneratedClassType.LOOKUP -> "Mappings" + else -> throw UnsupportedOperationException("$type is not a context-free type") + } + } + + override fun field(model: FieldAccessor, index: Int): String { + val fieldName = "FIELD_${model.upperName}" + if (index != 0) { + return "${fieldName}_$index" + } + + return fieldName + } + + override fun fieldHandle(model: FieldAccessor, index: Int, mutating: Boolean): String { + val fieldName = field(model, index) + if (mutating) { + return "${fieldName}_SETTER" + } + + return "${fieldName}_GETTER" + } + + override fun constant(model: FieldAccessor, index: Int): String { + return field(model, index) + } + + override fun constructor(model: ConstructorAccessor, index: Int): String { + return "CONSTRUCTOR_$index" + } + + override fun method(model: MethodAccessor, index: Int): String { + val methodName = "METHOD_${model.upperName}" + if (index != 0) { + return "${methodName}_$index" + } + + return methodName + } + + override fun member(type: GeneratedMemberType): String { + return type.name + } + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/StandardNamingStrategies.kt b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/StandardNamingStrategies.kt new file mode 100644 index 00000000000..a4828c2a716 --- /dev/null +++ b/generator/accessor/src/main/kotlin/me/kcra/takenaka/generator/accessor/naming/StandardNamingStrategies.kt @@ -0,0 +1,116 @@ +/* + * This file is part of takenaka, licensed under the Apache License, Version 2.0 (the "License"). + * + * Copyright (c) 2023-2024 Matous Kucera + * + * 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 me.kcra.takenaka.generator.accessor.naming + +import me.kcra.takenaka.generator.accessor.GeneratedClassType +import me.kcra.takenaka.generator.accessor.model.ClassAccessor + +/** + * Standard [NamingStrategy] implementations. + * + * @author Michal Turek + * @author Matouš Kučera + */ +object StandardNamingStrategies { + /** + * A simple, package-less strategy. + * + * **Does not account for conflicts for classes with the same simple name, + * but different package; use [resolveSimpleConflicts].** + */ + val SIMPLE: NamingStrategy = SimpleNamingStrategy() + + /** + * A fully-qualified class name strategy, based on [SIMPLE]. + * + * Uses the entire mapped name, including the full package. + */ + val FULLY_QUALIFIED: NamingStrategy = FullyQualifiedNamingStrategy() + + /** + * A semi-qualified class name strategy, based on [SIMPLE]. + * + * Uses the entire mapped name, including the package, but with `net.minecraft`/`com.mojang` removed. + */ + val SEMI_QUALIFIED: NamingStrategy = SemiQualifiedNamingStrategy() +} + +/** + * Prefixes (simple) class names with a package. + * + * @param basePackage the package + */ +fun NamingStrategy.prefixed(basePackage: String): NamingStrategy { + if (basePackage.isEmpty()) { + return this + } + + return PrefixingNamingStrategy(this, basePackage) +} + +/** + * Resolves simple class name conflicts by suffixing them with an occurrence index. + * + * *Zero indexes are skipped, as they point to the original class - conflicting classes start numbering at 1.* + */ +fun NamingStrategy.resolveSimpleConflicts(): NamingStrategy { + if (this is SimpleConflictResolvingNamingStrategy) { + return this + } + + return SimpleConflictResolvingNamingStrategy(this) +} + +private class PrefixingNamingStrategy(next: NamingStrategy, val basePackage: String) : ForwardingNamingStrategy(next) { + override fun klass(model: ClassAccessor, type: GeneratedClassType): String { + return "$basePackage.${super.klass(model, type)}" + } + + override fun klass(type: GeneratedClassType): String { + return "$basePackage.${super.klass(type)}" + } + + companion object { + private const val serialVersionUID = 1L + } +} + +private data class TypedName(val name: String, val type: GeneratedClassType) + +private class SimpleConflictResolvingNamingStrategy(next: NamingStrategy) : ForwardingNamingStrategy(next) { + @Transient + private val simpleNames = mutableMapOf>() // {simple name + type: {fq name: occurrence index}} + + override fun klass(model: ClassAccessor, type: GeneratedClassType): String { + val simpleName = model.internalName.substringAfterLast('/') + val index = synchronized(simpleNames) { + val names = simpleNames.getOrPut(TypedName(simpleName, type), ::mutableMapOf) + + names.getOrPut(model.internalName) { names.size } + } + + if (index > 0) { // copy accessor with a unique name + return super.klass(model.withName(model.name + index), type) + } + return super.klass(model, type) + } + + companion object { + private const val serialVersionUID = 1L + } +} diff --git a/generator/common/build.gradle.kts b/generator/common/build.gradle.kts index ebd3be12aea..44fe5d5559a 100644 --- a/generator/common/build.gradle.kts +++ b/generator/common/build.gradle.kts @@ -1,10 +1,9 @@ plugins { id("takenaka.base-conventions") + id("takenaka.kotlin-conventions") id("takenaka.publish-conventions") } -apply(plugin = "org.jetbrains.kotlin.jvm") - dependencies { api(project(":core")) api(libs.kotlin.logging.jvm) diff --git a/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/BundledMappingProvider.kt b/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/BundledMappingProvider.kt index 8e0fe35a077..fceb556b005 100644 --- a/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/BundledMappingProvider.kt +++ b/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/BundledMappingProvider.kt @@ -18,6 +18,7 @@ package me.kcra.takenaka.generator.common.provider.impl import com.fasterxml.jackson.databind.ObjectMapper +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.kcra.takenaka.core.VersionManifest @@ -25,8 +26,9 @@ import me.kcra.takenaka.core.mapping.MutableMappingsMap import me.kcra.takenaka.core.mapping.analysis.MappingAnalyzer import me.kcra.takenaka.core.util.objectMapper import me.kcra.takenaka.core.versionManifest +import me.kcra.takenaka.core.versionManifestOf import me.kcra.takenaka.generator.common.provider.MappingProvider -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.format.Tiny2Reader import net.fabricmc.mappingio.tree.MemoryMappingTree import java.nio.file.Path @@ -46,7 +48,7 @@ private val logger = KotlinLogging.logger {} * @property manifest the Mojang version manifest * @author Matouš Kučera */ -class BundledMappingProvider(val file: Path, val versions: List, val manifest: VersionManifest) : MappingProvider { +class BundledMappingProvider(val file: Path, val versions: List, val manifest: VersionManifest = versionManifestOf()) : MappingProvider { /** * Constructs this provider with a new manifest. * @@ -54,6 +56,11 @@ class BundledMappingProvider(val file: Path, val versions: List, val man * @param versions a version subset of the bundle to be loaded, everything is loaded if empty * @param objectMapper a JSON object mapper instance */ + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("BundledMappingProvider(file, versions)") + ) + @Suppress("DEPRECATION") constructor(file: Path, versions: List, objectMapper: ObjectMapper = objectMapper()) : this(file, versions, objectMapper.versionManifest()) @@ -63,38 +70,39 @@ class BundledMappingProvider(val file: Path, val versions: List, val man * @param analyzer an analyzer which the mappings should be visited to as they are resolved * @return the mappings */ - override suspend fun get(analyzer: MappingAnalyzer?): MutableMappingsMap { - return withContext(Dispatchers.IO) { + override suspend fun get(analyzer: MappingAnalyzer?): MutableMappingsMap = + withContext(Dispatchers.IO + CoroutineName("io-coro")) { ZipFile(file.toFile()).use { zf -> - zf.entries().asSequence().mapNotNull { entry -> - if (entry.isDirectory || !entry.name.endsWith(".tiny")) return@mapNotNull null + zf.entries() + .asSequence() + .mapNotNull { entry -> + if (entry.isDirectory || !entry.name.endsWith(".tiny")) return@mapNotNull null - val versionString = entry.name.substringAfterLast('/').removeSuffix(".tiny") + val versionString = entry.name.substringAfterLast('/').removeSuffix(".tiny") - // skip this version, it's not in the targeted subset - if (versions.isNotEmpty() && versionString !in versions) return@mapNotNull null - try { - val version = checkNotNull(manifest[versionString]) { - "Version $versionString was not found in manifest" - } + // skip this version, it's not in the targeted subset + if (versions.isNotEmpty() && versionString !in versions) return@mapNotNull null + try { + val version = checkNotNull(manifest[versionString]) { + "Version $versionString was not found in manifest" + } - return@mapNotNull version to MemoryMappingTree().apply { - zf.getInputStream(entry).reader().use { r -> Tiny2Reader.read(r, this) } - logger.info { "read ${version.id} mapping file from ${entry.name}" } + return@mapNotNull version to MemoryMappingTree().apply { + zf.getInputStream(entry).reader().use { r -> Tiny2Reader.read(r, this) } + logger.info { "read ${version.id} mapping file from ${entry.name}" } - if (analyzer != null) { - val time = measureTimeMillis { analyzer.accept(this) } - logger.info { "analyzed ${version.id} mappings in ${time}ms" } + if (analyzer != null) { + val time = measureTimeMillis { analyzer.accept(this) } + logger.info { "analyzed ${version.id} mappings in ${time}ms" } + } } + } catch (e: Exception) { + logger.error(e) { "failed to read mapping file from ${entry.name}" } } - } catch (e: Exception) { - logger.error(e) { "failed to read mapping file from ${entry.name}" } - } - return@mapNotNull null - } - .toMap() + return@mapNotNull null + } + .toMap() } } - } } diff --git a/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/ResolvingMappingProvider.kt b/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/ResolvingMappingProvider.kt index 68688566c78..38f50129207 100644 --- a/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/ResolvingMappingProvider.kt +++ b/generator/common/src/main/kotlin/me/kcra/takenaka/generator/common/provider/impl/ResolvingMappingProvider.kt @@ -29,8 +29,9 @@ import me.kcra.takenaka.core.mapping.resolve.OutputContainer import me.kcra.takenaka.core.mapping.unwrap import me.kcra.takenaka.core.util.objectMapper import me.kcra.takenaka.core.versionManifest +import me.kcra.takenaka.core.versionManifestOf import me.kcra.takenaka.generator.common.provider.MappingProvider -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.format.Tiny2Reader import net.fabricmc.mappingio.format.Tiny2Writer import net.fabricmc.mappingio.tree.MemoryMappingTree @@ -49,21 +50,46 @@ private val logger = KotlinLogging.logger {} * @property xmlMapper an XML object mapper instance for this provider * @author Matouš Kučera */ -class ResolvingMappingProvider( +class ResolvingMappingProvider @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("ResolvingMappingProvider(mappingConfig, manifest)") +) constructor( val mappingConfig: MappingConfiguration, val manifest: VersionManifest, - val xmlMapper: ObjectMapper = XmlMapper() + @Deprecated("Jackson will be an implementation detail in the future.") + val xmlMapper: ObjectMapper ) : MappingProvider { /** * Constructs this provider with a new manifest. * - * @property mappingConfig configuration to alter the mapping fetching and correction process - * @property objectMapper a JSON object mapper instance - * @property xmlMapper an XML object mapper instance + * @param mappingConfig configuration to alter the mapping fetching and correction process + * @param objectMapper a JSON object mapper instance + * @param xmlMapper an XML object mapper instance */ + @Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("ResolvingMappingProvider(mappingConfig)") + ) + @Suppress("DEPRECATION") constructor(mappingConfig: MappingConfiguration, objectMapper: ObjectMapper = objectMapper(), xmlMapper: ObjectMapper = XmlMapper()) : this(mappingConfig, objectMapper.versionManifest(), xmlMapper) + /** + * Constructs this provider with a supplied manifest. + * + * @param mappingConfig configuration to alter the mapping fetching and correction process + * @param manifest the Mojang version manifest + */ + @Suppress("DEPRECATION") + constructor(mappingConfig: MappingConfiguration, manifest: VersionManifest) : this(mappingConfig, manifest, XmlMapper()) + + /** + * Constructs this provider with a new manifest. + * + * @property mappingConfig configuration to alter the mapping fetching and correction process + */ + constructor(mappingConfig: MappingConfiguration) : this(mappingConfig, versionManifestOf()) + /** * Resolves the mappings. * @@ -79,17 +105,19 @@ class ResolvingMappingProvider( } } } - .parallelMap(Dispatchers.Default + CoroutineName("mapping-coro")) { workspace -> + .parallelMap(Dispatchers.Default + CoroutineName("resolve-coro")) { workspace -> val outputFile = mappingConfig.joinedOutputProvider(workspace) if (outputFile != null && outputFile.isRegularFile()) { // load mapping tree from file try { - return@parallelMap workspace.version to MemoryMappingTree().apply { - outputFile.reader().use { r -> Tiny2Reader.read(r, this) } - logger.info { "read ${workspace.version.id} joined mapping file from ${outputFile.pathString}" } + return@parallelMap workspace.version to MemoryMappingTree().also { tree -> + withContext(Dispatchers.IO + CoroutineName("io-coro")) { + outputFile.reader().use { r -> Tiny2Reader.read(r, tree) } + logger.info { "read ${workspace.version.id} joined mapping file from ${outputFile.pathString}" } + } if (analyzer != null) { - val time = measureTimeMillis { analyzer.accept(this) } + val time = measureTimeMillis { analyzer.accept(tree) } logger.info { "analyzed ${workspace.version.id} mappings in ${time}ms" } } } @@ -128,8 +156,10 @@ class ResolvingMappingProvider( } if (outputFile != null && !outputFile.isDirectory()) { - Tiny2Writer(outputFile.writer(), false).use { w -> tree.accept(MissingDescriptorFilter(w)) } - logger.info { "wrote ${workspace.version.id} joined mapping file to ${outputFile.pathString}" } + withContext(Dispatchers.IO + CoroutineName("io-coro")) { + Tiny2Writer(outputFile.writer(), false).use { w -> tree.accept(MissingDescriptorFilter(w)) } + logger.info { "wrote ${workspace.version.id} joined mapping file to ${outputFile.pathString}" } + } } workspace.version to tree diff --git a/generator/web-cli/build.gradle.kts b/generator/web-cli/build.gradle.kts index b37f6880e49..ab6e19f6164 100644 --- a/generator/web-cli/build.gradle.kts +++ b/generator/web-cli/build.gradle.kts @@ -1,12 +1,11 @@ plugins { id("takenaka.base-conventions") + id("takenaka.kotlin-conventions") id("takenaka.publish-conventions") alias(libs.plugins.shadow) application } -apply(plugin = "org.jetbrains.kotlin.jvm") - dependencies { implementation(project(":generator-web")) implementation(libs.kotlinx.cli.jvm) diff --git a/generator/web-cli/src/main/kotlin/me/kcra/takenaka/generator/web/cli/Main.kt b/generator/web-cli/src/main/kotlin/me/kcra/takenaka/generator/web/cli/Main.kt index 365179e8a48..749f357451a 100644 --- a/generator/web-cli/src/main/kotlin/me/kcra/takenaka/generator/web/cli/Main.kt +++ b/generator/web-cli/src/main/kotlin/me/kcra/takenaka/generator/web/cli/Main.kt @@ -19,16 +19,17 @@ package me.kcra.takenaka.generator.web.cli -import com.fasterxml.jackson.dataformat.xml.XmlMapper import kotlinx.cli.* import kotlinx.coroutines.runBlocking import me.kcra.takenaka.core.compositeWorkspace +import me.kcra.takenaka.core.mapping.MappingContributor import me.kcra.takenaka.core.mapping.adapter.* import me.kcra.takenaka.core.mapping.analysis.impl.AnalysisOptions import me.kcra.takenaka.core.mapping.analysis.impl.MappingAnalyzerImpl import me.kcra.takenaka.core.mapping.analysis.impl.StandardProblemKinds import me.kcra.takenaka.core.mapping.resolve.impl.* -import me.kcra.takenaka.core.util.objectMapper +import me.kcra.takenaka.core.util.md5Digest +import me.kcra.takenaka.core.util.updateAndHex import me.kcra.takenaka.core.workspace import me.kcra.takenaka.generator.common.provider.impl.ResolvingMappingProvider import me.kcra.takenaka.generator.common.provider.impl.SimpleAncestryProvider @@ -37,12 +38,25 @@ import me.kcra.takenaka.generator.common.provider.impl.buildMappingConfig import me.kcra.takenaka.generator.web.* import me.kcra.takenaka.generator.web.transformers.CSSInliningTransformer import me.kcra.takenaka.generator.web.transformers.MinifyingTransformer -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging import kotlin.system.exitProcess import kotlin.system.measureTimeMillis private val logger = KotlinLogging.logger {} +/** + * Available namespaces. + */ +val NAMESPACES = mapOf( + "mojang" to NamespaceDescription("Mojang", "#4D7C0F", AbstractMojangMappingResolver.META_LICENSE), + "spigot" to NamespaceDescription("Spigot", "#CA8A04", AbstractSpigotMappingResolver.META_LICENSE), + "yarn" to NamespaceDescription("Yarn", "#626262", YarnMappingResolver.META_LICENSE), + "quilt" to NamespaceDescription("Quilt", "#9722ff", QuiltMappingResolver.META_LICENSE), + "searge" to NamespaceDescription("Searge", "#B91C1C", SeargeMappingResolver.META_LICENSE), + "intermediary" to NamespaceDescription("Intermediary", "#0369A1", IntermediaryMappingResolver.META_LICENSE), + "hashed" to NamespaceDescription("Hashed", "#3344ff", null), +) + /** * Minification options. */ @@ -70,6 +84,8 @@ fun main(args: Array) { val parser = ArgParser("web-cli") val output by parser.option(ArgType.String, shortName = "o", description = "Output directory").default("output") val version by parser.option(ArgType.String, shortName = "v", description = "Target Minecraft version, can be specified multiple times").multiple().required() + val namespace by parser.option(ArgType.String, shortName = "n", description = "Target namespace, can be specified multiple times, order matters").multiple() + val ancestryNamespace by parser.option(ArgType.String, shortName = "a", description = "Target ancestry namespace, can be specified multiple times, has to be a subset of --namespace").multiple() val cache by parser.option(ArgType.String, shortName = "c", description = "Caching directory for mappings and other resources").default("cache") val server by parser.option(ArgType.Boolean, description = "Include server mappings in the documentation").default(false) val client by parser.option(ArgType.Boolean, description = "Include client mappings in the documentation").default(false) @@ -92,6 +108,28 @@ fun main(args: Array) { exitProcess(-1) } + val namespaceKeys = namespace.distinct().ifEmpty { NAMESPACES.keys } + fun MutableCollection.addIfSupported(contributor: T) { + if (contributor.targetNamespace in namespaceKeys) add(contributor) + } + + val resolvedNamespaces = namespaceKeys.map { nsKey -> + val ns = NAMESPACES[nsKey] + if (ns == null) { + logger.error { "namespace $nsKey not found, has to be one of [${NAMESPACES.keys.joinToString()}]" } + exitProcess(-1) + } + + nsKey to ns + } + + val ancestryNamespaces = namespaceKeys + .filter(ancestryNamespace::contains) + .ifEmpty { + listOf("mojang", "spigot", "searge", "intermediary") + .filter(namespaceKeys::contains) + } + val workspace = workspace { rootDirectory(output) } @@ -106,9 +144,6 @@ fun main(args: Array) { // generator setup below - val objectMapper = objectMapper() - val xmlMapper = XmlMapper() - val mappingsCache = cacheWorkspace.createCompositeWorkspace { name = "mappings" } @@ -116,7 +151,8 @@ fun main(args: Array) { name = "shared" } - val yarnProvider = YarnMetadataProvider(sharedCache, xmlMapper, relaxedCache = !strictCache) + val yarnProvider = YarnMetadataProvider(sharedCache, relaxedCache = !strictCache) + val quiltProvider = QuiltMetadataProvider(sharedCache, relaxedCache = !strictCache) val mappingConfig = buildMappingConfig { version(version) workspace(mappingsCache) @@ -131,56 +167,73 @@ fun main(args: Array) { intercept(::ObjectOverrideFilter) // remove obfuscated method parameter names, they are a filler from Searge intercept(::MethodArgSourceFilter) + // intern names to save memory + intercept(::StringPoolingAdapter) contributors { versionWorkspace -> - val mojangProvider = MojangManifestAttributeProvider(versionWorkspace, objectMapper, relaxedCache = !strictCache) - val spigotProvider = SpigotManifestProvider(versionWorkspace, objectMapper, relaxedCache = !strictCache) + val mojangProvider = MojangManifestAttributeProvider(versionWorkspace, relaxedCache = !strictCache) + val spigotProvider = SpigotManifestProvider(versionWorkspace, relaxedCache = !strictCache) buildList { if (server) { add(VanillaServerMappingContributor(versionWorkspace, mojangProvider, relaxedCache = !strictCache)) - add(MojangServerMappingResolver(versionWorkspace, mojangProvider)) + addIfSupported(MojangServerMappingResolver(versionWorkspace, mojangProvider)) } if (client) { add(VanillaClientMappingContributor(versionWorkspace, mojangProvider, relaxedCache = !strictCache)) - add(MojangClientMappingResolver(versionWorkspace, mojangProvider)) + addIfSupported(MojangClientMappingResolver(versionWorkspace, mojangProvider)) } - add(IntermediaryMappingResolver(versionWorkspace, sharedCache)) - add(YarnMappingResolver(versionWorkspace, yarnProvider, relaxedCache = !strictCache)) - add(SeargeMappingResolver(versionWorkspace, sharedCache, relaxedCache = !strictCache)) + addIfSupported(IntermediaryMappingResolver(versionWorkspace, sharedCache)) + addIfSupported(HashedMappingResolver(versionWorkspace)) + addIfSupported(YarnMappingResolver(versionWorkspace, yarnProvider, relaxedCache = !strictCache)) + addIfSupported(QuiltMappingResolver(versionWorkspace, quiltProvider, relaxedCache = !strictCache)) + addIfSupported(SeargeMappingResolver(versionWorkspace, sharedCache, relaxedCache = !strictCache)) // Spigot resolvers have to be last if (server) { val link = LegacySpigotMappingPrepender.Link() - add( - // 1.16.5 mappings have been republished with proper packages, even though the reobfuscated JAR does not have those - // See: https://hub.spigotmc.org/stash/projects/SPIGOT/repos/builddata/commits/80d35549ec67b87a0cdf0d897abbe826ba34ac27 + addIfSupported( link.createPrependingContributor( - SpigotClassMappingResolver(versionWorkspace, xmlMapper, spigotProvider, relaxedCache = !strictCache), + SpigotClassMappingResolver( + versionWorkspace, + spigotProvider, + relaxedCache = !strictCache + ), + // 1.16.5 mappings have been republished with proper packages, even though the reobfuscated JAR does not have those + // See: https://hub.spigotmc.org/stash/projects/SPIGOT/repos/builddata/commits/80d35549ec67b87a0cdf0d897abbe826ba34ac27 prependEverything = versionWorkspace.version.id == "1.16.5" ) ) - add(link.createPrependingContributor(SpigotMemberMappingResolver(versionWorkspace, xmlMapper, spigotProvider, relaxedCache = !strictCache))) + addIfSupported( + link.createPrependingContributor( + SpigotMemberMappingResolver( + versionWorkspace, + spigotProvider, + relaxedCache = !strictCache + ) + ) + ) } } } joinedOutputPath { workspace -> if (noJoined) return@joinedOutputPath null - val fileName = when { - client && server -> "client+server.tiny" - client -> "client.tiny" - else -> "server.tiny" + val platform = when { + client && server -> "client+server" + client -> "client" + else -> "server" } - workspace[fileName] + workspace["${md5Digest.updateAndHex(namespaceKeys.sorted().joinToString(","))}.$platform.tiny"] } } - val mappingProvider = ResolvingMappingProvider(mappingConfig, objectMapper, xmlMapper) + val mappingProvider = ResolvingMappingProvider(mappingConfig) val analyzer = MappingAnalyzerImpl( + // if the namespaces are missing, nothing happens anyway - no need to configure based on resolvedNamespaces AnalysisOptions( innerClassNameCompletionCandidates = setOf("spigot"), inheritanceAdditionalNamespaces = setOf("searge") // mojang could be here too for maximal parity, but that's in exchange for a little bit of performance @@ -222,28 +275,31 @@ fun main(args: Array) { transformer(MinifyingTransformer(isDeterministic = minifier == MinifierImpls.DETERMINISTIC)) } - val indexers = mutableListOf(objectMapper.modularClassSearchIndexOf(JDK_17_BASE_URL)) + val indexers = mutableListOf(modularClassSearchIndexOf(JDK_21_BASE_URL)) javadoc.mapTo(indexers) { javadocDef -> val javadocParams = javadocDef.split('+', limit = 2) when (javadocParams.size) { 2 -> classSearchIndexOf(javadocParams[1], javadocParams[0]) - else -> objectMapper.modularClassSearchIndexOf(javadocParams[0]) + else -> modularClassSearchIndexOf(javadocParams[0]) } } + index(indexers) logger.info { "using ${indexers.size} javadoc indexer(s)" } - index(indexers) + namespaces += resolvedNamespaces + friendlyNamespaces(resolvedNamespaces.map { (name, _) -> name }) - replaceCraftBukkitVersions("spigot") - friendlyNamespaces("mojang", "spigot", "yarn", "searge", "intermediary", "source") - namespace("mojang", "Mojang", "#4D7C0F", AbstractMojangMappingResolver.META_LICENSE) - namespace("spigot", "Spigot", "#CA8A04", AbstractSpigotMappingResolver.META_LICENSE) - namespace("yarn", "Yarn", "#626262", YarnMappingResolver.META_LICENSE) - namespace("searge", "Searge", "#B91C1C", SeargeMappingResolver.META_LICENSE) - namespace("intermediary", "Intermediary", "#0369A1", IntermediaryMappingResolver.META_LICENSE) + // replace CraftBukkit version strings if a Spigot namespace is requested + if ("spigot" in namespaces) replaceCraftBukkitVersions("spigot") + + // source/Obfuscated is always present + friendlyNamespaces("source") namespace("source", "Obfuscated", "#581C87") + + logger.info { "using [${namespaceFriendlinessIndex.joinToString()}] namespaces" } + logger.info { "using [${ancestryNamespaces.joinToString()}] ancestry namespaces" } } val generator = WebGenerator(workspace, webConfig) @@ -260,7 +316,7 @@ fun main(args: Array) { generator.generate( SimpleMappingProvider(mappings), - SimpleAncestryProvider(null, listOf("mojang", "spigot", "searge", "intermediary")) + SimpleAncestryProvider(null, ancestryNamespaces) ) } } diff --git a/generator/web/build.gradle.kts b/generator/web/build.gradle.kts index 1779930d0d3..fe7d26e6fd2 100644 --- a/generator/web/build.gradle.kts +++ b/generator/web/build.gradle.kts @@ -1,10 +1,9 @@ plugins { id("takenaka.base-conventions") + id("takenaka.kotlin-conventions") id("takenaka.publish-conventions") } -apply(plugin = "org.jetbrains.kotlin.jvm") - dependencies { api(project(":generator-common")) api(libs.kotlinx.html.jvm) diff --git a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/ClassSearchIndex.kt b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/ClassSearchIndex.kt index fe75cae714b..5b3a5541cd0 100644 --- a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/ClassSearchIndex.kt +++ b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/ClassSearchIndex.kt @@ -20,6 +20,7 @@ package me.kcra.takenaka.generator.web import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import me.kcra.takenaka.core.mapping.fromInternalName import me.kcra.takenaka.core.mapping.toInternalName @@ -28,6 +29,11 @@ import me.kcra.takenaka.core.util.readText import java.net.HttpURLConnection import java.net.URL +/** + * An internal instance of an [ObjectMapper]. + */ +private val MAPPER = jacksonObjectMapper() + /** * A no-op implementation of a search index. */ @@ -40,14 +46,24 @@ private val DUMMY_IMPL = object : ClassSearchIndex { /** * The base URL of the Java 17 JDK documentation. */ +@Deprecated("Use JDK_21_BASE_URL.", ReplaceWith("JDK_21_BASE_URL")) const val JDK_17_BASE_URL = "https://docs.oracle.com/en/java/javase/17/docs/api" +/** + * The base URL of the Java 21 JDK documentation. + */ +const val JDK_21_BASE_URL = "https://docs.oracle.com/en/java/javase/21/docs/api" + /** * Fetches and deserializes the package index on the base URL. * * @param baseUrl the base URL of the Javadoc site * @return the index */ +@Deprecated( + "Jackson will be an implementation detail in the future.", + ReplaceWith("modularClassSearchIndexOf(baseUrl)", "me.kcra.takenaka.generator.web.modularClassSearchIndexOf") +) fun ObjectMapper.modularClassSearchIndexOf(baseUrl: String): ModularClassSearchIndex { val content = URL("$baseUrl/package-search-index.js").httpRequest(action = HttpURLConnection::readText) val nodeArray = content.substring( @@ -58,6 +74,17 @@ fun ObjectMapper.modularClassSearchIndexOf(baseUrl: String): ModularClassSearchI return ModularClassSearchIndex(baseUrl, readValue(nodeArray)) } +/** + * Fetches and deserializes the package index on the base URL. + * + * @param baseUrl the base URL of the Javadoc site + * @return the index + */ +fun modularClassSearchIndexOf(baseUrl: String): ModularClassSearchIndex { + @Suppress("DEPRECATION") + return MAPPER.modularClassSearchIndexOf(baseUrl) +} + /** * Creates a class indexer for multiple Javadoc sites. * diff --git a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/GenerationContext.kt b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/GenerationContext.kt index 08907d1028c..bf8d3574ab1 100644 --- a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/GenerationContext.kt +++ b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/GenerationContext.kt @@ -52,8 +52,8 @@ class GenerationContext( * @param workspace the workspace * @param path the path, relative in the workspace */ - fun Document.serialize(workspace: Workspace, path: String) { - launch(Dispatchers.IO + CoroutineName("save-coro")) { + suspend fun Document.serialize(workspace: Workspace, path: String) { + withContext(Dispatchers.IO + CoroutineName("io-coro")) { val file = workspace[path] file.parent.createDirectories() diff --git a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/NamespaceDescription.kt b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/NamespaceDescription.kt index 58fcece5e59..cb99eae3011 100644 --- a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/NamespaceDescription.kt +++ b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/NamespaceDescription.kt @@ -27,5 +27,14 @@ package me.kcra.takenaka.generator.web data class NamespaceDescription( val friendlyName: String, val color: String, - val license: LicenseReference? -) + val license: LicenseReference? = null +) { + /** + * @param friendlyName the namespace friendly name, which will be shown on the site + * @param color the namespace badge color + * @param licenseContentKey the license content metadata key for this namespace version + * @param licenseSourceKey the license source metadata key for this namespace version + */ + constructor(friendlyName: String, color: String, licenseContentKey: String, licenseSourceKey: String = "${licenseContentKey}_source") : + this(friendlyName, color, LicenseReference(licenseContentKey, licenseSourceKey)) +} diff --git a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebConfiguration.kt b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebConfiguration.kt index 29d43f06395..91850a2fb79 100644 --- a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebConfiguration.kt +++ b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebConfiguration.kt @@ -201,7 +201,7 @@ class WebConfigurationBuilder { * @param licenseSourceKey the license source metadata key in the mapping tree */ fun namespace(name: String, friendlyName: String, color: String, licenseContentKey: String, licenseSourceKey: String = "${licenseContentKey}_source") { - namespaces += name to NamespaceDescription(friendlyName, color, LicenseReference(licenseContentKey, licenseSourceKey)) + namespaces += name to NamespaceDescription(friendlyName, color, licenseContentKey, licenseSourceKey) } /** diff --git a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebGenerator.kt b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebGenerator.kt index 067e2faa031..0e85976c602 100644 --- a/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebGenerator.kt +++ b/generator/web/src/main/kotlin/me/kcra/takenaka/generator/web/WebGenerator.kt @@ -15,10 +15,13 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package me.kcra.takenaka.generator.web import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.html.dom.serialize import me.kcra.takenaka.core.Workspace @@ -36,14 +39,17 @@ import me.kcra.takenaka.generator.web.components.navComponent import me.kcra.takenaka.generator.web.pages.* import me.kcra.takenaka.generator.web.transformers.MinifyingTransformer import me.kcra.takenaka.generator.web.transformers.Transformer +import io.github.oshai.kotlinlogging.KotlinLogging import net.fabricmc.mappingio.tree.MappingTreeView -import org.w3c.dom.Document import java.io.BufferedReader import java.nio.file.Files import java.nio.file.StandardCopyOption import java.util.* import kotlin.io.path.appendText import kotlin.io.path.writeText +import kotlin.system.measureTimeMillis + +private val logger = KotlinLogging.logger {} /** * A generator that generates documentation in an HTML format. @@ -94,101 +100,109 @@ open class WebGenerator(override val workspace: Workspace, val config: WebConfig val mappings = mappingProvider.get() val tree = ancestryProvider.klass<_, MappingTreeView.ClassMappingView>(mappings) + logger.info { "mappings loaded, starting generation" } val styleProvider: StyleProvider? = if (config.emitPseudoElements) StyleProviderImpl() else null generationContext(ancestryProvider, styleProvider) { - // used for looking up history hashes - for linking - val hashMap = IdentityHashMap() - - tree.forEach { node -> + val historyNodes = tree.map { node -> val (_, firstKlass) = node.first - val fileHash = firstKlass.hash.take(10) + node to firstKlass.hash.take(10) + } + + // used for looking up history hashes - for linking + val hashMap = IdentityHashMap() + historyNodes.forEach { (node, fileHash) -> node.forEach { (_, klass) -> hashMap[klass] = fileHash } + } - launch(Dispatchers.Default + CoroutineName("page-coro")) { - historyPage(node) - .serialize(historyWorkspace, "$fileHash.html") + launch(DISPATCHER + CoroutineName("gen-coro")) { + val time = measureTimeMillis { + historyNodes.forEach { (node, fileHash) -> + historyPage(node).serialize(historyWorkspace, "$fileHash.html") + } } + logger.info { "history pages generated in ${time}ms" } } mappings.forEach { (version, tree) -> val nmsVersion = tree.craftBukkitNmsVersion - launch(Dispatchers.Default + CoroutineName("generate-coro")) { - val versionWorkspace = currentComposite.createVersionedWorkspace { - this.version = version - } - - val friendlyNameRemapper = ElementRemapper(tree, ::getFriendlyDstName) - val classMap = sortedMapOf>>() - - // class index format, similar to a CSV: - // first line is a "header", this is a tab-delimited string with friendly namespace names + its badge colors, which are delimited by a colon ("namespace:#color") - // following lines are tab-separated strings with the mappings, each substring belongs to its column (header.split("\t").get(substringIndex) -> "namespace:#color") - // the column order is based on namespaceFriendlinessIndex, so the first non-null column is the friendly name - // "net/minecraft" and "com/mojang" are replaced with "%nm" and "%cm" to reduce duplication - - // example: - // Mojang:#4D7C0F Intermediary:#0369A1 Obfuscated:#581C87 - // %cm/math/Matrix3f %nm/class_4581 a - // %cm/math/Matrix4f %nm/class_1159 b - // ... (repeats like this for all classes) - val classIndex = buildString { - val namespaces = tree.allNamespaceIds - .mapNotNull { nsId -> - val nsName = tree.getNamespaceName(nsId) - if (nsName in namespaceFriendlyNames) nsName to nsId else null - } - .sortedBy { config.namespaceFriendlinessIndex.indexOf(it.first) } - .toMap() - - appendLine(namespaces.keys.joinToString("\t") { "${namespaceFriendlyNames[it]}:${getNamespaceBadgeColor(it)}" }) + launch(DISPATCHER + CoroutineName("gen-coro")) { + val time = measureTimeMillis { + val versionWorkspace = currentComposite.createVersionedWorkspace { + this.version = version + } - tree.classes.forEach { klass -> - val type = classTypeOf(klass.modifiers) - val friendlyName = getFriendlyDstName(klass) + val friendlyNameRemapper = ElementRemapper(tree, ::getFriendlyDstName) + val classMap = sortedMapOf>>() + + // class index format, similar to a CSV: + // first line is a "header", this is a tab-delimited string with friendly namespace names + its badge colors, which are delimited by a colon ("namespace:#color") + // following lines are tab-separated strings with the mappings, each substring belongs to its column (header.split("\t").get(substringIndex) -> "namespace:#color") + // the column order is based on namespaceFriendlinessIndex, so the first non-null column is the friendly name + // "net/minecraft" and "com/mojang" are replaced with "%nm" and "%cm" to reduce duplication + + // example: + // Mojang:#4D7C0F Intermediary:#0369A1 Obfuscated:#581C87 + // %cm/math/Matrix3f %nm/class_4581 a + // %cm/math/Matrix4f %nm/class_1159 b + // ... (repeats like this for all classes) + val classIndex = buildString { + val namespaces = tree.allNamespaceIds + .mapNotNull { nsId -> + val nsName = tree.getNamespaceName(nsId) + if (nsName in namespaceFriendlyNames) nsName to nsId else null + } + .sortedBy { config.namespaceFriendlinessIndex.indexOf(it.first) } + .toMap() + + appendLine(namespaces.keys.joinToString("\t") { "${namespaceFriendlyNames[it]}:${getNamespaceBadgeColor(it)}" }) + + tree.classes.forEach { klass -> + val type = classTypeOf(klass.modifiers) + val friendlyName = getFriendlyDstName(klass) - launch(Dispatchers.Default + CoroutineName("page-coro")) { classPage(klass, type, hashMap[klass], nmsVersion, versionWorkspace, friendlyNameRemapper) .serialize(versionWorkspace, "$friendlyName.html") - } - classMap.getOrPut(friendlyName.substringBeforeLast('/')) { sortedMapOf(compareBy(ClassType::ordinal)) } - .getOrPut(type, ::sortedSetOf) += friendlyName.substringAfterLast('/') + classMap.getOrPut(friendlyName.substringBeforeLast('/')) { sortedMapOf(compareBy(ClassType::ordinal)) } + .getOrPut(type, ::sortedSetOf) += friendlyName.substringAfterLast('/') - appendLine(namespaces.values.joinToString("\t") { nsId -> - val namespacedNmsVersion = if (tree.getNamespaceName(nsId) in versionReplaceCandidates) nmsVersion else null + appendLine(namespaces.values.joinToString("\t") { nsId -> + val namespacedNmsVersion = if (tree.getNamespaceName(nsId) in versionReplaceCandidates) nmsVersion else null - klass.getName(nsId)?.replaceCraftBukkitNMSVersion(namespacedNmsVersion) ?: "" - }) - } - }.replace("net/minecraft", "%nm").replace("com/mojang", "%cm") + klass.getName(nsId)?.replaceCraftBukkitNMSVersion(namespacedNmsVersion) ?: "" + }) + } + }.replace("net/minecraft", "%nm").replace("com/mojang", "%cm") - classMap.forEach { (packageName, classes) -> - packagePage(versionWorkspace, packageName, classes) - .serialize(versionWorkspace, "$packageName/index.html") - } + classMap.forEach { (packageName, classes) -> + packagePage(versionWorkspace, packageName, classes) + .serialize(versionWorkspace, "$packageName/index.html") + } - val licenses = config.namespaces - .mapNotNull { (ns, nsDesc) -> - if (nsDesc.license == null) return@mapNotNull null + val licenses = config.namespaces + .mapNotNull { (ns, nsDesc) -> + if (nsDesc.license == null) return@mapNotNull null - val content = nsDesc.license.content.let(tree::getMetadata) ?: return@mapNotNull null - val source = nsDesc.license.source.let(tree::getMetadata) ?: return@mapNotNull null + val content = nsDesc.license.content.let(tree::getMetadata) ?: return@mapNotNull null + val source = nsDesc.license.source.let(tree::getMetadata) ?: return@mapNotNull null - return@mapNotNull ns to License(content, source) - } - .toMap() + return@mapNotNull ns to License(content, source) + } + .toMap() - licensePage(versionWorkspace, licenses) - .serialize(versionWorkspace, "licenses.html") + licensePage(versionWorkspace, licenses) + .serialize(versionWorkspace, "licenses.html") - overviewPage(versionWorkspace, classMap.keys) - .serialize(versionWorkspace, "index.html") + overviewPage(versionWorkspace, classMap.keys) + .serialize(versionWorkspace, "index.html") - versionWorkspace["class-index.js"].writeText("updateClassIndex(`${classIndex.trim()}`);") // do not minify this file + versionWorkspace["class-index.js"].writeText("updateClassIndex(`${classIndex.trim()}`);") // do not minify this file + } + logger.info { "${version.id} pages generated in ${time}ms" } } } @@ -228,7 +242,7 @@ open class WebGenerator(override val workspace: Workspace, val config: WebConfig licensesLink.remove(); searchInput.remove(); searchBox.remove(); - e.dataset.collapsed = "yes"; + e.dataset.collapsed = "true"; } """.trimIndent() }, @@ -279,11 +293,6 @@ open class WebGenerator(override val workspace: Workspace, val config: WebConfig * @return the content of the component file */ fun generateComponentScript(vararg components: ComponentDefinition): String = buildString { - fun Document.serializeAsComponent(): String = transformHtml( - serialize(prettyPrint = false) - .substringAfter("\n") // remove - ) - append( """ const replaceComponent = (tag, component, callback) => { @@ -302,7 +311,7 @@ open class WebGenerator(override val workspace: Workspace, val config: WebConfig ) components.forEach { (tag, content, callback) -> - appendLine("const ${tag}Component = `${content.serializeAsComponent()}`;") + appendLine("const ${tag}Component = `${transformHtml(content.documentElement.serialize(prettyPrint = false))}`;") appendLine("const ${tag}ComponentCallback = (e) => {") if (callback != null) { appendLine(callback) @@ -342,4 +351,15 @@ open class WebGenerator(override val workspace: Workspace, val config: WebConfig * @return the transformed stylesheet */ override fun transformCss(content: String): String = config.transformers.fold(content) { content0, transformer -> transformer.transformCss(content0) } + + companion object { + /** + * The dispatcher used for individual version generation (+ history). + * + * The lower the value is, the fewer versions can be processed in parallel. + */ + private val DISPATCHER = Dispatchers.Default.limitedParallelism( + System.getProperty("me.kcra.takenaka.generator.web.parallelism")?.toIntOrNull() ?: Runtime.getRuntime().availableProcessors() + ) + } } diff --git a/generator/web/src/main/resources/assets/main.css b/generator/web/src/main/resources/assets/main.css index 0f59e764e66..1f7814c217f 100644 --- a/generator/web/src/main/resources/assets/main.css +++ b/generator/web/src/main/resources/assets/main.css @@ -98,7 +98,7 @@ nav { background-color: var(--nav); } -nav[data-collapsed="yes"] { +nav[data-collapsed="true"] { flex-direction: row; } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f21fdccded..de5232ba82e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin = "1.9.22" asm = "9.7" jackson = "2.17.1" -kotlinpoet = "1.16.0" +kotlinpoet = "1.17.0" [libraries] kotlin-stdlib-jdk8 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -13,9 +13,9 @@ jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackso jackson-datatype-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jackson" } jackson-dataformat-xml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-xml", version.ref = "jackson" } mapping-io = { group = "net.fabricmc", name = "mapping-io", version = "0.4.2" } -kotlin-logging-jvm = { group = "io.github.microutils", name = "kotlin-logging-jvm", version = "3.0.5" } +kotlin-logging-jvm = { group = "io.github.oshai", name = "kotlin-logging-jvm", version = "6.0.9" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.13" } -kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version = "1.8.0" } +kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version = "1.8.1" } kotlinx-html-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-html-jvm", version = "0.11.0" } kotlinx-cli-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-cli-jvm", version = "0.3.6" } jb-annotations = { group = "org.jetbrains", name = "annotations", version = "24.1.0" } @@ -24,6 +24,7 @@ kotlinpoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotli kotlinpoet-javapoet = { group = "com.squareup", name = "kotlinpoet-javapoet", version.ref = "kotlinpoet" } build-licenser = { group = "org.cadixdev.licenser", name = "org.cadixdev.licenser.gradle.plugin", version = "0.6.1" } build-gradle-versions = { group = "com.github.ben-manes", name = "gradle-versions-plugin", version = "0.46.0" } +build-kotlin-jvm = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } [bundles] kotlin = ["kotlin-stdlib-jdk8", "kotlin-reflect"] @@ -31,7 +32,6 @@ asm = ["asm", "asm-commons"] jackson = ["jackson-module-kotlin", "jackson-datatype-jsr310", "jackson-dataformat-xml"] [plugins] -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } shadow = { id = "io.github.goooler.shadow", version = "8.1.7" } gradle-plugin-publish = { id = "com.gradle.plugin-publish", version = "1.2.1" } build-config = { id = "com.github.gmazzo.buildconfig", version = "5.3.5" } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755